🏡 index : github.com/captn3m0/youtube-cue.git

author Nemo <commits@captnemo.in> 2021-07-29 11:11:58.0 +05:30:00
committer GitHub <noreply@github.com> 2021-07-29 11:11:58.0 +05:30:00
commit
610c13fceff3a5154a718421a3f83ff9e3e5821c [patch]
tree
4dae28c41693d4f52a61e276654bc128e647578e
parent
a710658b4980fa77f91c833f3c5465dcbf6c43c6
parent
089c46d9784fb77d558c58c840080f7404ca6d88
download
610c13fceff3a5154a718421a3f83ff9e3e5821c.tar.gz

Parse Duration



Diff

 CHANGELOG.md                 |   2 ++
 README.md                    |  19 ++++++++++++++-----
 index.js                     |  21 +++++++++++++++++++++
 package.json                 |   2 +-
 src/parser.js                | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
 test/parser_test.js          | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 .github/workflows/action.yml |   7 +++++--
 7 files changed, 316 insertions(+), 67 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8d5a37..83118d5 100644
--- a/CHANGELOG.md
+++ a/CHANGELOG.md
@@ -5,6 +5,8 @@
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Tracklists using duration instead of timestamps are now supported.

## 1.0.5 - 2021-07-21
### Changed
diff --git a/README.md b/README.md
index bd50a81..b24b317 100644
--- a/README.md
+++ a/README.md
@@ -8,7 +8,7 @@
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.
`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

@@ -37,11 +37,20 @@
      --help, Show help

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

    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.

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

@@ -66,7 +75,7 @@

## HACKING

- If this video does not work on a specific video, please attach the complete 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

diff --git a/index.js b/index.js
index 6f59533..441756e 100755
--- a/index.js
+++ a/index.js
@@ -1,9 +1,10 @@
#!/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 exit from 'process'

let argv = minimist(process.argv.slice(2), {
  string: 'audio-file'
@@ -22,6 +23,15 @@

    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 --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
@@ -34,6 +44,15 @@
    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",
@@ -41,7 +60,7 @@
    });
    let [artist, album] = res
    artist = (info.videoDetails.media ? info.videoDetails.media.artist : artist)
    let tracks = parse(info.videoDetails.description, {artist})
    let tracks = parse(info.videoDetails.description, {artist, forceTimestamps, forceDurations})
    generate({tracks, artist, audioFile, album}, output_file)
    console.log(`"${output_file}" saved`)
  })
diff --git a/package.json b/package.json
index fcb6eb4..9156a36 100644
--- a/package.json
+++ a/package.json
@@ -1,6 +1,6 @@
{
  "name": "youtube-cue",
  "version": "1.0.5",
  "version": "1.0.6-beta.0",
  "description": "Generates Cue sheet from Youtube URL",
  "main": "index.js",
  "scripts": {
diff --git a/src/parser.js b/src/parser.js
index 10b9c09..5e4b701 100644
--- a/src/parser.js
+++ a/src/parser.js
@@ -21,93 +21,171 @@
 * It is suggested to check their lengths and pick one to parse as the Track 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'
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;
}

// Only picks out lines which have a timestamp in them
var filterTimestamp = function(line) {
  return TS_REGEX.test(line)
  return TS_REGEX.test(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)
  let track = matches.groups["trackl"]
    ? +matches.groups["trackl"]
    : matches.groups["trackr"]
    ? +matches.groups["trackr"]
    : null;
  return {
    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"],
    },
  };
};

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

// 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
}
  obj.title =
    obj._.left_text.length > obj._.right_text.length
      ? obj._.left_text
      : obj._.right_text;
  return 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,
  });
  obj.artist = artist
  obj.title = title
  return obj
  obj.artist = artist;
  obj.title = title;
  return obj;
};

// If track numbers are not present, add them accordingly
var addTrack = function(obj, index) {
  if (obj.track==null) {
    obj.track = index+1
  if (obj.track == null) {
    obj.track = index + 1;
  }
  return obj
}
  return obj;
};

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

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

export function parse (text, options = { artist: 'Unknown' }) {
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)
    .map(addEnd);
}
diff --git a/test/parser_test.js b/test/parser_test.js
index b4c3c12..bb3e592 100644
--- a/test/parser_test.js
+++ a/test/parser_test.js
@@ -5,10 +5,10 @@

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
@@ -65,15 +65,153 @@

  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 `)
      [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" },
      })
      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() {
diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml
index 8779e4d..0a3c917 100644
--- a/.github/workflows/action.yml
+++ a/.github/workflows/action.yml
@@ -1,13 +1,16 @@
on: push
name: Main Workflow
jobs:
  runNpmStuff:
  tests:
    strategy:
      matrix:
        node: ['16', '14', '12']
    name: Run NPM Stuff
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-node@v2
      with:
        node-version: '16'
        node-version: ${{matrix.node}}
    - run: npm install
    - run: npm test