Skip to content

Instantly share code, notes, and snippets.

@erdesigns-eu
Last active August 5, 2022 20:37
Show Gist options
  • Save erdesigns-eu/0f2030010d71e99e121a15c609f3a504 to your computer and use it in GitHub Desktop.
Save erdesigns-eu/0f2030010d71e99e121a15c609f3a504 to your computer and use it in GitHub Desktop.
M3U Reader Class
/**
* 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