mirror of
https://github.com/captn3m0/youtube-cue.git
synced 2024-09-27 19:23:12 +00:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
4d1bea0d0f | |||
a94395ea9d | |||
8019310674 | |||
1a2d485d10 | |||
|
7cce1333f1 | ||
1b9a02fa20 | |||
|
cbc90f7110 | ||
|
b785434c45 | ||
|
7c43700c11 | ||
|
33741a50e7 | ||
|
3bd269e517 | ||
|
2a67cc7c0b | ||
|
51a50b8a11 | ||
|
7c97cc9bc3 | ||
|
ce3c11a820 | ||
|
d376b3ad3d | ||
|
6af3c1da60 | ||
|
68bf09689a | ||
|
84c23243d3 | ||
|
1e9bace48d | ||
|
00af711bf6 | ||
|
278aebf32f | ||
|
1743561886 | ||
f4f0e4db76 | |||
8a570f74ad | |||
5ba24e95db | |||
91242ce61f | |||
34acfe769e | |||
0012c3b083 | |||
93321c6fb9 | |||
25e0081c6d | |||
9245d84332 | |||
bb11d6e9a8 | |||
ce4074f829 | |||
|
25ff38e93e | ||
|
343fbda261 | ||
|
f53aed7dec | ||
|
531ad3de3f | ||
|
c185212b14 | ||
|
869487e153 | ||
98f17e55d3 | |||
f281a42a2e | |||
242e08bc1b | |||
|
e83debe2bf | ||
|
9925d6e578 | ||
0986305a50 | |||
a08e1cd3b3 | |||
|
4744bb604f | ||
|
42a3dfb872 | ||
8c3a3a7ec6 | |||
|
3acd787d0d | ||
|
4f136dcf42 | ||
|
5b65a76e6c | ||
|
f6acc1bdd8 | ||
|
1cb210d64c | ||
|
666e802cfe | ||
|
5046823854 | ||
|
741d74ddf1 | ||
|
67012ca4ac | ||
8724ba85aa | |||
68e53896b9 | |||
30effce43b | |||
2eadb53b8f | |||
44e68fc140 | |||
9cc3cd4228 | |||
bcae4ebeb1 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
github: captn3m0
|
||||||
|
ko_fi: captn3m0
|
||||||
|
liberapay: captn3m0
|
5
.github/workflows/action.yml
vendored
5
.github/workflows/action.yml
vendored
@ -4,7 +4,7 @@ jobs:
|
|||||||
tests:
|
tests:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ['16', '14', '12']
|
node: ["18", "20", "21"]
|
||||||
name: Run NPM Stuff
|
name: Run NPM Stuff
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -13,4 +13,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{matrix.node}}
|
node-version: ${{matrix.node}}
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm run lint
|
||||||
|
- run: npm run test
|
||||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -7,6 +7,20 @@ 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
|
||||||
|
@ -52,14 +52,12 @@ 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=THzUassmQwE"
|
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=WzpmVxvoBoc" "The Groovy Nobody - Solarium.cue"
|
||||||
"T A Y L O R S W I F T – Folklore [Full album].cue" saved
|
"The Groovy Nobody - Solarium.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:
|
I have this in my `.bashrc` to download, split, tag, and import albums using beet:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
function ytdl.album() {
|
function ytdl.album() {
|
||||||
|
61
index.js
61
index.js
@ -1,16 +1,22 @@
|
|||||||
#!/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._.length <1 || argv.help ){
|
if (argv.version) {
|
||||||
|
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]
|
||||||
|
|
||||||
@ -32,36 +38,45 @@ if (argv._.length <1 || argv.help ){
|
|||||||
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']? argv['audio-file'] : `${info.videoDetails.title}.m4a`
|
let audioFile = argv["audio-file"]
|
||||||
|
? 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;
|
||||||
|
artist = info.videoDetails.media ? info.videoDetails.media.artist : artist;
|
||||||
|
let tracks = parse(info.videoDetails.description, {
|
||||||
|
artist,
|
||||||
|
forceTimestamps,
|
||||||
|
forceDurations,
|
||||||
|
length: Number(info.videoDetails.lengthSeconds),
|
||||||
|
});
|
||||||
|
generate({ tracks, artist, audioFile, album }, output_file);
|
||||||
|
console.log(`"${output_file}" saved`);
|
||||||
});
|
});
|
||||||
let [artist, album] = res
|
|
||||||
artist = (info.videoDetails.media ? info.videoDetails.media.artist : artist)
|
|
||||||
let tracks = parse(info.videoDetails.description, {artist, forceTimestamps, forceDurations})
|
|
||||||
generate({tracks, artist, audioFile, album}, output_file)
|
|
||||||
console.log(`"${output_file}" saved`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
2125
package-lock.json
generated
2125
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,22 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-cue",
|
"name": "youtube-cue",
|
||||||
"version": "1.0.6",
|
"version": "1.0.10",
|
||||||
"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": "^9.0.0"
|
"mocha": "^10.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.5",
|
"minimist": "^1.2.8",
|
||||||
"ytdl-core": "^4.8.2"
|
"update-notifier": "^7.0.0",
|
||||||
|
"ytdl-core": "^4.11.5"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
14
src/cue.js
14
src/cue.js
@ -1,9 +1,14 @@
|
|||||||
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`);
|
||||||
@ -11,11 +16,12 @@ export function generate(data, outputFile) {
|
|||||||
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 = (song.start.hh * 60) + (song.start.mm)
|
let minutes = String(song.start.hh * 60 + song.start.mm).padStart(2, "0");
|
||||||
|
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}:${song.start.ss}:00\n`);
|
fs.appendFileSync(outputFile, ` INDEX 01 ${minutes}:${seconds}:00\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
src/package.js
Normal file
5
src/package.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createRequire } from "module";
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const data = require("../package.json");
|
||||||
|
|
||||||
|
export default data;
|
@ -20,7 +20,8 @@
|
|||||||
*
|
*
|
||||||
* 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 = /^((?<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>.*?)$/;
|
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 = {};
|
var _options = {};
|
||||||
|
|
||||||
@ -152,9 +153,19 @@ 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 = { artist: "Unknown", forceTimestamps: false, forceDurations: false }
|
options = {
|
||||||
|
artist: "Unknown",
|
||||||
|
forceTimestamps: false,
|
||||||
|
forceDurations: false,
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
_options = options;
|
_options = options;
|
||||||
let durations = false;
|
let durations = false;
|
||||||
@ -187,5 +198,6 @@ export function parse(
|
|||||||
.map(parseTitle)
|
.map(parseTitle)
|
||||||
.map(parseArtist)
|
.map(parseArtist)
|
||||||
.map(addTrack)
|
.map(addTrack)
|
||||||
.map(addEnd);
|
.map(addEnd)
|
||||||
|
.filter(dropInvalid);
|
||||||
}
|
}
|
||||||
|
48
test/cue_test.js
Normal file
48
test/cue_test.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*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"));
|
||||||
|
});
|
||||||
|
});
|
@ -64,7 +64,8 @@ describe("Parser", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should parse timestamps with square brackets", function () {
|
it("should parse timestamps with square brackets", function () {
|
||||||
let result = parse(`[00:00:00] 1. Steve Kroeger x Skye Holland - Through The Dark
|
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], {
|
assert.deepEqual(result[0], {
|
||||||
artist: "Steve Kroeger x Skye Holland",
|
artist: "Steve Kroeger x Skye Holland",
|
||||||
@ -140,7 +141,10 @@ 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: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -148,7 +152,7 @@ describe("Parser", 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,7 +174,7 @@ describe("Parser", 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,
|
||||||
|
Loading…
Reference in New Issue
Block a user