🏡 index : github.com/captn3m0/youtube-cue.git

author Nemo <me@captnemo.in> 2021-09-12 23:14:11.0 +05:30:00
committer Nemo <me@captnemo.in> 2021-09-12 23:14:11.0 +05:30:00
commit
085913ad698d3bcc66716bf8c5c2126efbe8e389 [patch]
tree
b1028a3462efc7d6961fdda4e95de4e0b6b97ed2
parent
666e802cfe9eb3ae80669dbf18c77c5c18fa04ec
download
085913ad698d3bcc66716bf8c5c2126efbe8e389.tar.gz

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.

Diff

 index.js             | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
 package-lock.json    |  1 +
 src/process.js       | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/process_test.js | 38 ++++++++++++++++++++++++++++++++++++++
 test/sample.txt      | 16 ++++++++++++++++
 5 files changed, 167 insertions(+), 52 deletions(-)

diff --git a/index.js b/index.js
index 0489b82..6354754 100755
--- a/index.js
+++ a/index.js
@@ -1,42 +1,52 @@
#!/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

    The default audio file is set to %VIDEOTITLE.m4a
    The default output file is set to %VIDEOTITLE.cue

    where $VIDEOTITLE is the title of the YouTube video.

    Generally the parser detects whether numbers are positional timestamps or track durations.
    --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

    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

@@ -44,38 +54,17 @@
    $ 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];

  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`);
  });
  let urlOrFile = argv._[0];

  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
+++ a/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 100644
--- /dev/null
+++ a/src/process.js
@@ -1,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 100644
--- /dev/null
+++ a/test/process_test.js
@@ -1,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 100644
--- /dev/null
+++ a/test/sample.txt
@@ -1,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