Skip to content

Commit

Permalink
feat(directives): ability to configure to parse custom or unknown dir…
Browse files Browse the repository at this point in the history
…ectives
  • Loading branch information
Raiper34 committed Jul 31, 2024
1 parent acde544 commit 73c3194
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 15 deletions.
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {M3uParser} from './m3u-parser'
export {M3uCustomDataMapping, M3uParser, M3uParserConfig} from './m3u-parser'
export {M3uGenerator} from './m3u-generator'
export {M3uPlaylist, M3uMedia, M3uAttributes} from './m3u-playlist'
export {M3uCustomData, M3uPlaylist, M3uMedia, M3uAttributes} from './m3u-playlist'
15 changes: 14 additions & 1 deletion src/m3u-generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
M3uCustomData,
M3uDirectives,
M3uMedia,
M3uPlaylist,
Expand All @@ -24,9 +25,10 @@ export class M3uGenerator {
*/
static generate(playlist: M3uPlaylist): string {
const pls = playlist.title ? `${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 [M3uDirectives.EXTM3U + attributesString, pls, medias].filter(item => item).join('\n');
return [M3uDirectives.EXTM3U + attributesString, pls, customData, medias].filter(item => item).join('\n');
}

/**
Expand All @@ -47,6 +49,7 @@ export class M3uGenerator {
const extraAttributesFromUrl = media.extraAttributesFromUrl ? `${M3uDirectives.EXTATTRFROMURL}:${media.extraAttributesFromUrl}` : null;
const extraHttpHeaders = media.extraHttpHeaders ? `${M3uDirectives.EXTHTTP}:${JSON.stringify(media.extraHttpHeaders)}` : null;
const kodiProps = media.kodiProps ? [...media.kodiProps].map(([key, value]) => `${M3uDirectives.KODIPROP}:${key}=${value}`).join('\n') : null;
const customData = this.getCustomDataDirective(media.customData);

return [
info,
Expand All @@ -59,10 +62,20 @@ export class M3uGenerator {
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
*/
private static getCustomDataDirective(customData: M3uCustomData[]): string {
return customData.map(data => `${data.directive}:${data.value}`).join('\n');
}

/**
* Get generated attributes media part string from m3u attributes object
* @param attributes - attributes object
Expand Down
74 changes: 65 additions & 9 deletions src/m3u-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,35 @@ import {
M3uMedia,
M3uAttributes,
M3uDirectives,
M3U_COMMENT
M3U_COMMENT,
} from "./m3u-playlist";

/**
* Custom data mapping, that defines parsing of unknown directives.
* Directive can belong to whole playlist, or specific media.
* Key is name of directive and value is if directive belongs to the specific media (otherwise whole playlist)
* ```ts
* {'#PlaylistDirective': false, '#MediaDirective': true}
* ```
* in this example #PlaylistDirective is configured as directive, that belongs to playlist and #MediaDirective to the specific media
*/
export interface M3uCustomDataMapping {[key: string]: boolean}


/**
* Interface to configure m3u parser
*/
export interface M3uParserConfig {
/**
* Ignore errors in file and try to parse it with it
*/
ignoreErrors?: boolean;
/**
* Custom mapping for unknown directives, that can be partially parsed too and added to parsed object
*/
customDataMapping?: M3uCustomDataMapping,
}

/**
* M3u parser class to parse m3u playlist string to playlist object
*/
Expand Down Expand Up @@ -52,11 +78,12 @@ export class M3uParser {
/**
* 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
*/
private static processDirective(item: string, playlist: M3uPlaylist, media: M3uMedia): void {
private static processDirective(item: string, customDataMapping: M3uCustomDataMapping, playlist: M3uPlaylist, media: M3uMedia): void {
const firstSemicolonIndex = item.indexOf(':');
const directive = item.substring(0, firstSemicolonIndex);
const trackInformation = item.substring(firstSemicolonIndex + 1);
Expand Down Expand Up @@ -112,6 +139,34 @@ export class M3uParser {
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
*/
private static processCustomData(
playlist: M3uPlaylist,
media: M3uMedia,
trackInformation: string,
directive: string,
customDataMapping: M3uCustomDataMapping,
): void {
if(directive in customDataMapping) {
if (customDataMapping[directive]) {
media.customData.push({directive, value: trackInformation});
} else {
playlist.customData.push({directive, value: trackInformation});
}
}
}

Expand All @@ -134,18 +189,19 @@ export class M3uParser {
/**
* 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
*/
private static getPlaylist(lines: string[]): M3uPlaylist {
private static getPlaylist(lines: string[], customDataMapping: M3uCustomDataMapping = {}): M3uPlaylist {
const playlist = new M3uPlaylist();
let media = new M3uMedia('');

this.processExtM3uAttributes(lines[0], playlist);

lines.forEach(item => {
if (this.isDirective(item)) {
this.processDirective(item, playlist, media);
this.processDirective(item, customDataMapping, playlist, media);
} else {
media.location = item;
playlist.medias.push(media);
Expand Down Expand Up @@ -180,24 +236,24 @@ export class M3uParser {
* 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 ignoreErrors - ignore errors in file and try to parse it with it
* @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: string, ignoreErrors = false): M3uPlaylist {
if (!ignoreErrors && !m3uString) {
static parse(m3uString: string, config?: M3uParserConfig): M3uPlaylist {
if (!config?.ignoreErrors && !m3uString) {
throw new Error(`m3uString can't be null!`);
}

const lines = m3uString.split('\n').map(item => item.trim()).filter(item => item != '');

if (!ignoreErrors && !this.isValidM3u(lines)) {
if (!config?.ignoreErrors && !this.isValidM3u(lines)) {
throw new Error(`Missing ${M3uDirectives.EXTM3U} directive!`);
}
return this.getPlaylist(lines);
return this.getPlaylist(lines, config?.customDataMapping);
}
}
26 changes: 26 additions & 0 deletions src/m3u-playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ export enum M3uDirectives {
KODIPROP = '#KODIPROP'
}

/**
* Custom data, that represents unknown directives, that can be parsed also, when mapping is presented
*/
export interface M3uCustomData {
/**
* Directive name with '#' symbol at the start
* e.g. #EXT-CUSTOM
*/
directive: string;
/**
* Value or parameters of directive
* in case of #EXT-CUSTOM:Something , the 'Something' string is the value
*/
value: string;
}

/**
* M3u playlist object
*/
Expand Down Expand Up @@ -70,6 +86,11 @@ export class M3uPlaylist {
*/
medias: M3uMedia[] = [];

/**
* Unknown directives, that belong to the whole playlist
*/
customData: M3uCustomData[] = [];

/**
* Get m3u string method to get m3u playlist string of current playlist object
* @returns m3u playlist string
Expand Down Expand Up @@ -151,6 +172,11 @@ export class M3uMedia {
*/
genre?: string = undefined;

/**
* Unknown directives, that belong to the specific media
*/
customData: M3uCustomData[] = [];

/**
* Constructor
* @param location - location of stream
Expand Down
15 changes: 12 additions & 3 deletions test/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
playlistWithExtraHTTPHeaders,
playlistWithKodiProps,
playlistWithExtraProps,
invalidExtM3uAttributes
invalidExtM3uAttributes, playlistWithCustomDirectives
} from "./test-m3u";
import {M3uCustomDataMapping} from "../src/m3u-parser";

describe('Parse and generate test', () => {
it('should be same as original after parse and generate', () => {
Expand Down Expand Up @@ -46,7 +47,7 @@ describe('Parse and generate test', () => {
});

it('should NOT raise exception when parsing invalid m3u string with ignoreErrors argument', () => {
expect(() => M3uParser.parse('', true)).not.toThrow(new Error(`m3uString can't be null!`));
expect(() => M3uParser.parse('', {ignoreErrors: true})).not.toThrow(new Error(`m3uString can't be null!`));
});

it('should parse with invalid attributes', () => {
Expand All @@ -66,7 +67,7 @@ describe('Parse and generate test', () => {
const expectedPlaylist = new M3uPlaylist();
expectedPlaylist.medias = [media1, media2]

expect(M3uParser.parse(invalidPlaylist, true)).toEqual(expectedPlaylist);
expect(M3uParser.parse(invalidPlaylist, {ignoreErrors: true})).toEqual(expectedPlaylist);
});

it('should parse url-tvg attribute', () => {
Expand Down Expand Up @@ -192,4 +193,12 @@ describe('Parse and generate test', () => {
const playlist = M3uParser.parse(invalidExtM3uAttributes);
expect(playlist.attributes).toEqual(new M3uAttributes());
});

it('should parse and generate with custom directives', () => {
const customDataMapping: M3uCustomDataMapping = {
'#EXTCUSTOMPLAYLIST': false,
'#EXTCUSTOMMEDIA': true,
}
expect(M3uParser.parse(playlistWithCustomDirectives, {customDataMapping}).getM3uString()).toEqual(playlistWithCustomDirectives);
})
});
7 changes: 7 additions & 0 deletions test/test-m3u.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,10 @@ export const playlistWithExtraProps = `#EXTM3U url-tvg="http://example.com/tvg.x
http://iptv.test1.com/playlist.m3u8`

export const invalidExtM3uAttributes = `#EXTM3U foo="bar`;

export const playlistWithCustomDirectives = `#EXTM3U
#EXTCUSTOMPLAYLIST:playlist
#EXTCUSTOMMEDIA:MEDIA1
http://iptv.test1.com/playlist.m3u8
#EXTCUSTOMMEDIA:MEDIA2
http://iptv.test1.com/playlist2.m3u8`

0 comments on commit 73c3194

Please sign in to comment.