This commit is contained in:
Nemo 2022-01-08 21:59:06 +05:30 committed by GitHub
commit 57278a5766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 49 deletions

View File

@ -1,81 +1,78 @@
#!/usr/bin/env node
import ytdl from "ytdl-core";
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";
import updateNotifier from "update-notifier";
import pkg from "./src/package.js";
import ytdl from 'ytdl-core';
import fs from 'fs';
import { generate } from './src/cue.js';
import minimist from 'minimist';
import updateNotifier from 'update-notifier';
import pkg from './src/package.js';
import { processFile, processYoutube } from './src/process.js';
updateNotifier({ pkg }).notify();
let argv = minimist(process.argv.slice(2), {
string: "audio-file",
string: ['audio-file', 'cue-title', 'cue-performer'],
});
if (argv.version) {
console.log(pkg.version);
} else if (argv._.length < 1 || argv.help) {
console.log(`Usage
$ youtube-cue [--audio-file audio.m4a] <youtube_url> [output_file]
$ youtube-cue [--audio-file audio.m4a] --cue-title "Album Name" --cue-performer "Album Artist" <youtube_url|input_file> [output_file]
youtube_url: Pass a Youtube URL from where description is fetched and parsed
input_file: Pass a plaintext file which contains the description text containing a timesheet.
Options
--help, Show help
--audio-file, Input Audio File (optional) that is written to the CUE sheet
--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
If a youtube URL is passed, then
The default audio file is set to %VIDEOTITLE.m4a
The default output file is set to %VIDEOTITLE.cue
Since video title is not available while parsing text files,
The default audio file is set to audio.m4a
The default output file is set to output.cue
where $VIDEOTITLE is the title of the YouTube video.
Generally the parser detects whether numbers are positional timestamps or track durations.
Generally the parser detects whether numbers (such as 00:12) 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
--durations Parse numbers as track durations instead.
The above 2 are only needed to force behaviour in very specific edge cases, they should
not be required for most files.
--cue-title "Title that goes into the CUE file for the complete CUE sheet"
--cue-performer "Performer for the entire collection. Commonly the album artist."
cue-title and cue-performer are recommended especially if you are reading the data from a text file.
--version Print version
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
$ youtube-cue "https://youtu.be/THzUassmQwE" folklore.cue
folklore.cue saved`);
folklore.cue saved
$ youtube-cue --audio-file audio.m4a --cue-performer "Magic Riders" description.txt
"output.cue" saved`);
} else {
let url = argv._[0];
let urlOrFile = argv._[0];
let r;
ytdl.getInfo(url).then((info) => {
let audioFile = argv["audio-file"]
? argv["audio-file"]
: `${info.videoDetails.title}.m4a`;
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,
forceTimestamps,
forceDurations,
});
generate({ tracks, artist, audioFile, album }, output_file);
console.log(`"${output_file}" saved`);
});
if (fs.existsSync(urlOrFile)) {
r = processFile(urlOrFile, argv);
} else {
r = await processYoutube(urlOrFile, argv);
}
generate(
{
tracks: r.tracks,
artist: r.artist,
audioFile: r.audioFile,
album: r.album,
},
r.outputFile
);
console.log(`"${r.outputFile}" saved`);
}

74
src/process.js Normal file
View File

@ -0,0 +1,74 @@
import fs from 'fs';
import { parse } from './parser.js';
import getArtistTitle from 'get-artist-title';
import ytdl from 'ytdl-core';
import exit from 'process';
const DEFAULT_AUDIO_FILE = 'audio.m4a';
const DEFAULT_ARTIST = 'Unknown Artist';
const DEFAULT_ALBUM = 'Unknown Album';
const DEFAULT_OUTPUT_FILE = 'output.cue';
function validateArgs(argv) {
if (forceTimestamps(argv) && forceDurations(argv)) {
console.error("You can't pass both --timestamps and durations");
exit(1);
}
}
function forceTimestamps(argv) {
return argv['timestamps'] ? argv['timestamps'] : false;
}
function forceDurations(argv) {
argv['durations'] ? argv['durations'] : false;
}
export function processFile(inputFile, argv) {
validateArgs(argv);
let audioFile = argv['audio-file'] ? argv['audio-file'] : `audio.m4a`;
let contents = fs.readFileSync(inputFile, 'utf8');
let tracks = parse(contents, {
artist: DEFAULT_ARTIST,
forceTimestamps: forceTimestamps(argv),
forceDurations: forceDurations(argv),
});
let artist = argv['cue-performer'] ? argv['cue-performer'] : DEFAULT_ARTIST;
let album = argv['cue-title'] ? argv['cue-title'] : DEFAULT_ALBUM;
return {
tracks: tracks,
artist: artist,
audioFile: audioFile,
album: album,
outputFile: argv._[1] ? argv._[1] : DEFAULT_OUTPUT_FILE,
};
}
export async function processYoutube(url, argv) {
let info = await ytdl.getInfo(url);
let audioFile = argv['audio-file']
? argv['audio-file']
: `${info.videoDetails.title}.m4a`;
let outputFile = argv._[1] ? argv._[1] : `${info.videoDetails.title}.cue`;
let res = getArtistTitle(info.videoDetails.title, {
defaultArtist: DEFAULT_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,
forceTimestamps,
forceDurations,
});
return {
tracks: tracks,
artist: artist,
audioFile: audioFile,
album: album,
outputFile: argv._[1] ? argv._[1] : DEFAULT_OUTPUT_FILE,
};
}

38
test/process_test.js Normal file
View File

@ -0,0 +1,38 @@
/*jshint esversion: 6 */
import { strict as assert } from 'assert';
import minimist from 'minimist';
import { processFile } from '../src/process.js';
describe('FileProcessor', function() {
it('should process simple files correctly', function() {
let r = processFile('test/sample.txt', minimist([]));
assert.equal(15, r.tracks.length);
assert.equal(r.artist, 'Unknown Artist');
assert.equal(r.audioFile, 'audio.m4a');
assert.equal(r.album, 'Unknown Album');
assert.equal(r.outputFile, 'output.cue');
});
it('should process files with arguments correctly', function() {
let args = minimist([
'--audio-file',
'demo.mp3',
'--cue-performer',
'Doors',
'--cue-title',
'Windows',
]);
let r = processFile('test/sample.txt', args);
assert.equal(15, r.tracks.length);
assert.equal(r.artist, 'Doors');
assert.equal(r.audioFile, 'demo.mp3');
assert.equal(r.album, 'Windows');
assert.equal(r.outputFile, 'output.cue');
});
it('should parse output files correctly', function() {
let args = minimist(['test/sample2.txt', 'test/o.cue']);
let r = processFile('test/sample.txt', args);
assert.equal(r.outputFile, 'test/o.cue');
});
});

16
test/sample.txt Normal file
View File

@ -0,0 +1,16 @@
00:40 The Coders - Hello World
1:00 This is not the end
Something else in the middle
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
05. Outre Lux 19:07 - 23:11
06. Balsam Massacre 23:11 - 26:24
07. Eco Friend 26:24 - 32:15
08. Off-Piste 32:15 - 36:53
09. Aura 36:53 - 41:44
10. Bombogenesis 41:44 - 48:20
Hello World 48:20
50:23 Bye World