From 9958b6ef0f5dc4400b04cf1a8a816b0d3f3d838c Mon Sep 17 00:00:00 2001
From: Nemo <me@captnemo.in>
Date: Sun, 30 May 2021 21:01:27 +0530
Subject: [PATCH] Get it working

---
 README.md           | 29 +++++------------------------
 index.js            | 45 +++++++++++++++++++++++++++++++++++++++++++++
 package-lock.json   | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json        | 24 +++++++++++-------------
 src/cue.js          | 20 ++++++++++++++++++++
 src/ffmpeg.js       |  0 
 src/parser.js       | 66 +++++++++++++++++++++++++++++++++++++++++++++++++-----------------
 src/utils.js        |  5 -----
 test/ffmpeg.js      | 11 -----------
 test/parser_test.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
 10 files changed, 250 insertions(+), 107 deletions(-)

diff --git a/README.md b/README.md
index 921421d..6a5e0d8 100644
--- a/README.md
+++ a/README.md
@@ -1,37 +1,18 @@
-# youtube-ripper
+# youtube-cue
 
-Helps you download music compilations from youtube.
-Will automatically download the video, split it into chunks,
-and apply proper id3v2 tags on all the files (including cover-art)
+Helps you tag music compilations from youtube by generating a Cue sheet. Use alongside [cuetag.sh](https://command-not-found.com/cuetag.sh).
 
 ## Dependencies
 
-- _Asssumes_ that `youtube-dl` and `ffmpeg` are available in `$PATH`
-- Takes care of everything else
+- None
 
-## Opinions
-
-This software has opinions:
-
-- You care about metadata and tagging your music properly
-- All music must have cover art embedded
-- You have `youtube-dl`  and `ffmpeg` already installed
-- I am smart enough to parse youtube descriptions
-
 ## Installation
 
-    npm install -g youtube-ripper
+    npm install -g youtube-cue
 
 ## Usage
-
-    youtube-ripper "https://www.youtube.com/watch?v=41Y6xov0ppw"
-
-## Configuration
 
-- Pass a cue file instead of using the youtube description
-- `--album-art` Pass custom album art image (Uses the youtube thumbnail by default)
-- `--album-artist` Pass a specific Album Artist. (Picks up the artist from the video by default)
-- `--genre` Pass a specific genre to use. (Picks up from the video by default)
+    youtube-cue "https://www.youtube.com/watch?v=41Y6xov0ppw" file.cue
 
 ## HACKING
 
diff --git a/index.js b/index.js
old mode 100644
new mode 100755
index e69de29..8b8dfdb
--- a/index.js
+++ a/index.js
@@ -1,0 +1,45 @@
+#!/usr/bin/env node
+import meow from 'meow';
+import ytdl from 'ytdl-core';
+import getArtistTitle from 'get-artist-title'
+import {parse} from './src/parser.js'
+import {generate} from './src/cue.js'
+
+const cli = meow(`
+  Usage
+    $ youtube-cue --audio-file <youtube_url> <output.cue>
+
+  Options
+    --help, Show help
+    --version, Show version
+    --audio-file, Input Audio File
+
+  Examples
+    $ youtube-cue "https://www.youtube.com/watch?v=THzUassmQwE" output.cue
+      output.cue saved
+`, {
+  importMeta: import.meta,
+  flags: {
+    audioFile: {type: 'string'}
+  },
+  allowUnknownFlags: false
+});
+
+if(cli.input.length==2) {
+  let url = cli.input[0]
+  let output_file = cli.input[1]
+
+  ytdl.getInfo(url).then(info=>{
+    let audioFile = cli.flags.audioFile? cli.flags.audioFile : `${info.videoDetails.title}.m4a`
+    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})
+    generate({tracks, artist, audioFile, album}, cli.input[1])
+  })
+} else {
+  cli.showHelp()
+}
diff --git a/package-lock.json b/package-lock.json
index 19cd4ff..b1ded81 100644
--- a/package-lock.json
+++ a/package-lock.json
@@ -1,17 +1,19 @@
 {
-  "name": "youtube-ripper",
+  "name": "youtube-cue",
   "version": "1.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
+      "name": "youtube-cue",
       "version": "1.0.0",
       "license": "MIT",
       "dependencies": {
         "console-log-level": "^1.4.1",
         "get-artist-title": "^1.3.1",
         "meow": "^10.0.0",
-        "ora": "^5.4.0"
+        "ora": "^5.4.0",
+        "ytdl-core": "^4.8.2"
       },
       "devDependencies": {
         "mocha": "^8.4.0"
@@ -944,6 +946,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/m3u8stream": {
+      "version": "0.8.4",
+      "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
+      "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
+      "dependencies": {
+        "miniget": "^4.0.0",
+        "sax": "^1.2.4"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/map-obj": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz",
@@ -996,6 +1010,14 @@
         "node": ">=4"
       }
     },
+    "node_modules/miniget": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
+      "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -1396,6 +1418,11 @@
         }
       ]
     },
+    "node_modules/sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+    },
     "node_modules/semver": {
       "version": "7.3.5",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -1806,6 +1833,19 @@
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ytdl-core": {
+      "version": "4.8.2",
+      "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.8.2.tgz",
+      "integrity": "sha512-O3n++YcgZawaXJwbPmnRDgfN6b4kU0DpNdkI9Na5yM3JAdfJmoq5UHc8v9Xjgjr1RilQUUh7mhDnRRPDtKr0Kg==",
+      "dependencies": {
+        "m3u8stream": "^0.8.3",
+        "miniget": "^4.0.0",
+        "sax": "^1.1.3"
+      },
+      "engines": {
+        "node": ">=10"
       }
     }
   },
@@ -2476,6 +2516,15 @@
       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
       "requires": {
         "yallist": "^4.0.0"
+      }
+    },
+    "m3u8stream": {
+      "version": "0.8.4",
+      "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
+      "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
+      "requires": {
+        "miniget": "^4.0.0",
+        "sax": "^1.2.4"
       }
     },
     "map-obj": {
@@ -2511,6 +2560,11 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
       "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
+    },
+    "miniget": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
+      "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ=="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -2786,6 +2840,11 @@
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
     },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+    },
     "semver": {
       "version": "7.3.5",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -3094,6 +3153,16 @@
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
       "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+    },
+    "ytdl-core": {
+      "version": "4.8.2",
+      "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.8.2.tgz",
+      "integrity": "sha512-O3n++YcgZawaXJwbPmnRDgfN6b4kU0DpNdkI9Na5yM3JAdfJmoq5UHc8v9Xjgjr1RilQUUh7mhDnRRPDtKr0Kg==",
+      "requires": {
+        "m3u8stream": "^0.8.3",
+        "miniget": "^4.0.0",
+        "sax": "^1.1.3"
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 1c9c959..6993e16 100644
--- a/package.json
+++ a/package.json
@@ -1,11 +1,12 @@
 {
-  "name": "youtube-ripper",
+  "name": "youtube-cue",
   "version": "1.0.0",
-  "description": "rips entire albums from youtube videos",
+  "description": "Generates Cue sheet from Youtube URL",
   "main": "index.js",
   "scripts": {
     "test": "mocha"
   },
+  "bin": "index.js",
   "author": "Nemo <npm@captnemo.in>",
   "license": "MIT",
   "devDependencies": {
@@ -15,26 +16,23 @@
     "console-log-level": "^1.4.1",
     "get-artist-title": "^1.3.1",
     "meow": "^10.0.0",
-    "ora": "^5.4.0"
+    "ora": "^5.4.0",
+    "ytdl-core": "^4.8.2"
   },
   "repository": {
     "type": "git",
-    "url": "https://github.com/captn3m0/youtube-ripper"
+    "url": "https://github.com/captn3m0/youtube-cue"
   },
+  "type": "module",
   "keywords": [
-    "youtube-ripper",
+    "youtube-cue",
     "youtube",
-    "download",
-    "youtube-dl",
-    "ffmpeg",
     "split",
     "album",
-    "avconv",
-    "cue",
-    "ripper"
+    "cue"
   ],
   "bugs": {
-    "url": "https://github.com/captn3m0/youtube-ripper/issues"
+    "url": "https://github.com/captn3m0/youtube-cue/issues"
   },
-  "homepage": "https://github.com/captn3m0/youtube-ripper"
+  "homepage": "https://github.com/captn3m0/youtube-cue"
 }
diff --git a/src/cue.js b/src/cue.js
new file mode 100644
index 0000000..5f7e70b 100644
--- /dev/null
+++ a/src/cue.js
@@ -1,0 +1,20 @@
+import fs from 'fs';
+
+// https://en.wikipedia.org/wiki/Cue_sheet_(computing)
+export function generate(data, outputFile) {
+  try {
+    fs.truncateSync(outputFile)
+  } catch {}
+  fs.appendFileSync(outputFile, `REM Generated using youtube-cue\n`);
+  fs.appendFileSync(outputFile, `PERFORMER "${data.artist}"\n`);
+  fs.appendFileSync(outputFile, `TITLE "${data.album}"\n`);
+  fs.appendFileSync(outputFile, `FILE "${data.audioFile}" M4A\n`);
+  for(var i in data.tracks) {
+    let song = data.tracks[i];
+    let minutes = (song.start.hh * 60) + (song.start.mm)
+    fs.appendFileSync(outputFile, `  TRACK ${song.track} AUDIO\n`);
+    fs.appendFileSync(outputFile, `    TITLE "${song.title}"\n`);
+    fs.appendFileSync(outputFile, `    PERFORMER "${song.artist}"\n`);
+    fs.appendFileSync(outputFile, `    INDEX 01 ${minutes}:${song.start.ss}:00\n`);
+  }
+}
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
deleted file mode 100644
index e69de29..0000000 100644
--- a/src/ffmpeg.js
+++ /dev/null
diff --git a/src/parser.js b/src/parser.js
index 8a91e08..abebabf 100644
--- a/src/parser.js
+++ a/src/parser.js
@@ -1,11 +1,6 @@
-const utils = require('./utils');
-var colors = require('mocha/lib/reporters/base').colors;
-
-colors['pass'] = '30;42';
-
 /*jshint esversion: 6 */
 /**
- * https://regex101.com/r/LEPUGb/1/
+ * https://regex101.com/r/XwBLUH/1/
  * This regex parses out the following groups:
  * tracknumber at the start of the line, optional
  * start_ts: complete track start timestamp (hh:mm:ss) (mm:ss is minimum)
@@ -24,19 +19,23 @@
  * It is suggested to check their lengths and pick one to parse as the Track Title
  */
 const TS_REGEX = /^((?<track>\d{1,3})\.)* *(?<text_1>.*?) *(?<start_ts>((?<start_hh>\d{1,2}):)?(?<start_mm>\d{1,2}):(?<start_ss>\d{1,2})) *-? *(?<end_ts>(?<end_hh>\d{1,2}:)?(?<end_mm>\d{1,2}):(?<end_ss>\d{1,2}))? *(?<text_2>.*?)$/;
-const getArtistTitle = require('get-artist-title');
+import getArtistTitle from 'get-artist-title'
 var _options = {};
+
+function convertTime(h,m,s) {
+  return (+h) * 60 * 60 + (+m) * 60 + (+s)
+}
 
 var filterTimestamp = function(line) {
   return TS_REGEX.test(line)
 };
 
-var parse = function(line) {
+var firstPass = function(line) {
   let matches = line.match(TS_REGEX);
   return {
-    track: parseInt(matches.groups['track'], 10),
+    track: matches.groups['track'] ? +matches.groups['track'] : null,
     start: {
-      ts: matches.groups['start_ts'],
+      ts: matches.groups['start_ts'].length<6 ? `00:${matches.groups['start_ts']}` : matches.groups['start_ts'],
       hh: matches.groups['start_hh'] ? +matches.groups['start_hh'] : 0,
       // These 2 are always set
       mm: +matches.groups['start_mm'],
@@ -57,9 +56,9 @@
 
 var calcTimestamp = function(obj) {
   if(obj.end) {
-    obj.end.calc = utils.convertTime(obj.end.hh,obj.end.mm,obj.end.ss)
+    obj.end.calc = convertTime(obj.end.hh,obj.end.mm,obj.end.ss)
   }
-  obj.start.calc = utils.convertTime(obj.start.hh,obj.start.mm,obj.start.ss)
+  obj.start.calc = convertTime(obj.start.hh,obj.start.mm,obj.start.ss)
   return obj
 }
 
@@ -73,19 +72,38 @@
 var parseArtist = function(obj) {
   let [artist, title] = getArtistTitle(obj.title, {
     defaultArtist: _options.artist,
+    defaultTitle: obj.title
   });
   return Object.assign({ artist: artist, title: title }, obj);
 };
 
-module.exports = {
-  parse: function(text, options = { artist: 'Unknown' }) {
-    _options = options;
-    return text
-      .split('\n')
-      .filter(filterTimestamp)
-      .map(parse)
-      .map(calcTimestamp)
-      .map(parseTitle)
-      .map(parseArtist);
-  },
-};
+var addTrack = function(obj, index) {
+  if (obj.track==null) {
+    obj.track = index+1
+  }
+  return obj
+}
+
+var addEnd = function(obj, index, arr) {
+  if (!obj.end) {
+    if(arr.length!=index+1) {
+      let next = arr[index+1]
+      obj.end = next.start
+      return obj
+    }
+  }
+  return obj
+}
+
+export function parse (text, options = { artist: 'Unknown' }) {
+  _options = options;
+  return text
+    .split('\n')
+    .filter(filterTimestamp)
+    .map(firstPass)
+    .map(calcTimestamp)
+    .map(parseTitle)
+    .map(parseArtist)
+    .map(addTrack)
+    .map(addEnd)
+}
diff --git a/src/utils.js b/src/utils.js
deleted file mode 100644
index 47a7cec..0000000 100644
--- a/src/utils.js
+++ /dev/null
@@ -1,5 +1,0 @@
-module.exports = {
-  convertTime : (h,m,s) => {
-    return hms = (+h) * 60 * 60 + (+m) * 60 + (+s)
-  }
-}
diff --git a/test/ffmpeg.js b/test/ffmpeg.js
deleted file mode 100644
index aafa675..0000000 100644
--- a/test/ffmpeg.js
+++ /dev/null
@@ -1,11 +1,0 @@
-/*jshint esversion: 6 */
-var assert = require('assert');
-var ffmpeg = require('../src/ffmpeg');
-
-describe('ffmpeg', function() {
-  describe('setup', function() {
-    it('should figure out if ffmpeg is installed', function() {
-      // assert.equal(parser.parse(TEXT).length, 3);
-    });
-  });
-});
diff --git a/test/parser_test.js b/test/parser_test.js
index 21cfce0..f4d334b 100644
--- a/test/parser_test.js
+++ a/test/parser_test.js
@@ -1,7 +1,8 @@
 /*jshint esversion: 6 */
-var assert = require('assert');
-var parser = require('../src/parser');
+import { strict as assert } from 'assert';
 
+import {parse} from '../src/parser.js'
+
 const TEXT = `
 00:40 The Coders - Hello World
 12:23 This is not the end
@@ -17,36 +18,63 @@
 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
 `;
 
 const TEXT_WITH_ARTIST = '12:23 Rolling Stones - Hello World';
 
 describe('Parser', function() {
-  describe('parser', function() {
-    var big_result;
-    before(function() {
-      big_result = parser.parse(TEXT)
-    });
-    it('should find all timestamps', function() {
-      assert.equal(big_result.length, 13);
-    });
-
-    it('should find artist names', function() {
-      let result = parser.parse(TEXT_WITH_ARTIST);
-      assert.equal(result[0].artist, 'Rolling Stones');
-    });
-
-    it('should find track numbers', function() {
-      assert.equal(big_result[3].track, 1)
-      assert.equal(big_result[4].track, 2)
-      assert.equal(big_result[5].track, 3)
-      assert.equal(big_result[6].track, 4)
-      assert.equal(big_result[7].track, 5)
-      assert.equal(big_result[8].track, 6)
-      assert.equal(big_result[9].track, 7)
-      assert.equal(big_result[10].track, 8)
-      assert.equal(big_result[11].track, 9)
-      assert.equal(big_result[12].track, 10)
-    })
+  var big_result;
+  before(function() {
+    big_result = parse(TEXT)
+  });
+  it('should find all timestamps', function() {
+    assert.equal(big_result.length, 15);
+  });
+
+  it('should find artist names', function() {
+    let result = parse(TEXT_WITH_ARTIST);
+    assert.equal(result[0].artist, 'Rolling Stones');
   });
+
+  it('should find track numbers', function() {
+    assert.equal(big_result[3].track, 1)
+    assert.equal(big_result[4].track, 2)
+    assert.equal(big_result[5].track, 3)
+    assert.equal(big_result[6].track, 4)
+    assert.equal(big_result[7].track, 5)
+    assert.equal(big_result[8].track, 6)
+    assert.equal(big_result[9].track, 7)
+    assert.equal(big_result[10].track, 8)
+    assert.equal(big_result[11].track, 9)
+    assert.equal(big_result[12].track, 10)
+  })
+
+  it('should ensure ending timestamps for all', function() {
+    assert.deepEqual(big_result[13].end, {calc: 3023, hh:0, mm:50, ss:23, ts: '50:23'})
+    // TODO
+    assert.deepEqual(big_result[14].end, null)
+  })
+
+  it('should parse taylor swift', function() {
+    let result = parse(`0:00 the 1
+    3:29 cardigan
+    9:30 the last great american dynasty
+    11:56 exile
+    16:46 my tears ricochet
+    21:03 mirrorball
+    24:35 seven
+    28:07 august
+    32:30 this is me trying
+    35:52 illicit affairs
+    39:05 invisible strings
+    43:22 mad woman
+    49:30 epiphany
+    52:17 betty
+    57:15 peace
+    1:01:10 hoax
+    1:04:50 the lakes`)
+    console.log(result)
+  })
 });
--
rgit 0.1.5