Parse Duration
This commit is contained in:
commit
610c13fcef
|
@ -1,13 +1,16 @@
|
|||
on: push
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
runNpmStuff:
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['16', '14', '12']
|
||||
name: Run NPM Stuff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: ${{matrix.node}}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
|
|
|
@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Tracklists using duration instead of timestamps are now supported.
|
||||
|
||||
## 1.0.5 - 2021-07-21
|
||||
### Changed
|
||||
|
|
19
README.md
19
README.md
|
@ -8,7 +8,7 @@ Generate CUE sheet from timestamps in youtube video description.
|
|||
2. The video has timestamps in the video description.
|
||||
3. The video is publicly available on Youtube.
|
||||
|
||||
`youtube-cue` will read the video description, get the timestamps and generate a [CUE sheet][cue] accordingly.
|
||||
`youtube-cue` will read the video description, get the timestamps and generate a [CUE sheet][cue] accordingly. It will also work if track durations are used instead of timestamps.
|
||||
|
||||
## Anti-features
|
||||
|
||||
|
@ -37,10 +37,19 @@ You need to pass 2 parameters, a Youtube URL and a output CUE filename. YouTube
|
|||
--help, Show help
|
||||
--audio-file, Input Audio File (optional) that is written to the CUE sheet
|
||||
|
||||
The default audio file is set to %VIDEOTITLE.m4a
|
||||
The default output file is set to %VIDEOTITLE.cue
|
||||
The default audio file is set to %VIDEOTITLE.m4a
|
||||
The default output file is set to %VIDEOTITLE.cue
|
||||
|
||||
where $VIDEOTITLE is the title of the YouTube video.
|
||||
where $VIDEOTITLE is the title of the YouTube video.
|
||||
|
||||
Generally the parser detects whether numbers are positional timestamps or track durations.
|
||||
To enforce a desired interpretation you can use these flags:
|
||||
|
||||
--timestamps Parse as positional timestamps (relative to the start of the playlist)
|
||||
--durations Parse as track durations
|
||||
|
||||
The above 2 are only needed to force behaviour in
|
||||
very specific edge cases, they should not be required for most files.
|
||||
|
||||
Examples
|
||||
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=THzUassmQwE"
|
||||
|
@ -66,7 +75,7 @@ function ytdl.album() {
|
|||
|
||||
## HACKING
|
||||
|
||||
- If this video does not work on a specific video, please attach the complete output
|
||||
- If it does not work on a specific video, please attach the complete output
|
||||
- Pull Requests are welcome that add support for a better parser without breaking the existing tests
|
||||
- Please add tests for any new functionality
|
||||
|
||||
|
|
21
index.js
21
index.js
|
@ -4,6 +4,7 @@ import getArtistTitle from 'get-artist-title'
|
|||
import {parse} from './src/parser.js'
|
||||
import {generate} from './src/cue.js'
|
||||
import minimist from 'minimist'
|
||||
import exit from 'process'
|
||||
|
||||
let argv = minimist(process.argv.slice(2), {
|
||||
string: 'audio-file'
|
||||
|
@ -22,6 +23,15 @@ if (argv._.length <1 || argv.help ){
|
|||
|
||||
where $VIDEOTITLE is the title of the YouTube video.
|
||||
|
||||
Generally the parser detects whether numbers are positional timestamps or track durations.
|
||||
To enforce a desired interpretation you can use these flags:
|
||||
|
||||
--timestamps Parse as positional timestamps (relative to the start of the playlist)
|
||||
--durations Parse as track durations
|
||||
|
||||
The above 2 are only needed to force behaviour in very specific edge cases, they should
|
||||
not be required for most files.
|
||||
|
||||
Examples
|
||||
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=THzUassmQwE"
|
||||
"T A Y L O R S W I F T – Folklore [Full album].cue" saved
|
||||
|
@ -35,13 +45,22 @@ if (argv._.length <1 || argv.help ){
|
|||
|
||||
let output_file = argv._[1]? argv._[1] : `${info.videoDetails.title}.cue`
|
||||
|
||||
let forceTimestamps = argv['timestamps']? argv['timestamps'] : false;
|
||||
|
||||
let forceDurations = argv['durations']? argv['durations'] : false;
|
||||
|
||||
if (forceTimestamps && forceDurations) {
|
||||
console.error("You can't pass both --timestamps and durations");
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let res = getArtistTitle(info.videoDetails.title,{
|
||||
defaultArtist: "Unknown Artist",
|
||||
defaultTitle: info.videoDetails.title
|
||||
});
|
||||
let [artist, album] = res
|
||||
artist = (info.videoDetails.media ? info.videoDetails.media.artist : artist)
|
||||
let tracks = parse(info.videoDetails.description, {artist})
|
||||
let tracks = parse(info.videoDetails.description, {artist, forceTimestamps, forceDurations})
|
||||
generate({tracks, artist, audioFile, album}, output_file)
|
||||
console.log(`"${output_file}" saved`)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "youtube-cue",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6-beta.0",
|
||||
"description": "Generates Cue sheet from Youtube URL",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
172
src/parser.js
172
src/parser.js
|
@ -21,93 +21,171 @@
|
|||
* It is suggested to check their lengths and pick one to parse as the Track Title
|
||||
*/
|
||||
const TS_REGEX = /^((?<trackl>\d{1,3})\.)? *(?<text_1>.*?) *[\(\[]?(?<start_ts>((?<start_hh>\d{1,2}):)?(?<start_mm>\d{1,2}):(?<start_ss>\d{1,2})) *-? *[\)\]]?(?<end_ts>(?<end_hh>\d{1,2}:)?(?<end_mm>\d{1,2}):(?<end_ss>\d{1,2}))? *((?<trackr>\d{1,3})\.)? *(?<text_2>.*?)$/;
|
||||
import getArtistTitle from 'get-artist-title'
|
||||
import getArtistTitle from "get-artist-title";
|
||||
var _options = {};
|
||||
|
||||
function convertTime(h,m,s) {
|
||||
return (+h) * 60 * 60 + (+m) * 60 + (+s)
|
||||
// Returns number of total seconds
|
||||
function convertTime(h, m, s) {
|
||||
return +h * 60 * 60 + +m * 60 + +s;
|
||||
}
|
||||
|
||||
// Only picks out lines which have a timestamp in them
|
||||
var filterTimestamp = function(line) {
|
||||
return TS_REGEX.test(line)
|
||||
return TS_REGEX.test(line);
|
||||
};
|
||||
|
||||
// Parse each line as per the regex
|
||||
var firstPass = function(line) {
|
||||
let matches = line.match(TS_REGEX);
|
||||
let track = matches.groups['trackl'] ? +matches.groups['trackl'] : (matches.groups['trackr'] ? +matches.groups['trackr'] : null)
|
||||
let track = matches.groups["trackl"]
|
||||
? +matches.groups["trackl"]
|
||||
: matches.groups["trackr"]
|
||||
? +matches.groups["trackr"]
|
||||
: null;
|
||||
return {
|
||||
track: track,
|
||||
start: {
|
||||
ts: matches.groups['start_ts'].length<6 ? `00:${matches.groups['start_ts']}` : matches.groups['start_ts'],
|
||||
hh: matches.groups['start_hh'] ? +matches.groups['start_hh'] : 0,
|
||||
ts:
|
||||
matches.groups["start_ts"].length < 6
|
||||
? `00:${matches.groups["start_ts"]}`
|
||||
: matches.groups["start_ts"],
|
||||
hh: matches.groups["start_hh"] ? +matches.groups["start_hh"] : 0,
|
||||
// These 2 are always set
|
||||
mm: +matches.groups['start_mm'],
|
||||
ss: +matches.groups['start_ss'],
|
||||
mm: +matches.groups["start_mm"],
|
||||
ss: +matches.groups["start_ss"],
|
||||
},
|
||||
end: (matches.groups['end_ts']!==undefined ? {
|
||||
ts: matches.groups['end_ts']? matches.groups['end_ts'] : null,
|
||||
hh: matches.groups['end_hh']? +matches.groups['end_hh'] : null,
|
||||
mm: matches.groups['end_mm']? +matches.groups['end_mm'] : null,
|
||||
ss: matches.groups['end_ss']? +matches.groups['end_ss'] : null,
|
||||
} : null),
|
||||
end:
|
||||
matches.groups["end_ts"] !== undefined
|
||||
? {
|
||||
ts: matches.groups["end_ts"] ? matches.groups["end_ts"] : null,
|
||||
hh: matches.groups["end_hh"] ? +matches.groups["end_hh"] : null,
|
||||
mm: matches.groups["end_mm"] ? +matches.groups["end_mm"] : null,
|
||||
ss: matches.groups["end_ss"] ? +matches.groups["end_ss"] : null,
|
||||
}
|
||||
: null,
|
||||
_: {
|
||||
left_text: matches.groups['text_1'],
|
||||
right_text: matches.groups['text_2']
|
||||
}
|
||||
}
|
||||
left_text: matches.groups["text_1"],
|
||||
right_text: matches.groups["text_2"],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Add a calc attribute with total seconds
|
||||
var calcTimestamp = function(obj) {
|
||||
if(obj.end) {
|
||||
obj.end.calc = convertTime(obj.end.hh,obj.end.mm,obj.end.ss)
|
||||
if (obj.end) {
|
||||
obj.end.calc = convertTime(obj.end.hh, obj.end.mm, obj.end.ss);
|
||||
}
|
||||
obj.start.calc = convertTime(obj.start.hh,obj.start.mm,obj.start.ss)
|
||||
return obj
|
||||
}
|
||||
obj.start.calc = convertTime(obj.start.hh, obj.start.mm, obj.start.ss);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Pick the longer "text" from left or right side.
|
||||
var parseTitle = function(obj) {
|
||||
obj.title = obj._.left_text.length > obj._.right_text.length
|
||||
? obj._.left_text : obj._.right_text;
|
||||
return obj
|
||||
}
|
||||
obj.title =
|
||||
obj._.left_text.length > obj._.right_text.length
|
||||
? obj._.left_text
|
||||
: obj._.right_text;
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Parse the text as the title/artist
|
||||
var parseArtist = function(obj) {
|
||||
let [artist, title] = getArtistTitle(obj.title, {
|
||||
defaultArtist: _options.artist,
|
||||
defaultTitle: obj.title
|
||||
defaultTitle: obj.title,
|
||||
});
|
||||
obj.artist = artist
|
||||
obj.title = title
|
||||
return obj
|
||||
obj.artist = artist;
|
||||
obj.title = title;
|
||||
return obj;
|
||||
};
|
||||
|
||||
// If track numbers are not present, add them accordingly
|
||||
var addTrack = function(obj, index) {
|
||||
if (obj.track==null) {
|
||||
obj.track = index+1
|
||||
if (obj.track == null) {
|
||||
obj.track = index + 1;
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Add "end" timestamps as next start timestamps
|
||||
var addEnd = function(obj, index, arr) {
|
||||
if (!obj.end) {
|
||||
if(arr.length!=index+1) {
|
||||
let next = arr[index+1]
|
||||
obj.end = next.start
|
||||
return obj
|
||||
if (arr.length != index + 1) {
|
||||
let next = arr[index + 1];
|
||||
obj.end = next.start;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
export function parse (text, options = { artist: 'Unknown' }) {
|
||||
var timeToObject = function(obj) {
|
||||
let d = new Date(obj.calc * 1000).toISOString();
|
||||
obj.hh = +d.substr(11, 2);
|
||||
obj.mm = +d.substr(14, 2);
|
||||
obj.ss = +d.substr(17, 2);
|
||||
obj.ts = d.substr(11, 8);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Instead of timestamps, some tracklists use durations
|
||||
// If durations are provided, use them to re-calculate
|
||||
// the starting and ending timestamps
|
||||
var fixDurations = function(list) {
|
||||
for (let i in list) {
|
||||
if (i == 0) {
|
||||
// Set the first one to start of track.
|
||||
list[i].start.hh = list[i].start.mm = list[i].start.ss = 0;
|
||||
// And end at the right time
|
||||
list[i].end = { calc: list[i].start.calc };
|
||||
list[i].start.calc = 0;
|
||||
} else {
|
||||
// All the others tracks start at the end of the previous one
|
||||
// And end at start time + duration
|
||||
let previous = list[i - 1];
|
||||
list[i].end = { calc: previous.end.calc + list[i].start.calc };
|
||||
list[i].start.calc = previous.end.calc;
|
||||
}
|
||||
|
||||
list[i].start = timeToObject(list[i].start);
|
||||
list[i].end = timeToObject(list[i].end);
|
||||
}
|
||||
};
|
||||
|
||||
export function parse(
|
||||
text,
|
||||
options = { artist: "Unknown", forceTimestamps: false, forceDurations: false }
|
||||
) {
|
||||
_options = options;
|
||||
return text
|
||||
.split('\n')
|
||||
let durations = false;
|
||||
let result = text
|
||||
.split("\n")
|
||||
.filter(filterTimestamp)
|
||||
.map(firstPass)
|
||||
.map(calcTimestamp)
|
||||
.map(calcTimestamp);
|
||||
|
||||
if (!options.forceTimestamps) {
|
||||
// If our timestamps are not in increasing order
|
||||
// Assume that we've been given a duration list instead
|
||||
if (result[0].start.calc!=0) {
|
||||
result.forEach((current, index, list) => {
|
||||
if (index > 0) {
|
||||
let previous = list[index - 1];
|
||||
if (current.start.calc < previous.start.calc) {
|
||||
durations = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (durations || options.forceDurations == true) {
|
||||
fixDurations(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
.map(parseTitle)
|
||||
.map(parseArtist)
|
||||
.map(addTrack)
|
||||
.map(addEnd)
|
||||
.map(addEnd);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import { parse } from "../src/parser.js";
|
|||
|
||||
const TEXT = `
|
||||
00:40 The Coders - Hello World
|
||||
12:23 This is not the end
|
||||
1:00 This is not the end
|
||||
Something else in the middle
|
||||
1:23:11 Not the last song
|
||||
01. Screens 0:00 - 5:40
|
||||
1:23 Not the last song
|
||||
01. Screens 1:40 - 5:40
|
||||
02. Inharmonious Slog 5:40 - 10:11
|
||||
03. The Everyday Push 10:11 - 15:46
|
||||
04. Storm 15:46 - 19:07
|
||||
|
@ -65,15 +65,153 @@ describe("Parser", function() {
|
|||
|
||||
it("should parse timestamps with square brackets", function() {
|
||||
let result = parse(`[00:00:00] 1. Steve Kroeger x Skye Holland - Through The Dark
|
||||
[00:02:53] 2. Gabri Ponte x Jerome - Lonely `)
|
||||
[00:02:53] 2. Gabri Ponte x Jerome - Lonely `);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Steve Kroeger x Skye Holland",
|
||||
title: "Through The Dark",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:02:53", hh: 0, mm: 2, ss: 53, calc: 173 },
|
||||
_: { left_text: "", right_text: "Steve Kroeger x Skye Holland - Through The Dark" },
|
||||
})
|
||||
artist: "Steve Kroeger x Skye Holland",
|
||||
title: "Through The Dark",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:02:53", hh: 0, mm: 2, ss: 53, calc: 173 },
|
||||
_: {
|
||||
left_text: "",
|
||||
right_text: "Steve Kroeger x Skye Holland - Through The Dark",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse durations when given", function() {
|
||||
let result = parse(`1. Artist - Title 6:19
|
||||
2. Another Artist - Another Title 6:59
|
||||
3. Yet Another Artist - Yet another title 5:12`);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:06:19", hh: 0, mm: 6, ss: 19, calc: 379 },
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
});
|
||||
|
||||
assert.deepEqual(result[1], {
|
||||
artist: "Another Artist",
|
||||
title: "Another Title",
|
||||
track: 2,
|
||||
start: { ts: "00:06:19", hh: 0, mm: 6, ss: 19, calc: 379 },
|
||||
end: { ts: "00:13:18", hh: 0, mm: 13, ss: 18, calc: 798 },
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
});
|
||||
assert.deepEqual(result[2], {
|
||||
artist: "Yet Another Artist",
|
||||
title: "Yet another title",
|
||||
track: 3,
|
||||
start: { ts: "00:13:18", hh: 0, mm: 13, ss: 18, calc: 798 },
|
||||
end: { ts: "00:18:30", hh: 0, mm: 18, ss: 30, calc: 1110 },
|
||||
_: {
|
||||
left_text: "Yet Another Artist - Yet another title",
|
||||
right_text: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse as timestamps if first timestamp is 00:00", function() {
|
||||
let result = parse(`1. Artist - Title 00:00
|
||||
2. Another Artist - Another Title 01:00
|
||||
3. Yet Another Artist - Yet another title 02:00`);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:01:00", hh: 0, mm: 1, ss: 0, calc: 60 },
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
});
|
||||
|
||||
assert.deepEqual(result[1], {
|
||||
artist: "Another Artist",
|
||||
title: "Another Title",
|
||||
track: 2,
|
||||
end: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 },
|
||||
start: { ts: "00:01:00", hh: 0, mm: 1, ss: 0, calc: 60 },
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
});
|
||||
assert.deepEqual(result[2], {
|
||||
artist: "Yet Another Artist",
|
||||
title: "Yet another title",
|
||||
track: 3,
|
||||
end: null,
|
||||
start: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 },
|
||||
_: { left_text: "Yet Another Artist - Yet another title", right_text: "" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse durations as timestamps when forced", function() {
|
||||
let result = parse(
|
||||
`1. Artist - Title 5:00
|
||||
2. Another Artist - Another Title 4:20`,
|
||||
{ forceTimestamps: true }
|
||||
);
|
||||
assert.deepEqual(result[0].end, {
|
||||
ts: "00:4:20",
|
||||
hh: 0,
|
||||
mm: 4,
|
||||
ss: 20,
|
||||
calc: 260,
|
||||
});
|
||||
assert.deepEqual(result[1].start, {
|
||||
ts: "00:4:20",
|
||||
hh: 0,
|
||||
mm: 4,
|
||||
ss: 20,
|
||||
calc: 260,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse timestamps as durations when forced", function() {
|
||||
let result = parse(
|
||||
`1. Artist - Title 1:00
|
||||
2. Another Artist - Another Title 1:15`,
|
||||
{ forceDurations: true }
|
||||
);
|
||||
assert.deepEqual(result[0], {
|
||||
track: 1,
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
title: "Title",
|
||||
artist: "Artist",
|
||||
end: {
|
||||
ts: "00:01:00",
|
||||
hh: 0,
|
||||
mm: 1,
|
||||
ss: 0,
|
||||
calc: 60,
|
||||
},
|
||||
start: {
|
||||
ts: "00:00:00",
|
||||
hh: 0,
|
||||
mm: 0,
|
||||
ss: 0,
|
||||
calc: 0,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result[1], {
|
||||
track: 2,
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
title: "Another Title",
|
||||
artist: "Another Artist",
|
||||
start: {
|
||||
ts: "00:01:00",
|
||||
hh: 0,
|
||||
mm: 1,
|
||||
ss: 0,
|
||||
calc: 60,
|
||||
},
|
||||
end: {
|
||||
ts: "00:02:15",
|
||||
hh: 0,
|
||||
mm: 2,
|
||||
ss: 15,
|
||||
calc: 135,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse taylor swift", function() {
|
||||
|
|
Loading…
Reference in New Issue