Compare commits

..

No commits in common. "master" and "v1.0.6" have entirely different histories.

12 changed files with 1150 additions and 1240 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
github: captn3m0
ko_fi: captn3m0
liberapay: captn3m0

View File

@ -4,14 +4,13 @@ jobs:
tests: tests:
strategy: strategy:
matrix: matrix:
node: ["18", "20", "21"] node: ['16', '14', '12']
name: Run NPM Stuff name: Run NPM Stuff
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: ${{matrix.node}} node-version: ${{matrix.node}}
- run: npm install - run: npm install
- run: npm run lint - run: npm test
- run: npm run test

View File

@ -7,20 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
## 1.0.9
### Changed
- Dependency Updates
- Minimum NodeJS version is now v14
## 1.0.8
### Added
- Dependency Updates
## 1.0.7
### Added
- `--version` is now supported
- An update notification is shown if the package isn't latest.
## 1.0.6 - 2021-07-29 ## 1.0.6 - 2021-07-29
### Added ### Added

View File

@ -52,12 +52,14 @@ You need to pass 2 parameters, a Youtube URL and a output CUE filename. YouTube
very specific edge cases, they should not be required for most files. very specific edge cases, they should not be required for most files.
Examples Examples
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=WzpmVxvoBoc" "The Groovy Nobody - Solarium.cue" $ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=THzUassmQwE"
"The Groovy Nobody - Solarium.cue" saved "T A Y L O R S W I F T Folklore [Full album].cue" saved
$ youtube-cue "https://youtu.be/THzUassmQwE" folklore.cue
folklore.cue saved
## Personal Usage ## Personal Usage
I have this in my `.bashrc` to download, split, tag, and import albums using beet: I have this in my `.bashrc` to download, split, tag, and import albums:
```shell ```shell
function ytdl.album() { function ytdl.album() {

View File

@ -1,22 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
import ytdl from "ytdl-core"; import ytdl from 'ytdl-core';
import getArtistTitle from "get-artist-title"; import getArtistTitle from 'get-artist-title'
import { parse } from "./src/parser.js"; import {parse} from './src/parser.js'
import { generate } from "./src/cue.js"; import {generate} from './src/cue.js'
import minimist from "minimist"; import minimist from 'minimist'
import exit from "process"; import exit from 'process'
import updateNotifier from "update-notifier";
import pkg from "./src/package.js";
updateNotifier({ pkg }).notify();
let argv = minimist(process.argv.slice(2), { let argv = minimist(process.argv.slice(2), {
string: "audio-file", string: 'audio-file'
}); });
if (argv.version) { if (argv._.length <1 || argv.help ){
console.log(pkg.version);
} else if (argv._.length < 1 || argv.help) {
console.log(`Usage console.log(`Usage
$ youtube-cue [--audio-file audio.m4a] <youtube_url> [output_file] $ youtube-cue [--audio-file audio.m4a] <youtube_url> [output_file]
@ -38,45 +32,36 @@ if (argv.version) {
The above 2 are only needed to force behaviour in very specific edge cases, they should The above 2 are only needed to force behaviour in very specific edge cases, they should
not be required for most files. not be required for most files.
--version Print version
Examples Examples
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=THzUassmQwE" $ 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 "T A Y L O R S W I F T Folklore [Full album].cue" saved
$ youtube-cue "https://youtu.be/THzUassmQwE" folklore.cue $ youtube-cue "https://youtu.be/THzUassmQwE" folklore.cue
folklore.cue saved`); folklore.cue saved`)
} else { } else {
let url = argv._[0]; let url = argv._[0]
ytdl.getInfo(url).then((info) => { ytdl.getInfo(url).then(info=>{
let audioFile = argv["audio-file"] let audioFile = argv['audio-file']? argv['audio-file'] : `${info.videoDetails.title}.m4a`
? argv["audio-file"]
: `${info.videoDetails.title}.m4a`;
let output_file = argv._[1] ? argv._[1] : `${info.videoDetails.title}.cue`; let output_file = argv._[1]? argv._[1] : `${info.videoDetails.title}.cue`
let forceTimestamps = argv["timestamps"] ? argv["timestamps"] : false; let forceTimestamps = argv['timestamps']? argv['timestamps'] : false;
let forceDurations = argv["durations"] ? argv["durations"] : false; let forceDurations = argv['durations']? argv['durations'] : false;
if (forceTimestamps && forceDurations) { if (forceTimestamps && forceDurations) {
console.error("You can't pass both --timestamps and durations"); console.error("You can't pass both --timestamps and durations");
exit(1); exit(1)
} }
let res = getArtistTitle(info.videoDetails.title, { let res = getArtistTitle(info.videoDetails.title,{
defaultArtist: "Unknown Artist", defaultArtist: "Unknown Artist",
defaultTitle: info.videoDetails.title, defaultTitle: info.videoDetails.title
}); });
let [artist, album] = res; let [artist, album] = res
artist = info.videoDetails.media ? info.videoDetails.media.artist : artist; artist = (info.videoDetails.media ? info.videoDetails.media.artist : artist)
let tracks = parse(info.videoDetails.description, { let tracks = parse(info.videoDetails.description, {artist, forceTimestamps, forceDurations})
artist, generate({tracks, artist, audioFile, album}, output_file)
forceTimestamps, console.log(`"${output_file}" saved`)
forceDurations, })
length: Number(info.videoDetails.lengthSeconds),
});
generate({ tracks, artist, audioFile, album }, output_file);
console.log(`"${output_file}" saved`);
});
} }

2127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,22 @@
{ {
"name": "youtube-cue", "name": "youtube-cue",
"version": "1.0.10", "version": "1.0.6",
"description": "Generates Cue sheet from Youtube URL", "description": "Generates Cue sheet from Youtube URL",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "mocha", "test": "mocha"
"lint": "prettier --check *.js src/*.js test/*.js"
}, },
"bin": "index.js", "bin": "index.js",
"author": "Nemo <npm@captnemo.in>", "author": "Nemo <npm@captnemo.in>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"mocha": "^10.0.0", "mocha": "^9.0.0"
"prettier": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"console-log-level": "^1.4.1", "console-log-level": "^1.4.1",
"get-artist-title": "^1.3.1", "get-artist-title": "^1.3.1",
"minimist": "^1.2.8", "minimist": "^1.2.5",
"update-notifier": "^7.0.0", "ytdl-core": "^4.8.2"
"ytdl-core": "^4.11.5"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,27 +1,21 @@
import fs from "fs"; import fs from 'fs';
/** code to create a new CUE file, as per the standard
* with a REM PERFORMER, TITLE, FILE attribute
* and a list of tracks provided as input
*/
// https://en.wikipedia.org/wiki/Cue_sheet_(computing) // https://en.wikipedia.org/wiki/Cue_sheet_(computing)
export function generate(data, outputFile) { export function generate(data, outputFile) {
try { try {
fs.truncateSync(outputFile); fs.truncateSync(outputFile)
} catch {} } catch {}
fs.appendFileSync(outputFile, `REM Generated using youtube-cue\n`); fs.appendFileSync(outputFile, `REM Generated using youtube-cue\n`);
fs.appendFileSync(outputFile, `PERFORMER "${data.artist}"\n`); fs.appendFileSync(outputFile, `PERFORMER "${data.artist}"\n`);
fs.appendFileSync(outputFile, `TITLE "${data.album}"\n`); fs.appendFileSync(outputFile, `TITLE "${data.album}"\n`);
fs.appendFileSync(outputFile, `FILE "${data.audioFile}" M4A\n`); fs.appendFileSync(outputFile, `FILE "${data.audioFile}" M4A\n`);
for (var i in data.tracks) { for(var i in data.tracks) {
let song = data.tracks[i]; let song = data.tracks[i];
let minutes = String(song.start.hh * 60 + song.start.mm).padStart(2, "0"); let minutes = (song.start.hh * 60) + (song.start.mm)
let seconds = String(song.start.ss).padStart(2, "0");
fs.appendFileSync(outputFile, ` TRACK ${song.track} AUDIO\n`); fs.appendFileSync(outputFile, ` TRACK ${song.track} AUDIO\n`);
fs.appendFileSync(outputFile, ` TITLE "${song.title}"\n`); fs.appendFileSync(outputFile, ` TITLE "${song.title}"\n`);
fs.appendFileSync(outputFile, ` PERFORMER "${song.artist}"\n`); fs.appendFileSync(outputFile, ` PERFORMER "${song.artist}"\n`);
// Cue File is always MINUTES:SECONDS:FRAME, where FRAME is 00 // Cue File is always MINUTES:SECONDS:FRAME, where FRAME is 00
fs.appendFileSync(outputFile, ` INDEX 01 ${minutes}:${seconds}:00\n`); fs.appendFileSync(outputFile, ` INDEX 01 ${minutes}:${song.start.ss}:00\n`);
} }
} }

View File

@ -1,5 +0,0 @@
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("../package.json");
export default data;

View File

@ -20,8 +20,7 @@
* *
* It is suggested to check their lengths and pick one to parse as the Track Title * It is suggested to check their lengths and pick one to parse as the Track Title
*/ */
const TS_REGEX = 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>.*?)$/;
/^((?<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 = {}; var _options = {};
@ -31,18 +30,18 @@ function convertTime(h, m, s) {
} }
// Only picks out lines which have a timestamp in them // Only picks out lines which have a timestamp in them
var filterTimestamp = function (line) { var filterTimestamp = function(line) {
return TS_REGEX.test(line); return TS_REGEX.test(line);
}; };
// Parse each line as per the regex // Parse each line as per the regex
var firstPass = function (line) { var firstPass = function(line) {
let matches = line.match(TS_REGEX); let matches = line.match(TS_REGEX);
let track = matches.groups["trackl"] let track = matches.groups["trackl"]
? +matches.groups["trackl"] ? +matches.groups["trackl"]
: matches.groups["trackr"] : matches.groups["trackr"]
? +matches.groups["trackr"] ? +matches.groups["trackr"]
: null; : null;
return { return {
track: track, track: track,
start: { start: {
@ -72,7 +71,7 @@ var firstPass = function (line) {
}; };
// Add a calc attribute with total seconds // Add a calc attribute with total seconds
var calcTimestamp = function (obj) { var calcTimestamp = function(obj) {
if (obj.end) { if (obj.end) {
obj.end.calc = convertTime(obj.end.hh, obj.end.mm, obj.end.ss); obj.end.calc = convertTime(obj.end.hh, obj.end.mm, obj.end.ss);
} }
@ -81,7 +80,7 @@ var calcTimestamp = function (obj) {
}; };
// Pick the longer "text" from left or right side. // Pick the longer "text" from left or right side.
var parseTitle = function (obj) { var parseTitle = function(obj) {
obj.title = obj.title =
obj._.left_text.length > obj._.right_text.length obj._.left_text.length > obj._.right_text.length
? obj._.left_text ? obj._.left_text
@ -90,7 +89,7 @@ var parseTitle = function (obj) {
}; };
// Parse the text as the title/artist // Parse the text as the title/artist
var parseArtist = function (obj) { var parseArtist = function(obj) {
let [artist, title] = getArtistTitle(obj.title, { let [artist, title] = getArtistTitle(obj.title, {
defaultArtist: _options.artist, defaultArtist: _options.artist,
defaultTitle: obj.title, defaultTitle: obj.title,
@ -101,7 +100,7 @@ var parseArtist = function (obj) {
}; };
// If track numbers are not present, add them accordingly // If track numbers are not present, add them accordingly
var addTrack = function (obj, index) { var addTrack = function(obj, index) {
if (obj.track == null) { if (obj.track == null) {
obj.track = index + 1; obj.track = index + 1;
} }
@ -109,7 +108,7 @@ var addTrack = function (obj, index) {
}; };
// Add "end" timestamps as next start timestamps // Add "end" timestamps as next start timestamps
var addEnd = function (obj, index, arr) { var addEnd = function(obj, index, arr) {
if (!obj.end) { if (!obj.end) {
if (arr.length != index + 1) { if (arr.length != index + 1) {
let next = arr[index + 1]; let next = arr[index + 1];
@ -120,7 +119,7 @@ var addEnd = function (obj, index, arr) {
return obj; return obj;
}; };
var timeToObject = function (obj) { var timeToObject = function(obj) {
let d = new Date(obj.calc * 1000).toISOString(); let d = new Date(obj.calc * 1000).toISOString();
obj.hh = +d.substr(11, 2); obj.hh = +d.substr(11, 2);
obj.mm = +d.substr(14, 2); obj.mm = +d.substr(14, 2);
@ -132,7 +131,7 @@ var timeToObject = function (obj) {
// Instead of timestamps, some tracklists use durations // Instead of timestamps, some tracklists use durations
// If durations are provided, use them to re-calculate // If durations are provided, use them to re-calculate
// the starting and ending timestamps // the starting and ending timestamps
var fixDurations = function (list) { var fixDurations = function(list) {
for (let i in list) { for (let i in list) {
if (i == 0) { if (i == 0) {
// Set the first one to start of track. // Set the first one to start of track.
@ -153,19 +152,9 @@ var fixDurations = function (list) {
} }
}; };
var dropInvalid = function (e) {
// All tracks should start before the closing time
if (_options.length) return e.start.calc < _options.length;
return true;
};
export function parse( export function parse(
text, text,
options = { options = { artist: "Unknown", forceTimestamps: false, forceDurations: false }
artist: "Unknown",
forceTimestamps: false,
forceDurations: false,
},
) { ) {
_options = options; _options = options;
let durations = false; let durations = false;
@ -178,7 +167,7 @@ export function parse(
if (!options.forceTimestamps) { if (!options.forceTimestamps) {
// If our timestamps are not in increasing order // If our timestamps are not in increasing order
// Assume that we've been given a duration list instead // Assume that we've been given a duration list instead
if (result[0].start.calc != 0) { if (result[0].start.calc!=0) {
result.forEach((current, index, list) => { result.forEach((current, index, list) => {
if (index > 0) { if (index > 0) {
let previous = list[index - 1]; let previous = list[index - 1];
@ -198,6 +187,5 @@ export function parse(
.map(parseTitle) .map(parseTitle)
.map(parseArtist) .map(parseArtist)
.map(addTrack) .map(addTrack)
.map(addEnd) .map(addEnd);
.filter(dropInvalid);
} }

View File

@ -1,48 +0,0 @@
/*jshint esversion: 6 */
import { strict as assert } from "assert";
import { generate } from "../src/cue.js";
import fs from "fs";
const DATA = {
artist: "Dumbledore",
album: "Curse of the Elder Wand",
audioFile: "audio.m4a",
tracks: [
{
artist: "Unknown",
title: "the 1",
track: 1,
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
end: { ts: "00:3:9", hh: 0, mm: 3, ss: 9, calc: 189 },
_: { left_text: "", right_text: "the 1" },
},
{
artist: "Unknown",
title: "cardigan",
track: 2,
start: { ts: "00:3:09", hh: 0, mm: 3, ss: 9, calc: 189 },
end: { ts: "00:9:30", hh: 0, mm: 9, ss: 30, calc: 570 },
_: { left_text: "", right_text: "cardigan" },
},
],
};
describe("CUE", function () {
it("should generate with leading zeroes", function () {
generate(DATA, "/tmp/test.cue");
const CUE_EXPECTED = `REM Generated using youtube-cue
PERFORMER "Dumbledore"
TITLE "Curse of the Elder Wand"
FILE "audio.m4a" M4A
TRACK 1 AUDIO
TITLE "the 1"
PERFORMER "Unknown"
INDEX 01 00:00:00
TRACK 2 AUDIO
TITLE "cardigan"
PERFORMER "Unknown"
INDEX 01 03:09:00
`;
assert.equal(CUE_EXPECTED, fs.readFileSync("/tmp/test.cue", "utf-8"));
});
});

View File

@ -24,21 +24,21 @@ Hello World 48:20
const TEXT_WITH_ARTIST = "12:23 Rolling Stones - Hello World"; const TEXT_WITH_ARTIST = "12:23 Rolling Stones - Hello World";
describe("Parser", function () { describe("Parser", function() {
var big_result; var big_result;
before(function () { before(function() {
big_result = parse(TEXT); big_result = parse(TEXT);
}); });
it("should find all timestamps", function () { it("should find all timestamps", function() {
assert.equal(big_result.length, 15); assert.equal(big_result.length, 15);
}); });
it("should find artist names", function () { it("should find artist names", function() {
let result = parse(TEXT_WITH_ARTIST); let result = parse(TEXT_WITH_ARTIST);
assert.equal(result[0].artist, "Rolling Stones"); assert.equal(result[0].artist, "Rolling Stones");
}); });
it("should find track numbers", function () { it("should find track numbers", function() {
assert.equal(big_result[3].track, 1); assert.equal(big_result[3].track, 1);
assert.equal(big_result[4].track, 2); assert.equal(big_result[4].track, 2);
assert.equal(big_result[5].track, 3); assert.equal(big_result[5].track, 3);
@ -51,7 +51,7 @@ describe("Parser", function () {
assert.equal(big_result[12].track, 10); assert.equal(big_result[12].track, 10);
}); });
it("should ensure ending timestamps for all", function () { it("should ensure ending timestamps for all", function() {
assert.deepEqual(big_result[13].end, { assert.deepEqual(big_result[13].end, {
calc: 3023, calc: 3023,
hh: 0, hh: 0,
@ -63,9 +63,8 @@ describe("Parser", function () {
assert.deepEqual(big_result[14].end, null); assert.deepEqual(big_result[14].end, null);
}); });
it("should parse timestamps with square brackets", function () { it("should parse timestamps with square brackets", function() {
let result = let result = parse(`[00:00:00] 1. Steve Kroeger x Skye Holland - Through The Dark
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], { assert.deepEqual(result[0], {
artist: "Steve Kroeger x Skye Holland", artist: "Steve Kroeger x Skye Holland",
@ -80,7 +79,7 @@ describe("Parser", function () {
}); });
}); });
it("should parse durations when given", function () { it("should parse durations when given", function() {
let result = parse(`1. Artist - Title 6:19 let result = parse(`1. Artist - Title 6:19
2. Another Artist - Another Title 6:59 2. Another Artist - Another Title 6:59
3. Yet Another Artist - Yet another title 5:12`); 3. Yet Another Artist - Yet another title 5:12`);
@ -114,7 +113,7 @@ describe("Parser", function () {
}); });
}); });
it("should parse as timestamps if first timestamp is 00:00", function () { it("should parse as timestamps if first timestamp is 00:00", function() {
let result = parse(`1. Artist - Title 00:00 let result = parse(`1. Artist - Title 00:00
2. Another Artist - Another Title 01:00 2. Another Artist - Another Title 01:00
3. Yet Another Artist - Yet another title 02:00`); 3. Yet Another Artist - Yet another title 02:00`);
@ -141,18 +140,15 @@ describe("Parser", function () {
track: 3, track: 3,
end: null, end: null,
start: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 }, start: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 },
_: { _: { left_text: "Yet Another Artist - Yet another title", right_text: "" },
left_text: "Yet Another Artist - Yet another title",
right_text: "",
},
}); });
}); });
it("should parse durations as timestamps when forced", function () { it("should parse durations as timestamps when forced", function() {
let result = parse( let result = parse(
`1. Artist - Title 5:00 `1. Artist - Title 5:00
2. Another Artist - Another Title 4:20`, 2. Another Artist - Another Title 4:20`,
{ forceTimestamps: true }, { forceTimestamps: true }
); );
assert.deepEqual(result[0].end, { assert.deepEqual(result[0].end, {
ts: "00:4:20", ts: "00:4:20",
@ -170,11 +166,11 @@ describe("Parser", function () {
}); });
}); });
it("should parse timestamps as durations when forced", function () { it("should parse timestamps as durations when forced", function() {
let result = parse( let result = parse(
`1. Artist - Title 1:00 `1. Artist - Title 1:00
2. Another Artist - Another Title 1:15`, 2. Another Artist - Another Title 1:15`,
{ forceDurations: true }, { forceDurations: true }
); );
assert.deepEqual(result[0], { assert.deepEqual(result[0], {
track: 1, track: 1,
@ -218,7 +214,7 @@ describe("Parser", function () {
}); });
}); });
it("should parse taylor swift", function () { it("should parse taylor swift", function() {
let result = parse(`0:00 the 1 let result = parse(`0:00 the 1
3:29 cardigan 3:29 cardigan
9:30 the last great american dynasty 9:30 the last great american dynasty