Get it working

This commit is contained in:
Nemo 2021-05-30 21:01:27 +05:30
parent 32156d2193
commit 9958b6ef0f
10 changed files with 250 additions and 107 deletions

View File

@ -1,37 +1,18 @@
# youtube-ripper # youtube-cue
Helps you download music compilations from youtube. Helps you tag music compilations from youtube by generating a Cue sheet. Use alongside [cuetag.sh](https://command-not-found.com/cuetag.sh).
Will automatically download the video, split it into chunks,
and apply proper id3v2 tags on all the files (including cover-art)
## Dependencies ## Dependencies
- _Asssumes_ that `youtube-dl` and `ffmpeg` are available in `$PATH` - None
- 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
## Installation ## Installation
npm install -g youtube-ripper npm install -g youtube-cue
## Usage ## Usage
youtube-ripper "https://www.youtube.com/watch?v=41Y6xov0ppw" youtube-cue "https://www.youtube.com/watch?v=41Y6xov0ppw" file.cue
## 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)
## HACKING ## HACKING

45
index.js Normal file → Executable file
View File

@ -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 <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()
}

73
package-lock.json generated
View File

@ -1,17 +1,19 @@
{ {
"name": "youtube-ripper", "name": "youtube-cue",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "youtube-cue",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"console-log-level": "^1.4.1", "console-log-level": "^1.4.1",
"get-artist-title": "^1.3.1", "get-artist-title": "^1.3.1",
"meow": "^10.0.0", "meow": "^10.0.0",
"ora": "^5.4.0" "ora": "^5.4.0",
"ytdl-core": "^4.8.2"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^8.4.0" "mocha": "^8.4.0"
@ -944,6 +946,18 @@
"node": ">=10" "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": { "node_modules/map-obj": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz",
@ -996,6 +1010,14 @@
"node": ">=4" "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": { "node_modules/minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "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": { "node_modules/semver": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -1807,6 +1834,19 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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": { "dependencies": {
@ -2478,6 +2518,15 @@
"yallist": "^4.0.0" "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": { "map-obj": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" "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": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "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", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" "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": { "semver": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -3094,6 +3153,16 @@
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" "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"
}
} }
} }
} }

View File

@ -1,11 +1,12 @@
{ {
"name": "youtube-ripper", "name": "youtube-cue",
"version": "1.0.0", "version": "1.0.0",
"description": "rips entire albums from youtube videos", "description": "Generates Cue sheet from Youtube URL",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "mocha" "test": "mocha"
}, },
"bin": "index.js",
"author": "Nemo <npm@captnemo.in>", "author": "Nemo <npm@captnemo.in>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
@ -15,26 +16,23 @@
"console-log-level": "^1.4.1", "console-log-level": "^1.4.1",
"get-artist-title": "^1.3.1", "get-artist-title": "^1.3.1",
"meow": "^10.0.0", "meow": "^10.0.0",
"ora": "^5.4.0" "ora": "^5.4.0",
"ytdl-core": "^4.8.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/captn3m0/youtube-ripper" "url": "https://github.com/captn3m0/youtube-cue"
}, },
"type": "module",
"keywords": [ "keywords": [
"youtube-ripper", "youtube-cue",
"youtube", "youtube",
"download",
"youtube-dl",
"ffmpeg",
"split", "split",
"album", "album",
"avconv", "cue"
"cue",
"ripper"
], ],
"bugs": { "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"
} }

20
src/cue.js Normal file
View File

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

View File

View File

@ -1,11 +1,6 @@
const utils = require('./utils');
var colors = require('mocha/lib/reporters/base').colors;
colors['pass'] = '30;42';
/*jshint esversion: 6 */ /*jshint esversion: 6 */
/** /**
* https://regex101.com/r/LEPUGb/1/ * https://regex101.com/r/XwBLUH/1/
* This regex parses out the following groups: * This regex parses out the following groups:
* tracknumber at the start of the line, optional * tracknumber at the start of the line, optional
* start_ts: complete track start timestamp (hh:mm:ss) (mm:ss is minimum) * 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 * 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 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 = {}; var _options = {};
function convertTime(h,m,s) {
return (+h) * 60 * 60 + (+m) * 60 + (+s)
}
var filterTimestamp = function(line) { var filterTimestamp = function(line) {
return TS_REGEX.test(line) return TS_REGEX.test(line)
}; };
var parse = function(line) { var firstPass = function(line) {
let matches = line.match(TS_REGEX); let matches = line.match(TS_REGEX);
return { return {
track: parseInt(matches.groups['track'], 10), track: matches.groups['track'] ? +matches.groups['track'] : null,
start: { 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, hh: matches.groups['start_hh'] ? +matches.groups['start_hh'] : 0,
// These 2 are always set // These 2 are always set
mm: +matches.groups['start_mm'], mm: +matches.groups['start_mm'],
@ -57,9 +56,9 @@ var parse = function(line) {
var calcTimestamp = function(obj) { var calcTimestamp = function(obj) {
if(obj.end) { 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 return obj
} }
@ -73,19 +72,38 @@ var parseTitle = function(obj) {
var parseArtist = function(obj) { var parseArtist = function(obj) {
let [artist, title] = getArtistTitle(obj.title, { let [artist, title] = getArtistTitle(obj.title, {
defaultArtist: _options.artist, defaultArtist: _options.artist,
defaultTitle: obj.title
}); });
return Object.assign({ artist: artist, title: title }, obj); return Object.assign({ artist: artist, title: title }, obj);
}; };
module.exports = { var addTrack = function(obj, index) {
parse: function(text, options = { artist: 'Unknown' }) { if (obj.track==null) {
_options = options; obj.track = index+1
return text }
.split('\n') return obj
.filter(filterTimestamp) }
.map(parse)
.map(calcTimestamp) var addEnd = function(obj, index, arr) {
.map(parseTitle) if (!obj.end) {
.map(parseArtist); 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)
}

View File

@ -1,5 +0,0 @@
module.exports = {
convertTime : (h,m,s) => {
return hms = (+h) * 60 * 60 + (+m) * 60 + (+s)
}
}

View File

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

View File

@ -1,6 +1,7 @@
/*jshint esversion: 6 */ /*jshint esversion: 6 */
var assert = require('assert'); import { strict as assert } from 'assert';
var parser = require('../src/parser');
import {parse} from '../src/parser.js'
const TEXT = ` const TEXT = `
00:40 The Coders - Hello World 00:40 The Coders - Hello World
@ -17,36 +18,63 @@ Something else in the middle
08. Off-Piste 32:15 - 36:53 08. Off-Piste 32:15 - 36:53
09. Aura 36:53 - 41:44 09. Aura 36:53 - 41:44
10. Bombogenesis 41:44 - 48:20 10. Bombogenesis 41:44 - 48:20
Hello World 48:20
50:23 Bye World
`; `;
const TEXT_WITH_ARTIST = '12:23 Rolling Stones - Hello World'; const TEXT_WITH_ARTIST = '12:23 Rolling Stones - Hello World';
describe('Parser', function() { describe('Parser', function() {
describe('parser', function() { var big_result;
var big_result; before(function() {
before(function() { big_result = parse(TEXT)
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)
})
}); });
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)
})
}); });