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