// Copyright (c) 2009 by Prathyush Thota // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. const Player = Module("player", { init: function () { this._lastSearchString = ""; this._lastSearchIndex = 0; this._lastSearchView = _SBGetCurrentView(); // Get the focus to the visible playlist first //window._SBShowMainLibrary(); gMM.addListener(this._mediaCoreListener); }, destroy: function () { gMM.removeListener(this._mediaCoreListener); }, // interval (milliseconds) _seek: function (interval, direction) { let position = gMM.playbackControl ? gMM.playbackControl.position : 0; player.seekTo(position + (direction ? interval : -interval)); }, _focusTrack: function (mediaItem) { SBGetBrowser().mediaTab.mediaPage.highlightItem(_SBGetCurrentView().getIndexForItem(mediaItem)); }, _mediaCoreListener: { onMediacoreEvent: function (event) { switch (event.type) { case Ci.sbIMediacoreEvent.BEFORE_TRACK_CHANGE: liberator.log("Before track changed: " + event.data); autocommands.trigger("TrackChangePre", { track: event.data }); break; case Ci.sbIMediacoreEvent.TRACK_CHANGE: autocommands.trigger("TrackChange", { track: event.data }); break; case Ci.sbIMediacoreEvent.BEFORE_VIEW_CHANGE: liberator.log("Before view changed: " + event.data); autocommands.trigger("ViewChangePre", { view: event.data }); break; case Ci.sbIMediacoreEvent.VIEW_CHANGE: liberator.log("View changed: " + event.data); autocommands.trigger("ViewChange", { view: event.data }); break; case Ci.sbIMediacoreEvent.STREAM_START: liberator.log("Track started: " + gMM.sequencer.currentItem); autocommands.trigger("StreamStart", { track: gMM.sequencer.currentItem }); break; case Ci.sbIMediacoreEvent.STREAM_PAUSE: liberator.log("Track paused: " + gMM.sequencer.currentItem); autocommands.trigger("StreamPause", { track: gMM.sequencer.currentItem }); break; case Ci.sbIMediacoreEvent.STREAM_END: liberator.log("Track ended: " + gMM.sequencer.currentItem); autocommands.trigger("StreamEnd", { track: gMM.sequencer.currentItem }); break; case Ci.sbIMediacoreEvent.STREAM_STOP: liberator.log("Track stopped: " + gMM.sequencer.currentItem); autocommands.trigger("StreamStop", { track: gMM.sequencer.currentItem }); break; } } }, // TODO: check bounds and round, 0 - 1 or 0 - 100? /** * @property {string} The player volume as a percentage. */ get volume() gMM.volumeControl.volume, set volume(value) { gMM.volumeControl.volume = value; }, // FIXME: can't be called from non-media tabs since 840e78 play: function play() { // Check if there is any selection in place, else play first item of the visible view. if (_SBGetCurrentView().selection.count != 0) { // Play the selection. gMM.sequencer.playView(_SBGetCurrentView(), _SBGetCurrentView().getIndexForItem(_SBGetCurrentView().selection.currentMediaItem)); this._focusTrack(gMM.sequencer.currentItem); } else { gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0); this._focusTrack(gMM.sequencer.currentItem); } }, stop: function stop() { gMM.sequencer.stop(); }, next: function next() { gSongbirdWindowController.doCommand("cmd_control_next"); gSongbirdWindowController.doCommand("cmd_find_current_track"); }, previous: function previous() { gSongbirdWindowController.doCommand("cmd_control_previous"); gSongbirdWindowController.doCommand("cmd_find_current_track"); }, togglePlayPause: function togglePlayPause() { gSongbirdWindowController.doCommand("cmd_control_playpause"); this._focusTrack(gMM.sequencer.currentItem); }, toggleShuffle: function toggleShuffle() { if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE) gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE; else gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD; }, // FIXME: not really toggling - good enough for now. toggleRepeat: function toggleRepeat() { switch (gMM.sequencer.repeatMode) { case gMM.sequencer.MODE_REPEAT_NONE: gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE; break; case gMM.sequencer.MODE_REPEAT_ONE: gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL; break; case gMM.sequencer.MODE_REPEAT_ALL: gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; break; default: gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; break; } }, /** * Seek forward interval milliseconds in the currently playing * track. * * @param {number} interval The time interval (ms) to advance the * current track. */ seekForward: function seekForward(interval) { this._seek(interval, true); }, /** * Seek backwards interval milliseconds in the currently * playing track. * * @param {number} interval The time interval (ms) to rewind the * current track. */ seekBackward: function seekBackward(interval) { this._seek(interval, false); }, /** * Seek to a specific position in the currently playing track. * * @param {number} The new position (ms) in the track. */ seekTo: function seekTo(position) { // FIXME: if not playing if (!gMM.playbackControl) this.play(); let min = 0; let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable? gMM.playbackControl.position = util.Math.constrain(position, min, max); }, // FIXME: 10% ? // I think just general increments of say 0.05 might be better --djk increaseVolume: function increaseVolume() { gMM.volumeControl.volume = gMM.volumeControl.volume * 1.1; }, decreaseVolume: function decreaseVolume() { if (gMM.volumeControl.volume == 0) gMM.volumeControl.volume = 0.1; else gMM.volumeControl.volume = gMM.volumeControl.volume * 0.9; }, focusPlayingTrack :function focusPlayingTrack() { this._focusTrack(gMM.sequencer.currentItem); }, listTracks: function listTracks(view) { //let myView = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args); let length = view.length; let tracksList = []; for (let i = 0; i < length; i++) { let mediaItem = view.getItemByIndex(i); let trackName = mediaItem.getProperty(SBProperties.trackName); let albumName = mediaItem.getProperty(SBProperties.albumName); let artistName = mediaItem.getProperty(SBProperties.artistName); tracksList[i] = [trackName, "Album : " + albumName + " Artist : " + artistName]; } return tracksList; }, searchView: function searchView(args) { let currentView = _SBGetCurrentView(); let mediaItemList = currentView.mediaList; let search = _getSearchString(currentView); let searchString = ""; if (search != "") searchString = args + " " + search; else searchString = args; this._lastSearchString = searchString; let mySearchView = LibraryUtils.createStandardMediaListView(mediaItemList, searchString); if (mySearchView.length) { this._lastSearchView = mySearchView; this._lastSearchIndex = 0; this._focusTrack(mySearchView.getItemByIndex(this._lastSearchIndex)); } else liberator.echoerr("E486 Pattern not found: " + searchString, commandline.FORCE_SINGLELINE); }, searchViewAgain: function searchViewAgain(reverse) { function echo(str) { setTimeout(function () { commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE); }, 0); } if (reverse) { if (this._lastSearchIndex == 0) { this._lastSearchIndex = this._lastSearchView.length - 1; echo("Search hit TOP, continuing at BOTTOM"); } else this._lastSearchIndex = this._lastSearchIndex - 1; } else { if (this._lastSearchIndex == (this._lastSearchView.length - 1)) { this._lastSearchIndex = 0; echo("Search hit BOTTOM, continuing at TOP"); } else this._lastSearchIndex = this._lastSearchIndex + 1; } // FIXME: Implement for "?" --ken commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE); this._focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex)); }, /** * The search dialog keypress callback. * * @param {string} str The contents of the search dialog. */ onSearchKeyPress: function (str) { if (options["incsearch"]) this.searchView(str); }, /** * The search dialog submit callback. * * @param {string} str The contents of the search dialog. */ onSearchSubmit: function (str) { this.searchView(str); }, /** * The search dialog cancel callback. */ onSearchCancel: function () { // TODO: restore the view state if altered by an 'incsearch' search }, getPlaylists: function getPlaylists() { let mainLibrary = LibraryUtils.mainLibrary; let playlists = [mainLibrary]; let listener = { onEnumerationBegin: function () { }, onEnumerationEnd: function () { }, onEnumeratedItem: function (list, item) { // FIXME: why are there null items and duplicates? if (!playlists.some(function (list) list.name == item.name) && item.name != null) playlists.push(item); return Ci.sbIMediaListEnumerationListener.CONTINUE; } }; mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener); return playlists; }, // Play track at 'row' in 'playlist' playPlaylist: function playPlaylist(playlist, row) { gMM.sequencer.playView(playlist.createView(), row); }, getMediaPages: function getMediaPages() { let list = gBrowser.currentMediaPage.mediaListView.mediaList; let pages = services.get("mediaPageManager").getAvailablePages(list); return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo)); }, loadMediaPage: function loadMediaList(page, list, view) { services.get("mediaPageManager").setPage(list, page); gBrowser.loadMediaList(list, null, null, view, null); }, rateMediaItem: function rateMediaItem(rating) { if (gMM.sequencer.currentItem) gMM.sequencer.currentItem.setProperty(SBProperties.rating, rating); }, getUserViewable: function getUserViewable() { let propManager = services.get("propertyManager"); let propEnumerator = propManager.propertyIDs; let properties = []; while (propEnumerator.hasMore()) { let propertyId = propEnumerator.getNext(); if (propManager.getPropertyInfo(propertyId).userViewable) { //liberator.dump("propertyId - " + propManager.getPropertyInfo(propertyId).id); properties.push(propManager.getPropertyInfo(propertyId).displayName); } } return properties; }, sortBy: function sortBy(property, order) { let pa = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"].createInstance(Ci.sbIMutablePropertyArray); liberator.dump("Property: " + property); switch (property.string) { case "#": case "Title": pa.appendProperty(SBProperties.trackName, "a"); break; case "Rating": pa.appendProperty(SBProperties.rating, 1); break; case "Album": pa.appendProperty(SBProperties.albumName, "a"); break; default: pa.appendProperty(SBProperties.trackName, "a"); break; } _SBGetCurrentView().setSort(pa); } }, { }, { commandline: function () { commandline.registerCallback("change", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchKeyPress(str); }); commandline.registerCallback("submit", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchSubmit(str); }); commandline.registerCallback("cancel", modes.SEARCH_VIEW_FORWARD, function () { player.onSearchCancel(); }); }, commands: function () { commands.add(["f[ilter]"], "Filter tracks based on keywords {genre/artist/album/track}", function (args) { let library = LibraryUtils.mainLibrary; let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg); if (view.length == 0) liberator.echoerr("No Tracks matching the keywords"); else { SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view, "chrome://songbird/content/mediapages/filtersPage.xul"); // TODO: make this this._focusTrack work ? this._focusTrack(view.getItemByIndex(0)); } }, { argCount: "1", literal: 0 //completer: function (context, args) completion.tracks(context, args); }); commands.add(["load"], "Load a playlist", function (args) { let arg = args.literalArg; if (arg) { // load the selected playlist/smart playlist let playlists = player.getPlaylists(); for ([i, list] in Iterator(playlists)) { if (util.compareIgnoreCase(arg, list.name) == 0) { SBGetBrowser().loadMediaList(playlists[i]); this._focusTrack(_SBGetCurrentView().getItemByIndex(0)); return; } } liberator.echoerr("E475: Invalid argument: " + arg); } else { // load main library if there are no args _SBShowMainLibrary(); } }, { argCount: "?", completer: function (context, args) completion.playlist(context, args), literal: 0 }); // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk commands.add(["playerp[lay]"], "Play track", function () { player.play(); }); commands.add(["playerpa[use]"], "Pause/unpause track", function () { player.togglePlayPause(); }); commands.add(["playern[ext]"], "Play next track", function () { player.next(); }); commands.add(["playerpr[ev]"], "Play previous track", function () { player.previous(); }); commands.add(["players[top]"], "Stop track", function () { player.stop(); }); commands.add(["see[k]"], "Seek to a track position", function (args) { let arg = args[0]; // intentionally supports 999:99:99 if (!/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg)) return void liberator.echoerr("E475: Invalid argument: " + arg); function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m]) if (/:/.test(arg)) { let [seconds, minutes, hours] = arg.split(":").reverse(); hours = hours || 0; var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h"); } else { if (!/[smh]/.test(arg.substr(-1))) arg += "s"; // default to seconds value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1)); } if (/^[-+]/.test(arg)) arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value); else player.seekTo(value); }, { argCount: "1" }); commands.add(["mediav[iew]"], "Change the current media view", function (args) { // FIXME: is this a SB restriction? --djk if (!gBrowser.currentMediaPage) return void liberator.echoerr("Exxx: Can only set the media view from the media tab"); // XXX let arg = args[0]; if (arg) { let pages = player.getMediaPages(); for ([, page] in Iterator(pages)) { if (util.compareIgnoreCase(arg, page.contentTitle) == 0) { player.loadMediaPage(page, gBrowser.currentMediaListView.mediaList, gBrowser.currentMediaListView); return; } } liberator.echoerr("E475: Invalid argument: " + arg); } }, { argCount: "1", completer: function (context) completion.mediaView(context), literal: 0 }); commands.add(["sort[view]"], "Sort the current media view", function (args) { player.sortBy(args, true); }); // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk commands.add(["qu[eue]"], "Queue tracks by artist/album/track", function (args) { // Store the old view // let prev_view = gMM.status.view; let library = LibraryUtils.mainLibrary; let mainView = library.createView(); let customProps = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"] .createInstance(Ci.sbIMutablePropertyArray); // args switch (args.length) { case 3: customProps.appendProperty(SBProperties.trackName, args[2]); case 2: customProps.appendProperty(SBProperties.albumName, args[1]); case 1: customProps.appendProperty(SBProperties.artistName, args[0]); break; default: break; } gMM.sequencer.playView(mainView, mainView.getIndexForItem(library.getItemsByProperties(customProps).queryElementAt(0, Ci.sbIMediaItem))); player.focusPlayingTrack(); }, { argCount: "+", completer: function (context, args) completion.song(context, args) }); // TODO: maybe :vol! could toggle mute on/off? --djk commands.add(["vol[ume]"], "Set the volume", function (args) { let arg = args[0]; if (!/^[+-]?\d+$/.test(arg)) return void liberator.echoerr("E488: Trailing characters"); let level = parseInt(arg, 10) / 100; if (/^[+-]/.test(arg)) level = player.volume + level; player.volume = util.Math.constrain(level, 0, 1); }, { argCount: "1" }); }, completion: function () { completion.song = function song(context, args) { // TODO: useful descriptions? function map(list) list.map(function (i) [i, ""]); let [artist, album] = [args[0], args[1]]; if (args.completeArg == 0) { context.title = ["Artists"]; context.completions = map(library.getArtists()); } else if (args.completeArg == 1) { context.title = ["Albums by " + artist]; context.completions = map(library.getAlbums(artist)); } else if (args.completeArg == 2) { context.title = ["Tracks from " + album + " by " + artist]; context.completions = map(library.getTracks(artist, album)); } }; completion.playlist = function playlist(context, args) { context.title = ["Playlist", "Type"]; context.keys = { text: "name", description: "type" }; context.completions = player.getPlaylists(); }; completion.mediaView = function mediaView(context) { context.title = ["Media View", "URL"]; context.anchored = false; context.keys = { text: "contentTitle", description: "contentUrl" }; context.completions = player.getMediaPages(); }; }, mappings: function () { mappings.add([modes.PLAYER], ["x"], "Play track", function () { player.play(); }); mappings.add([modes.PLAYER], ["z"], "Previous track", function () { player.previous(); }); mappings.add([modes.PLAYER], ["c"], "Pause/unpause track", function () { player.togglePlayPause(); }); mappings.add([modes.PLAYER], ["b"], "Next track", function () { player.next(); }); mappings.add([modes.PLAYER], ["v"], "Stop track", function () { player.stop(); }); mappings.add([modes.PLAYER], ["Q"], "Queue tracks by artist/album/track", function () { commandline.open(":", "queue ", modes.EX); }); mappings.add([modes.PLAYER], ["f"], "Loads current view filtered by the keywords", function () { commandline.open(":", "filter ", modes.EX); }); mappings.add([modes.PLAYER], ["i"], "Select current track", function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); }); mappings.add([modes.PLAYER], ["s"], "Toggle shuffle", function () { player.toggleShuffle(); }); mappings.add([modes.PLAYER], ["r"], "Toggle repeat", function () { player.toggleRepeat(); }); mappings.add([modes.PLAYER], ["h", ""], "Seek -10s", function (count) { player.seekBackward(Math.max(1, count) * 10000); }, { count: true }); mappings.add([modes.PLAYER], ["l", ""], "Seek +10s", function (count) { player.seekForward(Math.max(1, count) * 10000); }, { count: true }); mappings.add([modes.PLAYER], ["H", ""], "Seek -1m", function (count) { player.seekBackward(Math.max(1, count) * 60000); }, { count: true }); mappings.add([modes.PLAYER], ["L", ""], "Seek +1m", function (count) { player.seekForward(Math.max(1, count) * 60000); }, { count: true }); mappings.add([modes.PLAYER], ["=", "+"], "Increase volume by 10%", function () { player.increaseVolume(); }); mappings.add([modes.PLAYER], ["-"], "Decrease volume by 10%", function () { player.decreaseVolume(); }); mappings.add([modes.PLAYER], ["/"], "Search forward for a track", function (args) { commandline.open("/", "", modes.SEARCH_VIEW_FORWARD); }); mappings.add([modes.PLAYER], ["n"], "Find the next track", function () { player.searchViewAgain(false);}); mappings.add([modes.PLAYER], ["N"], "Find the previous track", function () { player.searchViewAgain(true);}); for (let i in util.range(0, 6)) { let (rating = i) { mappings.add([modes.PLAYER], [""], "Rate the current media item " + rating, function () { player.rateMediaItem(rating); }); }; } }, options: function () { options.add(["repeat"], "Set the playback repeat mode", "number", 0, { setter: function (value) gMM.sequencer.repeatMode = value, getter: function () gMM.sequencer.repeatMode, completer: function (context) [ ["0", "Repeat none"], ["1", "Repeat one"], ["2", "Repeat all"] ], validator: Option.validateCompleter }); options.add(["shuffle"], "Play tracks in shuffled order", "boolean", false, { setter: function (value) value ? gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE : gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD, getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE }); }, services: function () { services.add("mediaPageManager", "@songbirdnest.com/Songbird/MediaPageManager;1", Ci.sbIMediaPageManager); services.add("propertyManager","@songbirdnest.com/Songbird/Properties/PropertyManager;1", Ci.sbIPropertyManager); } }); // vim: set fdm=marker sw=4 ts=4 et: