Skip to content

Commit

Permalink
WIP: Use similarity in AutoDJ functionality
Browse files Browse the repository at this point in the history
- Use the compilation library to pull in similarity stats for the current
  song, and use this as factors in an overall similarity ranking.
- Convert lastQueueDate ranking into one of several factors contributing to
  the rank, with the intention of making the proportion of each criteria
  configurable, possibly using sliders.
- Make the randomness of the selection (how likely it follows the ranking)
  configurable.
  • Loading branch information
gwicke committed Mar 12, 2016
1 parent b488471 commit 6bf9841
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 36 deletions.
194 changes: 158 additions & 36 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var findit = require('findit2');
var shuffle = require('mess');
var mv = require('mv');
var MusicLibraryIndex = require('music-library-index');
var Compilation = require('compilation');
var keese = require('keese');
var safePath = require('./safe_path');
var PassThrough = require('stream').PassThrough;
Expand Down Expand Up @@ -307,7 +308,8 @@ function Player(db, config) {
this.pausedTime = 0;
this.autoDjOn = false;
this.autoDjHistorySize = 10;
this.autoDjFutureSize = 10;
this.autoDjFutureSize = 15;
this.compilation = new Compilation();

this.ongoingScans = {};
this.scanQueue = new DedupedQueue({
Expand Down Expand Up @@ -2187,61 +2189,181 @@ Player.prototype.performScan = function(args, cb) {
}
};

function normalizeName(name) {
return ('' + name).toLowerCase()
.replace(/[\[\(][\d]+[\]\)]/g, ' ').trim();
}

Player.prototype.checkAutoDj = function() {
var self = this;
if (!self.autoDjOn) return;
if (!self.autoDjOn || self.autoDjActive || !self.tracksInOrder.length) return;

// if no track is playing, assume the first track is about to be
var currentIndex = self.currentTrack ? self.currentTrack.index : 0;
var currentKey = self.tracksInOrder[currentIndex].key;
var currentTrack = self.libraryIndex.trackTable[currentKey];

var deleteCount = Math.max(currentIndex - self.autoDjHistorySize, 0);
if (self.autoDjHistorySize < 0) deleteCount = 0;
var addCount = Math.max(self.autoDjFutureSize + 1 - (self.tracksInOrder.length - currentIndex), 0);

if (!addCount) {
return;
}

self.autoDjActive = true;

var idsToDelete = [];
for (var i = 0; i < deleteCount; i += 1) {
idsToDelete.push(self.tracksInOrder[i].id);
}
var keys = getRandomSongKeys(addCount);
self.removeQueueItems(idsToDelete);
self.appendTracks(keys, true);
var currentTrackInfo = {
artist: currentTrack.artistName,
album: currentTrack.albumName,
track: currentTrack.name,
};
if (!currentTrackInfo.artist || !currentTrackInfo.track) {
return;
}
console.log(currentTrackInfo);
return self.compilation.similarTracks(currentTrackInfo)
.then(function(similarTracks) {
// console.log(similarTracks.map(function(track) {
// return [track.artist.name, track.name, track.similarity];
// }));
// Convert into object for quick lookup
var similarMap = {};
similarTracks.forEach(function(track) {
var artistMap = similarMap[normalizeName(track.artist.name)];
if (!artistMap) {
artistMap = similarMap[normalizeName(track.artist.name)] = {
tracks: {},
// Initialize artist similarity to track similarity. This is
// normally overwritten later.
similarity: track.similarity / 4,
};
}
artistMap.tracks[normalizeName(track.name)] = track.similarity;
});

// Add the current artist as similar to itself
return self.compilation.similarArtists(currentTrackInfo)
.then(function(similarArtists) {
// The current artist is fairly similar to itself, but we don't want
// to completely overpower other factors either.
var maxSimilarity = similarArtists.length ?
similarArtists[0].similarity : 0.7;
console.log(similarArtists[0]);
similarArtists.push({
name: normalizeName(currentTrackInfo.artist),
// Slightly prefer another artist to encourage variety. Track
// similarity tends to be high for the current artist.
similarity: maxSimilarity * 0.75,
});
similarArtists.forEach(function(artist) {
var artistName = normalizeName(artist.name);
var mapEntry = similarMap[artistName] = similarMap[artistName] || {tracks:{}};
mapEntry.similarity = artist.similarity;
});
return similarMap;
});
})
.then(function(similarMap) {
// console.log(JSON.stringify(similarMap, null, 2));
var keys = getRandomSongKeys(addCount, similarMap);
self.removeQueueItems(idsToDelete);
self.appendTracks(keys, true);
})
.finally(function() {
self.autoDjActive = false;
});


function getRandomSongKeys(count) {
function getRandomSongKeys(count, similarMap) {
if (count === 0) return [];
var neverQueued = [];
var sometimesQueued = [];
for (var key in self.libraryIndex.trackTable) {
var keys = Object.keys(self.libraryIndex.trackTable);
var items = [];
var prevKey = null;
var similarCount = 0;
keys.forEach(function(key) {
prevKey = key;
var dbFile = self.libraryIndex.trackTable[key];
if (dbFile.lastQueueDate == null) {
neverQueued.push(dbFile);
} else {
sometimesQueued.push(dbFile);
var item = {
key: key,
info: {
key: dbFile.key,
artist: normalizeName(dbFile.artistName),
track: normalizeName(dbFile.name),
},
// Seed with some randomness
weight: Math.random() / 50
};
items.push(item);

// Consider similarity
var trackSimilarityFactor = 0.9;
// Artist similarity, considered less important than track similarity.
var artistSimilarityFactor = 0.75;

var similarMapEntry = similarMap[item.info.artist];
if (similarMapEntry) {
var trackSimilarity = similarMapEntry.tracks[item.info.track];
if (trackSimilarity) {
item.weight += trackSimilarity * trackSimilarityFactor;
}
item.weight += similarMapEntry.similarity * artistSimilarityFactor;
similarCount++;
}
}
// backwards by time
sometimesQueued.sort(function(a, b) {
return b.lastQueueDate - a.lastQueueDate;

// Penalize items that were queued recently.
var daysSinceLastQueued = (Date.now() - (dbFile.lastQueueDate || 0)) / 86400000;
var lastQueueWeight = 2 - (1 / (1 + daysSinceLastQueued));
// Weigh last-played
item.weight += 0.6 * lastQueueWeight;

// Boost titles that have been played fully in the past.
var playBoost = 1 - (1 / (1 + (dbFile.playCount || 0)));
item.weight += 0.3 * playBoost;
});
// Sort by weight, descending.
items.sort(function(a, b) {
return b.weight - a.weight;
});
// distribution is a triangle for ever queued, and a rectangle for never queued
// ___
// /| |
// / | |
// /__|_|
var maxWeight = sometimesQueued.length;
var triangleArea = Math.floor(maxWeight * maxWeight / 2);
if (maxWeight === 0) maxWeight = 1;
var rectangleArea = maxWeight * neverQueued.length;
var totalSize = triangleArea + rectangleArea;
if (totalSize === 0) return [];
// decode indexes through the distribution shape
var keys = [];
for (var i = 0; i < count; i += 1) {
var index = Math.random() * totalSize;
if (index < triangleArea) {
// triangle
keys.push(sometimesQueued[Math.floor(Math.sqrt(index))].key);

function printItems(a) {
for (var i = a.length - 1; i; i--) {
var item = a[i];
console.log(item.info.artist, '-', item.info.track, item.weight);
}
}
//printItems(items.slice(0, 10));
printItems(items.slice(0, 100));

keys = [];

// How much we should focus on the top-ranked candidates. Values between 5
// and 1000 seem to work well.
var focus = 50;

// Only consider 'similar' tracks for now.
console.log('similarCount', similarCount);
var baseIndex = items.length - similarCount;
var constFac = Math.pow(similarCount, focus);
for (i = 0; i < count; i += 1) {
var rand = Math.random();
var index = Math.floor(similarCount * (1 / (1 + rand * focus) - rand / (1 + focus)));
console.log(index);
var item = items[index];
if (item.weight !== null &&
(item.info.artist !== normalizeName(currentTrackInfo.artist) ||
// Penalize large collections of the same title.
Math.random() < (4 / (1 + index)))) {
console.log('push', index, item.info.artist, '-', item.info.track);
keys.push(item.info.key);
item.weight = null;
} else {
keys.push(neverQueued[Math.floor((index - triangleArea) / maxWeight)].key);
// retry
i--;
}
}
return keys;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"url": "git://github.com/andrewrk/groovebasin.git"
},
"dependencies": {
"compilation": "^0.1.5",
"connect-static": "~1.5.0",
"content-disposition": "~0.5.0",
"cookies": "~0.5.0",
Expand Down

0 comments on commit 6bf9841

Please sign in to comment.