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:
parent
666e802cfe
commit
085913ad69
87
index.js
87
index.js
|
@ -1,81 +1,70 @@
|
||||||
#!/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 { 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 updateNotifier from "update-notifier";
|
import pkg from './src/package.js';
|
||||||
import pkg from "./src/package.js";
|
import { processFile, processYoutube } from './src/process.js';
|
||||||
|
|
||||||
updateNotifier({ pkg }).notify();
|
updateNotifier({ pkg }).notify();
|
||||||
|
|
||||||
let argv = minimist(process.argv.slice(2), {
|
let argv = minimist(process.argv.slice(2), {
|
||||||
string: "audio-file",
|
string: ['audio-file', 'cue-title', 'cue-performer'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (argv.version) {
|
if (argv.version) {
|
||||||
console.log(pkg.version);
|
console.log(pkg.version);
|
||||||
} else if (argv._.length < 1 || argv.help) {
|
} 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] --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
|
Options
|
||||||
--help, Show help
|
--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
|
If a youtube URL is passed, then
|
||||||
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
|
||||||
|
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 (such as 00:12) are positional timestamps or track durations.
|
||||||
|
|
||||||
Generally the parser detects whether numbers are positional timestamps or track durations.
|
|
||||||
To enforce a desired interpretation you can use these flags:
|
To enforce a desired interpretation you can use these flags:
|
||||||
|
|
||||||
--timestamps Parse as positional timestamps (relative to the start of the playlist)
|
--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
|
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.
|
||||||
|
|
||||||
|
--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
|
--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
|
||||||
|
$ youtube-cue --audio-file audio.m4a --cue-performer "Magic Riders" description.txt
|
||||||
|
"output.cue" saved`);
|
||||||
} else {
|
} else {
|
||||||
let url = argv._[0];
|
let urlOrFile = argv._[0];
|
||||||
|
|
||||||
ytdl.getInfo(url).then((info) => {
|
if (fs.existsSync(urlOrFile)) {
|
||||||
let audioFile = argv["audio-file"]
|
let r = processFile(urlOrFile, argv);
|
||||||
? argv["audio-file"]
|
} else {
|
||||||
: `${info.videoDetails.title}.m4a`;
|
let r = processYoutube(urlOrFile, argv);
|
||||||
|
}
|
||||||
let output_file = argv._[1] ? argv._[1] : `${info.videoDetails.title}.cue`;
|
generate({ r.tracks, r.artist, r.audioFile, r.album }, r.outputFile);
|
||||||
|
console.log(`"${outputFile}" saved`);
|
||||||
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`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "youtube-cue",
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
Loading…
Reference in New Issue