├── .gitignore
├── .npmrc
├── lib
├── plugins
│ ├── quoted-title.js
│ ├── remove-file-extension.js
│ ├── common-fluff.js
│ ├── _create-quotes-plugin.js
│ └── base.js
├── fallBackToArtist.js
├── fallBackToTitle.js
├── index.js
└── core.js
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── test
├── fallback-to-artist.js
├── fallback-to-title.js
├── fluff.js
├── separators.js
├── removes-file-extensions.js
├── simple.js
├── jypentertainment.js
└── sub-pop-2014.js
├── index.d.ts
├── .editorconfig
├── cli.js
├── package.json
├── LICENSE
├── CHANGELOG.md
├── README.md
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/lib/plugins/quoted-title.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | module.exports = require('./_create-quotes-plugin')([
5 | '“”',
6 | '""',
7 | '\'\''
8 | ])
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 | ignore:
10 | - dependency-name: chalk
11 | versions:
12 | - ">= 4.a, < 5"
13 |
--------------------------------------------------------------------------------
/test/fallback-to-artist.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | options: { defaultArtist: 'Artist' },
7 | tests: [
8 | { input: 'Title',
9 | expected: ['Artist', 'Title'] }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/test/fallback-to-title.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | options: { defaultTitle: 'Title' },
7 | tests: [
8 | { input: 'Artist',
9 | expected: ['Artist', 'Title'] }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/lib/fallBackToArtist.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | // Create a plugin to fall back to the given artist name when no other plugins
5 | // detected an artist/title combination.
6 | module.exports = function fallBackToArtist (artist) {
7 | return {
8 | split: function (title) {
9 | return [artist, title]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/fallBackToTitle.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | // Create a plugin to fall back to the given title name when no other plugins
5 | // detected an artist/title combination.
6 | module.exports = function fallBackToTitle (title) {
7 | return {
8 | split: function (artist) {
9 | return [artist, title]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | interface GetArtistTitleOptions {
2 | defaultArtist?: string;
3 | defaultTitle?: string;
4 | }
5 |
6 | /**
7 | * Get an artist and song title from a string.
8 | *
9 | * Returns `undefined` if no artist/title pair is detected.
10 | */
11 | declare function getArtistTitle(input: string, options?: GetArtistTitleOptions): [string, string] | undefined
12 |
13 | export = getArtistTitle
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 |
17 | [*.js]
18 | indent_style = space
19 | indent_size = 2
20 |
21 |
22 | [*.md]
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/lib/plugins/remove-file-extension.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var videoExtensions = require('video-extensions')
5 | var audioExtensions = require('audio-extensions')
6 |
7 | var fileExtensions = videoExtensions.concat(audioExtensions)
8 | var fileExtensionRx = new RegExp('\\.(' + fileExtensions.join('|') + ')$', 'i')
9 | function removeFileExtension (title) {
10 | return title.replace(fileExtensionRx, '')
11 | }
12 |
13 | exports.before = removeFileExtension
14 |
--------------------------------------------------------------------------------
/lib/plugins/common-fluff.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var mapTitle = require('../core').mapTitle
5 |
6 | function cleanTitle (title) {
7 | return title
8 | // Sub Pop includes "(not the video)" on audio tracks.
9 | // The " video" part might be stripped by other plugins.
10 | .replace(/\(not the( video)?\)\s*$/, '')
11 | // remove (audio) from title -> https://www.youtube.com/watch?v=vyrFeUsO59E
12 | .replace(/\(audio\)\s*$/i, '')
13 | // Lyrics videos
14 | .replace(/(\s*[-~_/]\s*)?\b(with\s+)?lyrics\s*/i, '')
15 | .replace(/\(\s*(with\s+)?lyrics\s*\)\s*/i, '')
16 | .trim()
17 | }
18 |
19 | exports.after = mapTitle(cleanTitle)
20 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var removeFileExtensionPlugin = require('./plugins/remove-file-extension')
5 | var basePlugin = require('./plugins/base')
6 | var quotedTitlePlugin = require('./plugins/quoted-title')
7 | var cleanFluffPlugin = require('./plugins/common-fluff')
8 |
9 | var core = require('./core')
10 |
11 | function getArtistTitle (str, options) {
12 | return core.getArtistTitle(str, options, [
13 | removeFileExtensionPlugin,
14 | basePlugin,
15 | quotedTitlePlugin,
16 | cleanFluffPlugin
17 | ])
18 | }
19 |
20 | getArtistTitle.fallBackToArtist = require('./fallBackToArtist')
21 | getArtistTitle.fallBackToTitle = require('./fallBackToTitle')
22 |
23 | module.exports = getArtistTitle
24 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* eslint-disable no-var */
4 | 'use strict'
5 |
6 | var getArtistTitle = require('./')
7 |
8 | var help = '\n' +
9 | 'Usage\n' +
10 | ' $ format-artist-title \n' +
11 | '\n' +
12 | 'Example\n' +
13 | ' $ format-artist-title "Ga-In (가인) - Nostalgia (노스텔지아) - Lyrics [Hangul+Translation] .mov"\n' +
14 | ' Ga-In (가인) – Nostalgia (노스텔지아)\n'
15 |
16 | var format = '%artist – %title'
17 | var input = process.argv[2]
18 |
19 | if (input) {
20 | var result = getArtistTitle(input)
21 | if (result) {
22 | console.log(format.replace('%artist', result[0]).replace('%title', result[1]))
23 | } else {
24 | console.error('Could not extract an artist and title.')
25 | process.exit(1)
26 | }
27 | } else {
28 | console.log(help)
29 | process.exit(1)
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "get-artist-title",
3 | "description": "Get the artist and title from a string, eg. a YouTube video title",
4 | "version": "1.3.1",
5 | "author": "Renée Kooi ",
6 | "bin": {
7 | "format-artist-title": "cli.js"
8 | },
9 | "bugs": "https://github.com/goto-bus-stop/get-artist-title/issues",
10 | "dependencies": {
11 | "audio-extensions": "0.0.0",
12 | "video-extensions": "^1.1.0"
13 | },
14 | "devDependencies": {
15 | "chalk": "^2.0.0",
16 | "minimist": "^1.2.0",
17 | "standard": "^17.0.0"
18 | },
19 | "homepage": "https://github.com/goto-bus-stop/get-artist-title",
20 | "keywords": [],
21 | "license": "MIT",
22 | "main": "lib/index.js",
23 | "repository": "goto-bus-stop/get-artist-title",
24 | "scripts": {
25 | "test": "node test",
26 | "lint": "standard"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/fluff.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | tests: [
7 | { input: 'Rush – Moving Pictures (Full Album)',
8 | expected: ['Rush', 'Moving Pictures'] },
9 | { input: 'Rush - Moving Pictures (album)',
10 | expected: ['Rush', 'Moving Pictures'] },
11 | { input: 'Rush - Moving Pictures (Official Album)',
12 | expected: ['Rush', 'Moving Pictures'] },
13 | { input: 'Rush - Moving Pictures (Full Album) (Official)',
14 | expected: ['Rush', 'Moving Pictures'] },
15 | { input: 'FILMMAKER - ETERNAL RETURN [FULL ALBUM]',
16 | expected: ['FILMMAKER', 'ETERNAL RETURN'] },
17 | { input: 'Dua Lipa - New Rules (Official Music Video) **NEW**',
18 | expected: ['Dua Lipa', 'New Rules'] },
19 | { input: 'Muse — The 2nd Law (Full Album) [HD]',
20 | expected: ['Muse', 'The 2nd Law'] },
21 | { input: 'BLESSED ~ Sorrows (Audio)',
22 | expected: ['BLESSED', 'Sorrows'] }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 René Kooi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/test/separators.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | tests: [
7 | // https://www.youtube.com/watch?v=dYnDCHUzzaY
8 | // ":" is a possible separator, but should not be used in this case.
9 | { input: 'HA:TFELT [핫펠트(예은)] "Truth" M/V',
10 | // Ideal would be to include the Hangul but it's in
11 | // [] which means it's deleted for now.
12 | expected: ['HA:TFELT', 'Truth'],
13 | optional: true },
14 | // https://www.youtube.com/watch?v=Qk52ypnGs68
15 | // "-" is a possible separator, but should not be used in this case.
16 | { input: 'T-ARA[티아라] "NUMBER NINE [넘버나인]" M/V',
17 | expected: ['T-ARA', 'NUMBER NINE'],
18 | optional: true },
19 | // https://www.youtube.com/watch?v=aeo_nWsu5cs
20 | { input: '[MV] YOUNHA(윤하) _ Get It?(알아듣겠지) (Feat. HA:TFELT, CHEETAH(치타))',
21 | expected: ['YOUNHA(윤하)', 'Get It?(알아듣겠지) (Feat. HA:TFELT, CHEETAH(치타))'] },
22 | // https://www.youtube.com/watch?v=vyrFeUsO59E
23 | { input: 'BLESSED ~ Sorrows (Audio)',
24 | expected: ['BLESSED', 'Sorrows'] }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: Run tests
8 | strategy:
9 | matrix:
10 | node-version:
11 | - '4.x'
12 | - '6.x'
13 | - '8.x'
14 | - '10.x'
15 | - '12.x'
16 | - '14.x'
17 | - '15.x'
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout sources
21 | uses: actions/checkout@v2
22 | - name: Install Node.js ${{matrix.node-version}}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{matrix.node-version}}
26 | - name: Install dependencies
27 | run: npm install
28 | - name: Run tests
29 | run: npm test
30 |
31 | lint:
32 | name: Standard Style
33 | runs-on: ubuntu-latest
34 | steps:
35 | - name: Checkout sources
36 | uses: actions/checkout@v2
37 | - name: Install Node.js
38 | uses: actions/setup-node@v1
39 | with:
40 | node-version: 14.x
41 | - name: Install dependencies
42 | run: npm install
43 | - name: Check style
44 | run: npm run lint
45 |
--------------------------------------------------------------------------------
/lib/plugins/_create-quotes-plugin.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var mapArtistTitle = require('../core').mapArtistTitle
5 |
6 | module.exports = function (quotes) {
7 | var matchLooseRxes = quotes.map(function (set) {
8 | var open = set[0]
9 | var close = set[1]
10 | return new RegExp(open + '(.*?)' + close)
11 | })
12 |
13 | var matchStartRxes = quotes.map(function (set) {
14 | var open = set[0]
15 | var close = set[1]
16 | return new RegExp('^' + open + '(.*?)' + close + '\\s*')
17 | })
18 |
19 | function split (string) {
20 | for (var i = 0; i < matchLooseRxes.length; i++) {
21 | string = string
22 | .replace(matchLooseRxes[i], function ($0) { return ' ' + $0 + ' ' })
23 | var match = string.match(matchLooseRxes[i])
24 | if (match) {
25 | var split = match.index
26 | var title = string.slice(split)
27 | var artist = string.slice(0, split)
28 | return [artist, title]
29 | }
30 | }
31 | }
32 |
33 | function clean (artistOrTitle) {
34 | return matchStartRxes.reduce(function (string, rx) {
35 | return string.replace(rx, '$1 ')
36 | }, artistOrTitle).trim()
37 | }
38 |
39 | return {
40 | split: split,
41 | after: mapArtistTitle(clean, clean)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/test/removes-file-extensions.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | tests: [
7 | // https://youtu.be/A2RwHnfI2y8
8 | { input: 'Ga-In (가인) - Nostalgia (노스텔지아) - Lyrics [Hangul+Translation] .mov',
9 | expected: ['Ga-In (가인)', 'Nostalgia (노스텔지아)'] },
10 | // https://www.youtube.com/watch?v=PYBuIwuD1DA
11 | { input: 'show me - B-free.m4v',
12 | expected: ['show me', 'B-free'] },
13 | // https://www.youtube.com/watch?v=5hINYNZslP0
14 | { input: '성시경 Sung Si Kyung - 내게 오는 길.mp4',
15 | expected: ['성시경 Sung Si Kyung', '내게 오는 길'] },
16 |
17 | // Things that are NOT file extensions are not removed:
18 | // https://www.youtube.com/watch?v=E2yLg9iW1_0
19 | { input: '에이핑크 - Mr.chu',
20 | expected: ['에이핑크', 'Mr.chu'] },
21 | // https://www.youtube.com/watch?v=P1Oya1PqKFc
22 | { input: 'Far East Movement - Live My Life (Feat. Justin Bieber) cover by J.Fla',
23 | expected: ['Far East Movement', 'Live My Life (Feat. Justin Bieber) cover by J.Fla'] },
24 | // https://www.youtube.com/watch?v=rnQBF2CIygg
25 | // Thing that ends in a file extension without a preceding `.`:
26 | { input: 'Baka Oppai - A Piece Of Toast',
27 | expected: ['Baka Oppai', 'A Piece Of Toast'] }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # get-artist-title change log
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## 1.3.1
8 | * Support `~` as a separator. ([@DaliborTrampota](https://github.com/DaliborTrampota) in [#34](https://github.com/goto-bus-stop/get-artist-title/pull/34))
9 | * Clean (Audio) fluff. ([@DaliborTrampota](https://github.com/DaliborTrampota) in [#34](https://github.com/goto-bus-stop/get-artist-title/pull/34))
10 |
11 | ## 1.3.0
12 | * Add Node.js 12 to CI.
13 | * Clean (Full Album) fluff. ([@jgchk](https://github.com/jgchk) in [#24](https://github.com/goto-bus-stop/get-artist-title/pull/24))
14 |
15 | ## 1.2.0
16 | * Add Node.js 10 to CI.
17 | * Add typescript type definitions.
18 |
19 | ## 1.1.1
20 | * Remove video size names like `1080p`, `4K` from titles.
21 |
22 | ## 1.1.0
23 | * Ignore potential artist/title separators inside quotes.
24 | Previously, a dash inside a quoted song title could cause this bug:
25 | ```js
26 | assert.strictEqual(
27 | oldGetArtistTitle('TWICE(트와이스) "OOH-AHH하게(Like OOH-AHH)"'),
28 | ['TWICE(트와이스) "OOH', 'AHH하게(Like OOH-AHH)"']
29 | )
30 | ```
31 | Now, it correctly identifies the title:
32 | ```js
33 | assert.strictEqual(
34 | getArtistTitle('TWICE(트와이스) "OOH-AHH하게(Like OOH-AHH)"'),
35 | ['TWICE(트와이스)', 'OOH-AHH하게(Like OOH-AHH)']
36 | )
37 | ```
38 |
39 | ## 1.0.0
40 | * Remove custom plugin functionality.
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # get-artist-title
2 | Get the artist and title from a full song name, eg. a YouTube video title.
3 |
4 | ## Installation
5 | ```bash
6 | npm install get-artist-title
7 | ```
8 |
9 | ## Usage
10 |
11 | ### JavaScript
12 |
13 | ```js
14 | const getArtistTitle = require('get-artist-title')
15 | getArtistTitle('Taylor Swift - Out Of The Woods')
16 | //→ ['Taylor Swift', 'Out Of The Woods']
17 |
18 | let [artist, title] = getArtistTitle('FEMM - PoW! (Music Video)')
19 | //→ ['FEMM', 'PoW!']
20 | ```
21 |
22 | ### CLI
23 | ```bash
24 | $ npm install --global get-artist-title
25 |
26 | $ format-artist-title
27 |
28 | Usage
29 | $ format-artist-title
30 |
31 | Example
32 | $ format-artist-title "Ga-In (가인) - Nostalgia (노스텔지아) - Lyrics [Hangul+Translation] .mov"
33 | Ga-In (가인) – Nostalgia (노스텔지아)
34 |
35 | ```
36 |
37 | ## API
38 |
39 | ### `getArtistTitle(string, options={})`
40 |
41 | Extract the artist and title from `string`. Returns an Array with two elements,
42 | `[artist, title]`, or `null` if no artist/title can be found.
43 |
44 | Possible `options` are:
45 |
46 | - `defaultArtist` - Artist name to use if an artist name/song title pair can't
47 | be extracted. The input string, minus any cruft, will be used as the song
48 | title.
49 | - `defaultTitle` - Song title to use if an artist name/song title pair can't
50 | be extracted. The input string, minus any cruft, will be used as the artist
51 | name.
52 |
53 | It's useful to provide defaults if you're passing strings from an external
54 | service. For example, a YouTube video title may not always contain the artist
55 | name, but the name of the channel that uploaded it might be relevant.
56 |
57 | ```js
58 | const [artist, title] = getArtistTitle('[MV] A Brand New Song!', {
59 | defaultArtist: 'Channel Name'
60 | })
61 | // → ['Channel Name', 'A Brand New Song!']
62 | ```
63 |
64 | ```js
65 | // Assuming `video` is a Video resource from the YouTube Data API:
66 | const [artist, title] = getArtistTitle(video.snippet.title, {
67 | defaultArtist: video.snippet.channelTitle
68 | })
69 | ```
70 |
71 | ## Licence
72 |
73 | [MIT](./LICENSE)
74 |
--------------------------------------------------------------------------------
/test/simple.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | tests: [
7 | // https://youtu.be/hn4EIv1-uz0
8 | { input: 'Boats & BIrds - Gregory & the Hawk',
9 | expected: ['Boats & BIrds', 'Gregory & the Hawk'] },
10 | // https://youtu.be/JoC3PUBmhFs
11 | { input: 'Sum 41 - In Too Deep (Official Video)',
12 | expected: ['Sum 41', 'In Too Deep'] },
13 | // Punctuation mark at the end
14 | // https://youtu.be/fz3jLeDvpu4
15 | { input: 'FEMM - PoW! (Music Video)',
16 | expected: ['FEMM', 'PoW!'] },
17 | // Song with a separator in its name (-), and unparenthesised "official video"
18 | // https://youtu.be/ti1W7Zu8j9k
19 | { input: 'The Wombats - Anti-D Official Video',
20 | expected: ['The Wombats', 'Anti-D'] },
21 | // Words containing "…ver" should not be removed--only standalone "ver(.)".
22 | { input: '4MINUTE – Whatever',
23 | expected: ['4MINUTE', 'Whatever'] },
24 | { input: '4MINUTE – Whatever (Test Ver)',
25 | expected: ['4MINUTE', 'Whatever'] },
26 | // Quoted song title
27 | // https://www.youtube.com/watch?v=VVF0zxw4tuM
28 | { input: 'Low Roar - "Half Asleep"',
29 | expected: ['Low Roar', 'Half Asleep'] },
30 | // Quoted song title _and_ "official video"
31 | // https://www.youtube.com/watch?v=qsWl1--Niyg
32 | { input: '4MINUTE - \'Volume Up\' (Official Music Video)',
33 | expected: ['4MINUTE', 'Volume Up'] },
34 | // Things with punctuation in front
35 | // https://www.youtube.com/watch?v=zsF5y1XhGuA
36 | { input: '...AND YOU WILL KNOW US BY THE TRAIL OF DEAD - Summer Of All Dead Souls',
37 | expected: ['...AND YOU WILL KNOW US BY THE TRAIL OF DEAD', 'Summer Of All Dead Souls'] },
38 | // File extensions _and_ "official video"
39 | // https://www.youtube.com/watch?v=ZPjwdiD24Kg
40 | { input: 'Low Roar - Give Up (Official Video).mov',
41 | expected: ['Low Roar', 'Give Up'] },
42 | // A separator with _only_ fluff like "MV" on one side
43 | // https://www.youtube.com/watch?v=yRMvzyN-__Q
44 | { input: 'MV_Planet Shiver_Rainbow [feat. Crush]',
45 | expected: ['Planet Shiver', 'Rainbow'] },
46 | // "Official MV"
47 | // https://www.youtube.com/watch?v=qSKPj--tyiM
48 | { input: '임정희 Lim Jeong Hee - I.O.U Official MV',
49 | expected: ['임정희 Lim Jeong Hee', 'I.O.U'] },
50 | // 4K, see https://github.com/goto-bus-stop/get-artist-title/issues/20
51 | { input: 'Big Limit [@RealBigLimit] - Samurai Jack [Music Video] (4K)',
52 | expected: ['Big Limit', 'Samurai Jack'] },
53 | // sizes like 720p
54 | { input: 'Big Limit [@RealBigLimit] - Samurai Jack [Music Video] (720p)',
55 | expected: ['Big Limit', 'Samurai Jack'] }
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/lib/core.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var fallBackToArtist = require('./fallBackToArtist')
5 | var fallBackToTitle = require('./fallBackToTitle')
6 |
7 | // Like `compose`, but left-to-right.
8 | // flow([f, g])(x) = g(f(x))
9 | function flow (functions) {
10 | if (!functions.length) {
11 | return function (arg) { return arg }
12 | }
13 |
14 | return function () {
15 | var result = functions[0].apply(this, arguments)
16 | for (var i = 1; i < functions.length; i++) {
17 | result = functions[i](result)
18 | }
19 | return result
20 | }
21 | }
22 |
23 | // Return the result of the first splitter function that matches.
24 | function combineSplitters (splitters) {
25 | var l = splitters.length
26 | return function (str) {
27 | for (var i = 0; i < l; i++) {
28 | var result = splitters[i](str)
29 | if (result) return result
30 | }
31 | }
32 | }
33 |
34 | // Combine multiple plugins into a single plugin.
35 | function reducePlugins (plugins) {
36 | var before = []
37 | var split = []
38 | var after = []
39 | plugins.forEach(function (plugin) {
40 | if (plugin.before) before.push(plugin.before)
41 | if (plugin.split) split.push(plugin.split)
42 | if (plugin.after) after.push(plugin.after)
43 | })
44 | return {
45 | before: flow(before),
46 | split: combineSplitters(split),
47 | after: flow(after)
48 | }
49 | }
50 |
51 | // Helpful-ish plugin checks
52 | function checkPlugin (plugin) {
53 | if (plugin.split.length === 0) {
54 | throw new Error('no title splitter was specified by any plugin')
55 | }
56 | }
57 |
58 | function mapArtist (fn) {
59 | return function (parts) {
60 | return [fn(parts[0]), parts[1]]
61 | }
62 | }
63 |
64 | function mapTitle (fn) {
65 | return function (parts) {
66 | return [parts[0], fn(parts[1])]
67 | }
68 | }
69 |
70 | function mapArtistTitle (mapArtist, mapTitle) {
71 | return function (parts) {
72 | return [mapArtist(parts[0]), mapTitle(parts[1])]
73 | }
74 | }
75 |
76 | // Get an artist name and song title from a string.
77 | function getSongArtistTitle (str, options, plugins) {
78 | if (options) {
79 | if (options.defaultArtist) {
80 | plugins.push(fallBackToArtist(options.defaultArtist))
81 | }
82 | if (options.defaultTitle) {
83 | plugins.push(fallBackToTitle(options.defaultTitle))
84 | }
85 | }
86 |
87 | var plugin = reducePlugins(plugins)
88 |
89 | checkPlugin(plugin)
90 |
91 | var split = plugin.split(plugin.before(str))
92 | if (!split) return
93 |
94 | return plugin.after(split)
95 | }
96 |
97 | exports.combineSplitters = combineSplitters
98 | exports.mapArtist = mapArtist
99 | exports.mapTitle = mapTitle
100 | exports.mapArtistTitle = mapArtistTitle
101 | exports.combinePlugins = reducePlugins
102 | exports.getArtistTitle = getSongArtistTitle
103 |
--------------------------------------------------------------------------------
/test/jypentertainment.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | // K-pop song titles taken from JYP entertainment's Most Popular page:
7 | // https://www.youtube.com/user/jypentertainment/videos?flow=grid&view=0&sort=p
8 | // jypentertainment uses a title format like:
9 | // Artist "Title" Fluff
10 | tests: [
11 | // This one is not so fun because there is a separator (-) inside the quotes,
12 | // and separators are usually tried first.
13 | { input: 'TWICE(트와이스) "OOH-AHH하게(Like OOH-AHH)" M/V',
14 | expected: ['TWICE(트와이스)', 'OOH-AHH하게(Like OOH-AHH)'] },
15 | { input: 'GOT7 "Just right(딱 좋아)" M/V',
16 | expected: ['GOT7', 'Just right(딱 좋아)'] },
17 | { input: 'miss A “Only You(다른 남자 말고 너)” M/V',
18 | expected: ['miss A', 'Only You(다른 남자 말고 너)'] },
19 | { input: 'GOT7 "If You Do(니가 하면)" M/V',
20 | expected: ['GOT7', 'If You Do(니가 하면)'] },
21 | { input: 'GOT7 "A" M/V',
22 | expected: ['GOT7', 'A'] },
23 | { input: 'J.Y. Park(박진영) "Who\'s your mama?(어머님이 누구니) (feat. Jessi)" M/V',
24 | expected: ['J.Y. Park(박진영)', 'Who\'s your mama?(어머님이 누구니) (feat. Jessi)'] },
25 | { input: 'GOT7 "Girls Girls Girls" M/V',
26 | expected: ['GOT7', 'Girls Girls Girls'] },
27 | { input: 'GOT7 “Stop stop it(하지하지마)” M/V',
28 | expected: ['GOT7', 'Stop stop it(하지하지마)'] },
29 | { input: '2PM “GO CRAZY!(미친거 아니야?)” M/V',
30 | expected: ['2PM', 'GO CRAZY!(미친거 아니야?)'] },
31 | { input: '2PM "A.D.T.O.Y.(하.니.뿐.)" M/V',
32 | expected: ['2PM', 'A.D.T.O.Y.(하.니.뿐.)'] },
33 | { input: '2PM “My House(우리집)” M/V',
34 | expected: ['2PM', 'My House(우리집)'] },
35 | { input: 'GOT7 “Fly” M/V',
36 | expected: ['GOT7', 'Fly'] },
37 | { input: 'Wonder Girls "I Feel You" M/V',
38 | expected: ['Wonder Girls', 'I Feel You'] },
39 | { input: 'GOT7 "I Like You(난 니가 좋아)" Dance Practice',
40 | expected: ['GOT7', 'I Like You(난 니가 좋아)'] },
41 | { input: 'GOT7 "Just right(딱 좋아)" Dance Practice #2 (Just Crazy Boyfriend Ver.)',
42 | expected: ['GOT7', 'Just right(딱 좋아)'] },
43 | { input: 'GOT7 "Stop stop it(하지하지마)" Dance Practice',
44 | expected: ['GOT7', 'Stop stop it(하지하지마)'] },
45 | { input: '2PM "Comeback When You Hear This Song(이 노래를 듣고 돌아와)" M/V',
46 | expected: ['2PM', 'Comeback When You Hear This Song(이 노래를 듣고 돌아와)'] },
47 | { input: 'Sunmi(선미) "Full Moon(보름달)" M/V',
48 | expected: ['Sunmi(선미)', 'Full Moon(보름달)'] },
49 | { input: 'GOT7 "Magnetic(너란 걸)" Dance Practice',
50 | expected: ['GOT7', 'Magnetic(너란 걸)'] },
51 | { input: 'miss A "Only You(다른 남자 말고 너)" Dance Practice',
52 | expected: ['miss A', 'Only You(다른 남자 말고 너)'] },
53 | { input: 'Baek A Yeon(백아연) “Shouldn’t Have…(이럴거면 그러지말지) (Feat. Young K)” M/V',
54 | expected: ['Baek A Yeon(백아연)', 'Shouldn’t Have…(이럴거면 그러지말지) (Feat. Young K)'] },
55 | { input: 'DAY6 "Congratulations" M/V',
56 | expected: ['DAY6', 'Congratulations'] },
57 | { input: 'Wonder Girls "Tell me" M/V',
58 | expected: ['Wonder Girls', 'Tell me'] },
59 | { input: 'GOT7 "Confession Song(고백송)" M/V',
60 | expected: ['GOT7', 'Confession Song(고백송)'] },
61 | { input: 'GOT7 "Stop stop it(하지하지마)" Dance Practice #2 (Crazy Boyfriend Ver.)',
62 | expected: ['GOT7', 'Stop stop it(하지하지마)'] },
63 | { input: 'GOT7 "A" Dance Practice',
64 | expected: ['GOT7', 'A'] },
65 | { input: 'GOT7 "Girls Girls Girls" Dance Practice #2',
66 | expected: ['GOT7', 'Girls Girls Girls'] },
67 | { input: 'GOT7 "If You Do(니가 하면)" Dance Practice',
68 | expected: ['GOT7', 'If You Do(니가 하면)'] }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var fs = require('fs')
5 | var chalk = require('chalk')
6 | var args = require('minimist')(process.argv.slice(2))
7 |
8 | var getArtistTitle = require('./')
9 |
10 | function stringifyTitle (o) {
11 | return o ? '"' + o[0] + '" - "' + o[1] + '"' : 'nothing'
12 | }
13 |
14 | function testFailed (test, result) {
15 | console.error(chalk.red(' ✖ expected ' + stringifyTitle(test.expected)))
16 | console.error(chalk.red(' but got ' + stringifyTitle(result)))
17 | }
18 | function optionalTestFailed (test, result) {
19 | console.error(chalk.yellow(' ⚠ expected ' + stringifyTitle(test.expected)))
20 | console.error(chalk.yellow(' but got ' + stringifyTitle(result)))
21 | }
22 | function testSucceeded (test, result) {
23 | console.log(chalk.green(' ✔ got ' + stringifyTitle(result)))
24 | }
25 |
26 | function runTest (options, test) {
27 | console.log(' ' + test.input)
28 | var result = getArtistTitle(test.input, options)
29 | if (!result || result[0] !== test.expected[0] || result[1] !== test.expected[1]) {
30 | if (test.optional && !args.strict) {
31 | optionalTestFailed(test, result)
32 | return 'optionalFail'
33 | } else {
34 | testFailed(test, result)
35 | return 'fail'
36 | }
37 | }
38 | testSucceeded(test, result)
39 | return 'success'
40 | }
41 |
42 | function runSuite (suite) {
43 | var score = { fail: 0, optionalFail: 0, success: 0 }
44 | suite.tests
45 | .map(runTest.bind(null, suite.options || {}))
46 | .forEach(function (result) {
47 | score[result]++
48 | })
49 | return score
50 | }
51 |
52 | function readSuite (suiteName) {
53 | var suite = require('./test/' + suiteName)
54 | return {
55 | name: suiteName,
56 | options: suite.options,
57 | tests: suite.tests
58 | }
59 | }
60 |
61 | function getMaxLength (strs) {
62 | return strs.reduce(function (max, str) {
63 | return max > str.length ? max : str.length
64 | }, 0)
65 | }
66 |
67 | function padTo (str, len, ch) {
68 | while (str.length < len) str += ch || ' '
69 | return str
70 | }
71 |
72 | var suites = fs.readdirSync('test').filter(function (name) {
73 | return /\.js$/.test(name)
74 | })
75 |
76 | if (args.grep) {
77 | suites = suites.filter(function (name) {
78 | return name.indexOf(args.grep) !== -1
79 | })
80 | }
81 |
82 | var total = { fail: 0, optionalFail: 0, success: 0 }
83 | var results = suites.map(function (suiteName) {
84 | var suite = readSuite(suiteName)
85 |
86 | var title = suiteName.replace(/\.js$/, '')
87 | console.log(title)
88 | console.log(title.replace(/./g, '-'))
89 |
90 | var result = runSuite(suite)
91 | console.log(
92 | chalk.red(' ✖ ' + result.fail) + ' ' +
93 | chalk.yellow(' ⚠ ' + result.optionalFail) + ' ' +
94 | chalk.green(' ✔ ' + result.success)
95 | )
96 |
97 | total.fail += result.fail
98 | total.optionalFail += result.optionalFail
99 | total.success += result.success
100 |
101 | result.name = title
102 | return result
103 | })
104 |
105 | console.log('')
106 | console.log('summary')
107 | console.log('-------')
108 |
109 | var maxL = getMaxLength(results.map(function (r) {
110 | return r.name
111 | }))
112 | results.forEach(function (result) {
113 | console.log(
114 | padTo(result.name, maxL) + ' | ' +
115 | chalk.red(' ✖ ' + padTo('' + result.fail, 3)) + ' ' +
116 | chalk.yellow(' ⚠ ' + padTo('' + result.optionalFail, 3)) + ' ' +
117 | chalk.green(' ✔ ' + padTo('' + result.success, 3))
118 | )
119 | })
120 |
121 | console.log('-------')
122 | console.log(
123 | padTo('', maxL + 2),
124 | chalk.red(' ✖ ' + padTo('' + total.fail, 3)) + ' ' +
125 | chalk.yellow(' ⚠ ' + padTo('' + total.optionalFail, 3)) + ' ' +
126 | chalk.green(' ✔ ' + padTo('' + total.success, 3))
127 | )
128 |
129 | if (total.fail > 0) {
130 | process.exit(1)
131 | }
132 |
--------------------------------------------------------------------------------
/lib/plugins/base.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | var mapArtistTitle = require('../core').mapArtistTitle
5 |
6 | var separators = [
7 | ' -- ',
8 | '--',
9 | ' - ',
10 | ' – ',
11 | ' — ',
12 | ' _ ',
13 | '-',
14 | '–',
15 | '—',
16 | ':',
17 | '|',
18 | '///',
19 | ' / ',
20 | '_',
21 | '/',
22 | '~'
23 | ]
24 |
25 | // Most of this is taken from the YouTube connector in David Šabata's Last.fm
26 | // web scrobbler: https://github.com/david-sabata/web-scrobbler
27 |
28 | // Remove various versions of "MV" and "PV" markers
29 | function cleanMVPV (string) {
30 | return string
31 | .replace(/\s*\[\s*(?:off?icial\s+)?([PM]\/?V)\s*]/i, '') // [MV] or [M/V]
32 | .replace(/\s*\(\s*(?:off?icial\s+)?([PM]\/?V)\s*\)/i, '') // (MV) or (M/V)
33 | .replace(/\s*【\s*(?:off?icial\s+)?([PM]\/?V)\s*】/i, '') // 【MV】 or 【M/V】
34 | .replace(/[\s\-–_]+(?:off?icial\s+)?([PM]\/?V)\s*/i, '') // MV or M/V at the end
35 | .replace(/(?:off?icial\s+)?([PM]\/?V)[\s\-–_]+/, '') // MV or M/V at the start
36 | }
37 |
38 | function cleanFluff (string) {
39 | return cleanMVPV(string)
40 | .replace(/\s*\[[^\]]+]$/, '') // [whatever] at the end
41 | .replace(/^\s*\[[^\]]+]\s*/, '') // [whatever] at the start
42 | .replace(/\s*\([^)]*\bver(\.|sion)?\s*\)$/i, '') // (whatever version)
43 | .replace(/\s*[a-z]*\s*\bver(\.|sion)?$/i, '') // ver. and 1 word before (no parens)
44 | .replace(/\s*(of+icial\s*)?(music\s*)?video/i, '') // (official)? (music)? video
45 | .replace(/\s*(full\s*)?album/i, '') // (full)? album
46 | .replace(/\s*(ALBUM TRACK\s*)?(album track\s*)/i, '') // (ALBUM TRACK)
47 | .replace(/\s*\(\s*of+icial\s*\)/i, '') // (official)
48 | .replace(/\s*\(\s*[0-9]{4}\s*\)/i, '') // (1999)
49 | .replace(/\s+\(\s*(HD|HQ|[0-9]{3,4}p|4K)\s*\)$/, '') // (HD) (HQ) (1080p) (4K)
50 | .replace(/[\s\-–_]+(HD|HQ|[0-9]{3,4}p|4K)\s*$/, '') // - HD - HQ - 720p - 4K
51 | }
52 |
53 | function cleanTitle (title) {
54 | return cleanFluff(title.trim())
55 | .replace(/\s*\*+\s?\S+\s?\*+$/, '') // **NEW**
56 | .replace(/\s*video\s*clip/i, '') // video clip
57 | .replace(/\s+\(?live\)?$/i, '') // live
58 | .replace(/\(\s*\)/, '') // Leftovers after e.g. (official video)
59 | .replace(/\[\s*]/, '') // Leftovers after e.g. [1080p]
60 | .replace(/【\s*】/, '') // Leftovers after e.g. 【MV】
61 | .replace(/^(|.*\s)"(.*)"(\s.*|)$/, '$2') // Artist - The new "Track title" featuring someone
62 | .replace(/^(|.*\s)'(.*)'(\s.*|)$/, '$2') // 'Track title'
63 | .replace(/^[/\s,:;~\-–_\s"]+/, '') // trim starting white chars and dash
64 | .replace(/[/\s,:;~\-–_\s"]+$/, '') // trim trailing white chars and dash
65 | }
66 |
67 | function cleanArtist (artist) {
68 | return cleanFluff(artist.trim())
69 | .replace(/\s*[0-1][0-9][0-1][0-9][0-3][0-9]\s*/, '') // date formats ex. 130624
70 | .replace(/^[/\s,:;~\-–_\s"]+/, '') // trim starting white chars and dash
71 | .replace(/[/\s,:;~\-–_\s"]+$/, '') // trim trailing white chars and dash
72 | }
73 |
74 | function inQuotes (str, idx) {
75 | var openChars = '([{«'
76 | var closeChars = ')]}»'
77 | var toggleChars = '"\''
78 | var open = {
79 | ')': 0,
80 | ']': 0,
81 | '}': 0,
82 | '»': 0, // eslint-disable-line quote-props
83 | '"': 0,
84 | '\'': 0
85 | }
86 | for (var i = 0; i < idx; i++) {
87 | var index = openChars.indexOf(str[i])
88 | if (index !== -1) {
89 | open[closeChars[index]]++
90 | } else if (closeChars.indexOf(str[i]) !== -1 && open[str[i]] > 0) {
91 | open[str[i]]--
92 | } if (toggleChars.indexOf(str[i]) !== -1) {
93 | open[str[i]] = 1 - open[str[i]]
94 | }
95 | }
96 |
97 | return Object.keys(open).reduce(function (acc, k) { return acc + open[k] }, 0) > 0
98 | }
99 |
100 | function splitArtistTitle (str) {
101 | for (var i = 0, l = separators.length; i < l; i++) {
102 | var sep = separators[i]
103 | var idx = str.indexOf(sep)
104 | if (idx > -1 && !inQuotes(str, idx)) {
105 | return [str.slice(0, idx), str.slice(idx + sep.length)]
106 | }
107 | }
108 | }
109 |
110 | exports.separators = separators
111 | exports.before = cleanFluff
112 | exports.split = splitArtistTitle
113 | exports.after = mapArtistTitle(cleanArtist, cleanTitle)
114 |
--------------------------------------------------------------------------------
/test/sub-pop-2014.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | 'use strict'
3 |
4 | /* eslint-disable object-curly-newline */
5 | module.exports = {
6 | // everything from https://www.youtube.com/playlist?list=PLbOGt8cOph4Xm92N3mrb0epYxnnuYoyiX :)
7 | tests: [
8 | { input: 'clipping. - Inside Out [OFFICIAL VIDEO]',
9 | expected: ['clipping.', 'Inside Out'] },
10 | { input: 'King Tuff - Black Moon Spell [OFFICIAL VIDEO]',
11 | expected: ['King Tuff', 'Black Moon Spell'] },
12 | { input: 'Sleater-Kinney - Bury Our Friends (feat. Miranda July)',
13 | expected: ['Sleater-Kinney', 'Bury Our Friends (feat. Miranda July)'] },
14 | { input: 'Pissed Jeans - Boring Girls [OFFICIAL VIDEO]',
15 | expected: ['Pissed Jeans', 'Boring Girls'] },
16 | { input: 'Father John Misty - Chateau Lobby #4 (in C for Two Virgins) [OFFICIAL VIDEO]',
17 | expected: ['Father John Misty', 'Chateau Lobby #4 (in C for Two Virgins)'] },
18 | { input: 'Chad VanGaalen - Monster [OFFICIAL VIDEO]',
19 | expected: ['Chad VanGaalen', 'Monster'] },
20 | { input: 'Luluc - Tangled Heart [OFFICIAL VIDEO]',
21 | expected: ['Luluc', 'Tangled Heart'] },
22 | { input: 'J Mascis - Every Morning [OFFICIAL VIDEO]',
23 | expected: ['J Mascis', 'Every Morning'] },
24 | { input: 'THEESatisfaction - Recognition (not the video)',
25 | expected: ['THEESatisfaction', 'Recognition'] },
26 | { input: 'Goat - Hide from the Sun [OFFICIAL VIDEO]',
27 | expected: ['Goat', 'Hide from the Sun'] },
28 | { input: 'Shabazz Palaces - Motion Sickness [OFFICIAL VIDEO]',
29 | expected: ['Shabazz Palaces', 'Motion Sickness'] },
30 | { input: 'The Afghan Whigs - Lost in the Woods [OFFICIAL VIDEO]',
31 | expected: ['The Afghan Whigs', 'Lost in the Woods'] },
32 | { input: 'The Head and the Heart - Another Story [OFFICIAL VIDEO]',
33 | expected: ['The Head and the Heart', 'Another Story'] },
34 | { input: 'Washed Out - Weightless [OFFICIAL VIDEO]',
35 | expected: ['Washed Out', 'Weightless'] },
36 | { input: 'Deaf Wish - Cool Comment [OFFICIAL VIDEO]',
37 | expected: ['Deaf Wish', 'Cool Comment'] },
38 | { input: 'Father John Misty - Bored In The USA',
39 | expected: ['Father John Misty', 'Bored In The USA'] },
40 | { input: 'Flake Music - Spanway Hits (not the video)',
41 | expected: ['Flake Music', 'Spanway Hits'] },
42 | { input: 'Sleater-Kinney - Surface Envy',
43 | expected: ['Sleater-Kinney', 'Surface Envy'] },
44 | { input: 'clipping. - Get Up [OFFICIAL VIDEO]',
45 | expected: ['clipping.', 'Get Up'] },
46 | { input: 'King Tuff - Headbanger',
47 | expected: ['King Tuff', 'Headbanger'] },
48 | { input: 'Shabazz Palaces - #CAKE [OFFICIAL VIDEO]',
49 | expected: ['Shabazz Palaces', '#CAKE'] },
50 | { input: 'The Afghan Whigs - Every Little Thing She Does is Magic (cover of The Police)',
51 | expected: ['The Afghan Whigs', 'Every Little Thing She Does is Magic (cover of The Police)'] },
52 | { input: 'Mirel Wagner - The Dirt [OFFICIAL VIDEO]',
53 | expected: ['Mirel Wagner', 'The Dirt'] },
54 | { input: 'Avi Buffalo - Memories of You (not the video)',
55 | expected: ['Avi Buffalo', 'Memories of You'] },
56 | { input: 'Goat - Words (not the video)',
57 | expected: ['Goat', 'Words'] },
58 | { input: 'clipping. - Story 2 [OFFICIAL VIDEO]',
59 | expected: ['clipping.', 'Story 2'] },
60 | { input: 'Shabazz Palaces - #CAKE (Animal Collective Premature Deflirt Mix)',
61 | expected: ['Shabazz Palaces', '#CAKE (Animal Collective Premature Deflirt Mix)'] },
62 | { input: 'Flake Music - The Shins',
63 | expected: ['Flake Music', 'The Shins'] },
64 | { input: 'J Mascis - Wide Awake (not the video)',
65 | expected: ['J Mascis', 'Wide Awake'] },
66 | { input: 'The Head and the Heart - Let\'s Be Still [OFFICIAL VIDEO]',
67 | expected: ['The Head and the Heart', 'Let\'s Be Still'] },
68 | { input: 'Luluc - Small Window [OFFICIAL VIDEO]',
69 | expected: ['Luluc', 'Small Window'] },
70 | { input: 'Lyla Foy - Honeymoon [OFFICIAL VIDEO]',
71 | expected: ['Lyla Foy', 'Honeymoon'] },
72 | { input: 'Avi Buffalo - So What [OFFICIAL VIDEO]',
73 | expected: ['Avi Buffalo', 'So What'] },
74 | { input: 'clipping. - Body & Blood [CENSORED VERSION of OFFICIAL VIDEO]',
75 | expected: ['clipping.', 'Body & Blood'] },
76 | { input: 'King Tuff - Eyes of the Muse (not the video)',
77 | expected: ['King Tuff', 'Eyes of the Muse'] },
78 | { input: 'Deaf Wish - St Vincent\'s',
79 | expected: ['Deaf Wish', 'St Vincent\'s'] },
80 | { input: 'The Afghan Whigs - Matamoros [OFFICIAL VIDEO]',
81 | expected: ['The Afghan Whigs', 'Matamoros'] },
82 | { input: 'Dum Dum Girls - Too True To Be Good [OFFICIAL VIDEO]',
83 | expected: ['Dum Dum Girls', 'Too True To Be Good'] },
84 | { input: 'J Mascis - Fade Into You (a Mazzy Star cover)',
85 | expected: ['J Mascis', 'Fade Into You (a Mazzy Star cover)'] },
86 | { input: 'Shabazz Palaces - Forerunner Foray',
87 | expected: ['Shabazz Palaces', 'Forerunner Foray'] },
88 | { input: 'Lee Bains III & The Glory Fires - The Company Man [OFFICIAL VIDEO]',
89 | expected: ['Lee Bains III & The Glory Fires', 'The Company Man'] },
90 | { input: 'The Postal Service - Nothing Better [LIVE]',
91 | expected: ['The Postal Service', 'Nothing Better'] },
92 | { input: 'Rose Windows - There is a Light [OFFICIAL VIDEO]',
93 | expected: ['Rose Windows', 'There is a Light'] },
94 | { input: 'Mirel Wagner - Oak Tree [OFFICIAL VIDEO]',
95 | expected: ['Mirel Wagner', 'Oak Tree'] },
96 | { input: 'clipping. - Work Work (feat. Cocc Pistol Cree) [OFFICIAL VIDEO]',
97 | expected: ['clipping.', 'Work Work (feat. Cocc Pistol Cree)'] },
98 | { input: 'Dum Dum Girls - Rimbaud Eyes [OFFICIAL VIDEO]',
99 | expected: ['Dum Dum Girls', 'Rimbaud Eyes'] },
100 | { input: 'The Head and the Heart - Summertime [OFFICIAL VIDEO]',
101 | expected: ['The Head and the Heart', 'Summertime'] },
102 | { input: 'Shabazz Palaces - They Come In Gold (not the video)',
103 | expected: ['Shabazz Palaces', 'They Come In Gold'] },
104 | { input: 'Lee Bains III & The Glory Fires - The Weeds Downtown (not the video)',
105 | expected: ['Lee Bains III & The Glory Fires', 'The Weeds Downtown'] },
106 | { input: 'Constantines - Young Lions (not the video)',
107 | expected: ['Constantines', 'Young Lions'] },
108 | { input: 'The Afghan Whigs - The Lottery (not the video)',
109 | expected: ['The Afghan Whigs', 'The Lottery'] },
110 | { input: 'Luluc - Without a Face',
111 | expected: ['Luluc', 'Without a Face'] },
112 | { input: 'Lyla Foy - Feather Tongue [OFFICIAL VIDEO]',
113 | expected: ['Lyla Foy', 'Feather Tongue'] },
114 | { input: 'Dum Dum Girls - Are You Okay? [OFFICIAL SHORT FILM VIDEO]',
115 | expected: ['Dum Dum Girls', 'Are You Okay?'] },
116 | { input: 'THUMPERS - Lungs (Originally Performed by Chvrches) [not the video]',
117 | expected: ['THUMPERS', 'Lungs (Originally Performed by Chvrches)'] },
118 | { input: 'Washed Out - All I Know [Moby Remix] (not the video)',
119 | expected: ['Washed Out', 'All I Know [Moby Remix]'] },
120 | { input: 'Goat - Dreambuilding (not the video)',
121 | expected: ['Goat', 'Dreambuilding'] },
122 | { input: 'Lee Bains III & The Glory Fires - The Company Man (not the video)',
123 | expected: ['Lee Bains III & The Glory Fires', 'The Company Man'] },
124 | { input: 'The Afghan Whigs - Algiers [OFFICIAL VIDEO]',
125 | expected: ['The Afghan Whigs', 'Algiers'] },
126 | { input: 'Shearwater - Black Is The Color (from the "Colors" episode of Radiolab)',
127 | expected: ['Shearwater', 'Black Is The Color (from the "Colors" episode of Radiolab)'],
128 | optional: true },
129 | { input: 'The Notwist - Kong [OFFICIAL VIDEO]',
130 | expected: ['The Notwist', 'Kong'] },
131 | { input: 'Death Vessel - Mercury Dime [OFFICIAL VIDEO]',
132 | expected: ['Death Vessel', 'Mercury Dime'] },
133 | { input: 'The Ruby Suns - Desert Of Pop [OFFICIAL VIDEO]',
134 | expected: ['The Ruby Suns', 'Desert Of Pop'] },
135 | { input: 'THUMPERS - Unkinder (A Tougher Love) [OFFICIAL VIDEO]',
136 | expected: ['THUMPERS', 'Unkinder (A Tougher Love)'] },
137 | { input: 'Chad VanGaalen - Where Are You?',
138 | expected: ['Chad VanGaalen', 'Where Are You?'] },
139 | { input: 'Mogwai - The Lord Is Out Of Control [OFFICIAL VIDEO]',
140 | expected: ['Mogwai', 'The Lord Is Out Of Control'] },
141 | { input: 'Lyla Foy - Impossible [OFFICIAL PERFORMANCE VIDEO]',
142 | expected: ['Lyla Foy', 'Impossible'] },
143 | { input: 'Death Vessel - Ilsa Drown (feat. Jónsi) [not the video]',
144 | expected: ['Death Vessel', 'Ilsa Drown (feat. Jónsi)'] },
145 | { input: 'The Notwist - Close To The Glass (not the video)',
146 | expected: ['The Notwist', 'Close To The Glass'] },
147 | { input: 'Dum Dum Girls - Lost Boys And Girls Club [OFFICIAL VIDEO]',
148 | expected: ['Dum Dum Girls', 'Lost Boys And Girls Club'] }
149 | ]
150 | }
151 |
--------------------------------------------------------------------------------