Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

m3u-parser

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

m3u-parser - npm Package Compare versions

Comparing version 1.0.3 to 2.0.0-alpha.1

.idea/codeStyleSettings.xml

289

m3u.js

@@ -1,46 +0,267 @@

var Promise = require('bluebird');
(function (root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) define([], factory);else if (typeof module === 'object' && module.exports) module.exports = factory();else root.m3uParser = factory();
})(this, function () {
'use strict';
function parse (data) {
if (Buffer.isBuffer(data))
data = data.toString();
else if (typeof data !== 'string')
return Promise.reject(new TypeError('Data passed to the parser should be a string'));
function parseByteRange(value) {
var match = /^(\d+)(@(\d+))?/.exec(value); // todo simple string token (@) split?
return new Promise(function (resolve, reject) {
data = data.split('\n')
.filter(function (str) {
return str.length > 0;
});
if (!match) return null;
if (data.shift().trim() !== '#EXTM3U')
return reject(new Error('Passed data is not valid M3U playlist'));
var byteRange = { length: +match[1] };
var buffer = [], isWaitingForLink = false, line;
if (match[3]) byteRange.offset = +match[3];
while ((line = data.shift())) {
line = line.trim();
return byteRange;
}
if (isWaitingForLink) {
buffer[buffer.length - 1].file = line;
isWaitingForLink = false;
} else if (line.slice(0, 7) === '#EXTINF') {
var result = /^#EXTINF:(-?)(\d+),(.*)$/.exec(line);
if (!result)
throw new Error('Invalid M3U format');
function parseAttributesList(value) {
var result = {};
buffer.push({
title: result[3].trim(),
duration: +(result[1] + result[2].trim())
});
var ATTR_LIST_REGEX = /([A-Z0-9-]+)=(?:"([^"]+)"|([^,"\s]+))/g;
isWaitingForLink = true;
} else {
throw new Error('Invalid data');
var match = void 0;
while ((match = ATTR_LIST_REGEX.exec(value)) !== null) {
result[match[1].toLowerCase()] = match[2] || match[3];
}
return result;
}
function parse(data, options, cb) {
if (arguments.length === 0) throw new Error('Parser should be called at least with the playlist parameter specified');else if (arguments.length === 1) // todo optimize
options = {};else if (arguments.length === 2) {
if (typeof options === 'function') {
// todo optimize
cb = options;
options = {};
}
}
resolve(buffer);
});
}
var promise = typeof cb !== 'function',
resolve = promise ? Promise.resolve.bind(Promise) : function (r) {
return cb(null, r);
},
reject = promise ? Promise.reject.bind(Promise) : cb;
module.exports.parse = parse;
if (typeof Buffer === 'function' && Buffer.isBuffer(data)) data = data.toString();else if (typeof data !== 'string') return reject(new TypeError('Data passed to the parser should be a string'));
data = data.split('\n').filter(function (str) {
return str.length > 0;
}); // empty lines are ignored
if (data.length === 0) return resolve([]);
data[0] = data[0].trim(); // trim first line
var isExtended = false;
if (data[0][0] === '#') {
var _line = data.shift();
if (_line === '#EXTM3U') isExtended = true;else if (options.strict && _line.slice(1, 4) === 'EXT') return reject(new Error('Extended format playlist should start with #EXTM3U tag'));
}
if (!isExtended) return resolve(data.filter(function (line) {
return line[0] !== '#';
}).map(function (file) {
return { file: file, title: null, duration: null };
}));
var buffer = [];
var line = void 0,
continuousAttributes = {};
while (line = data.shift()) {
line = line.trim();
if (buffer.length === 0) buffer.push({ file: null, title: null, duration: null });
var _item = buffer[buffer.length - 1];
if (line[0] === '#' && line.slice(1, 4) === 'EXT') {
// process only tags, ignore comments
var colonPos = line.indexOf(':', 4);
if (colonPos === -1) return reject(new Error('#EXT tag used but no data provided'));
var tagName = line.slice(4, colonPos),
value = line.slice(colonPos + 1).trim();
switch (tagName) {
/*
#EXT-X-VERSION:<n>
<n> is an integer indicating the protocol compatibility version number
A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.
*/
case '-X-VERSION':
if (buffer.hasOwnProperty('version')) return reject(new Error('EXT-X-VERSION tag must appear only once in the playlist'));
if (!isFinite(value)) return reject(new Error('Invalid format of #EXT-X-VERSION - unable to parse'));
buffer.version = +value;
break;
/*
#EXTINF:<duration>,[<title>]
<duration> is a decimal-floating-point or decimal-integer number
<title> is an optional human-readable informative title of the Media Segment expressed as raw
UTF-8 text
*/
case 'INF':
{
var commaPos = value.lastIndexOf(',');
if (commaPos === -1) return reject(new Error('Invalid format of #EXTINF - unable to parse title'));
_item.title = value.slice(commaPos + 1).trim();
var match = /^(-?\d+)/.exec(value);
if (!match) return reject(new Error('Invalid format of #EXTINF - unable to parse duration'));
_item.duration = +match[1];
var EXTINF_ATTR_REGEX = / ([A-z0-9_-]+)="(.+?)"/g,
details = value.slice(match[1].length, commaPos);
while ((match = EXTINF_ATTR_REGEX.exec(details)) !== null) {
if (!_item.hasOwnProperty('attributes')) _item.attributes = {};
_item.attributes[match[1]] = match[2];
}
break;
}
/*
#EXT-X-BYTERANGE:<n>[@<o>]
<n> is a decimal-integer indicating the length of the sub-range in bytes
<o> is a decimal-integer indicating the start of the sub-range, as a byte offset from the
beginning of the resource.
Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version number of 4 or greater.
*/
case '-X-BYTERANGE':
{
var byteRange = parseByteRange(value);
if (!byteRange) return reject(new Error('Invalid format of #EXT-X-BYTERANGE - unable to parse'));
_item.byteRange = byteRange;
break;
}
/*
#EXT-X-DISCONTINUITY
Indicates a discontinuity between the Media Segment that follows it and the one that
preceded it.
*/
case '-X-DISCONTINUITY':
continuousAttributes = {};
break;
/*
#EXT-X-KEY:<attribute-list>
It applies to every Media Segment that appears between it and the next EXT-X-KEY tag in the
Playlist file with the same KEYFORMAT attribute (or the end of the Playlist file). Two or
more EXT-X-KEY tags with different KEYFORMAT attributes MAY apply to the same Media Segment
if they ultimately produce the same decryption key.
The following attributes are defined:
- METHOD is an enumerated-string that specifies the encryption method (REQUIRED)
The methods defined are: NONE, AES-128, and SAMPLE-AES.
An encryption method of NONE means that Media Segments are not encrypted. If the
encryption method is NONE, other attributes MUST NOT be present.
- URI is a quoted-string
- IV is a hexadecimal-sequence that specifies a 128-bit unsigned integer Initialization
Vector to be used with the key
- KEYFORMAT is a quoted-string
- KEYFORMATVERSIONS is a quoted-string containing one or more positive integers separated by
the "/" character
*/
case '-X-KEY':
{
continuousAttributes.key = parseAttributesList(value);
break;
}
/*
#EXT-X-MAP:<attribute-list>
It applies to every Media Segment that appears after it in the Playlist until the next
EXT-X-MAP tag or until the end of the playlist.
The following attributes are defined:
- URI is a quoted-string (REQUIRED)
- BYTERANGE is a quoted-string
*/
case '-X-MAP':
{
continuousAttributes.map = parseAttributesList(value);
if (continuousAttributes.map.hasOwnProperty('byterange')) {
var _byteRange = parseByteRange(continuousAttributes.map['byterange']);
if (!_byteRange) return reject(new Error('Invalid byte range in #EXT-X-MAP BYTERANGE attribute'));
continuousAttributes.map['byterange'] = _byteRange;
}
break;
}
/*
#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
It applies only to the next Media Segment
The date/time representation is ISO/IEC 8601:2004
*/
case '-X-PROGRAM-DATE-TIME':
_item.programDateTime = Date.parse(value);
break;
/*
#EXT-X-DATERANGE:<attribute-list>
- ID (REQUIRED) is a quoted-string
- CLASS is a quoted-string
- START-DATE is a quoted-string containing the ISO-8601 date
- END-DATE is a quoted-string containing the ISO-8601 date
- DURATION is a decimal-floating-point number of seconds. It MUST NOT be negative.
- PLANNED-DURATION is a decimal-floating-point number of seconds. It MUST NOT be negative.
- X-<client-attribute> value MUST be a quoted-string, a hexadecimal-sequence, or a
decimal-floating-point
- SCTE35-CMD, SCTE35-OUT, SCTE35-IN
- END-ON-NEXT is an enumerated-string whose value MUST be YES
*/
case '-X-DATERANGE':
_item.dateRange = parseAttributesList(value);
break;
default:
_item[line.slice(0, colonPos)] = value; // todo get value
break;
}
} else if (_item.file === null && _item.title !== null) {
Object.assign(_item, continuousAttributes);
_item.file = line;
buffer.push({ file: null, title: null, duration: null });
} else return reject(new Error('Invalid data'));
}
var item = buffer[buffer.length - 1];
if (item.title === null && item.duration === null && item.file === null) buffer.pop();
item = buffer[buffer.length - 1];
if (item.title === null || item.duration === null || item.file === null) return reject(new Error('Invalid playlist'));
return resolve(buffer);
}
return { parse: parse };
});

26

package.json
{
"name": "m3u-parser",
"version": "1.0.3",
"version": "2.0.0-alpha.1",
"description": "Simple library for hassle-free M3U playlists parsing",
"main": "m3u.js",
"scripts": {
"prepublish": "npm run lint && npm run build",
"build": "babel src/m3u.js --out-file m3u.js",
"test": "mocha -R list",
"ci-test": "istanbul cover _mocha -- -R tap > result.tap && istanbul report clover"
"lint": "eslint m3u.js",
"ci-test": "istanbul cover _mocha -- -R tap --compilers js:babel-register,js:babel-polyfill > result.tap && istanbul report clover"
},

@@ -20,11 +23,16 @@ "keywords": [

"license": "ISC",
"dependencies": {
"bluebird": "^3.1.1"
},
"devDependencies": {
"chai": "^3.4.1",
"chai-as-promised": "^5.0.0",
"istanbul": "^0.4.1",
"mocha": "^2.2.5"
"async": "2.0.0-rc.3",
"babel-cli": "^6.6.5",
"babel-plugin-transform-es2015-arrow-functions": "^6.5.2",
"babel-plugin-transform-es2015-block-scoping": "^6.7.1",
"babel-plugin-transform-es2015-shorthand-properties": "^6.5.0",
"babel-polyfill": "^6.7.4",
"babel-register": "^6.7.2",
"chai": "3.5.0",
"chai-as-promised": "5.3.0",
"eslint": "2.7.0",
"istanbul": "^1.0.0-alpha.2",
"mocha": "2.4.5"
}
}
# M3U Parser
[![Build Status](https://travis-ci.org/v12/m3u-parser.svg)](https://travis-ci.org/v12/m3u-parser) [![Test Coverage](https://codeclimate.com/github/v12/m3u-parser/badges/coverage.svg)](https://codeclimate.com/github/v12/m3u-parser) [![Dependency Status](https://david-dm.org/v12/m3u-parser.svg)](https://david-dm.org/v12/m3u-parser)
[![Build Status](https://travis-ci.org/v12/m3u-parser.svg?branch=master)](https://travis-ci.org/v12/m3u-parser) [![Test Coverage](https://codeclimate.com/github/v12/m3u-parser/badges/coverage.svg)](https://codeclimate.com/github/v12/m3u-parser) [![Dependency Status](https://david-dm.org/v12/m3u-parser.svg)](https://david-dm.org/v12/m3u-parser)
## Installation
```npm install m3u-parser --save```
```npm install m3u-parser --save```
'use strict';
var fs = require('fs');
var Promise = require('bluebird');
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
var fs = require('fs');
var path = require('path');
var async = require('async');
var chai = require('chai');
chai.use(chaiAsPromised);
chai.should();
chai.use(require('chai-as-promised'));
var m3u = require('../m3u');
const expect = chai.expect;
var files = ['extended', 'invalid_extended']
.map(function (filename) { return fs.readFileSync(__dirname + '/playlists/' + filename + '.m3u'); });
const parse = require('../src/m3u').parse;
describe('M3U playlist parser', function () {
before(function (done) {
const playlistDir = path.resolve(__dirname, 'playlists');
fs.readdir(playlistDir, (err, files) => {
if (err)
return done(err);
async.map(files,
(file, cb) => fs.readFile(path.resolve(playlistDir, file), cb),
(err, filesContent) => {
if (err)
return done(err);
this.files = {};
files.forEach((filename, i) =>
this.files[filename.replace(/\.m3u(8)?$/, (s, e) => e ? '.ext' : '')] = filesContent[i]);
done();
});
});
});
it('should throw when no arguments were specified', function () {
expect(parse).to.throw(Error);
});
it('should be rejected when invalid data passed', function () {
return Promise.all([
m3u.parse().should.eventually.be.rejected,
m3u.parse(123).should.eventually.be.rejected,
m3u.parse('invalid').should.eventually.be.rejected,
m3u.parse(files[1]).should.eventually.be.rejected,
m3u.parse('#EXTM3U\n\ninvalid').should.eventually.be.rejected
]);
const invalidPlaylists = [
undefined,
123,
this.files['invalid.ext'],
this.files['invalid-extinf.ext'],
this.files['invalid-noextinf.ext'],
this.files['invalid-duration.ext'],
this.files['invalid-no-items.ext'],
'#EXTM3U\n\ninvalid',
'#EXTENDED_M3U'
];
return Promise.all(invalidPlaylists.map(data => expect(parse(data, { strict: true })).to.be.eventually.rejected));
});
it('should return empty array when empty playlist is provided', function () {
return expect(parse('')).to.eventually.have.length(0);
});
it('should call callback function when done', function (done) {
parse(this.files['simple.ext'], done);
});
it('should call callback function when error happens', function (done) {
parse(this.files['invalid.ext'], err => {
done(err instanceof Error ? null : new Error('Callback didn\'t fire with error'));
});
});
describe('simple format', function () {
it('should be parsed properly', function () {
return parse(this.files['simple']).then(function (data) {
expect(data).to.have.length(7);
expect(data[4]).to.deep.equal({
file: '..\\Other Music\\Bar.mp3',
duration: null,
title: null
});
});
});
describe('with comments', function () {
it('should be parsed properly', function () {
return parse(this.files['simple-with-comment']).then(function (data) {
expect(data).to.have.length(7);
expect(data[4]).to.deep.equal({
file: '..\\Other Music\\Bar.mp3',
duration: null,
title: null
});
});
});
});
});
describe('extended format', function () {
it('should return array with playlist items', function () {
return m3u.parse(files[0])
.then(function (data) {
return Promise.all([
data.should.be.an.instanceOf(Array),
data.should.have.length(5),
data[0].should.be.an('object'),
data[0].should.have.all.keys('file', 'title', 'duration'),
data[0].duration.should.be.equal(123),
data[0].title.should.be.equal('Sample artist - Sample title'),
data[0].file.should.be.equal('Sample.mp3')
]);
return parse(this.files['simple.ext']).then(function (data) {
expect(data).to.be.an.instanceOf(Array);
expect(data[0]).to.be.deep.equal({
duration: 123,
title: 'Sample artist - Sample title',
file: 'Sample.mp3'
});
});
});
it('should parse EXTINF attributes', function () {
return parse(this.files['simple.ext']).then(function (data) {
expect(data[5]).to.be.deep.equal({
duration: -1,
title: 'Some Interesting Stream',
file: 'http://example.org/livestream.mp4',
attributes: {
'tvg-id': 'test_id_1',
'tvg-name': 'Some Stream',
'channel-id': '1'
}
});
});
});
it('should parse negative duration properly', function () {
return m3u.parse(files[ 0 ]).should.be.eventually.fulfilled
.and.have.deep.property('[4].duration', -1);
return expect(parse(this.files['simple.ext'])).to.eventually.have.deep.property('[4].duration', -1);
});
it('should handle unknown tags', function () {
return expect(parse(this.files['unknown-tag.ext'])).to.eventually.deep.equal([{
duration: 123,
title: 'Sample artist - Sample title',
file: 'Sample.mp3',
'#EXTGRP': 'Test group'
}
]);
});
describe('tag #EXT-X-BYTERANGE', function () {
it('should be parsed for each playlist item', function () {
return expect(parse(this.files['x-byterange.ext'])).to.be.eventually.deep.equal([{
duration: 0,
title: 'Some Stream',
file: 'http://example.com/stream.mp4',
byteRange: { length: 100, offset: 222 }
}, {
duration: 1233,
title: 'Another stream',
file: 'http://example.com/stream.webm',
byteRange: { length: 33 }
}]);
});
it('should have valid format', function () {
return expect(parse('#EXTM3U\n#EXTINF:0,Some' +
' Stream\n#EXT-X-BYTERANGE:wow@222\nhttp://example.com/stream.mp4')).to.be.eventually
.rejectedWith(Error, 'Invalid format of #EXT-X-BYTERANGE');
});
});
describe('tag #EXT-X-VERSION', function () {
it('should be parsed and exposed as an own property of the playlist array', function () {
return expect(parse(this.files['live.ext'])).to.eventually.have.ownProperty('version')
.and.property('version').equals(3);
});
it('should appear only once in a playlist EXT-X-VERSION', function () {
return expect(parse('#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-VERSION:4')).to.be.eventually
.rejectedWith(Error, 'EXT-X-VERSION tag must appear only once in the playlist');
});
it('should have valid integer value', function () {
return expect(parse('#EXTM3U\n#EXT-X-VERSION:shit')).to.be.eventually
.rejectedWith(Error, 'Invalid format of #EXT-X-VERSION');
});
});
describe('tag #EXT-X-KEY', function () {
it('should be parsed and applied to every Media Segment that appears between it and the next EXT-X-KEY' +
' tag in the Playlist file with the same KEYFORMAT attribute', function () {
return parse(this.files['encrypted-segments.ext']).then(data => {
expect(data[0]).to.have.property('key');
expect(data[3]).to.have.property('key');
expect(data[0].key).to.deep.equal({
method: 'AES-128',
uri: 'https://priv.example.com/key.php?r=52'
});
expect(data[3].key).to.deep.equal({
method: 'AES-128',
uri: 'https://priv.example.com/key.php?r=53'
});
});
});
it('should be parsed and applied to every Media Segment the end of the Playlist file');
});
describe('tag #EXT-X-MAP', function () {
it('should be parsed and applied to every Media Segment until the next EXT-X-MAP tag');
it('should be parsed and applied to every Media Segment until the end of the Playlist file', function () {
return parse(this.files['x-map.ext']).then(data => {
data.forEach(item => {
expect(item).to.have.property('map');
expect(item.map).to.deep.equal({
uri: 'main.mp4',
byterange: { length: 560, offset: 0 }
});
});
});
});
});
describe('tag #EXT-X-DISCONTINUITY', function () {
it('should reset continuous attributes');
});
describe('tag #EXT-X-PROGRAM-DATE-TIME', function () {
it('should be parsed and applied to media segment');
});
describe('tag #EXT-X-DATERANGE', function () {
it('should be parsed and applied to media segment');
});
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc