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

author Nemo <me@captnemo.in> 2021-05-30 21:01:27.0 +05:30:00
committer Nemo <me@captnemo.in> 2021-05-31 13:26:47.0 +05:30:00
commit
9958b6ef0f5dc4400b04cf1a8a816b0d3f3d838c [patch]
tree
f90221afca34a99d44cb666da8eb226bf95b8066
parent
32156d21938b47ece1c149ea8075c78c4dbf2a49
download
9958b6ef0f5dc4400b04cf1a8a816b0d3f3d838c.tar.gz

Get it working



Diff

 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)
  })
});