UNPKG

m3u-parser-generator

Version:

Library to parse and generate m3u or m3u8 IPTV playlist files

211 lines (210 loc) 9.13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.M3uParser = void 0; const m3u_playlist_1 = require("./m3u-playlist"); /** * M3u parser class to parse m3u playlist string to playlist object */ class M3uParser { /** * Get m3u attributes object from attributes string * @param attributesString e.g. 'tvg-id="" group-title=""' * @returns attributes object e.g. {"tvg-id": "", "group-title": ""} * @private */ static getAttributes(attributesString) { var _a; const attributes = new m3u_playlist_1.M3uAttributes(); if (!attributesString) { return attributes; } const attributeValuePair = (_a = attributesString.match(/[^ ]*?=".*?"/g)) !== null && _a !== void 0 ? _a : []; // regex to find `attribute="value"` attributeValuePair.forEach((item) => { const [key, value] = item.split('="'); attributes[key] = value.replace('"', ''); }); return attributes; } /** * Process media method parse trackInformation and fill media with parsed info * @param trackInformation - media substring of m3u string line e.g. '-1 tvg-id="" group-title="",Tv Name' * @param media - actual m3u media object * @private */ static processMedia(trackInformation, media) { const lastCommaIndex = trackInformation.lastIndexOf(','); const durationAttributes = trackInformation.substring(0, lastCommaIndex); media.name = trackInformation.substring(lastCommaIndex + 1); const firstSpaceIndex = durationAttributes.indexOf(' '); const durationEndIndex = firstSpaceIndex > 0 ? firstSpaceIndex : durationAttributes.length; media.duration = Number(durationAttributes.substring(0, durationEndIndex)); const attributes = durationAttributes.substring(durationEndIndex + 1); media.attributes = this.getAttributes(attributes); } /** * Process directive method detects directive on line and call proper method to another processing * @param item - actual line of m3u playlist string e.g. '#EXTINF:-1 tvg-id="" group-title="",Tv Name' * @param customDataMapping - whole custom directive data mapping configuration * @param playlist - m3u playlist object processed until now * @param media - actual m3u media object * @private */ static processDirective(item, customDataMapping, playlist, media) { const firstSemicolonIndex = item.indexOf(':'); const directive = item.substring(0, firstSemicolonIndex); const trackInformation = item.substring(firstSemicolonIndex + 1); switch (directive) { case m3u_playlist_1.M3uDirectives.EXTINF: { this.processMedia(trackInformation, media); break; } case m3u_playlist_1.M3uDirectives.EXTGRP: { media.group = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTBYT: { media.bytes = Number(trackInformation); break; } case m3u_playlist_1.M3uDirectives.EXTIMG: { media.image = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTALB: { media.album = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTART: { media.artist = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTGENRE: { media.genre = trackInformation; break; } case m3u_playlist_1.M3uDirectives.PLAYLIST: { playlist.title = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTATTRFROMURL: { media.extraAttributesFromUrl = trackInformation; break; } case m3u_playlist_1.M3uDirectives.EXTHTTP: { media.extraHttpHeaders = JSON.parse(trackInformation); break; } case m3u_playlist_1.M3uDirectives.KODIPROP: { const [key, ...valueParts] = trackInformation.split('='); const value = valueParts.join('='); // in case value contains '=', ie. '#KODIPROP:inputstream.adaptive.license_key=https://example.com/license.php?id=example' if (!media.kodiProps) { media.kodiProps = new Map(); } media.kodiProps.set(key, value); break; } default: { this.processCustomData(playlist, media, trackInformation, directive, customDataMapping); } } } /** * Process custom unknown directive and add it into playlist or media object, based on mapping configuration * @param playlist - m3u playlist object processed until now * @param media - actual m3u media object * @param trackInformation - track information, whole part of string after directive and semicolon * @param directive - unknown directive e.g. #EXT-CUSTOM * @param customDataMapping - whole custom directive data mapping configuration * @private */ static processCustomData(playlist, media, trackInformation, directive, customDataMapping) { if (directive in customDataMapping) { if (customDataMapping[directive]) { media.customData.push({ directive, value: trackInformation }); } else { playlist.customData.push({ directive, value: trackInformation }); } } } /** * Process attributes in #EXTM3U line * @param item - first line of m3u playlist string e.g. '#EXTM3U url-tvg="http://example.com/tvg.xml"' * @param playlist - m3u playlist object processed until now * @private */ static processExtM3uAttributes(item, playlist) { if (item.startsWith(m3u_playlist_1.M3uDirectives.EXTM3U)) { const firstSpaceIndex = item.indexOf(' '); if (firstSpaceIndex > 0) { const attributes = item.substring(firstSpaceIndex + 1); playlist.attributes = this.getAttributes(attributes); } } } /** * Get playlist returns m3u playlist object parsed from m3u string lines * @param lines - m3u string lines * @param customDataMapping - whole custom directive data mapping configuration * @returns parsed m3u playlist object * @private */ static getPlaylist(lines, customDataMapping = {}) { const playlist = new m3u_playlist_1.M3uPlaylist(); let media = new m3u_playlist_1.M3uMedia(''); this.processExtM3uAttributes(lines[0], playlist); lines.forEach(item => { if (this.isDirective(item)) { this.processDirective(item, customDataMapping, playlist, media); } else { media.location = item; playlist.medias.push(media); media = new m3u_playlist_1.M3uMedia(''); } }); return playlist; } /** * Is directive method detect if line contains m3u directive * @param item - string line of playlist * @returns true if it is line with directive, otherwise false * @private */ static isDirective(item) { return item[0] === m3u_playlist_1.M3U_COMMENT; } /** * Is valid m3u method detect if first line of playlist contains #EXTM3U directive * @param firstLine - first line of m3u playlist string * @returns true if line starts with #EXTM3U, false otherwise * @private */ static isValidM3u(firstLine) { return firstLine[0].startsWith(m3u_playlist_1.M3uDirectives.EXTM3U); } /** * Parse is static method to parse m3u playlist string into m3u playlist object. * Playlist need to contain #EXTM3U directive on first line. * All lines are trimmed and blank ones are removed. * @param m3uString - whole m3u playlist string * @param config - additional parsing configuration * @returns parsed m3u playlist object * @example * ```ts * const playlist = M3uParser.parse(m3uString); * playlist.medias.forEach(media => media.location); * ``` */ static parse(m3uString, config) { if (!(config === null || config === void 0 ? void 0 : config.ignoreErrors) && !m3uString) { throw new Error(`m3uString can't be null!`); } const lines = m3uString.split('\n').map(item => item.trim()).filter(item => item != ''); if (!(config === null || config === void 0 ? void 0 : config.ignoreErrors) && !this.isValidM3u(lines)) { throw new Error(`Missing ${m3u_playlist_1.M3uDirectives.EXTM3U} directive!`); } return this.getPlaylist(lines, config === null || config === void 0 ? void 0 : config.customDataMapping); } } exports.M3uParser = M3uParser;