Initial work on file processor

This adds a new file processor mode. So the command changes from:

youtube-cue https://youtu.be/watch?v=dfjosufoadsi

to additionally supporting:

youtube-cue description.nfo

This however results in some problems. It is hard to get album and album
artist information from a plain text file, so we instead now have 2
additional flags:

--cue-performer "Performer for the entire track collection"
--cue-title "Title for the entire collection"

They usually, (but not always) will correspond to the album artist and
album title.

The changes are fairly obvious - I've also tried to make this more
modular, so hopefully this is easier to use as a module.
This commit is contained in:
Nemo 2021-09-12 23:14:11 +05:30
parent 666e802cfe
commit 085913ad69
5 changed files with 164 additions and 49 deletions

View File

@ -1,81 +1,70 @@
#!/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 getArtistTitle from 'get-artist-title';
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 { 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.
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];
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);
if (fs.existsSync(urlOrFile)) {
let r = processFile(urlOrFile, argv);
} else {
let r = processYoutube(urlOrFile, argv);
}
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`);
});
generate({ r.tracks, r.artist, r.audioFile, r.album }, r.outputFile);
console.log(`"${outputFile}" saved`);
}

1
package-lock.json generated
View File

@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "youtube-cue",
"version": "1.0.6",
"license": "MIT",
"dependencies": {

71
src/process.js Normal file
View File

@ -0,0 +1,71 @@
import fs from 'fs';
import { parse } from './parser.js';
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