Last active
August 5, 2022 20:37
-
-
Save erdesigns-eu/0f2030010d71e99e121a15c609f3a504 to your computer and use it in GitHub Desktop.
M3U Reader Class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Constantes used for parsing. | |
*/ | |
const HEADER = 'HEADER'; | |
const DIRECTIVE = 'DIRECTIVE'; | |
const ATTRIBUTE = 'ATTRIBUTE'; | |
const EXTGRP = 'EXTGRP'; | |
const EXTPLS = 'PLAYLIST'; | |
const FILENAME = 'FILENAME'; | |
const GROUPTITLE = 'group-title'; | |
/** | |
* M3U Reader Class | |
*/ | |
export class m3uReader { | |
// Check file header | |
#checkFileHeader = false; | |
// Available attributes | |
#attributes = []; | |
// Available directives | |
#directives = []; | |
// Class constructor | |
constructor(options) { | |
const vm = this; | |
// Check file header | |
if (options && options.checkFileHeader) { | |
vm.#checkFileHeader = options.checkFileHeader; | |
} | |
// Set attributes | |
if (options && options.attributes && Array.isArray(options.attributes)) { | |
vm.#attributes = [...options.attributes]; | |
} | |
// Set directives | |
if (options && options.directives && Array.isArray(options.directives)) { | |
vm.#directives = [...options.directives]; | |
} | |
} | |
// Filter array of values for unique entries | |
unique(arr) { | |
var seen = {}; | |
var out = []; | |
var len = arr.length; | |
var j = 0; | |
for (var i = 0; i < len; i++) { | |
var item = arr[i]; | |
if (seen[item] !== 1) { | |
seen[item] = 1; | |
out[j++] = item; | |
} | |
} | |
return out; | |
} | |
// Parse attribute | |
parseAttribute(line, attribute) { | |
const match = new RegExp(`${attribute}="(.*?)"`, 'i').exec(line); | |
return match && match[1] ? `${match[1]}` : ''; | |
} | |
// Parse attributes (attribute="value") | |
parseAttributes(line) { | |
const vm = this; | |
// Temp list with found attributes | |
let res = []; | |
// Loop over attributes and extract data | |
vm.#attributes.map((attribute) => { | |
let value = vm.parseAttribute(line, attribute); | |
if (value && value.length) { | |
res.push({ | |
attribute: `${attribute.toLowerCase()}`, | |
value: `${value}` | |
}); | |
} | |
}); | |
// Return list with found attributes | |
return res; | |
} | |
// Parse directive from line | |
parseDirective(line, directive, full) { | |
const match = new RegExp(full ? `(?:#${directive}:)(.*)` : `(?:#${directive}:)([\\w]+)(?:[ ]?)`, 'i').exec(line); | |
return match && match[1] ? `${match[1]}` : ''; | |
} | |
// Parse directives (#EXT...) | |
parseDirectives(line, full) { | |
const vm = this; | |
// Temp list with found directives | |
let res = []; | |
// Loop over directives and extract data | |
vm.#directives.map((directive) => { | |
let value = vm.parseDirective(line, directive, full); | |
if (value && value.length) { | |
res.push({ | |
directive: `${directive.toUpperCase()}`, | |
value: `${value}` | |
}); | |
} | |
}); | |
// Return list with found directives | |
return res; | |
} | |
// Parse title from #EXTINF line | |
parseTitle(line) { | |
const match = new RegExp('(?!.*=",?.*")[,](.*?)$', 'i').exec(line); | |
return match && match[1] ? match[1] : ''; | |
} | |
// Parse play length from #EXTINF line | |
parsePlayLength(line) { | |
const match = new RegExp('#EXTINF:(.*?) ', 'i').exec(line); | |
return match && match[1] ? parseInt(match[1]) : -1; | |
} | |
// Type of line | |
lineType(line) { | |
// #EXTM3U (File header) | |
if (/^#EXTM3U/i.test(line)) { | |
return HEADER; | |
} | |
// #EXTINF (Attributes) | |
if (/^#EXTINF/i.test(line)) { | |
return ATTRIBUTE; | |
} | |
// #EXT (Directives) | |
if (/^#.*?/i.test(line)) { | |
return DIRECTIVE; | |
} | |
// URL / Filename | |
if (/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/gi.test(line)) { | |
return FILENAME; | |
} | |
} | |
// Parse M3U file | |
parse(content) { | |
const vm = this; | |
// Return a promise (Async) | |
return new Promise((resolve, reject) => { | |
// Split content to array of strings and remove empty lines. | |
const lines = content.replace(/(?:\r\n|\r)/g, '\n').split('\n').filter((line) => line.length); | |
// Playlist object | |
const playlist = { | |
title: '', | |
attributes: [], | |
groups: [] | |
} | |
// Temp - list with groups | |
let groups = ['']; | |
// Temp - list with streams | |
let streams = []; | |
// Temp - Stream Object | |
let stream = { | |
title: '', | |
url: '', | |
length: 0, | |
directives: [], | |
attributes: [], | |
group: '' | |
} | |
// Loop over lines and parse file | |
for (var i = 0; i < lines.length; i++) { | |
// Current line | |
let line = `${lines[i]}`; | |
// Check file header | |
if (vm.#checkFileHeader && i === 0) { | |
if (!line || !/^#EXTM3U/i.test(line)) { | |
reject('error.invalid-file-format'); | |
return; | |
} | |
} | |
// Line type | |
let lineType = vm.lineType(line); | |
// M3U File Header - Parse #EXTM3U tags | |
if (lineType === HEADER) { | |
playlist.attributes = [...vm.parseAttributes(line)]; | |
continue; | |
} | |
// Directive | |
if (lineType === DIRECTIVE) { | |
// Is this a group directive? | |
if (line.startsWith(`#${EXTGRP}`)) { | |
const group = vm.parseDirective(line, EXTGRP, true); | |
if (group.length) { | |
groups.push(`${group}`); | |
stream.group = `${group}`; | |
} | |
continue; | |
} | |
// Is this a playlist directive | |
if (line.startsWith(`#${EXTPLS}`)) { | |
const title = vm.parseDirective(line, EXTPLS, true); | |
if (title.length) { | |
playlist.title = `${title}`; | |
} | |
continue; | |
} | |
stream.directives = [...stream.directives, ...vm.parseDirectives(line, true)]; | |
} | |
// Attributes | |
if (lineType === ATTRIBUTE) { | |
// Extract stream title | |
stream.title = vm.parseTitle(line); | |
// Extract play length | |
stream.length = vm.parsePlayLength(line); | |
// Extract attributes | |
stream.attributes = [...stream.attributes, ...vm.parseAttributes(line)]; | |
// If we have a group attribute, add it to the list | |
const group = vm.parseAttribute(line, GROUPTITLE); | |
if (group.length) { | |
groups.push(`${group}`); | |
stream.group = `${group}`; | |
} | |
} | |
// URL / Filename | |
if (lineType === FILENAME) { | |
// Add URL to the stream | |
stream.url = `${line}`; | |
// Add the stream to the TEMP list of streams | |
streams.push(Object.assign({}, stream)); | |
// Clear the stream object | |
stream.title = ''; | |
stream.url = ''; | |
stream.length = 0; | |
stream.attributes = []; | |
stream.directives = []; | |
stream.group = ''; | |
} | |
} | |
// Add streams to the groups | |
vm.unique(groups).map((group) => { | |
// Filter streams for current GROUPTITLE | |
let filtered = streams.filter((stream) => stream.group == group); | |
// Delete group identifier from stream | |
filtered.forEach((stream) => { | |
delete stream.group; | |
}); | |
// Add group to playlist | |
if (filtered.length) { | |
playlist.groups.push({ | |
title: `${group}`, | |
streams: filtered | |
}); | |
} | |
}); | |
// Finished parsing! | |
resolve(playlist); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment