From 9958b6ef0f5dc4400b04cf1a8a816b0d3f3d838c Mon Sep 17 00:00:00 2001 From: Nemo 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(-) mode change 100644 => 100755 index.js create mode 100644 src/cue.js delete mode 100644 src/ffmpeg.js delete mode 100644 src/utils.js delete mode 100644 test/ffmpeg.js diff --git a/README.md b/README.md index 921421d..6a5e0d8 100644 --- a/README.md +++ b/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 - -## 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 +- None ## 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 +++ b/index.js @@ -0,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 + + 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 +++ b/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", @@ -1807,6 +1834,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" + } } }, "dependencies": { @@ -2478,6 +2518,15 @@ "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": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", @@ -2512,6 +2561,11 @@ "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", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -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 +++ b/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 ", "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 --- /dev/null +++ b/src/cue.js @@ -0,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 diff --git a/src/parser.js b/src/parser.js index 8a91e08..abebabf 100644 --- a/src/parser.js +++ b/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 @@ colors['pass'] = '30;42'; * It is suggested to check their lengths and pick one to parse as the Track Title */ const TS_REGEX = /^((?\d{1,3})\.)* *(?.*?) *(?((?\d{1,2}):)?(?\d{1,2}):(?\d{1,2})) *-? *(?(?\d{1,2}:)?(?\d{1,2}):(?\d{1,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 parse = function(line) { 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 parseTitle = function(obj) { 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 --- a/src/utils.js +++ /dev/null @@ -1,5 +0,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 --- a/test/ffmpeg.js +++ /dev/null @@ -1,11 +0,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 +++ b/test/parser_test.js @@ -1,6 +1,7 @@ /*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 @@ -17,36 +18,63 @@ Something else in the middle 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) + }) });