diff --git a/index.js b/index.js index 0489b82..6354754 100755 --- a/index.js +++ b/index.js @@ -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] [output_file] + $ youtube-cue [--audio-file audio.m4a] --cue-title "Album Name" --cue-performer "Album Artist" [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]; - 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)) { + let r = processFile(urlOrFile, argv); + } else { + let r = processYoutube(urlOrFile, argv); + } + generate({ r.tracks, r.artist, r.audioFile, r.album }, r.outputFile); + console.log(`"${outputFile}" saved`); } diff --git a/package-lock.json b/package-lock.json index 3b40edf..f988a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "youtube-cue", "version": "1.0.6", "license": "MIT", "dependencies": { diff --git a/src/process.js b/src/process.js new file mode 100644 index 0000000..90700bc --- /dev/null +++ b/src/process.js @@ -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, + }; +} diff --git a/test/process_test.js b/test/process_test.js new file mode 100644 index 0000000..1a5eb45 --- /dev/null +++ b/test/process_test.js @@ -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'); + }); +}); diff --git a/test/sample.txt b/test/sample.txt new file mode 100644 index 0000000..ada57b7 --- /dev/null +++ b/test/sample.txt @@ -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