pax_global_header00006660000000000000000000000064126227427170014524gustar00rootroot0000000000000052 comment=a1e5efa0d5af13d89856b292cd6f5d95857beff3 node-music-library-index-2.1.0/000077500000000000000000000000001262274271700163365ustar00rootroot00000000000000node-music-library-index-2.1.0/.gitignore000066400000000000000000000000161262274271700203230ustar00rootroot00000000000000/node_modules node-music-library-index-2.1.0/.jshintrc000066400000000000000000000070671262274271700201750ustar00rootroot00000000000000{ // Settings "passfail" : false, // Stop on first error. "maxerr" : 100, // Maximum errors before stopping. // Predefined globals whom JSHint will ignore. "browser" : false, // Standard browser globals e.g. `window`, `document`. "predef" : [ // extra globals "describe", "beforeEach", "afterEach", "it" ], "node" : true, "rhino" : false, "couch" : false, "wsh" : false, // Windows Scripting Host. "jquery" : false, "prototypejs" : false, "mootools" : false, "dojo" : false, // Development. "debug" : true, // Allow debugger statements e.g. browser breakpoints. "devel" : true, // Allow development statements e.g. `console.log();`. // EcmaScript 5. "es5" : true, // Allow EcmaScript 5 syntax. "strict" : false, // Require `use strict` pragma in every file. "globalstrict" : true, // Allow global "use strict" (also enables 'strict'). // The Good Parts. "asi" : true, // Tolerate Automatic Semicolon Insertion (no semicolons). "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. "laxcomma" : true, "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. "curly" : false, // Require {} for every new block or scope. "eqeqeq" : true, // Require triple equals i.e. `===`. "eqnull" : true, // Tolerate use of `== null`. "evil" : false, // Tolerate use of `eval`. "expr" : false, // Tolerate `ExpressionStatement` as Programs. "forin" : false, // Prohibt `for in` loops without `hasOwnProperty`. "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` "latedef" : false, // Prohibit variable use before definition. "loopfunc" : false, // Allow functions to be defined within loops. "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. "regexp" : false, // Prohibit `.` and `[^...]` in regular expressions. "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. "scripturl" : false, // Tolerate script-targeted URLs. "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. "undef" : true, // Require all non-global variables be declared before they are used. // Persone styling prefrences. "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. "noempty" : true, // Prohibit use of empty blocks. "nonew" : true, // Prohibit use of constructors for side-effects. "nomen" : false, // Prohibit use of initial or trailing underbars in names. "onevar" : false, // Allow only one `var` statement per function. "plusplus" : false, // Prohibit use of `++` & `--`. "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. "trailing" : true, // Prohibit trailing whitespaces. "white" : false // Check against strict whitespace and indentation rules. } node-music-library-index-2.1.0/LICENSE000066400000000000000000000020411262274271700173400ustar00rootroot00000000000000Copyright (c) 2014 Andrew Kelley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. node-music-library-index-2.1.0/README.md000066400000000000000000000065041262274271700176220ustar00rootroot00000000000000# Music Library Index Given track metadata objects, constructs a searchable object model. This module is used both in the client and the server of [Groove Basin](https://github.com/andrewrk/groovebasin). ## Features * Sort order ignores 'a', 'an' and 'the' in artists, albums, and names. * Sorting and searching is case insensitive and [diacritics-insensitive](https://github.com/andrewrk/diacritics). * Searching uses word-based filtering (this is how most music player applications implement filtering) on all track fields. * Distinguishes albums by name, date, and album artist. * Produces these indexes: * Artists in sorted order - For each of these artists, albums in sorted order. - For each of these albums, tracks in sorted order. * Albums in sorted order - For each of these albums, tracks in sorted order. * Tracks by user-defined key. * Artists by library-defined key. * Albums by library-defined key. * Searching allows the use of special constructs: * Quoted terms (`"..."`) exactly match every character, including spaces, upper- and lower-case letters, and characters with diacritics. Inside quotes, use `\"` for a literal quote and `\\` for a literal backslash. * Terms starting with `not:` match anything not matched by the rest of the term. For example `metal not:metalica` or `just not:"Justin Bieber"`. * Parentheses can be used to group terms. For example `tool not:(opiate live)`. * Use `or:(...)` to match any of the terms in the parentheses. For example `or:(chopin mozart bach)`. Use further nested parentheses to return to "and"-style matching. For example `or:(chopin mozart (johann sebastian bach))`. ## Usage ```js var MusicLibraryIndex = require('music-library-index'); var library = new MusicLibraryIndex(); library.addTrack({ key: "Anberlin/Never Take Friendship Personal/02. Paperthin Hymn.mp3", name: "Paperthin Hymn", artistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 2, albumArtistName: "Anberlin", }); library.addTrack({ key: "Anberlin/Never Take Friendship Personal/08. The Feel Good Drag.mp3", name: "The Feel Good Drag", artistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 8, albumArtistName: "Anberlin", label: {"favorites_id": 1}, }); library.rebuildTracks(); library.addLabel({ id: "favorites_id", name: "favorites", }); library.rebuildLabels(); console.log(library.artistList[0]); console.log(library.trackTable); ``` ## Tests ``` basic index building ✓ trackTable ✓ artistList ✓ albumList ✓ searching compilation album ✓ filed in various artists tracks from same album missing year metadata ✓ still knows they're in the same album different albums with same name ✓ detects that they are different album with a few tracks by different artists ✓ only creates one album album by an artist ✓ should be filed under the artist album by an artist ✓ sorts by disc before track album artist with no album ✓ shouldn't be various artists unknown artist, unknown album ✓ should be put into the same album ✓ searching should not affect anything album with album artist ✓ shouldn't be various artists ``` node-music-library-index-2.1.0/index.js000066400000000000000000000412231262274271700200050ustar00rootroot00000000000000var removeDiacritics = require('diacritics').remove; module.exports = MusicLibraryIndex; MusicLibraryIndex.defaultPrefixesToStrip = [ /^\s*the\s+/, /^\s*a\s+/, /^\s*an\s+/, ]; MusicLibraryIndex.defaultVariousArtistsKey = "VariousArtists"; MusicLibraryIndex.defaultVariousArtistsName = "Various Artists"; MusicLibraryIndex.defaultSearchFields = [ 'artistName', 'albumArtistName', 'albumName', 'name', ]; function MusicLibraryIndex(options) { options = options || {}; this.searchFields = options.searchFields || MusicLibraryIndex.defaultSearchFields; this.variousArtistsKey = options.variousArtistsKey || MusicLibraryIndex.defaultVariousArtistsKey; this.variousArtistsName = options.variousArtistsName || MusicLibraryIndex.defaultVariousArtistsName; this.prefixesToStrip = options.prefixesToStrip || MusicLibraryIndex.defaultPrefixesToStrip; this.artistComparator = this.artistComparator.bind(this); this.albumComparator = this.albumComparator.bind(this); this.trackComparator = this.trackComparator.bind(this); this.labelComparator = this.labelComparator.bind(this); this.clearTracks(); this.clearLabels(); } MusicLibraryIndex.prototype.stripPrefixes = function(str) { for (var i = 0; i < this.prefixesToStrip.length; i += 1) { var regex = this.prefixesToStrip[i]; str = str.replace(regex, ''); break; } return str; }; MusicLibraryIndex.prototype.sortableTitle = function(title) { return this.stripPrefixes(formatSearchable(title)); }; MusicLibraryIndex.prototype.titleCompare = function(a, b) { var _a = this.sortableTitle(a); var _b = this.sortableTitle(b); if (_a < _b) { return -1; } else if (_a > _b) { return 1; } else { if (a < b) { return -1; } else if (a > b) { return 1; } else { return 0; } } }; MusicLibraryIndex.prototype.trackComparator = function(a, b) { if (a.disc < b.disc) { return -1; } else if (a.disc > b.disc) { return 1; } else if (a.track < b.track) { return -1; } else if (a.track > b.track) { return 1; } else { return this.titleCompare(a.name, b.name); } } MusicLibraryIndex.prototype.albumComparator = function(a, b) { if (a.year < b.year) { return -1; } else if (a.year > b.year) { return 1; } else { return this.titleCompare(a.name, b.name); } } MusicLibraryIndex.prototype.artistComparator = function(a, b) { return this.titleCompare(a.name, b.name); } MusicLibraryIndex.prototype.labelComparator = function(a, b) { return this.titleCompare(a.name, b.name); } MusicLibraryIndex.prototype.getAlbumKey = function(track) { var artistName = track.albumArtistName || (track.compilation ? this.variousArtistsName : track.artistName); return formatSearchable(track.albumName + "\n" + artistName); }; MusicLibraryIndex.prototype.getArtistKey = function(artistName) { return formatSearchable(artistName); }; MusicLibraryIndex.prototype.clearTracks = function() { this.trackTable = {}; this.artistTable = {}; this.artistList = []; this.albumTable = {}; this.albumList = []; this.dirtyTracks = false; }; MusicLibraryIndex.prototype.clearLabels = function() { this.labelTable = {}; this.labelList = []; this.dirtyLabels = false; }; MusicLibraryIndex.prototype.rebuildAlbumTable = function() { // builds everything from trackTable this.artistTable = {}; this.artistList = []; this.albumTable = {}; this.albumList = []; var thisAlbumList = this.albumList; for (var trackKey in this.trackTable) { var track = this.trackTable[trackKey]; this.trackTable[track.key] = track; var searchTags = ""; for (var i = 0; i < this.searchFields.length; i += 1) { searchTags += track[this.searchFields[i]] + "\n"; } track.exactSearchTags = searchTags; track.fuzzySearchTags = formatSearchable(searchTags); if (track.albumArtistName === this.variousArtistsName) { track.albumArtistName = ""; track.compilation = true; } track.albumArtistName = track.albumArtistName || ""; var albumKey = this.getAlbumKey(track); var album = getOrCreate(albumKey, this.albumTable, createAlbum); track.album = album; album.trackList.push(track); if (album.year == null) { album.year = track.year; } } function createAlbum() { var album = { name: track.albumName, year: track.year, trackList: [], key: albumKey, }; thisAlbumList.push(album); return album; } }; MusicLibraryIndex.prototype.rebuildTracks = function() { if (!this.dirtyTracks) return; this.rebuildAlbumTable(); this.albumList.sort(this.albumComparator); var albumArtistName, artistKey, artist; var albumKey, track, album; var i; for (albumKey in this.albumTable) { album = this.albumTable[albumKey]; var albumArtistSet = {}; album.trackList.sort(this.trackComparator); albumArtistName = ""; var isCompilation = false; for (i = 0; i < album.trackList.length; i += 1) { track = album.trackList[i]; track.index = i; if (track.albumArtistName) { albumArtistName = track.albumArtistName; albumArtistSet[this.getArtistKey(albumArtistName)] = true; } if (!albumArtistName) albumArtistName = track.artistName; albumArtistSet[this.getArtistKey(albumArtistName)] = true; isCompilation = isCompilation || track.compilation; } if (isCompilation || moreThanOneKey(albumArtistSet)) { albumArtistName = this.variousArtistsName; artistKey = this.variousArtistsKey; for (i = 0; i < album.trackList.length; i += 1) { track = album.trackList[i]; track.compilation = true; } } else { artistKey = this.getArtistKey(albumArtistName); } artist = getOrCreate(artistKey, this.artistTable, createArtist); album.artist = artist; artist.albumList.push(album); } this.artistList = []; var variousArtist = null; for (artistKey in this.artistTable) { artist = this.artistTable[artistKey]; artist.albumList.sort(this.albumComparator); for (i = 0; i < artist.albumList.length; i += 1) { album = artist.albumList[i]; album.index = i; } if (artist.key === this.variousArtistsKey) { variousArtist = artist; } else { this.artistList.push(artist); } } this.artistList.sort(this.artistComparator); if (variousArtist) { this.artistList.unshift(variousArtist); } for (i = 0; i < this.artistList.length; i += 1) { artist = this.artistList[i]; artist.index = i; } this.dirtyTracks = false; function createArtist() { return { name: albumArtistName, albumList: [], key: artistKey, }; } } MusicLibraryIndex.prototype.rebuildLabels = function() { if (!this.dirtyLabels) return; this.labelList = []; for (var id in this.labelTable) { var label = this.labelTable[id]; this.labelList.push(label); } this.labelList.sort(this.labelComparator); this.labelList.forEach(function(label, index) { label.index = index; }); this.dirtyLabels = false; } MusicLibraryIndex.prototype.addTrack = function(track) { this.trackTable[track.key] = track; this.dirtyTracks = true; } MusicLibraryIndex.prototype.removeTrack = function(key) { delete this.trackTable[key]; this.dirtyTracks = true; } MusicLibraryIndex.prototype.addLabel = function(label) { this.labelTable[label.id] = label; this.dirtyLabels = true; } MusicLibraryIndex.prototype.removeLabel = function(id) { delete this.labelTable[id]; this.dirtyLabels = true; } MusicLibraryIndex.prototype.search = function(query) { var searchResults = new MusicLibraryIndex({ searchFields: this.searchFields, variousArtistsKey: this.variousArtistsKey, variousArtistsName: this.variousArtistsName, prefixesToStrip: this.prefixesToStrip, }); var matcher = this.parseQuery(query); var track; for (var trackKey in this.trackTable) { track = this.trackTable[trackKey]; if (matcher(track)) { searchResults.trackTable[track.key] = track; } } searchResults.dirtyTracks = true; searchResults.rebuildTracks(); return searchResults; }; var tokenizerRegex = new RegExp( '( +)' +'|'+ // 1: whitespace between terms (not in quotes) '(\\()' +'|'+ // 2: open parenthesis at the start of a term '(\\))' +'|'+ // 3: end parenthesis '(not:)' +'|'+ // 4: not: prefix '(or:\\()' +'|'+ // 5: or: prefix '(label:)' +'|'+ // 6: label: prefix '("(?:[^"\\\\]|\\\\.)*"\\)*)' +'|'+ // 7: quoted thing. can end with parentheses '([^ ]+)', // 8: normal word. can end with parentheses "g"); var WHITESPACE = 1; var OPEN_PARENTHESIS = 2; var CLOSE_PARENTHESIS = 3; var NOT = 4; var OR = 5; var LABEL = 6; var QUOTED_THING = 7; var NORMAL_WORD = 8; MusicLibraryIndex.prototype.parseQuery = function(query) { var self = this; return parse(query); function parse(query) { var tokens = tokenizeQuery(query); var tokenIndex = 0; return parseList(makeAndMatcher, null); function parseList(makeMatcher, waitForTokenType) { var matchers = []; var justSawWhitespace = true; while (tokenIndex < tokens.length) { var token = tokens[tokenIndex++]; switch (token.type) { case OPEN_PARENTHESIS: var subMatcher = parseList(makeAndMatcher, CLOSE_PARENTHESIS); matchers.push(subMatcher); break; case CLOSE_PARENTHESIS: if (waitForTokenType === CLOSE_PARENTHESIS) return makeMatcher(matchers); // misplaced ) var previousMatcher = matchers[matchers.length - 1]; if (!justSawWhitespace && previousMatcher != null && previousMatcher.fuzzyTerm != null) { // slap it on the back of the last guy previousMatcher.fuzzyTerm += token.text; } else { // it's its own term matchers.push(makeFuzzyTextMatcher(token.text)); } break; case NOT: matchers.push(parseNot()); break; case OR: var subMatcher = parseList(makeOrMatcher, CLOSE_PARENTHESIS); matchers.push(subMatcher); break; case LABEL: matchers.push(parseLabel()); break; case QUOTED_THING: if (token.text.length !== 0) { matchers.push(makeExactTextMatcher(token.text)); } break; case NORMAL_WORD: matchers.push(makeFuzzyTextMatcher(token.text)); break; } var justSawWhitespace = token.type === WHITESPACE; } return makeMatcher(matchers); } function parseNot() { if (tokenIndex >= tokens.length) { // "not:" then EOF. treat it as a fuzzy matcher for "not:" return makeFuzzyTextMatcher(tokens[tokenIndex - 1].text); } var token = tokens[tokenIndex++]; switch (token.type) { case WHITESPACE: case CLOSE_PARENTHESIS: // "not: " or "not:)" // Treat the "not:" as a fuzzy matcher, // and let the parent deal with this token tokenIndex--; return makeFuzzyTextMatcher(tokens[tokenIndex - 1].text); case OPEN_PARENTHESIS: // "not:(" return makeNotMatcher(parseList(makeAndMatcher, CLOSE_PARENTHESIS)); case NOT: // double negative all the way. return makeNotMatcher(parseNot()); case OR: // "not:or(" return makeNotMatcher(parseList(makeOrMatcher, CLOSE_PARENTHESIS)); case LABEL: return makeNotMatcher(parseLabel()); case QUOTED_THING: return makeNotMatcher(makeExactTextMatcher(token.text)); case NORMAL_WORD: return makeNotMatcher(makeFuzzyTextMatcher(token.text)); } throw new Error("unreachable"); } function parseLabel() { if (tokenIndex >= tokens.length) { // "label:" then EOF. treat it as a fuzzy matcher for "label:" return makeFuzzyTextMatcher(tokens[tokenIndex - 1].text); } var token = tokens[tokenIndex++]; switch (token.type) { case WHITESPACE: case CLOSE_PARENTHESIS: // "label: " or "label:)" // Treat the "label:" as a fuzzy matcher, // and let the parent deal with this token tokenIndex--; return makeFuzzyTextMatcher(tokens[tokenIndex - 1].text); case OPEN_PARENTHESIS: // "label:(" case NOT: // "label:not:" case OR: // "label:or:(" case LABEL: // "label:label:" case QUOTED_THING: // 'label:"Asdf"' case NORMAL_WORD: // "label:Asdf" return makeLabelMatcher(token.text); } throw new Error("unreachable"); } } function makeFuzzyTextMatcher(term) { // make this publicly modifiable fuzzyTextMatcher.fuzzyTerm = formatSearchable(term);; fuzzyTextMatcher.toString = function() { return "(fuzzy " + JSON.stringify(fuzzyTextMatcher.fuzzyTerm) + ")" }; return fuzzyTextMatcher; function fuzzyTextMatcher(track) { return track.fuzzySearchTags.indexOf(fuzzyTextMatcher.fuzzyTerm) !== -1; } } function makeExactTextMatcher(term) { exactTextMatcher.toString = function() { return "(exact " + JSON.stringify(term) + ")" }; return exactTextMatcher; function exactTextMatcher(track) { return track.exactSearchTags.indexOf(term) !== -1; } } function makeAndMatcher(children) { if (children.length === 1) return children[0]; andMatcher.toString = function() { return "(" + children.join(" AND ") + ")"; }; return andMatcher; function andMatcher(track) { for (var i = 0; i < children.length; i++) { if (!children[i](track)) return false; } return true; } } function makeOrMatcher(children) { if (children.length === 1) return children[0]; orMatcher.toString = function() { return "(" + children.join(" OR ") + ")"; }; return orMatcher; function orMatcher(track) { for (var i = 0; i < children.length; i++) { if (children[i](track)) return true; } return false; } } function makeNotMatcher(subMatcher) { notMatcher.toString = function() { return "(not " + subMatcher.toString() + ")"; }; return notMatcher; function notMatcher(track) { return !subMatcher(track); } } function makeLabelMatcher(text) { var id = (function() { for (var id in self.labelTable) { if (self.labelTable[id].name === text) { return id; } } return null; })(); if (id != null) { labelMatcher.toString = function() { return "(label " + JSON.stringify(id) + ")"; }; return labelMatcher; } else { // not even a real label alwaysFail.toString = function() { return "(label )"; }; return alwaysFail; } function labelMatcher(track) { return track.labels != null && track.labels[id]; } function alwaysFail() { return false; } } function tokenizeQuery(query) { tokenizerRegex.lastIndex = 0; var tokens = []; while (true) { var match = tokenizerRegex.exec(query); if (match == null) break; var term = match[0]; var type; for (var i = 1; i < match.length; i++) { if (match[i] != null) { type = i; break; } } switch (type) { case WHITESPACE: case OPEN_PARENTHESIS: case CLOSE_PARENTHESIS: case NOT: case OR: case LABEL: tokens.push({type: type, text: term}); break; case QUOTED_THING: case NORMAL_WORD: var endParensCount = /\)*$/.exec(term)[0].length; term = term.substr(0, term.length - endParensCount); if (type === QUOTED_THING) { // strip quotes term = /^"(.*)"$/.exec(term)[1]; // handle escapes term = term.replace(/\\(.)/g, "$1"); } tokens.push({type: type, text: term}); for (var i = 0; i < endParensCount; i++) { tokens.push({type: CLOSE_PARENTHESIS, text: ")"}); } break; } } return tokens; } }; function getOrCreate(key, table, initObjFunc) { var result = table[key]; if (result == null) { result = initObjFunc(); table[key] = result; } return result; } function moreThanOneKey(object){ var count = -2; for (var k in object) { if (!++count) { return true; } } return false; } function formatSearchable(str) { return removeDiacritics(str).toLowerCase(); } node-music-library-index-2.1.0/package.json000066400000000000000000000013301262274271700206210ustar00rootroot00000000000000{ "name": "music-library-index", "version": "2.1.0", "description": "build a searchable javascript object model given track metadata objects", "main": "index.js", "scripts": { "test": "mocha --reporter spec test/test.js" }, "author": "Andrew Kelley ", "license": "MIT", "dependencies": { "diacritics": "~1.2.3" }, "devDependencies": { "mocha": "~2.3.4" }, "repository": { "type": "git", "url": "git://github.com/andrewrk/node-music-library-index.git" }, "bugs": { "url": "https://github.com/andrewrk/node-music-library-index/issues" }, "homepage": "https://github.com/andrewrk/node-music-library-index", "directories": { "test": "test" } } node-music-library-index-2.1.0/test/000077500000000000000000000000001262274271700173155ustar00rootroot00000000000000node-music-library-index-2.1.0/test/benchmark.js000066400000000000000000000015251262274271700216100ustar00rootroot00000000000000// usage: node benchmark.js path/to/index.json ["search query" ...] var fs = require('fs'); var MusicLibraryIndex = require('../'); var indexPath = process.argv[2]; fs.readFile(indexPath, function(err, data) { if (err) throw err; var input = JSON.parse(data); var library = new MusicLibraryIndex(); for (var key in input) { library.addTrack(input[key]); } timed("rebuildTracks()", function() { library.rebuildTracks(); }); for (var i = 3; i < process.argv.length; i++) { timedSearch(process.argv[i]); } function timedSearch(query) { timed("search(" + JSON.stringify(query) + ")", function() { library.search(query); }); } }); function timed(name, func) { process.stdout.write(name + "..."); var start = new Date(); func(); var duration = new Date() - start; console.log(duration + "ms"); } node-music-library-index-2.1.0/test/test.js000066400000000000000000000517431262274271700206440ustar00rootroot00000000000000var assert = require('assert'); var MusicLibraryIndex = require('../'); describe("basic index building", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "Anberlin/Never Take Friendship Personal/02. Paperthin Hymn.mp3", name: "Paperthin Hymn", artistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 2, albumArtistName: "Anberlin", }); library.addTrack({ key: "Anberlin/Never Take Friendship Personal/08. The Feel Good Drag.mp3", name: "The Feel Good Drag", artistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 8, albumArtistName: "Anberlin", }); library.rebuildTracks(); it("trackTable", function() { var track = library.trackTable["Anberlin/Never Take Friendship Personal/08. The Feel Good Drag.mp3"]; assert.strictEqual(track.name, "The Feel Good Drag"); assert.strictEqual(track.artistName, "Anberlin"); assert.strictEqual(track.album.name, "Never Take Friendship Personal"); assert.strictEqual(track.year, 2005); }); it("artistList", function() { assert.strictEqual(library.artistList.length, 1); var artist = library.artistList[0]; assert.strictEqual(artist.name, "Anberlin"); assert.strictEqual(artist.index, 0); assert.strictEqual(artist.albumList.length, 1); var album = artist.albumList[0]; assert.strictEqual(album.name, "Never Take Friendship Personal"); assert.strictEqual(album.year, 2005); assert.strictEqual(album.index, 0); assert.strictEqual(album.trackList.length, 2); assert.strictEqual(album.trackList[0].name, "Paperthin Hymn"); assert.strictEqual(album.trackList[0].index, 0); assert.strictEqual(album.trackList[1].name, "The Feel Good Drag"); assert.strictEqual(album.trackList[1].index, 1); }); it("albumList", function() { assert.strictEqual(library.albumList.length, 1); var album = library.albumList[0]; assert.strictEqual(album.name, "Never Take Friendship Personal"); assert.strictEqual(album.year, 2005); assert.strictEqual(album.index, 0); assert.strictEqual(album.trackList.length, 2); assert.strictEqual(album.trackList[0].name, "Paperthin Hymn"); assert.strictEqual(album.trackList[0].index, 0); assert.strictEqual(album.trackList[1].name, "The Feel Good Drag"); assert.strictEqual(album.trackList[1].index, 1); }); it("searching", function() { var results = library.search("never drag"); assert.strictEqual(results.albumList.length, 1); assert.strictEqual(results.albumList[0].trackList.length, 1); assert.strictEqual(results.albumList[0].trackList[0].name, "The Feel Good Drag"); }); }); describe("compilation album", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "jqvq-tpiu", name: "No News Is Good News", artistName: "New Found Glory", albumName: "2004 Warped Tour Compilation [Disc 1]", year: 2004, disc: 1, discCount: 2, genre: "Alternative & Punk", albumArtistName: "Various Artists", track: 1, }); library.addTrack({ key: "dldd-itve", name: "American Errorist (I Hate Hate Haters)", artistName: "NOFX", albumName: "2004 Warped Tour Compilation [Disc 1]", year: 2004, disc: 1, discCount: 2, genre: "Alternative & Punk", albumArtistName: "Various Artists", track: 2, }); library.addTrack({ key: "ukjv-ndsz", name: "Fire Down Below", artistName: "Alkaline Trio", albumName: "2007 Warped Tour Compilation [Disc 1]", compilation: true, year: 2007, genre: "Alternative & Punk", track: 1, trackCount: 25, }); library.addTrack({ key: "gfkt-esqz", name: "Requiem For Dissent", artistName: "Bad Religion", albumName: "2007 Warped Tour Compilation [Disc 1]", compilation: true, year: 2007, genre: "Alternative & Punk", track: 2, trackCount: 25, }); library.rebuildTracks(); it("filed in various artists", function() { assert.strictEqual(library.albumList.length, 2); assert.strictEqual(library.artistList.length, 1); var artist = library.artistList[0]; assert.strictEqual(artist.name, "Various Artists"); assert.strictEqual(artist.albumList.length, 2); assert.strictEqual(library.albumList.length, 2); assert.strictEqual(library.trackTable["jqvq-tpiu"].albumArtistName, ""); assert.strictEqual(library.trackTable["dldd-itve"].albumArtistName, ""); assert.strictEqual(library.trackTable["ukjv-ndsz"].albumArtistName, ""); assert.strictEqual(library.trackTable["gfkt-esqz"].albumArtistName, ""); }); }); describe("tracks from same album missing year metadata", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "wwxj-unhr", name: "Dog-Eared Page", artistName: "The Matches", albumName: "E. Von Dahl Killed the Locals", year: 2004, genre: "Punk", track: 1, }); library.addTrack({ key: "xekw-lvne", name: "Audio Blood", artistName: "The Matches", albumName: "E. Von Dahl Killed the Locals", // missing year genre: "Rock", track: 2, }); library.addTrack({ key: "lpka-dugc", name: "Chain Me Free", artistName: "The Matches", albumName: "E. Von Dahl Killed the Locals", year: 2004, genre: "Rock", track: 3, }); library.rebuildTracks(); it("still knows they're in the same album", function() { assert.strictEqual(library.albumList.length, 1); assert.strictEqual(library.albumList[0].year, 2004); assert.strictEqual(library.trackTable["xekw-lvne"].album.year, 2004); }); }); describe("different albums with same name", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "sbao-lcvn", name: "6:00", artistName: "Dream Theater", albumName: "Awake", year: 1994, genre: "Progressive Rock", track: 1, }); library.addTrack({ key: "qtru-gdtp", name: "Awake", artistName: "Godsmack", albumName: "Awake", year: 2000, genre: "Rock", track: 2, }); library.rebuildTracks(); it("detects that they are different", function() { assert.strictEqual(library.albumList.length, 2); }); }); describe("album with a few tracks by different artists", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "ikoe-nujf", name: "Paperthin Hymn", artistName: "Anberlin", albumArtistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 2, }); library.addTrack({ key: "msnq-swpc", name: "The Feel Good Drag", artistName: "Anberlin, some other band", albumArtistName: "Anberlin", albumName: "Never Take Friendship Personal", year: 2005, genre: "Other", track: 8, }); library.rebuildTracks(); it("only creates one album", function() { assert.strictEqual(library.albumList.length, 1); }); }); describe("album by an artist", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "ynji-lcfu", name: "The Truth", artistName: "Relient K", albumName: "Apathetic ep", track: 1, trackCount: 7, }); library.addTrack({ key: "lxed-bsor", name: "Apathetic Way to Be", artistName: "Relient K", albumName: "Apathetic ep", track: 2, trackCount: 7, }); library.rebuildTracks(); it("should be filed under the artist", function() { assert.strictEqual(library.artistList.length, 1); assert.strictEqual(library.artistList[0].name, "Relient K"); }); }); describe("album by an artist", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "jqvq-tpiu", name: "No News Is Good News", artistName: "New Found Glory", albumName: "2004 Warped Tour Compilation", year: 2004, disc: 1, discCount: 2, genre: "Alternative & Punk", albumArtistName: "Various Artists", track: 1, }); library.addTrack({ key: "dldd-itve", name: "American Errorist (I Hate Hate Haters)", artistName: "NOFX", albumName: "2004 Warped Tour Compilation", year: 2004, disc: 1, discCount: 2, genre: "Alternative & Punk", albumArtistName: "Various Artists", track: 2, }); library.addTrack({ key: "ukjv-ndsz", name: "Fire Down Below", artistName: "Alkaline Trio", albumName: "2004 Warped Tour Compilation", disc: 2, compilation: true, year: 2004, genre: "Alternative & Punk", track: 1, trackCount: 25, }); library.addTrack({ key: "gfkt-esqz", name: "Requiem For Dissent", artistName: "Bad Religion", albumName: "2004 Warped Tour Compilation", disc: 2, compilation: true, year: 2004, genre: "Alternative & Punk", track: 2, trackCount: 25, }); library.rebuildTracks(); it("sorts by disc before track", function() { assert.strictEqual(library.albumList[0].trackList[0].name, "No News Is Good News"); assert.strictEqual(library.albumList[0].trackList[1].name, "American Errorist (I Hate Hate Haters)"); assert.strictEqual(library.albumList[0].trackList[2].name, "Fire Down Below"); assert.strictEqual(library.albumList[0].trackList[3].name, "Requiem For Dissent"); }); }); describe("album artist with no album", function() { var library = new MusicLibraryIndex(); var id = '5a89ea73-71aa-4c22-97a5-3b3509131cca'; library.addTrack({ key: id, name: 'I Miss You', artistName: 'Blink 182', composerName: '', performerName: '', albumArtistName: 'blink-182', albumName: '', compilation: false, track: 3, duration: 227.6815, year: 2003, genre: 'Rock', }); library.rebuildTracks(); it("shouldn't be various artists", function() { assert.notStrictEqual(library.trackTable[id].albumArtistName, "Various Artists"); }); }); describe("unknown artist, unknown album", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: 'imnd-sxde', name: '01 Shining Armor', artistName: '', albumArtistName: '', albumName: '', }); library.addTrack({ key: 'jakg-nbfg', name: '02 Weird Kids', artistName: '', albumArtistName: '', albumName: '', }); library.rebuildTracks(); var results = library; it("should be put into the same album", check); library.search("n"); results = library.search(""); it("searching should not affect anything", check); function check() { assert.strictEqual(results.artistList.length, 1); assert.strictEqual(results.artistList[0].albumList.length, 1); assert.strictEqual(results.artistList[0].albumList[0].trackList.length, 2); assert.strictEqual(results.artistList[0].albumList[0].trackList[0].album.trackList.length, 2); } }); describe("album with album artist", function() { var library = new MusicLibraryIndex(); var id1 = 'imnd-sxde'; library.addTrack({ key: id1, name: 'Palladio', artistName: 'Escala', albumArtistName: 'Escala', albumName: 'Escala', track: 1, }); var id2 = 'vewu-hqbx'; library.addTrack({ key: id2, name: 'Requiem for a Tower', artistName: 'Escala', albumArtistName: 'Escala', albumName: 'Escala', track: 2, }); var id3 = 'ixbc-oshh'; library.addTrack({ key: id3, name: 'Kashmir', artistName: 'Escala; Slash', albumArtistName: 'Escala', albumName: 'Escala', track: 3, }); library.rebuildTracks(); it("shouldn't be various artists", function() { assert.strictEqual(library.trackTable[id1].albumArtistName, "Escala"); assert.strictEqual(library.trackTable[id2].albumArtistName, "Escala"); assert.strictEqual(library.artistList.length, 1); }); }); describe("label management", function() { var library = new MusicLibraryIndex(); library.addLabel({ id: "wrong_id", name: "wrong", }); library.rebuildLabels(); library.clearLabels(); library.addLabel({ id: "techno_id", name: "techno", }); library.addLabel({ id: "jazz_id", name: "jazz", }); library.rebuildLabels(); it("clearLabels, addLabel", function() { assert.strictEqual(library.labelList[0].name, "jazz"); assert.strictEqual(library.labelList[1].name, "techno"); }); }); describe("parseQuery", function() { var library = new MusicLibraryIndex(); it("works", function() { assert.strictEqual(library.parseQuery('').toString(), '()'); assert.strictEqual(library.parseQuery('a').toString(), '(fuzzy "a")'); assert.strictEqual(library.parseQuery(' ab cd ').toString(), '((fuzzy "ab") AND (fuzzy "cd"))'); assert.strictEqual(library.parseQuery('"a b"').toString(), '(exact "a b")'); assert.strictEqual(library.parseQuery('"a b\\" c"').toString(), '(exact "a b\\\" c")'); assert.strictEqual(library.parseQuery('\\"a b"').toString(), '((fuzzy "\\\\\\"a") AND (fuzzy "b\\""))'); assert.strictEqual(library.parseQuery('"').toString(), '(fuzzy "\\\"")'); assert.strictEqual(library.parseQuery('\\').toString(), '(fuzzy "\\\\")'); assert.strictEqual(library.parseQuery('""').toString(), '()'); assert.strictEqual(library.parseQuery('a" b"c').toString(), '((fuzzy "a\\\"") AND (fuzzy "b\\\"c"))'); assert.strictEqual(library.parseQuery('not:A').toString(), '(not (fuzzy "a"))'); assert.strictEqual(library.parseQuery('not:"A"').toString(), '(not (exact "A"))'); assert.strictEqual(library.parseQuery('not:(a b)').toString(), '(not ((fuzzy "a") AND (fuzzy "b")))'); assert.strictEqual(library.parseQuery('not:(a)').toString(), '(not (fuzzy "a"))'); assert.strictEqual(library.parseQuery('not:not:a').toString(), '(not (not (fuzzy "a")))'); assert.strictEqual(library.parseQuery('not:').toString(), '(fuzzy "not:")'); assert.strictEqual(library.parseQuery('not: a').toString(), '((fuzzy "not:") AND (fuzzy "a"))'); assert.strictEqual(library.parseQuery('not:)a').toString(), '((fuzzy "not:)") AND (fuzzy "a"))'); assert.strictEqual(library.parseQuery('or:()').toString(), '()'); assert.strictEqual(library.parseQuery('or:(' ).toString(), '()'); assert.strictEqual(library.parseQuery('or:').toString(), '(fuzzy "or:")'); assert.strictEqual(library.parseQuery('or:(a)').toString(), '(fuzzy "a")'); assert.strictEqual(library.parseQuery('or:(a' ).toString(), '(fuzzy "a")'); assert.strictEqual(library.parseQuery('or:(a b)').toString(), '((fuzzy "a") OR (fuzzy "b"))'); assert.strictEqual(library.parseQuery('or:(a b' ).toString(), '((fuzzy "a") OR (fuzzy "b"))'); assert.strictEqual(library.parseQuery('or:(a (b c))').toString(), '((fuzzy "a") OR ((fuzzy "b") AND (fuzzy "c")))'); assert.strictEqual(library.parseQuery('or:((a b) c)').toString(), '(((fuzzy "a") AND (fuzzy "b")) OR (fuzzy "c"))'); }); }); describe("parseQuery with labels", function() { var library = new MusicLibraryIndex(); library.addLabel({ id: "techno_id", name: "techno", }); library.addLabel({ id: "jazz_id", name: "jazz music", }); library.addLabel({ id: "not_id", name: "not:", }); library.rebuildLabels(); it("works", function() { assert.strictEqual(library.parseQuery('label:techno').toString(), '(label "techno_id")'); assert.strictEqual(library.parseQuery('not:label:techno').toString(), '(not (label "techno_id"))'); assert.strictEqual(library.parseQuery('label:asdf').toString(), '(label )'); assert.strictEqual(library.parseQuery('label:Techno').toString(), '(label )'); assert.strictEqual(library.parseQuery('label:"jazz music"').toString(), '(label "jazz_id")'); assert.strictEqual(library.parseQuery('label:jazz music').toString(), '((label ) AND (fuzzy "music"))'); assert.strictEqual(library.parseQuery('label:""').toString(), '(label )'); assert.strictEqual(library.parseQuery('label:').toString(), '(fuzzy "label:")'); assert.strictEqual(library.parseQuery('label: ').toString(), '(fuzzy "label:")'); assert.strictEqual(library.parseQuery('label:not:').toString(), '(label "not_id")'); assert.strictEqual(library.parseQuery('not:(this label:)').toString(), '(not ((fuzzy "this") AND (fuzzy "label:")))'); }); }); describe("searching with quoted seach terms", function() { var library = new MusicLibraryIndex(); library.addTrack({ key: "fUPmxjMc", name: "Été (Original Mix)", artistName: "AKA AKA & Thalstroem", albumName: "Varieté", }); library.addTrack({ key: "zyGaKkrU", name: "Tribute to Young Stroke AKA Young Muscle", artistName: "Andy Kelley", albumName: "The Weekend Challenge #3", }); library.addTrack({ key: "v7zwEPLs", name: "Mista veri pakenee", artistName: "Turmion Katilot (no diacritics)", albumName: "Pirun nyrkki", }); library.addTrack({ key: "sobHcy0I", name: "Mistä veri pakenee", artistName: "Turmion Kätilöt (with diacritics)", albumName: "Pirun nyrkki", }); var literalQuoteKey = "G5FqXeJZ"; library.addTrack({ key: literalQuoteKey, name: "A song with a literal \" in it", artistName: "Tester", albumName: "literalQuote", }); var literalBackslashKey = "Xsc4+ril"; library.addTrack({ key: literalBackslashKey, name: "A song with a literal \\ in it", artistName: "Tester", albumName: "literalBackslash", }); library.rebuildTracks(); it("single search term returns both", function() { var results = library.search("aka aka"); assert.strictEqual(results.artistList.length, 2); }); it("quoted search term is case sensitive", function() { assert.strictEqual(library.search("\"andy\"").artistList.length, 0); assert.strictEqual(library.search("\"ANDY\"").artistList.length, 0); assert.strictEqual(library.search("\"Andy\"").artistList.length, 1); }); it("quoted search terms include spaces", function() { var results = library.search("\"AKA AKA\""); assert.strictEqual(results.artistList.length, 1); assert.strictEqual(results.artistList[0].name, "AKA AKA & Thalstroem"); }); it("quoted search terms preserve diacritics", function() { assert.strictEqual(library.search("Mistä").artistList.length, 2); assert.strictEqual(library.search("\"Mistä\"").artistList.length, 1); }); it("matches a song with a literal quote", function() { var results = library.search("\""); assert.strictEqual(results.albumList.length, 1); assert.strictEqual(results.albumList[0].trackList.length, 1); assert.strictEqual(results.albumList[0].trackList[0].key, literalQuoteKey); }); it("matches a song with a literal backslash", function() { var results = library.search("\\"); assert.strictEqual(results.albumList.length, 1); assert.strictEqual(results.albumList[0].trackList.length, 1); assert.strictEqual(results.albumList[0].trackList[0].key, literalBackslashKey); }); }); describe("searching with expressions", function() { var library = new MusicLibraryIndex(); library.addLabel({ id: "techno_id", name: "techno", }); library.addLabel({ id: "jazz_id", name: "jazz", }); library.rebuildLabels(); library.addTrack({ key: "fUPmxjMc", name: "Été (Original Mix)", artistName: "AKA AKA & Thalstroem", albumName: "Varieté", labels: {"jazz_id": 1}, }); library.addTrack({ key: "v7zwEPLs", name: "Été (Remix)", artistName: "Some Remixer", albumName: "Varieté", labels: {"techno_id": 1, "jazz_id": 1}, }); library.addTrack({ key: "zyGaKkrU", name: "Tribute to Young Stroke AKA Young Muscle", artistName: "Andy Kelley", albumName: "The Weekend Challenge #3", }); library.rebuildTracks(); it("'not:'", function() { assert.strictEqual(library.search('not:andy').artistList.length, 2); assert.strictEqual(library.search('not:remix variete').artistList.length, 1); assert.strictEqual(library.search('not:"AKA AKA"').artistList.length, 2); assert.strictEqual(library.search('not:"aka aka"').artistList.length, 3); assert.strictEqual(library.search('not:(aka young)').artistList.length, 2); assert.strictEqual(library.search('not:').artistList.length, 0); }); it("'label:'", function() { assert.strictEqual(library.search('label:techno').artistList.length, 1); assert.strictEqual(library.search('label:jazz').artistList.length, 2); assert.strictEqual(library.search('not:label:techno').artistList.length, 2); assert.strictEqual(library.search('"label:techno"').artistList.length, 0); assert.strictEqual(library.search('not:"label:techno"').artistList.length, 3); assert.strictEqual(library.search('label:wrong').artistList.length, 0); assert.strictEqual(library.search('not:label:wrong').artistList.length, 3); assert.strictEqual(library.search('not:(label:jazz not:label:techno)').artistList.length, 2); }); it("'or:'", function() { assert.strictEqual(library.search('or:()').artistList.length, 0); assert.strictEqual(library.search('or:(aka)').artistList.length, 2); assert.strictEqual(library.search('or:(variete)').artistList.length, 2); assert.strictEqual(library.search('or:(label:techno not:label:jazz)').artistList.length, 2); }); });