m3u-parser-generator
Version:
Library to parse and generate m3u or m3u8 IPTV playlist files
484 lines (479 loc) • 20.6 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.m3uParserGenerator = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.M3uAttributes = exports.M3uMedia = exports.M3uPlaylist = exports.M3uGenerator = exports.M3uParser = void 0;
var m3u_parser_1 = require("./m3u-parser");
Object.defineProperty(exports, "M3uParser", { enumerable: true, get: function () { return m3u_parser_1.M3uParser; } });
var m3u_generator_1 = require("./m3u-generator");
Object.defineProperty(exports, "M3uGenerator", { enumerable: true, get: function () { return m3u_generator_1.M3uGenerator; } });
var m3u_playlist_1 = require("./m3u-playlist");
Object.defineProperty(exports, "M3uPlaylist", { enumerable: true, get: function () { return m3u_playlist_1.M3uPlaylist; } });
Object.defineProperty(exports, "M3uMedia", { enumerable: true, get: function () { return m3u_playlist_1.M3uMedia; } });
Object.defineProperty(exports, "M3uAttributes", { enumerable: true, get: function () { return m3u_playlist_1.M3uAttributes; } });
},{"./m3u-generator":2,"./m3u-parser":3,"./m3u-playlist":4}],2:[function(require,module,exports){
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.M3uGenerator = void 0;
const m3u_playlist_1 = require("./m3u-playlist");
/**
* M3u generator class to generate m3u playlist string from playlist object
*/
class M3uGenerator {
/**
* Generate is static method to generate m3u playlist string from playlist object
* @param playlist - playlist object to generate m3u playlist string
* @returns final m3u playlist string
* @example
* ```ts
* const playlist = new M3uPlaylist();
* playlist.title = 'Test playlist';
* M3uGenerator.generate(playlist);
* ```
*/
static generate(playlist) {
const pls = playlist.title ? `${m3u_playlist_1.M3uDirectives.PLAYLIST}:${playlist.title}` : undefined;
const customData = this.getCustomDataDirective(playlist.customData);
const medias = playlist.medias.map(item => this.getMedia(item)).join('\n');
const attributesString = this.getAttributes(playlist.attributes);
return [m3u_playlist_1.M3uDirectives.EXTM3U + attributesString, pls, customData, medias].filter(item => item).join('\n');
}
/**
* Get generated media part string from m3u playlist media object
* @param media - media object
* @returns media part string with info, group and location each on separated line
* @private
*/
static getMedia(media) {
const attributesString = this.getAttributes(media.attributes);
const info = this.shouldAddInfoDirective(media, attributesString) ? `${m3u_playlist_1.M3uDirectives.EXTINF}:${media.duration}${attributesString},${media.name}` : null;
const group = media.group ? `${m3u_playlist_1.M3uDirectives.EXTGRP}:${media.group}` : null;
const bytes = media.bytes ? `${m3u_playlist_1.M3uDirectives.EXTBYT}:${media.bytes}` : null;
const image = media.image ? `${m3u_playlist_1.M3uDirectives.EXTIMG}:${media.image}` : null;
const album = media.album ? `${m3u_playlist_1.M3uDirectives.EXTALB}:${media.album}` : null;
const artist = media.artist ? `${m3u_playlist_1.M3uDirectives.EXTART}:${media.artist}` : null;
const genre = media.genre ? `${m3u_playlist_1.M3uDirectives.EXTGENRE}:${media.genre}` : null;
const extraAttributesFromUrl = media.extraAttributesFromUrl ? `${m3u_playlist_1.M3uDirectives.EXTATTRFROMURL}:${media.extraAttributesFromUrl}` : null;
const extraHttpHeaders = media.extraHttpHeaders ? `${m3u_playlist_1.M3uDirectives.EXTHTTP}:${JSON.stringify(media.extraHttpHeaders)}` : null;
const kodiProps = media.kodiProps ? [...media.kodiProps].map(([key, value]) => `${m3u_playlist_1.M3uDirectives.KODIPROP}:${key}=${value}`).join('\n') : null;
const customData = this.getCustomDataDirective(media.customData);
return [
info,
group,
bytes,
image,
album,
artist,
genre,
extraAttributesFromUrl,
extraHttpHeaders,
kodiProps,
customData,
media.location
].filter(item => item).join('\n');
}
/**
* Get generated string of custom directives for both, playlist and media
* @param customData - custom data object, that represents unknown directives
* @private
*/
static getCustomDataDirective(customData) {
return customData.map(data => `${data.directive}:${data.value}`).join('\n');
}
/**
* Get generated attributes media part string from m3u attributes object
* @param attributes - attributes object
* @returns attributes generated string (attributeName="attributeValue" ...)
* @private
*/
static getAttributes(attributes) {
const keys = Object.keys(attributes);
return keys.length ? ' ' + keys.map(key => `${key}="${attributes[key]}"`).join(' ') : '';
}
/**
* Method to determine if we need to add info directive or not based on media object and attributes string.
* At least media duration, media name or some attributes must be present to return true
* @param media - m3u media object
* @param attributesString - m3u attributes string
* @returns boolean if we should add info directive into final media
* @private
*/
static shouldAddInfoDirective(media, attributesString) {
return media.duration !== m3u_playlist_1.DEFAULT_MEDIA_DURATION || attributesString !== '' || media.name !== undefined;
}
}
exports.M3uGenerator = M3uGenerator;
},{"./m3u-playlist":4}],3:[function(require,module,exports){
;
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;
},{"./m3u-playlist":4}],4:[function(require,module,exports){
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.M3uAttributes = exports.M3uMedia = exports.M3uPlaylist = exports.M3uDirectives = exports.DEFAULT_MEDIA_DURATION = exports.M3U_COMMENT = void 0;
const m3u_generator_1 = require("./m3u-generator");
exports.M3U_COMMENT = '#';
exports.DEFAULT_MEDIA_DURATION = -1;
var M3uDirectives;
(function (M3uDirectives) {
M3uDirectives["EXTM3U"] = "#EXTM3U";
M3uDirectives["EXTINF"] = "#EXTINF";
M3uDirectives["PLAYLIST"] = "#PLAYLIST";
M3uDirectives["EXTGRP"] = "#EXTGRP";
M3uDirectives["EXTBYT"] = "#EXTBYT";
M3uDirectives["EXTIMG"] = "#EXTIMG";
M3uDirectives["EXTALB"] = "#EXTALB";
M3uDirectives["EXTART"] = "#EXTART";
M3uDirectives["EXTGENRE"] = "#EXTGENRE";
M3uDirectives["EXTATTRFROMURL"] = "#EXTATTRFROMURL";
M3uDirectives["EXTHTTP"] = "#EXTHTTP";
M3uDirectives["KODIPROP"] = "#KODIPROP";
})(M3uDirectives = exports.M3uDirectives || (exports.M3uDirectives = {}));
/**
* M3u playlist object
*/
class M3uPlaylist {
constructor() {
/**
* Title of playlist
* @example code
* ```ts
* const playlist = new M3uPlaylist();
* playlist.title = 'Test playlist';
* ```
* @example example output in final m3u string
* ```
* #PLAYLIST:Test TV
* ```
*/
this.title = '';
/**
* Attributes of of the EXTM3U tag. Default value is empty attributes object.
*/
this.attributes = new M3uAttributes();
/**
* M3u media objects
* @example
* ```ts
* const playlist = new M3uPlaylist();
* const media1 = new M3uMedia('http://my-stream-ulr.com/playlist.m3u8');
* playlist.medias.push(media1);
* ```
*/
this.medias = [];
/**
* Unknown directives, that belong to the whole playlist
*/
this.customData = [];
}
/**
* Get url-tvg url
* @returns url-tvg url
* @deprecated The method should not be used, use playlist.attributes['url-tvg'] instead
*/
get urlTvg() {
return this.attributes['url-tvg'];
}
/**
* Set url-tvg url
* @param urlTvg - url-tvg url
* @deprecated The method should not be used, use playlist.attributes['url-tvg'] instead
*/
set urlTvg(urlTvg) {
this.attributes = Object.assign(Object.assign({}, this.attributes), { 'url-tvg': urlTvg });
}
/**
* Get m3u string method to get m3u playlist string of current playlist object
* @returns m3u playlist string
*/
getM3uString() {
return m3u_generator_1.M3uGenerator.generate(this);
}
}
exports.M3uPlaylist = M3uPlaylist;
/**
* M3u media object
* @example code example
* ```ts
* const media1 = new M3uMedia('http://my-stream-ulr.com/playlist.m3u8');
* ```
* @example example output in final m3u string
* ```
* #EXTINF:-1 tvg-id="Test tv 1" tvg-country="CZ" tvg-language="CS" tvg-logo="logo1.png" group-title="Test1" unknown="0",Test tv 1 [CZ]
* #EXTGRP:Test TV group 1
* http://iptv.test1.com/playlist.m3u8
* ```
*/
class M3uMedia {
/**
* Constructor
* @param location - location of stream
*/
constructor(location) {
this.location = location;
/**
* Duration of media. Default value is -1 (infinity).
*/
this.duration = exports.DEFAULT_MEDIA_DURATION;
/**
* Attributes of media. Default value is empty attributes object.
*/
this.attributes = new M3uAttributes();
/**
* Extra attributes from url
*/
this.extraAttributesFromUrl = undefined;
/**
* Extra HTTP headers
*/
this.extraHttpHeaders = undefined;
/**
* Kodi props
*/
this.kodiProps = new Map();
/**
* Size of media in bytes.
*/
this.bytes = undefined;
/**
* image (e.g. cover) URL
*/
this.image = undefined;
/**
* album
*/
this.album = undefined;
/**
* artist
*/
this.artist = undefined;
/**
* genre
*/
this.genre = undefined;
/**
* Unknown directives, that belong to the specific media
*/
this.customData = [];
}
}
exports.M3uMedia = M3uMedia;
/**
* M3u media attributes. Can contains know attributes, or unknown custom user defined.
* @example
* ```ts
* const media1 = new M3uMedia('http://my-stream-ulr.com/playlist.m3u8');
* media1.attributes = {'tvg-id': '5', 'tvg-language': 'EN', 'unknown': 'my custom attribute'};
* ```
*/
class M3uAttributes {
}
exports.M3uAttributes = M3uAttributes;
},{"./m3u-generator":2}]},{},[1])(1)
});