const CLASSNAME = "SongbirdPlayHistoryService"; const CONTRACTID = "@skrul.com/songbird-play-history-service;1"; const CID = Components.ID("{89c91bde-ebe2-4960-9a6b-fd9342099131}"); const Cc = Components.classes; const Ci = Components.interfaces; const DEBUG = false; const PLAYER_CONTROL_NONE = 0; const PLAYER_CONTROL_PLAY = 1; const PLAYER_CONTROL_PAUSE = 2; const PLAYER_CONTROL_FORWARD = 3; const PLAYER_CONTROL_BACKWARD = 4; const PLAYER_CONTROL_SELECT = 5; const PLAYER_CONTROL_SHUTDOWN = 6; var playHistoryService = { _lastActionType : PLAYER_CONTROL_NONE, _lastActionTime : null, _lastActionPosition : null, _currentItem : null, _lastItem : null, _observers : [] }; function d(s) { if(DEBUG) { dump(">>>>>>>>>>>>> playHistoryService: " + s + "\n"); Cc["@mozilla.org/consoleservice;1"] .getService(Ci.nsIConsoleService) .logStringMessage("playHistoryService: " + s); } } // nsISupports playHistoryService.QueryInterface = function(aIID) { if(!aIID.equals(Ci.nsISupports) && !aIID.equals(Ci.sbIPlayHistoryService) && !aIID.equals(Ci.nsIObserver)) throw Components.results.NS_ERROR_NO_INTERFACE; return this; } // nsIObserver playHistoryService.observe = function(aSubject, aTopic, aData) { if(aTopic == "app-startup") { var os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); os.addObserver(this, "profile-after-change", false); os.addObserver(this, "profile-before-change", false); } if(aTopic == "profile-after-change") { this._startup(); } if(aTopic == "profile-before-change") { this._shutdown(); } if(aTopic == "nsPref:changed") { this._trackChanged(); } } // sbHistoryService playHistoryService.addPlayButton = function(aButton) { aButton.addEventListener("click", this._playOnClick, false); } playHistoryService.removePlayButton = function(aButton) { aButton.removeEventListener("click", this._playOnClick, false); } playHistoryService.addForwardButton = function(aButton) { aButton.addEventListener("click", this._forwardOnClick, false); } playHistoryService.removeForwardButton = function(aButton) { aButton.removeEventListener("click", this._forwardOnClick, false); } playHistoryService.addBackwardButton = function(aButton) { aButton.addEventListener("click", this._backwardOnClick, false); } playHistoryService.removeBackwardButton = function(aButton) { aButton.removeEventListener("click", this._backwardOnClick, false); } playHistoryService.addPlaylist = function(aButton) { aButton.addEventListener("playlist-play", this._playlistOnPlaylistPlay, true); } playHistoryService.removePlaylist = function(aButton) { aButton.removeEventListener("playlist-play", this._playlistOnPlaylistPlay, true); } playHistoryService.addObserver = function(aObserver) { this._add(this._observers, aObserver); } playHistoryService.removeObserver = function(aObserver) { this._observers = this._remove(this._observers, aObserver); } playHistoryService.clearAll = function() { this._executeQuery("deleteAllItems"); } playHistoryService.getItems = function(aWhere, aOrderBy, count) { this._executeQuery("selectItems", [aWhere, aOrderBy]); var result = this._query.GetResultObject(); var a = this._readItemQueryResult(result); count.value = a.length; return a; } playHistoryService._startup = function() { var sbs = Cc["@mozilla.org/intl/stringbundle;1"] .getService(Ci.nsIStringBundleService); this._sb = sbs.createBundle("chrome://audioscrobbler/content/playHistoryService.properties"); // Get the playlist source this._pls = Cc["@mozilla.org/rdf/datasource;1?name=playlist"] .getService(Ci.sbIPlaylistsource); // Open a connection to the history db this._query = Cc["@songbird.org/Songbird/DatabaseQuery;1"] .createInstance(Ci.sbIDatabaseQuery); this._query.SetAsyncQuery(false); this._query.SetDatabaseGUID("history"); // Create play history table and set up indexes if needed try { this._executeQuery("playHistotryTableCreate"); this._executeQuery("playHistotryIdxIndexCreate"); this._executeQuery("playHistotryTsIndexCreate"); } catch(e) { // If we get here, the table already existed, which is OK } // Delete old items from play history. Hardcode this at 10 days until I // set up a pref // TODO: make a pref var days = 10; var oldest = (new Date()).getTime() - (1000 * 60 * 60 * 24 * days); this._executeQuery("deleteOldItems", [oldest]); // Watch the "metadata.url" pref for track changes var prefService = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefService); this._preferences = prefService.getBranch(""); this._preferences.QueryInterface(Ci.nsIPrefBranch2) .addObserver("metadata.url", this, false); } playHistoryService._shutdown = function() { // If a track is still playing and we are shutting down, add it as wel this._lastActionType = PLAYER_CONTROL_SHUTDOWN; this._trackChanged(); this._preferences.QueryInterface(Ci.nsIPrefBranch2) .removeObserver("metadata.length.str", this); } playHistoryService._insertItem = function(item) { var args = [ (new Date()).getTime(), item.playlistRef, item.playlistIndex, item.metadataTitle, item.metadataArtist, item.metadataAlbum, item.metadataUrl, item.metadataUuid, item.metadataLength, item.startState, item.startTime, item.stopState, item.stopTime, item.stopPosition ]; // Insert the new item this._executeQuery("itemInsert", args); // Get the idx that was assigned to the new item this._executeQuery("selectMaxIdx"); var idx = this._query.GetResultObject().GetRowCell(0, 0); // Get the ts that was assigned to the new item this._executeQuery("selectTsForIdx", [idx]); var ts = this._query.GetResultObject().GetRowCell(0, 0); item.setIdxTs(idx, ts); } playHistoryService._trackChanged = function() { var s = ""; var now = (new Date()).getTime(); // Get info about the new track var playlistRef = this._preferences.getCharPref("playing.ref"); var playlistIndex = this._preferences.getCharPref("playlist.index"); var metadataUrl = isnull(this._pls.GetRefRowCellByColumn(playlistRef,playlistIndex, "url")); var metadataTitle = isnull(this._pls.GetRefRowCellByColumn(playlistRef, playlistIndex, "title")); var metadataArtist = isnull(this._pls.GetRefRowCellByColumn(playlistRef, playlistIndex, "artist")); var metadataAlbum = isnull(this._pls.GetRefRowCellByColumn(playlistRef, playlistIndex, "album")); var metadataUuid = isnull(this._pls.GetRefRowCellByColumn(playlistRef, playlistIndex, "uuid")); var metadataLength = isnull(this._pls.GetRefRowCellByColumn(playlistRef, playlistIndex, "length")); var startTime = now; var startState; var stopState; switch(this._lastActionType) { case PLAYER_CONTROL_NONE: s = "none"; // If the track changes without the user doing anything, assume it was // the next item in the playlist stopState = Ci.sbIPlayHistoryItem.STOP_STATE_FINISHED; startState = Ci.sbIPlayHistoryItem.START_STATE_NEXT; // Since there was no user action, these values were not filled in. // Use the current time and the item's length if(this._currentItem) { this._lastActionTime = now; this._lastActionPosition = this._currentItem.metadataLength; } break; case PLAYER_CONTROL_PLAY: s = "play"; // Not used yet break; case PLAYER_CONTROL_PAUSE: s = "pause"; // Not used yet break; case PLAYER_CONTROL_FORWARD: s = "forward"; // Clicking the forward button caused the track to change to the next // item in the playlist stopState = Ci.sbIPlayHistoryItem.STOP_STATE_SKIPPED; startState = Ci.sbIPlayHistoryItem.START_STATE_NEXT; break; case PLAYER_CONTROL_BACKWARD: s = "backward"; // Clicking the backward button cased the track to change to the previous // item in the playlist stopState = Ci.sbIPlayHistoryItem.STOP_STATE_SKIPPED; startState = Ci.sbIPlayHistoryItem.START_STATE_PREVIOUS; break; case PLAYER_CONTROL_SELECT: s = "select"; // The user started playing a track by selecting it from the playlist. // This interrupts the currenly playing track stopState = Ci.sbIPlayHistoryItem.STOP_STATE_INTERRUPTED; startState = Ci.sbIPlayHistoryItem.START_STATE_REQUESTED; break; case PLAYER_CONTROL_SHUTDOWN: s = "shutdown"; // The player is shutting down. Set the proper stop state. stopState = Ci.sbIPlayHistoryItem.STOP_STATE_SHUTDOWN; startState = Ci.sbIPlayHistoryItem.START_STATE_NEXT; // Since there was no user action, these values were not filled in. // Since the track is still playing, we can grab the position here this._lastActionTime = now; this._lastActionPosition = this._preferences .getCharPref("metadata.position");; break; } // If there was a current item, update it with the new data and save it if(this._currentItem) { // TODO: Check for 0 last action position this._currentItem.stopped(stopState, this._lastActionTime, this._lastActionPosition); this._insertItem(this._currentItem); } this._lastItem = this._currentItem; // Create a new item representing the new track this._currentItem = Cc["@skrul.com/songbird-play-history-item;1"] .createInstance(Ci.sbIPlayHistoryItem); this._currentItem.started(playlistRef, playlistIndex, metadataTitle, metadataArtist, metadataAlbum, metadataUrl, metadataUuid, metadataLength, startState, startTime); d("track changed to: " + this._currentItem); // Notify observers of the track change unless we're shutting down if(this._lastActionType = PLAYER_CONTROL_SHUTDOWN) { for(var i = 0; i < this._observers.length; i++) { try { this._observers[i].onTrackChange(this._lastItem, this._currentItem); } catch(e) { Components.reportError(e); } } } this._lastActionType = PLAYER_CONTROL_NONE; this._lastActionTime = null; this._lastActionPosition = null; } playHistoryService._getSql = function(name, args) { if(args) { var i = 0; var s = ""; // Work around the 10 arg limit do { var a = args.slice(i * 10, (i * 10) + 10); // Escape single quotes for(var j = 0; j < a.length; j++) { if(typeof(a[j]) == "string") { a[j] = a[j].replace("'", "''", "g"); } } try { s += this._sb.formatStringFromName(name + (i == 0 ? "" : i), a, a.length); } catch(e) { Components.reportError("formatStringFromName failed for '" + name + "' message: " + e); } i++; } while(i * 10 < args.length); return s; } else { return this._sb.GetStringFromName(name); } } playHistoryService._executeQuery = function(name, args) { this._query.ResetQuery(); var sql = this._getSql(name, args); this._query.AddQuery(sql); var rv = this._query.Execute(); if(rv != 0) { throw Error("Error running query '" + name + "' " + args ? "with args '" + args + "'" : "" + ", rv = " + rv); } } playHistoryService._readItemQueryResult = function(result) { var a = []; var count = result.GetRowCount(); for(var i = 0; i < count; i++) { var item = Cc["@skrul.com/songbird-play-history-item;1"] .createInstance(Ci.sbIPlayHistoryItem); item.init( result.GetRowCellByColumn(i, "idx"), result.GetRowCellByColumn(i, "ts"), result.GetRowCellByColumn(i, "playlistRef"), result.GetRowCellByColumn(i, "playlistIndex"), result.GetRowCellByColumn(i, "metadataTitle"), result.GetRowCellByColumn(i, "metadataArtist"), result.GetRowCellByColumn(i, "metadataAlbum"), result.GetRowCellByColumn(i, "metadataUrl"), result.GetRowCellByColumn(i, "metadataUuid"), result.GetRowCellByColumn(i, "metadataLength"), result.GetRowCellByColumn(i, "startState"), result.GetRowCellByColumn(i, "startTime"), result.GetRowCellByColumn(i, "stopState"), result.GetRowCellByColumn(i, "stopTime"), result.GetRowCellByColumn(i, "stopPosition")); a.push(item); } return a; } playHistoryService._add = function(set, object) { if(set.some(function(o) { return (o == object); })) { return; } set.push(object); } playHistoryService._remove = function(set, object) { var newSet = set.filter( function(o) { return (o != object); }); return newSet; } playHistoryService._onAction = function(actionType) { this._lastActionType = actionType; this._lastActionTime = (new Date()).getTime(); this._lastActionPosition = this._preferences.getCharPref("metadata.position"); } // Note that these are called in the xul button's scope playHistoryService._playOnClick = function() { playHistoryService._onAction(PLAYER_CONTROL_PLAY); } playHistoryService._forwardOnClick = function() { playHistoryService._onAction(PLAYER_CONTROL_FORWARD); } playHistoryService._backwardOnClick = function() { playHistoryService._onAction(PLAYER_CONTROL_BACKWARD); } playHistoryService._playlistOnPlaylistPlay = function() { playHistoryService._onAction(PLAYER_CONTROL_SELECT); } function isnull(a, b) { return a == null ? b : a; } /** * XPCOM Registration */ var Module = new Object(); Module.registerSelf = function(compMgr, fileSpec, location, type) { compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); compMgr.registerFactoryLocation(CID, CLASSNAME, CONTRACTID, fileSpec, location, type); var catMgr = Cc["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager); catMgr.addCategoryEntry("app-startup", CLASSNAME, "service," + CONTRACTID, true, true); } Module.getClassObject = function(compMgr, cid, iid) { if(!cid.equals(CID)) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; } if(!iid.equals(Ci.nsIFactory)) { throw Components.results.NS_ERROR_NO_INTERFACE; } return Factory; } Module.canUnload = function(compMgr) { return true; } var Factory = {}; Factory.createInstance = function(outer, iid) { if(outer != null) { throw Components.results.NS_ERROR_NO_AGGREGATION; } return playHistoryService; } function NSGetModule(compMgr, fileSpec) { return Module; }