mirror of
https://github.com/captn3m0/youtube-cue.git
synced 2024-09-07 22:46:26 +00:00
Get it working
This commit is contained in:
parent
32156d2193
commit
9958b6ef0f
29
README.md
29
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
|
||||
|
||||
|
45
index.js
Normal file → Executable file
45
index.js
Normal file → Executable 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
73
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
package.json
24
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"
|
||||
}
|
||||
|
20
src/cue.js
Normal file
20
src/cue.js
Normal 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`);
|
||||
}
|
||||
}
|
@ -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' }) {
|
||||
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(parse)
|
||||
.map(firstPass)
|
||||
.map(calcTimestamp)
|
||||
.map(parseTitle)
|
||||
.map(parseArtist);
|
||||
},
|
||||
};
|
||||
.map(parseArtist)
|
||||
.map(addTrack)
|
||||
.map(addEnd)
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
convertTime : (h,m,s) => {
|
||||
return hms = (+h) * 60 * 60 + (+m) * 60 + (+s)
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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,22 +18,23 @@ 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)
|
||||
big_result = parse(TEXT)
|
||||
});
|
||||
it('should find all timestamps', function() {
|
||||
assert.equal(big_result.length, 13);
|
||||
assert.equal(big_result.length, 15);
|
||||
});
|
||||
|
||||
it('should find artist names', function() {
|
||||
let result = parser.parse(TEXT_WITH_ARTIST);
|
||||
let result = parse(TEXT_WITH_ARTIST);
|
||||
assert.equal(result[0].artist, 'Rolling Stones');
|
||||
});
|
||||
|
||||
@ -48,5 +50,31 @@ describe('Parser', function() {
|
||||
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)
|
||||
})
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user