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.
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

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",
"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"
}
}
}
}

View File

@ -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"
}

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 */
/**
* 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 = /^((?<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 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)
}

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