Compare commits
92 Commits
Author | SHA1 | Date |
---|---|---|
Nemo | 4d1bea0d0f | |
Nemo | a94395ea9d | |
Nemo | 8019310674 | |
Nemo | 1a2d485d10 | |
dependabot[bot] | 7cce1333f1 | |
Nemo | 1b9a02fa20 | |
dependabot[bot] | cbc90f7110 | |
dependabot[bot] | b785434c45 | |
dependabot[bot] | 7c43700c11 | |
dependabot[bot] | 33741a50e7 | |
dependabot[bot] | 3bd269e517 | |
dependabot[bot] | 2a67cc7c0b | |
dependabot[bot] | 51a50b8a11 | |
dependabot[bot] | 7c97cc9bc3 | |
dependabot[bot] | ce3c11a820 | |
dependabot[bot] | d376b3ad3d | |
dependabot[bot] | 6af3c1da60 | |
dependabot[bot] | 68bf09689a | |
dependabot[bot] | 84c23243d3 | |
dependabot[bot] | 1e9bace48d | |
dependabot[bot] | 00af711bf6 | |
dependabot[bot] | 278aebf32f | |
dependabot[bot] | 1743561886 | |
Nemo | f4f0e4db76 | |
Nemo | 8a570f74ad | |
Nemo | 5ba24e95db | |
Nemo | 91242ce61f | |
Nemo | 34acfe769e | |
Nemo | 0012c3b083 | |
Nemo | 93321c6fb9 | |
Nemo | 25e0081c6d | |
Nemo | 9245d84332 | |
Nemo | bb11d6e9a8 | |
Nemo | ce4074f829 | |
dependabot[bot] | 25ff38e93e | |
dependabot[bot] | 343fbda261 | |
dependabot[bot] | f53aed7dec | |
dependabot[bot] | 531ad3de3f | |
dependabot[bot] | c185212b14 | |
dependabot[bot] | 869487e153 | |
Nemo | 98f17e55d3 | |
Nemo | f281a42a2e | |
Nemo | 242e08bc1b | |
dependabot[bot] | e83debe2bf | |
dependabot[bot] | 9925d6e578 | |
Nemo | 0986305a50 | |
Nemo | a08e1cd3b3 | |
dependabot[bot] | 4744bb604f | |
dependabot[bot] | 42a3dfb872 | |
Nemo | 8c3a3a7ec6 | |
dependabot[bot] | 3acd787d0d | |
dependabot[bot] | 4f136dcf42 | |
dependabot[bot] | 5b65a76e6c | |
dependabot[bot] | f6acc1bdd8 | |
dependabot[bot] | 1cb210d64c | |
dependabot[bot] | 666e802cfe | |
dependabot[bot] | 5046823854 | |
dependabot[bot] | 741d74ddf1 | |
dependabot[bot] | 67012ca4ac | |
Nemo | 8724ba85aa | |
Nemo | 68e53896b9 | |
Nemo | 30effce43b | |
Nemo | 2eadb53b8f | |
Nemo | 44e68fc140 | |
Nemo | 9cc3cd4228 | |
Nemo | bcae4ebeb1 | |
Nemo | e0f05fe646 | |
Nemo | 610c13fcef | |
Nemo | 089c46d978 | |
Nemo | 1aa0be59da | |
Nemo | c4b1b42461 | |
dependabot[bot] | a710658b49 | |
Nemo | a737d37105 | |
Nemo | 7247b7be66 | |
Nemo | 10981c62ce | |
Nemo | b0fd29cbdb | |
Nemo | deca378937 | |
Nemo | 3fdb7739ff | |
Nemo | ce845195cf | |
Nemo | d47844fe3b | |
Nemo | 3f9eb44d58 | |
Nemo | 3c916c93ec | |
Nemo | ecd52f60d1 | |
Nemo | ff5a7b5234 | |
Nemo | d4a99bf98f | |
Nemo | 819b990dfc | |
Nemo | 4b5c16d699 | |
Nemo | f00efc7806 | |
Nemo | cbaa488962 | |
Nemo | 2c23c1c7aa | |
Nemo | ab31ec4f91 | |
Nemo | 9d30f09aac |
|
@ -0,0 +1,3 @@
|
|||
github: captn3m0
|
||||
ko_fi: captn3m0
|
||||
liberapay: captn3m0
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: 'Bug Report: Incorrect CUE sheet'
|
||||
about: Create a bug report if youtube-cue fails to work on a specific Youtube Video
|
||||
title: "[BUG] Failed on a Youtube link"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Youtube URL**
|
||||
|
||||
Type the Youtube URL you tried here.
|
||||
|
||||
**Command you ran**
|
||||
|
||||
Type the complete command you typed, such as:
|
||||
|
||||
youtube-cue https://www.youtube.com/watch?v=XsRAAp-Ivuc
|
||||
|
||||
**Output**
|
||||
|
||||
Give complete output here, if you got any
|
||||
|
||||
**Version Information**
|
||||
|
||||
Paste the output of `npm list -g --depth=0 youtube-cue` here
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Need support?
|
||||
url: https://github.com/captn3m0/youtube-cue/discussions/new?category=q-a
|
||||
about: Please ask and answer questions here.
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,17 @@
|
|||
on: push
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["18", "20", "21"]
|
||||
name: Run NPM Stuff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{matrix.node}}
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
|
@ -1 +1,2 @@
|
|||
node_modules/
|
||||
*.cue
|
||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
|
@ -1,4 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
@ -6,10 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 1.0.2 - 2020-06-10
|
||||
### Changed
|
||||
- Switches from `mocha` to `minimist` to parse arguments. Now down to 9 total (recursive) dependencies.
|
||||
|
||||
## 1.01 - 2020-06-10
|
||||
## 1.0.9
|
||||
### Changed
|
||||
- Dependency Updates
|
||||
- Minimum NodeJS version is now v14
|
||||
|
||||
## 1.0.8
|
||||
### Added
|
||||
- Dependency Updates
|
||||
|
||||
## 1.0.7
|
||||
### Added
|
||||
- `--version` is now supported
|
||||
- An update notification is shown if the package isn't latest.
|
||||
|
||||
## 1.0.6 - 2021-07-29
|
||||
|
||||
### Added
|
||||
|
||||
- Tracklists using duration instead of timestamps are now supported.
|
||||
|
||||
## 1.0.5 - 2021-07-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependency Updates
|
||||
|
||||
## 1.0.4 - 2021-06-28
|
||||
|
||||
### Added
|
||||
|
||||
- Support for timestamps in square brackets (#64)
|
||||
- Improved formatting for helptext
|
||||
|
||||
### Changed
|
||||
|
||||
- output filename has a default value, and is now optional
|
||||
|
||||
## 1.0.3 - 2021-06-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependency Updates
|
||||
|
||||
## 1.0.2 - 2021-06-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Switches from `mocha` to `minimist` to parse arguments. Now down to 9 total (recursive) dependencies.
|
||||
|
||||
## 1.01 - 2021-06-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependency Updates
|
||||
|
|
57
README.md
57
README.md
|
@ -1,34 +1,63 @@
|
|||
# youtube-cue
|
||||
# youtube-cue ![npm](https://img.shields.io/npm/v/youtube-cue) ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/youtube-cue) ![NPM](https://img.shields.io/npm/l/youtube-cue) ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/youtube-cue) [![Total Dependencies](https://img.shields.io/badge/Total%20Dependencies-4-orange)](https://www.npmjs.com/package/youtube-cue)
|
||||
|
||||
Helps you tag music compilations from youtube by generating a Cue sheet. Use alongside [cuetag.sh](https://command-not-found.com/cuetag.sh), [m4acut](https://github.com/nu774/m4acut), or [mp3splt](https://sourceforge.net/p/mp3splt/) or any other Cue sheet tooling.
|
||||
Generate CUE sheet from timestamps in youtube video description.
|
||||
|
||||
## Dependencies
|
||||
## What is this for?
|
||||
|
||||
1. If you have DJ-mix or album on YouTube that you'd like to generate a [CUE sheet][cue] for.
|
||||
2. The video has timestamps in the video description.
|
||||
3. The video is publicly available on Youtube.
|
||||
|
||||
`youtube-cue` will read the video description, get the timestamps and generate a [CUE sheet][cue] accordingly. It will also work if track durations are used instead of timestamps.
|
||||
|
||||
## Anti-features
|
||||
|
||||
1. It does not download tracks from YouTube
|
||||
2. It does not split your tracks
|
||||
3. It does not tag your tracks.
|
||||
|
||||
For all of the above, there are better tools available, such as [youtube-dl](https://ytdl-org.github.io/youtube-dl/), [m4acut](https://github.com/nu774/m4acut), [mp3splt](https://sourceforge.net/projects/mp3splt/), [cuetools](https://github.com/svend/cuetools), [beets](https://beets.io) and many more. youtube-cue tries to [do one thing well](https://onethingwell.org/).
|
||||
|
||||
- None
|
||||
|
||||
## Installation
|
||||
|
||||
npm install -g youtube-cue
|
||||
|
||||
## Upgrade
|
||||
|
||||
npm update -g youtube-cue
|
||||
|
||||
## Usage
|
||||
|
||||
Generates Cue sheet from Youtube URL
|
||||
You need to pass 2 parameters, a Youtube URL and a output CUE filename. YouTube short URLs (`youtu.be`) are accepted. You can additionally pass a `audio-file` argument which is used for the [`FILE` specified in the CUE file][cuefile].
|
||||
|
||||
Usage
|
||||
$ youtube-cue --audio-file <youtube_url> <output.cue>
|
||||
youtube-cue [--audio-file audio.m4a] <youtube_url> [output_file]
|
||||
|
||||
Options
|
||||
--help, Show help
|
||||
--version, Show version
|
||||
--audio-file, Input Audio File
|
||||
--audio-file, Input Audio File (optional) that is written to the CUE sheet
|
||||
|
||||
The default audio file is set to %VIDEOTITLE.m4a
|
||||
The default output file is set to %VIDEOTITLE.cue
|
||||
|
||||
where $VIDEOTITLE is the title of the YouTube video.
|
||||
|
||||
Generally the parser detects whether numbers are positional timestamps or track durations.
|
||||
To enforce a desired interpretation you can use these flags:
|
||||
|
||||
--timestamps Parse as positional timestamps (relative to the start of the playlist)
|
||||
--durations Parse as track durations
|
||||
|
||||
The above 2 are only needed to force behaviour in
|
||||
very specific edge cases, they should not be required for most files.
|
||||
|
||||
Examples
|
||||
$ youtube-cue "https://www.youtube.com/watch?v=THzUassmQwE" output.cue
|
||||
output.cue saved
|
||||
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=WzpmVxvoBoc" "The Groovy Nobody - Solarium.cue"
|
||||
"The Groovy Nobody - Solarium.cue" saved
|
||||
|
||||
## Personal Usage
|
||||
|
||||
I have this in my `.bashrc` to download, split, tag, and import albums:
|
||||
I have this in my `.bashrc` to download, split, tag, and import albums using beet:
|
||||
|
||||
```shell
|
||||
function ytdl.album() {
|
||||
|
@ -44,7 +73,7 @@ function ytdl.album() {
|
|||
|
||||
## HACKING
|
||||
|
||||
- If this video does not work on a specific video, please attach the debug log output
|
||||
- If it does not work on a specific video, please attach the complete output
|
||||
- Pull Requests are welcome that add support for a better parser without breaking the existing tests
|
||||
- Please add tests for any new functionality
|
||||
|
||||
|
@ -54,3 +83,5 @@ Licensed under the [MIT License][mit]
|
|||
|
||||
[mit]: https://nemo.mit-license.org/
|
||||
[rdd]: http://tom.preston-werner.com/2010/08/23/readme-driven-development.html
|
||||
[cue]: https://en.wikipedia.org/wiki/Cue_sheet_(computing)
|
||||
[cuefile]: https://en.wikipedia.org/wiki/Cue_sheet_(computing)#Essential_commands
|
||||
|
|
88
index.js
88
index.js
|
@ -1,38 +1,82 @@
|
|||
#!/usr/bin/env node
|
||||
import ytdl from 'ytdl-core';
|
||||
import getArtistTitle from 'get-artist-title'
|
||||
import {parse} from './src/parser.js'
|
||||
import {generate} from './src/cue.js'
|
||||
import minimist from 'minimist'
|
||||
import ytdl from "ytdl-core";
|
||||
import getArtistTitle from "get-artist-title";
|
||||
import { parse } from "./src/parser.js";
|
||||
import { generate } from "./src/cue.js";
|
||||
import minimist from "minimist";
|
||||
import exit from "process";
|
||||
import updateNotifier from "update-notifier";
|
||||
import pkg from "./src/package.js";
|
||||
|
||||
updateNotifier({ pkg }).notify();
|
||||
|
||||
let argv = minimist(process.argv.slice(2), {
|
||||
string: 'audio-file'
|
||||
string: "audio-file",
|
||||
});
|
||||
|
||||
if (argv._.length <2 || argv.help ){
|
||||
if (argv.version) {
|
||||
console.log(pkg.version);
|
||||
} else if (argv._.length < 1 || argv.help) {
|
||||
console.log(`Usage
|
||||
$ youtube-cue --audio-file <youtube_url> <output.cue>
|
||||
$ youtube-cue [--audio-file audio.m4a] <youtube_url> [output_file]
|
||||
|
||||
Options
|
||||
--help, Show help
|
||||
--audio-file, Input Audio File
|
||||
--audio-file, Input Audio File (optional) that is written to the CUE sheet
|
||||
|
||||
The default audio file is set to %VIDEOTITLE.m4a
|
||||
The default output file is set to %VIDEOTITLE.cue
|
||||
|
||||
where $VIDEOTITLE is the title of the YouTube video.
|
||||
|
||||
Generally the parser detects whether numbers are positional timestamps or track durations.
|
||||
To enforce a desired interpretation you can use these flags:
|
||||
|
||||
--timestamps Parse as positional timestamps (relative to the start of the playlist)
|
||||
--durations Parse as track durations
|
||||
|
||||
The above 2 are only needed to force behaviour in very specific edge cases, they should
|
||||
not be required for most files.
|
||||
|
||||
--version Print version
|
||||
|
||||
Examples
|
||||
$ youtube-cue "https://www.youtube.com/watch?v=THzUassmQwE" output.cue
|
||||
output.cue saved`)
|
||||
$ youtube-cue --audio-file audio.m4a "https://www.youtube.com/watch?v=THzUassmQwE"
|
||||
"T A Y L O R S W I F T – Folklore [Full album].cue" saved
|
||||
$ youtube-cue "https://youtu.be/THzUassmQwE" folklore.cue
|
||||
folklore.cue saved`);
|
||||
} else {
|
||||
let url = argv._[0]
|
||||
let output_file = argv._[1]
|
||||
let url = argv._[0];
|
||||
|
||||
ytdl.getInfo(url).then(info=>{
|
||||
let audioFile = argv['audio-file']? argv['audio-file'] : `${info.videoDetails.title}.m4a`
|
||||
let res = getArtistTitle(info.videoDetails.title,{
|
||||
ytdl.getInfo(url).then((info) => {
|
||||
let audioFile = argv["audio-file"]
|
||||
? argv["audio-file"]
|
||||
: `${info.videoDetails.title}.m4a`;
|
||||
|
||||
let output_file = argv._[1] ? argv._[1] : `${info.videoDetails.title}.cue`;
|
||||
|
||||
let forceTimestamps = argv["timestamps"] ? argv["timestamps"] : false;
|
||||
|
||||
let forceDurations = argv["durations"] ? argv["durations"] : false;
|
||||
|
||||
if (forceTimestamps && forceDurations) {
|
||||
console.error("You can't pass both --timestamps and durations");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let res = getArtistTitle(info.videoDetails.title, {
|
||||
defaultArtist: "Unknown Artist",
|
||||
defaultTitle: info.videoDetails.title
|
||||
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}, output_file)
|
||||
})
|
||||
let [artist, album] = res;
|
||||
artist = info.videoDetails.media ? info.videoDetails.media.artist : artist;
|
||||
let tracks = parse(info.videoDetails.description, {
|
||||
artist,
|
||||
forceTimestamps,
|
||||
forceDurations,
|
||||
length: Number(info.videoDetails.lengthSeconds),
|
||||
});
|
||||
generate({ tracks, artist, audioFile, album }, output_file);
|
||||
console.log(`"${output_file}" saved`);
|
||||
});
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -1,22 +1,25 @@
|
|||
{
|
||||
"name": "youtube-cue",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.10",
|
||||
"description": "Generates Cue sheet from Youtube URL",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha"
|
||||
"test": "mocha",
|
||||
"lint": "prettier --check *.js src/*.js test/*.js"
|
||||
},
|
||||
"bin": "index.js",
|
||||
"author": "Nemo <npm@captnemo.in>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"mocha": "^9.0.0"
|
||||
"mocha": "^10.0.0",
|
||||
"prettier": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"console-log-level": "^1.4.1",
|
||||
"get-artist-title": "^1.3.1",
|
||||
"minimist": "^1.2.5",
|
||||
"ytdl-core": "^4.8.2"
|
||||
"minimist": "^1.2.8",
|
||||
"update-notifier": "^7.0.0",
|
||||
"ytdl-core": "^4.11.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
16
src/cue.js
16
src/cue.js
|
@ -1,21 +1,27 @@
|
|||
import fs from 'fs';
|
||||
import fs from "fs";
|
||||
|
||||
/** code to create a new CUE file, as per the standard
|
||||
* with a REM PERFORMER, TITLE, FILE attribute
|
||||
* and a list of tracks provided as input
|
||||
*/
|
||||
|
||||
// https://en.wikipedia.org/wiki/Cue_sheet_(computing)
|
||||
export function generate(data, outputFile) {
|
||||
try {
|
||||
fs.truncateSync(outputFile)
|
||||
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) {
|
||||
for (var i in data.tracks) {
|
||||
let song = data.tracks[i];
|
||||
let minutes = (song.start.hh * 60) + (song.start.mm)
|
||||
let minutes = String(song.start.hh * 60 + song.start.mm).padStart(2, "0");
|
||||
let seconds = String(song.start.ss).padStart(2, "0");
|
||||
fs.appendFileSync(outputFile, ` TRACK ${song.track} AUDIO\n`);
|
||||
fs.appendFileSync(outputFile, ` TITLE "${song.title}"\n`);
|
||||
fs.appendFileSync(outputFile, ` PERFORMER "${song.artist}"\n`);
|
||||
// Cue File is always MINUTES:SECONDS:FRAME, where FRAME is 00
|
||||
fs.appendFileSync(outputFile, ` INDEX 01 ${minutes}:${song.start.ss}:00\n`);
|
||||
fs.appendFileSync(outputFile, ` INDEX 01 ${minutes}:${seconds}:00\n`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const data = require("../package.json");
|
||||
|
||||
export default data;
|
208
src/parser.js
208
src/parser.js
|
@ -1,8 +1,10 @@
|
|||
/*jshint esversion: 6 */
|
||||
/**
|
||||
* https://regex101.com/r/XwBLUH/1/
|
||||
* https://regex101.com/r/XwBLUH/2
|
||||
* This regex parses out the following groups:
|
||||
* tracknumber at the start of the line, optional
|
||||
* trackl track number at the left of the timestamp, optional and optionally enclosed in square brackets or parantheses
|
||||
* trackr track number at the of the timestamp, optional and optionally enclosed in square brackets or parantheses
|
||||
*
|
||||
* start_ts: complete track start timestamp (hh:mm:ss) (mm:ss is minimum)
|
||||
* start_hh: starting hh, optional
|
||||
* start_mm: starting minutes, required
|
||||
|
@ -13,97 +15,189 @@
|
|||
* end:mm: track end minute, optional
|
||||
* end:ss: track end seconds, optional
|
||||
*
|
||||
* text_1: text found to the left of the timestamp
|
||||
* text_2: text found to the right of the timestamp
|
||||
* text_1: text found to the left of the timestamp, ignoring the track number
|
||||
* text_2: text found to the right of the timestamp, ignoring the track number
|
||||
*
|
||||
* 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>.*?)$/;
|
||||
import getArtistTitle from 'get-artist-title'
|
||||
const TS_REGEX =
|
||||
/^((?<trackl>\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}))? *((?<trackr>\d{1,3})\.)? *(?<text_2>.*?)$/;
|
||||
import getArtistTitle from "get-artist-title";
|
||||
var _options = {};
|
||||
|
||||
function convertTime(h,m,s) {
|
||||
return (+h) * 60 * 60 + (+m) * 60 + (+s)
|
||||
// Returns number of total seconds
|
||||
function convertTime(h, m, s) {
|
||||
return +h * 60 * 60 + +m * 60 + +s;
|
||||
}
|
||||
|
||||
var filterTimestamp = function(line) {
|
||||
return TS_REGEX.test(line)
|
||||
// Only picks out lines which have a timestamp in them
|
||||
var filterTimestamp = function (line) {
|
||||
return TS_REGEX.test(line);
|
||||
};
|
||||
|
||||
var firstPass = function(line) {
|
||||
// Parse each line as per the regex
|
||||
var firstPass = function (line) {
|
||||
let matches = line.match(TS_REGEX);
|
||||
let track = matches.groups["trackl"]
|
||||
? +matches.groups["trackl"]
|
||||
: matches.groups["trackr"]
|
||||
? +matches.groups["trackr"]
|
||||
: null;
|
||||
return {
|
||||
track: matches.groups['track'] ? +matches.groups['track'] : null,
|
||||
track: track,
|
||||
start: {
|
||||
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,
|
||||
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'],
|
||||
ss: +matches.groups['start_ss'],
|
||||
mm: +matches.groups["start_mm"],
|
||||
ss: +matches.groups["start_ss"],
|
||||
},
|
||||
end: (matches.groups['end_ts']!==undefined ? {
|
||||
ts: matches.groups['end_ts']? matches.groups['end_ts'] : null,
|
||||
hh: matches.groups['end_hh']? +matches.groups['end_hh'] : null,
|
||||
mm: matches.groups['end_mm']? +matches.groups['end_mm'] : null,
|
||||
ss: matches.groups['end_ss']? +matches.groups['end_ss'] : null,
|
||||
} : null),
|
||||
end:
|
||||
matches.groups["end_ts"] !== undefined
|
||||
? {
|
||||
ts: matches.groups["end_ts"] ? matches.groups["end_ts"] : null,
|
||||
hh: matches.groups["end_hh"] ? +matches.groups["end_hh"] : null,
|
||||
mm: matches.groups["end_mm"] ? +matches.groups["end_mm"] : null,
|
||||
ss: matches.groups["end_ss"] ? +matches.groups["end_ss"] : null,
|
||||
}
|
||||
: null,
|
||||
_: {
|
||||
left_text: matches.groups['text_1'],
|
||||
right_text: matches.groups['text_2']
|
||||
}
|
||||
}
|
||||
left_text: matches.groups["text_1"],
|
||||
right_text: matches.groups["text_2"],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
var calcTimestamp = function(obj) {
|
||||
if(obj.end) {
|
||||
obj.end.calc = convertTime(obj.end.hh,obj.end.mm,obj.end.ss)
|
||||
// Add a calc attribute with total seconds
|
||||
var calcTimestamp = function (obj) {
|
||||
if (obj.end) {
|
||||
obj.end.calc = convertTime(obj.end.hh, obj.end.mm, obj.end.ss);
|
||||
}
|
||||
obj.start.calc = convertTime(obj.start.hh,obj.start.mm,obj.start.ss)
|
||||
return obj
|
||||
}
|
||||
obj.start.calc = convertTime(obj.start.hh, obj.start.mm, obj.start.ss);
|
||||
return obj;
|
||||
};
|
||||
|
||||
var parseTitle = function(obj) {
|
||||
let title = obj._.left_text.length > obj._.right_text.length
|
||||
? obj._.left_text : obj._.right_text;
|
||||
// Pick the longer "text" from left or right side.
|
||||
var parseTitle = function (obj) {
|
||||
obj.title =
|
||||
obj._.left_text.length > obj._.right_text.length
|
||||
? obj._.left_text
|
||||
: obj._.right_text;
|
||||
return obj;
|
||||
};
|
||||
|
||||
return Object.assign({title: title}, obj)
|
||||
}
|
||||
|
||||
var parseArtist = function(obj) {
|
||||
// Parse the text as the title/artist
|
||||
var parseArtist = function (obj) {
|
||||
let [artist, title] = getArtistTitle(obj.title, {
|
||||
defaultArtist: _options.artist,
|
||||
defaultTitle: obj.title
|
||||
defaultTitle: obj.title,
|
||||
});
|
||||
return Object.assign({ artist: artist, title: title }, obj);
|
||||
obj.artist = artist;
|
||||
obj.title = title;
|
||||
return obj;
|
||||
};
|
||||
|
||||
var addTrack = function(obj, index) {
|
||||
if (obj.track==null) {
|
||||
obj.track = index+1
|
||||
// If track numbers are not present, add them accordingly
|
||||
var addTrack = function (obj, index) {
|
||||
if (obj.track == null) {
|
||||
obj.track = index + 1;
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
var addEnd = function(obj, index, arr) {
|
||||
// Add "end" timestamps as next start timestamps
|
||||
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
|
||||
if (arr.length != index + 1) {
|
||||
let next = arr[index + 1];
|
||||
obj.end = next.start;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
export function parse (text, options = { artist: 'Unknown' }) {
|
||||
var timeToObject = function (obj) {
|
||||
let d = new Date(obj.calc * 1000).toISOString();
|
||||
obj.hh = +d.substr(11, 2);
|
||||
obj.mm = +d.substr(14, 2);
|
||||
obj.ss = +d.substr(17, 2);
|
||||
obj.ts = d.substr(11, 8);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Instead of timestamps, some tracklists use durations
|
||||
// If durations are provided, use them to re-calculate
|
||||
// the starting and ending timestamps
|
||||
var fixDurations = function (list) {
|
||||
for (let i in list) {
|
||||
if (i == 0) {
|
||||
// Set the first one to start of track.
|
||||
list[i].start.hh = list[i].start.mm = list[i].start.ss = 0;
|
||||
// And end at the right time
|
||||
list[i].end = { calc: list[i].start.calc };
|
||||
list[i].start.calc = 0;
|
||||
} else {
|
||||
// All the others tracks start at the end of the previous one
|
||||
// And end at start time + duration
|
||||
let previous = list[i - 1];
|
||||
list[i].end = { calc: previous.end.calc + list[i].start.calc };
|
||||
list[i].start.calc = previous.end.calc;
|
||||
}
|
||||
|
||||
list[i].start = timeToObject(list[i].start);
|
||||
list[i].end = timeToObject(list[i].end);
|
||||
}
|
||||
};
|
||||
|
||||
var dropInvalid = function (e) {
|
||||
// All tracks should start before the closing time
|
||||
if (_options.length) return e.start.calc < _options.length;
|
||||
return true;
|
||||
};
|
||||
|
||||
export function parse(
|
||||
text,
|
||||
options = {
|
||||
artist: "Unknown",
|
||||
forceTimestamps: false,
|
||||
forceDurations: false,
|
||||
},
|
||||
) {
|
||||
_options = options;
|
||||
return text
|
||||
.split('\n')
|
||||
let durations = false;
|
||||
let result = text
|
||||
.split("\n")
|
||||
.filter(filterTimestamp)
|
||||
.map(firstPass)
|
||||
.map(calcTimestamp)
|
||||
.map(calcTimestamp);
|
||||
|
||||
if (!options.forceTimestamps) {
|
||||
// If our timestamps are not in increasing order
|
||||
// Assume that we've been given a duration list instead
|
||||
if (result[0].start.calc != 0) {
|
||||
result.forEach((current, index, list) => {
|
||||
if (index > 0) {
|
||||
let previous = list[index - 1];
|
||||
if (current.start.calc < previous.start.calc) {
|
||||
durations = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (durations || options.forceDurations == true) {
|
||||
fixDurations(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
.map(parseTitle)
|
||||
.map(parseArtist)
|
||||
.map(addTrack)
|
||||
.map(addEnd)
|
||||
.filter(dropInvalid);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*jshint esversion: 6 */
|
||||
import { strict as assert } from "assert";
|
||||
import { generate } from "../src/cue.js";
|
||||
import fs from "fs";
|
||||
|
||||
const DATA = {
|
||||
artist: "Dumbledore",
|
||||
album: "Curse of the Elder Wand",
|
||||
audioFile: "audio.m4a",
|
||||
tracks: [
|
||||
{
|
||||
artist: "Unknown",
|
||||
title: "the 1",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:3:9", hh: 0, mm: 3, ss: 9, calc: 189 },
|
||||
_: { left_text: "", right_text: "the 1" },
|
||||
},
|
||||
{
|
||||
artist: "Unknown",
|
||||
title: "cardigan",
|
||||
track: 2,
|
||||
start: { ts: "00:3:09", hh: 0, mm: 3, ss: 9, calc: 189 },
|
||||
end: { ts: "00:9:30", hh: 0, mm: 9, ss: 30, calc: 570 },
|
||||
_: { left_text: "", right_text: "cardigan" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("CUE", function () {
|
||||
it("should generate with leading zeroes", function () {
|
||||
generate(DATA, "/tmp/test.cue");
|
||||
const CUE_EXPECTED = `REM Generated using youtube-cue
|
||||
PERFORMER "Dumbledore"
|
||||
TITLE "Curse of the Elder Wand"
|
||||
FILE "audio.m4a" M4A
|
||||
TRACK 1 AUDIO
|
||||
TITLE "the 1"
|
||||
PERFORMER "Unknown"
|
||||
INDEX 01 00:00:00
|
||||
TRACK 2 AUDIO
|
||||
TITLE "cardigan"
|
||||
PERFORMER "Unknown"
|
||||
INDEX 01 03:09:00
|
||||
`;
|
||||
assert.equal(CUE_EXPECTED, fs.readFileSync("/tmp/test.cue", "utf-8"));
|
||||
});
|
||||
});
|
|
@ -1,14 +1,14 @@
|
|||
/*jshint esversion: 6 */
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from "assert";
|
||||
|
||||
import {parse} from '../src/parser.js'
|
||||
import { parse } from "../src/parser.js";
|
||||
|
||||
const TEXT = `
|
||||
00:40 The Coders - Hello World
|
||||
12:23 This is not the end
|
||||
1:00 This is not the end
|
||||
Something else in the middle
|
||||
1:23:11 Not the last song
|
||||
01. Screens 0:00 - 5:40
|
||||
1:23 Not the last song
|
||||
01. Screens 1:40 - 5:40
|
||||
02. Inharmonious Slog 5:40 - 10:11
|
||||
03. The Everyday Push 10:11 - 15:46
|
||||
04. Storm 15:46 - 19:07
|
||||
|
@ -22,42 +22,203 @@ 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 () {
|
||||
var big_result;
|
||||
before(function() {
|
||||
big_result = parse(TEXT)
|
||||
before(function () {
|
||||
big_result = parse(TEXT);
|
||||
});
|
||||
it('should find all timestamps', function() {
|
||||
it("should find all timestamps", function () {
|
||||
assert.equal(big_result.length, 15);
|
||||
});
|
||||
|
||||
it('should find artist names', function() {
|
||||
it("should find artist names", function () {
|
||||
let result = parse(TEXT_WITH_ARTIST);
|
||||
assert.equal(result[0].artist, 'Rolling Stones');
|
||||
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 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: '00:50:23'})
|
||||
it("should ensure ending timestamps for all", function () {
|
||||
assert.deepEqual(big_result[13].end, {
|
||||
calc: 3023,
|
||||
hh: 0,
|
||||
mm: 50,
|
||||
ss: 23,
|
||||
ts: "00:50:23",
|
||||
});
|
||||
// TODO
|
||||
assert.deepEqual(big_result[14].end, null)
|
||||
})
|
||||
assert.deepEqual(big_result[14].end, null);
|
||||
});
|
||||
|
||||
it('should parse taylor swift', function() {
|
||||
it("should parse timestamps with square brackets", function () {
|
||||
let result =
|
||||
parse(`[00:00:00] 1. Steve Kroeger x Skye Holland - Through The Dark
|
||||
[00:02:53] 2. Gabri Ponte x Jerome - Lonely `);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Steve Kroeger x Skye Holland",
|
||||
title: "Through The Dark",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:02:53", hh: 0, mm: 2, ss: 53, calc: 173 },
|
||||
_: {
|
||||
left_text: "",
|
||||
right_text: "Steve Kroeger x Skye Holland - Through The Dark",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse durations when given", function () {
|
||||
let result = parse(`1. Artist - Title 6:19
|
||||
2. Another Artist - Another Title 6:59
|
||||
3. Yet Another Artist - Yet another title 5:12`);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:06:19", hh: 0, mm: 6, ss: 19, calc: 379 },
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
});
|
||||
|
||||
assert.deepEqual(result[1], {
|
||||
artist: "Another Artist",
|
||||
title: "Another Title",
|
||||
track: 2,
|
||||
start: { ts: "00:06:19", hh: 0, mm: 6, ss: 19, calc: 379 },
|
||||
end: { ts: "00:13:18", hh: 0, mm: 13, ss: 18, calc: 798 },
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
});
|
||||
assert.deepEqual(result[2], {
|
||||
artist: "Yet Another Artist",
|
||||
title: "Yet another title",
|
||||
track: 3,
|
||||
start: { ts: "00:13:18", hh: 0, mm: 13, ss: 18, calc: 798 },
|
||||
end: { ts: "00:18:30", hh: 0, mm: 18, ss: 30, calc: 1110 },
|
||||
_: {
|
||||
left_text: "Yet Another Artist - Yet another title",
|
||||
right_text: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse as timestamps if first timestamp is 00:00", function () {
|
||||
let result = parse(`1. Artist - Title 00:00
|
||||
2. Another Artist - Another Title 01:00
|
||||
3. Yet Another Artist - Yet another title 02:00`);
|
||||
assert.deepEqual(result[0], {
|
||||
artist: "Artist",
|
||||
title: "Title",
|
||||
track: 1,
|
||||
start: { ts: "00:00:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:01:00", hh: 0, mm: 1, ss: 0, calc: 60 },
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
});
|
||||
|
||||
assert.deepEqual(result[1], {
|
||||
artist: "Another Artist",
|
||||
title: "Another Title",
|
||||
track: 2,
|
||||
end: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 },
|
||||
start: { ts: "00:01:00", hh: 0, mm: 1, ss: 0, calc: 60 },
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
});
|
||||
assert.deepEqual(result[2], {
|
||||
artist: "Yet Another Artist",
|
||||
title: "Yet another title",
|
||||
track: 3,
|
||||
end: null,
|
||||
start: { ts: "00:02:00", hh: 0, mm: 2, ss: 0, calc: 120 },
|
||||
_: {
|
||||
left_text: "Yet Another Artist - Yet another title",
|
||||
right_text: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse durations as timestamps when forced", function () {
|
||||
let result = parse(
|
||||
`1. Artist - Title 5:00
|
||||
2. Another Artist - Another Title 4:20`,
|
||||
{ forceTimestamps: true },
|
||||
);
|
||||
assert.deepEqual(result[0].end, {
|
||||
ts: "00:4:20",
|
||||
hh: 0,
|
||||
mm: 4,
|
||||
ss: 20,
|
||||
calc: 260,
|
||||
});
|
||||
assert.deepEqual(result[1].start, {
|
||||
ts: "00:4:20",
|
||||
hh: 0,
|
||||
mm: 4,
|
||||
ss: 20,
|
||||
calc: 260,
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse timestamps as durations when forced", function () {
|
||||
let result = parse(
|
||||
`1. Artist - Title 1:00
|
||||
2. Another Artist - Another Title 1:15`,
|
||||
{ forceDurations: true },
|
||||
);
|
||||
assert.deepEqual(result[0], {
|
||||
track: 1,
|
||||
_: { left_text: "Artist - Title", right_text: "" },
|
||||
title: "Title",
|
||||
artist: "Artist",
|
||||
end: {
|
||||
ts: "00:01:00",
|
||||
hh: 0,
|
||||
mm: 1,
|
||||
ss: 0,
|
||||
calc: 60,
|
||||
},
|
||||
start: {
|
||||
ts: "00:00:00",
|
||||
hh: 0,
|
||||
mm: 0,
|
||||
ss: 0,
|
||||
calc: 0,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result[1], {
|
||||
track: 2,
|
||||
_: { left_text: "Another Artist - Another Title", right_text: "" },
|
||||
title: "Another Title",
|
||||
artist: "Another Artist",
|
||||
start: {
|
||||
ts: "00:01:00",
|
||||
hh: 0,
|
||||
mm: 1,
|
||||
ss: 0,
|
||||
calc: 60,
|
||||
},
|
||||
end: {
|
||||
ts: "00:02:15",
|
||||
hh: 0,
|
||||
mm: 2,
|
||||
ss: 15,
|
||||
calc: 135,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse taylor swift", function () {
|
||||
let result = parse(`0:00 the 1
|
||||
3:29 cardigan
|
||||
9:30 the last great american dynasty
|
||||
|
@ -74,145 +235,145 @@ describe('Parser', function() {
|
|||
52:17 betty
|
||||
57:15 peace
|
||||
1:01:10 hoax
|
||||
1:04:50 the lakes`)
|
||||
1:04:50 the lakes`);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'the 1',
|
||||
artist: "Unknown",
|
||||
title: "the 1",
|
||||
track: 1,
|
||||
start: { ts: '00:0:00', hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: '00:3:29', hh: 0, mm: 3, ss: 29, calc: 209 },
|
||||
_: { left_text: '', right_text: 'the 1' }
|
||||
start: { ts: "00:0:00", hh: 0, mm: 0, ss: 0, calc: 0 },
|
||||
end: { ts: "00:3:29", hh: 0, mm: 3, ss: 29, calc: 209 },
|
||||
_: { left_text: "", right_text: "the 1" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'cardigan',
|
||||
artist: "Unknown",
|
||||
title: "cardigan",
|
||||
track: 2,
|
||||
start: { ts: '00:3:29', hh: 0, mm: 3, ss: 29, calc: 209 },
|
||||
end: { ts: '00:9:30', hh: 0, mm: 9, ss: 30, calc: 570 },
|
||||
_: { left_text: '', right_text: 'cardigan' }
|
||||
start: { ts: "00:3:29", hh: 0, mm: 3, ss: 29, calc: 209 },
|
||||
end: { ts: "00:9:30", hh: 0, mm: 9, ss: 30, calc: 570 },
|
||||
_: { left_text: "", right_text: "cardigan" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'the last great american dynasty',
|
||||
artist: "Unknown",
|
||||
title: "the last great american dynasty",
|
||||
track: 3,
|
||||
start: { ts: '00:9:30', hh: 0, mm: 9, ss: 30, calc: 570 },
|
||||
end: { ts: '00:11:56', hh: 0, mm: 11, ss: 56, calc: 716 },
|
||||
_: { left_text: '', right_text: 'the last great american dynasty' }
|
||||
start: { ts: "00:9:30", hh: 0, mm: 9, ss: 30, calc: 570 },
|
||||
end: { ts: "00:11:56", hh: 0, mm: 11, ss: 56, calc: 716 },
|
||||
_: { left_text: "", right_text: "the last great american dynasty" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'exile',
|
||||
artist: "Unknown",
|
||||
title: "exile",
|
||||
track: 4,
|
||||
start: { ts: '00:11:56', hh: 0, mm: 11, ss: 56, calc: 716 },
|
||||
end: { ts: '00:16:46', hh: 0, mm: 16, ss: 46, calc: 1006 },
|
||||
_: { left_text: '', right_text: 'exile' }
|
||||
start: { ts: "00:11:56", hh: 0, mm: 11, ss: 56, calc: 716 },
|
||||
end: { ts: "00:16:46", hh: 0, mm: 16, ss: 46, calc: 1006 },
|
||||
_: { left_text: "", right_text: "exile" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'my tears ricochet',
|
||||
artist: "Unknown",
|
||||
title: "my tears ricochet",
|
||||
track: 5,
|
||||
start: { ts: '00:16:46', hh: 0, mm: 16, ss: 46, calc: 1006 },
|
||||
end: { ts: '00:21:03', hh: 0, mm: 21, ss: 3, calc: 1263 },
|
||||
_: { left_text: '', right_text: 'my tears ricochet' }
|
||||
start: { ts: "00:16:46", hh: 0, mm: 16, ss: 46, calc: 1006 },
|
||||
end: { ts: "00:21:03", hh: 0, mm: 21, ss: 3, calc: 1263 },
|
||||
_: { left_text: "", right_text: "my tears ricochet" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'mirrorball',
|
||||
artist: "Unknown",
|
||||
title: "mirrorball",
|
||||
track: 6,
|
||||
start: { ts: '00:21:03', hh: 0, mm: 21, ss: 3, calc: 1263 },
|
||||
end: { ts: '00:24:35', hh: 0, mm: 24, ss: 35, calc: 1475 },
|
||||
_: { left_text: '', right_text: 'mirrorball' }
|
||||
start: { ts: "00:21:03", hh: 0, mm: 21, ss: 3, calc: 1263 },
|
||||
end: { ts: "00:24:35", hh: 0, mm: 24, ss: 35, calc: 1475 },
|
||||
_: { left_text: "", right_text: "mirrorball" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'seven',
|
||||
artist: "Unknown",
|
||||
title: "seven",
|
||||
track: 7,
|
||||
start: { ts: '00:24:35', hh: 0, mm: 24, ss: 35, calc: 1475 },
|
||||
end: { ts: '00:28:07', hh: 0, mm: 28, ss: 7, calc: 1687 },
|
||||
_: { left_text: '', right_text: 'seven' }
|
||||
start: { ts: "00:24:35", hh: 0, mm: 24, ss: 35, calc: 1475 },
|
||||
end: { ts: "00:28:07", hh: 0, mm: 28, ss: 7, calc: 1687 },
|
||||
_: { left_text: "", right_text: "seven" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'august',
|
||||
artist: "Unknown",
|
||||
title: "august",
|
||||
track: 8,
|
||||
start: { ts: '00:28:07', hh: 0, mm: 28, ss: 7, calc: 1687 },
|
||||
end: { ts: '00:32:30', hh: 0, mm: 32, ss: 30, calc: 1950 },
|
||||
_: { left_text: '', right_text: 'august' }
|
||||
start: { ts: "00:28:07", hh: 0, mm: 28, ss: 7, calc: 1687 },
|
||||
end: { ts: "00:32:30", hh: 0, mm: 32, ss: 30, calc: 1950 },
|
||||
_: { left_text: "", right_text: "august" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'this is me trying',
|
||||
artist: "Unknown",
|
||||
title: "this is me trying",
|
||||
track: 9,
|
||||
start: { ts: '00:32:30', hh: 0, mm: 32, ss: 30, calc: 1950 },
|
||||
end: { ts: '00:35:52', hh: 0, mm: 35, ss: 52, calc: 2152 },
|
||||
_: { left_text: '', right_text: 'this is me trying' }
|
||||
start: { ts: "00:32:30", hh: 0, mm: 32, ss: 30, calc: 1950 },
|
||||
end: { ts: "00:35:52", hh: 0, mm: 35, ss: 52, calc: 2152 },
|
||||
_: { left_text: "", right_text: "this is me trying" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'illicit affairs',
|
||||
artist: "Unknown",
|
||||
title: "illicit affairs",
|
||||
track: 10,
|
||||
start: { ts: '00:35:52', hh: 0, mm: 35, ss: 52, calc: 2152 },
|
||||
end: { ts: '00:39:05', hh: 0, mm: 39, ss: 5, calc: 2345 },
|
||||
_: { left_text: '', right_text: 'illicit affairs' }
|
||||
start: { ts: "00:35:52", hh: 0, mm: 35, ss: 52, calc: 2152 },
|
||||
end: { ts: "00:39:05", hh: 0, mm: 39, ss: 5, calc: 2345 },
|
||||
_: { left_text: "", right_text: "illicit affairs" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'invisible strings',
|
||||
artist: "Unknown",
|
||||
title: "invisible strings",
|
||||
track: 11,
|
||||
start: { ts: '00:39:05', hh: 0, mm: 39, ss: 5, calc: 2345 },
|
||||
end: { ts: '00:43:22', hh: 0, mm: 43, ss: 22, calc: 2602 },
|
||||
_: { left_text: '', right_text: 'invisible strings' }
|
||||
start: { ts: "00:39:05", hh: 0, mm: 39, ss: 5, calc: 2345 },
|
||||
end: { ts: "00:43:22", hh: 0, mm: 43, ss: 22, calc: 2602 },
|
||||
_: { left_text: "", right_text: "invisible strings" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'mad woman',
|
||||
artist: "Unknown",
|
||||
title: "mad woman",
|
||||
track: 12,
|
||||
start: { ts: '00:43:22', hh: 0, mm: 43, ss: 22, calc: 2602 },
|
||||
end: { ts: '00:49:30', hh: 0, mm: 49, ss: 30, calc: 2970 },
|
||||
_: { left_text: '', right_text: 'mad woman' }
|
||||
start: { ts: "00:43:22", hh: 0, mm: 43, ss: 22, calc: 2602 },
|
||||
end: { ts: "00:49:30", hh: 0, mm: 49, ss: 30, calc: 2970 },
|
||||
_: { left_text: "", right_text: "mad woman" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'epiphany',
|
||||
artist: "Unknown",
|
||||
title: "epiphany",
|
||||
track: 13,
|
||||
start: { ts: '00:49:30', hh: 0, mm: 49, ss: 30, calc: 2970 },
|
||||
end: { ts: '00:52:17', hh: 0, mm: 52, ss: 17, calc: 3137 },
|
||||
_: { left_text: '', right_text: 'epiphany' }
|
||||
start: { ts: "00:49:30", hh: 0, mm: 49, ss: 30, calc: 2970 },
|
||||
end: { ts: "00:52:17", hh: 0, mm: 52, ss: 17, calc: 3137 },
|
||||
_: { left_text: "", right_text: "epiphany" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'betty',
|
||||
artist: "Unknown",
|
||||
title: "betty",
|
||||
track: 14,
|
||||
start: { ts: '00:52:17', hh: 0, mm: 52, ss: 17, calc: 3137 },
|
||||
end: { ts: '00:57:15', hh: 0, mm: 57, ss: 15, calc: 3435 },
|
||||
_: { left_text: '', right_text: 'betty' }
|
||||
start: { ts: "00:52:17", hh: 0, mm: 52, ss: 17, calc: 3137 },
|
||||
end: { ts: "00:57:15", hh: 0, mm: 57, ss: 15, calc: 3435 },
|
||||
_: { left_text: "", right_text: "betty" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'peace',
|
||||
artist: "Unknown",
|
||||
title: "peace",
|
||||
track: 15,
|
||||
start: { ts: '00:57:15', hh: 0, mm: 57, ss: 15, calc: 3435 },
|
||||
end: { ts: '1:01:10', hh: 1, mm: 1, ss: 10, calc: 3670 },
|
||||
_: { left_text: '', right_text: 'peace' }
|
||||
start: { ts: "00:57:15", hh: 0, mm: 57, ss: 15, calc: 3435 },
|
||||
end: { ts: "1:01:10", hh: 1, mm: 1, ss: 10, calc: 3670 },
|
||||
_: { left_text: "", right_text: "peace" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'hoax',
|
||||
artist: "Unknown",
|
||||
title: "hoax",
|
||||
track: 16,
|
||||
start: { ts: '1:01:10', hh: 1, mm: 1, ss: 10, calc: 3670 },
|
||||
end: { ts: '1:04:50', hh: 1, mm: 4, ss: 50, calc: 3890 },
|
||||
_: { left_text: '', right_text: 'hoax' }
|
||||
start: { ts: "1:01:10", hh: 1, mm: 1, ss: 10, calc: 3670 },
|
||||
end: { ts: "1:04:50", hh: 1, mm: 4, ss: 50, calc: 3890 },
|
||||
_: { left_text: "", right_text: "hoax" },
|
||||
},
|
||||
{
|
||||
artist: 'Unknown',
|
||||
title: 'the lakes',
|
||||
artist: "Unknown",
|
||||
title: "the lakes",
|
||||
track: 17,
|
||||
start: { ts: '1:04:50', hh: 1, mm: 4, ss: 50, calc: 3890 },
|
||||
start: { ts: "1:04:50", hh: 1, mm: 4, ss: 50, calc: 3890 },
|
||||
end: null,
|
||||
_: { left_text: '', right_text: 'the lakes' }
|
||||
}
|
||||
_: { left_text: "", right_text: "the lakes" },
|
||||
},
|
||||
]);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue