1 // Copyright (c) 2009 by Prathyush Thota <prathyushthota@gmail.com>
2 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
8 const Player = Module("player", {
9 init: function init() {
10 this._lastSearchString = "";
11 this._lastSearchIndex = 0;
12 this._lastSearchView = this._currentView; //XXX
14 // Get the focus to the visible playlist first
15 //window._SBShowMainLibrary();
17 gMM.addListener(this._mediaCoreListener);
20 destroy: function destroy() {
21 gMM.removeListener(this._mediaCoreListener);
25 * Moves the track position *interval* milliseconds forwards or backwards.
27 * @param {number} interval The time interval (ms) to move the track
29 * @param {boolean} direction The direction in which to move the track
30 * position, forward if true otherwise backwards.
33 _seek: function _seek(interval, direction) {
34 let position = gMM.playbackControl ? gMM.playbackControl.position : 0;
35 player.seekTo(position + (direction ? interval : -interval));
39 * Listens for media core events and in response dispatches the appropriate
44 onMediacoreEvent: function (event) {
46 case Ci.sbIMediacoreEvent.BEFORE_TRACK_CHANGE:
47 dactyl.log(_("player.preTrackChange", event.data));
48 autocommands.trigger("TrackChangePre", { track: event.data });
50 case Ci.sbIMediacoreEvent.TRACK_CHANGE:
51 dactyl.log(_("player.trackChanged", event.data));
52 autocommands.trigger("TrackChange", { track: event.data });
54 case Ci.sbIMediacoreEvent.BEFORE_VIEW_CHANGE:
55 dactyl.log(_("player.preViewChange", event.data));
56 autocommands.trigger("ViewChangePre", { view: event.data });
58 case Ci.sbIMediacoreEvent.VIEW_CHANGE:
59 dactyl.log(_("player.viewChange", event.data));
60 autocommands.trigger("ViewChange", { view: event.data });
62 case Ci.sbIMediacoreEvent.STREAM_START:
63 dactyl.log(_("player.trackStart", gMM.sequencer.currentItem));
64 autocommands.trigger("StreamStart", { track: gMM.sequencer.currentItem });
66 case Ci.sbIMediacoreEvent.STREAM_PAUSE:
67 dactyl.log(_("player.trackPause", gMM.sequencer.currentItem));
68 autocommands.trigger("StreamPause", { track: gMM.sequencer.currentItem });
70 case Ci.sbIMediacoreEvent.STREAM_END:
71 dactyl.log(_("player.trackEnd", gMM.sequencer.currentItem));
72 autocommands.trigger("StreamEnd", { track: gMM.sequencer.currentItem });
74 case Ci.sbIMediacoreEvent.STREAM_STOP:
75 dactyl.log(_("player.trackStop", gMM.sequencer.currentItem));
76 autocommands.trigger("StreamStop", { track: gMM.sequencer.currentItem });
82 /** @property {sbIMediaListView} The current media list view. @private */
83 get _currentView() SBGetBrowser().currentMediaListView,
86 * @property {number} The player volume in the range 0.0-1.0.
88 get volume() gMM.volumeControl.volume,
90 gMM.volumeControl.volume = value;
94 * Focuses the specified media item in the current media list view.
96 * @param {sbIMediaItem} mediaItem The media item to focus.
98 focusTrack: function focusTrack(mediaItem) {
99 SBGetBrowser().mediaTab.mediaPage.highlightItem(this._currentView.getIndexForItem(mediaItem));
103 * Plays the currently selected media item. If no item is selected the
104 * first item in the current media view is played.
106 play: function play() {
107 // Check if there is any selection in place, else play first item of the visible view.
108 // TODO: this approach, or similar, should be generalised for all commands, PT? --djk
109 if (this._currentView.selection.count != 0)
110 gMM.sequencer.playView(this._currentView,
111 this._currentView.getIndexForItem(this._currentView.selection.currentMediaItem));
113 gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0);
115 this.focusTrack(gMM.sequencer.currentItem);
119 * Stops playback of the currently playing media item.
121 stop: function stop() {
122 gMM.sequencer.stop();
126 * Plays the *count*th next media item in the current media view.
128 * @param {number} count
130 next: function next(count) {
131 for (let i = 0; i < count; i++)
132 gSongbirdWindowController.doCommand("cmd_control_next");
133 gSongbirdWindowController.doCommand("cmd_find_current_track");
137 * Plays the *count*th previous media item in the current media view.
139 * @param {number} count
141 previous: function previous(count) {
142 for (let i = 0; i < count; i++)
143 gSongbirdWindowController.doCommand("cmd_control_previous");
144 gSongbirdWindowController.doCommand("cmd_find_current_track");
148 * Toggles the play/pause status of the current media item.
150 togglePlayPause: function togglePlayPause() {
151 ["cmd_control_playpause", "cmd_find_current_track"].forEach(gSongbirdWindowController.doCommand);
155 * Toggles the shuffle status of the sequencer.
157 toggleShuffle: function toggleShuffle() {
158 if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE)
159 gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE;
161 gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD;
164 // FIXME: not really toggling (depending on your definition) - good enough for now.
166 * Toggles between the sequencer's three repeat modes: Repeat-One,
167 * Repeat-All and Repeat-None.
169 toggleRepeat: function toggleRepeat() {
170 switch (gMM.sequencer.repeatMode) {
171 case gMM.sequencer.MODE_REPEAT_NONE:
172 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE;
174 case gMM.sequencer.MODE_REPEAT_ONE:
175 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL;
177 case gMM.sequencer.MODE_REPEAT_ALL:
178 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
181 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
187 * Seeks forward *interval* milliseconds in the currently playing track.
189 * @param {number} interval The time interval (ms) to advance the
192 seekForward: function seekForward(interval) {
193 this._seek(interval, true);
197 * Seeks backwards *interval* milliseconds in the currently playing track.
199 * @param {number} interval The time interval (ms) to rewind the
202 seekBackward: function seekBackward(interval) {
203 this._seek(interval, false);
207 * Seeks to a specific position in the currently playing track.
209 * @param {number} The new position (ms) in the track.
211 seekTo: function seekTo(position) {
212 // FIXME: if not playing
213 if (!gMM.playbackControl)
217 let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable?
219 gMM.playbackControl.position = Math.constrain(position, min, max);
223 * Increases the volume by 5% of the maximum volume.
225 increaseVolume: function increaseVolume() {
226 this.volume = Math.constrain(this.volume + 0.05, 0, 1);
230 * Decreases the volume by 5% of the maximum volume.
232 decreaseVolume: function decreaseVolume() {
233 this.volume = Math.constrain(this.volume - 0.05, 0, 1);
236 // TODO: Document what this buys us over and above cmd_find_current_track
238 * Focuses the currently playing track.
240 focusPlayingTrack: function focusPlayingTrack() {
241 this.focusTrack(gMM.sequencer.currentItem);
245 * Searches the current media view for *str*
247 * @param {string} str The search string.
249 searchView: function searchView(str) {
250 let search = _getSearchString(this._currentView);
251 let searchString = "";
253 if (search != "") // XXX
254 searchString = str + " " + search;
258 this._lastSearchString = searchString;
260 let searchView = LibraryUtils.createStandardMediaListView(this._currentView.mediaList, searchString);
262 if (searchView.length) {
263 this._lastSearchView = searchView;
264 this._lastSearchIndex = 0;
265 this.focusTrack(searchView.getItemByIndex(this._lastSearchIndex));
268 dactyl.echoerr(_("finder.notFound", searchString), commandline.FORCE_SINGLELINE);
272 * Repeats the previous view search.
274 * @param {boolean} reverse Search in the reverse direction to the previous
277 searchViewAgain: function searchViewAgain(reverse) {
279 this.timeout(function () {
280 commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
285 if (this._lastSearchIndex == 0) {
286 this._lastSearchIndex = this._lastSearchView.length - 1;
287 echo(_("finder.atTop"));
290 this._lastSearchIndex = this._lastSearchIndex - 1;
293 if (this._lastSearchIndex == (this._lastSearchView.length - 1)) {
294 this._lastSearchIndex = 0;
295 echo(_("finder.atBottom"));
298 this._lastSearchIndex = this._lastSearchIndex + 1;
301 // TODO: Implement for "?" --ken
302 commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE);
303 this.focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex));
308 * The search dialog keypress callback.
310 * @param {string} str The contents of the search dialog.
312 onSearchKeyPress: function onSearchKeyPress(str) {
313 if (options["incsearch"])
314 this.searchView(str);
318 * The search dialog submit callback.
320 * @param {string} str The contents of the search dialog.
322 onSearchSubmit: function onSearchSubmit(str) {
323 this.searchView(str);
327 * The search dialog cancel callback.
329 onSearchCancel: function onSearchCancel() {
330 // TODO: restore the view state if altered by an 'incsearch' search
334 * Returns an array of all available playlists.
336 * @returns {[sbIMediaList]}
338 getPlaylists: function getPlaylists() {
339 let mainLibrary = LibraryUtils.mainLibrary;
340 let playlists = [mainLibrary];
342 onEnumerationBegin: function () { },
343 onEnumerationEnd: function () { },
344 onEnumeratedItem: function (list, item) {
345 // FIXME: why are there null items and duplicates?
346 if (!playlists.some(function (list) list.name == item.name) && item.name != null)
347 playlists.push(item);
348 return Ci.sbIMediaListEnumerationListener.CONTINUE;
352 mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener);
358 * Plays the media item at *index* in *playlist*.
360 * @param {sbIMediaList} playlist
361 * @param {number} index
363 playPlaylist: function playPlaylist(playlist, index) {
364 gMM.sequencer.playView(playlist.createView(), index);
368 * Returns an array of all available media pages.
370 * @returns {[sbIMediaPageInfo]}
372 getMediaPages: function getMediaPages() {
373 let list = SBGetBrowser().currentMediaPage.mediaListView.mediaList;
374 let pages = services.mediaPageManager.getAvailablePages(list);
375 return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo));
379 * Loads the specified media page into *view* with the given *list* of
382 * @param {sbIMediaPage} page
383 * @param {sbIMediaList} list
384 * @param {sbIMediaView} view
386 loadMediaPage: function loadMediaPage(page, list, view) {
387 services.mediaPageManager.setPage(list, page);
388 SBGetBrowser().loadMediaList(list, null, null, view, null);
392 * Applys the specified *rating* to *mediaItem*.
394 * @param {sbIMediaItem} mediaItem The media item to rate.
395 * @param {number} rating The star rating (1-5).
397 rateMediaItem: function rateMediaItem(mediaItem, rating) {
398 mediaItem.setProperty(SBProperties.rating, rating);
401 // TODO: add more fields, and generate the list dynamically. PT should the
402 // available fields reflect only the visible view fields or offer others? --djk
404 * Sorts the current media view by *field* in the order specified by
407 * @param {string} field The sort field.
408 * @param {boolean} ascending If true sort in ascending order, otherwise in
411 sortBy: function sortBy(field, ascending) {
412 let order = ascending ? "a" : "d";
413 let properties = services.MutablePropertyArray();
414 properties.strict = false;
418 properties.appendProperty(SBProperties.trackName, order);
421 properties.appendProperty(SBProperties.duration, order);
424 properties.appendProperty(SBProperties.artistName, order);
427 properties.appendProperty(SBProperties.albumName, order);
430 properties.appendProperty(SBProperties.genre, order);
433 properties.appendProperty(SBProperties.rating, order);
436 properties.appendProperty(SBProperties.trackName, order);
440 this._currentView.setSort(properties);
444 modes: function initModes(dactyl, modules, window) {
445 modes.addMode("SEARCH_VIEW", {
446 description: "Search View mode",
447 bases: [modes.COMMAND_LINE],
449 modes.addMode("SEARCH_VIEW_FORWARD", {
450 description: "Forward Search View mode",
451 bases: [modes.SEARCH_VIEW]
453 modes.addMode("SEARCH_VIEW_BACKWARD", {
454 description: "Backward Search View mode",
455 bases: [modes.SEARCH_VIEW]
459 commandline: function initCommandline() {
460 player.CommandMode = Class("CommandSearchViewMode", modules.CommandMode, {
461 init: function init(mode) {
463 init.supercall(this);
466 historyKey: "search-view",
468 get prompt() this.mode === modules.modes.SEARCH_VIEW_BACKWARD ? "?" : "/",
470 get onCancel() player.closure.onSearchCancel,
471 get onChange() player.closure.onSearchKeyPress,
472 get onSubmit() player.closure.onSearchSubmit
475 commands: function initCommands() {
476 commands.add(["f[ilter]"],
477 "Filter tracks based on keywords {genre/artist/album/track}",
479 let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg);
481 dactyl.assert(view.length, "No matching tracks");
483 SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view,
484 "chrome://songbird/content/mediapages/filtersPage.xul");
485 // TODO: make this player.focusTrack work ?
486 player.focusTrack(view.getItemByIndex(0));
491 //completer: function (context, args) completion.tracks(context, args);
494 commands.add(["load"],
497 let arg = args.literalArg;
500 // load the selected playlist/smart playlist
501 for ([, playlist] in Iterator(player.getPlaylists())) {
502 if (util.compareIgnoreCase(arg, playlist.name) == 0) {
503 SBGetBrowser().loadMediaList(playlist);
504 player.focusTrack(player._currentView.getItemByIndex(0));
508 dactyl.echoerr(_("error.invalidArgument", arg));
511 // load main library if there are no args
512 _SBShowMainLibrary();
517 completer: function (context, args) completion.playlist(context),
521 // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk
522 commands.add(["playerp[lay]"],
524 function () { player.play(); });
526 commands.add(["playerpa[use]"],
527 "Pause/unpause track",
528 function () { player.togglePlayPause(); });
530 commands.add(["playern[ext]"],
532 function (args) { player.next(Math.max(args.count, 1)); },
535 commands.add(["playerpr[ev]"],
536 "Play previous track",
537 function (args) { player.previous(Math.max(args.count, 1)); },
540 commands.add(["players[top]"],
542 function () { player.stop(); });
544 commands.add(["see[k]"],
545 "Seek to a track position",
549 // intentionally supports 999:99:99
550 dactyl.assert(/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg),
551 _("error.invalidArgument", arg));
553 function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m])
556 let [seconds, minutes, hours] = arg.split(":").reverse();
558 var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h");
561 if (!/[smh]/.test(arg.substr(-1)))
562 arg += "s"; // default to seconds
564 value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1));
567 if (/^[-+]/.test(arg))
568 arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value);
570 player.seekTo(value);
575 commands.add(["mediav[iew]"],
576 "Change the current media view",
578 // FIXME: is this a SB restriction? --djk
579 dactyl.assert(SBGetBrowser().currentMediaPage,
580 "Exxx: Can only set the media view from the media tab"); // XXX
585 for ([, page] in Iterator(player.getMediaPages())) {
586 if (util.compareIgnoreCase(arg, page.contentTitle) == 0) {
587 player.loadMediaPage(page, SBGetBrowser().currentMediaListView.mediaList,
588 SBGetBrowser().currentMediaListView);
592 dactyl.echoerr(_("error.invalidArgument", arg));
597 completer: function (context) completion.mediaView(context),
601 commands.add(["sort[view]"],
602 "Sort the current media view",
604 player.sortBy(args[0], args["-order"] == "up");
608 completer: function (context) completion.mediaListSort(context),
611 names: ["-order", "-o"], type: CommandOption.STRING,
613 description: "Specify the sorting order of the given field",
614 validator: function (arg) /^(up|down)$/.test(arg),
615 completer: function () [["up", "Sort in ascending order"], ["down", "Sort in descending order"]]
620 // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk
621 commands.add(["qu[eue]"],
622 "Queue tracks by artist/album/track",
624 let properties = services.MutablePropertyArray();
627 switch (args.length) {
629 properties.appendProperty(SBProperties.trackName, args[2]);
631 properties.appendProperty(SBProperties.albumName, args[1]);
633 properties.appendProperty(SBProperties.artistName, args[0]);
639 let library = LibraryUtils.mainLibrary;
640 let mainView = library.createView();
641 gMM.sequencer.playView(mainView,
642 mainView.getIndexForItem(library.getItemsByProperties(properties).queryElementAt(0, Ci.sbIMediaItem)));
643 player.focusPlayingTrack();
647 completer: function (context, args) {
648 if (args.completeArg == 0)
649 completion.artist(context);
650 else if (args.completeArg == 1)
651 completion.album(context, args[0]);
652 else if (args.completeArg == 2)
653 completion.song(context, args[0], args[1]);
657 // TODO: maybe :vol! could toggle mute on/off? --djk
658 commands.add(["vol[ume]"],
663 dactyl.assert(arg, _("error.argumentRequired"));
664 dactyl.assert(/^[+-]?\d+$/.test(arg), _("error.trailingCharacters"));
666 let level = parseInt(arg, 10) / 100;
668 if (/^[+-]/.test(arg))
669 level = player.volume + level;
671 player.volume = Math.constrain(level, 0, 1);
675 completion: function initCompletion() {
676 completion.album = function album(context, artist) {
677 context.title = ["Album"];
678 context.completions = [[v, ""] for ([, v] in Iterator(library.getAlbums(artist)))];
681 completion.artist = function artist(context) {
682 context.title = ["Artist"];
683 context.completions = [[v, ""] for ([, v] in Iterator(library.getArtists()))];
686 completion.playlist = function playlist(context) {
687 context.title = ["Playlist", "Type"];
688 context.keys = { text: "name", description: "type" };
689 context.completions = player.getPlaylists();
692 completion.mediaView = function mediaView(context) {
693 context.title = ["Media View", "URL"];
694 context.anchored = false;
695 context.keys = { text: "contentTitle", description: "contentUrl" };
696 context.completions = player.getMediaPages();
699 completion.mediaListSort = function mediaListSort(context) {
700 context.title = ["Media List Sort Field", "Description"];
701 context.anchored = false;
702 context.completions = [["title", "Track name"], ["time", "Duration"], ["artist", "Artist name"],
703 ["album", "Album name"], ["genre", "Genre"], ["rating", "Rating"]]; // FIXME: generate this list dynamically - see #sortBy
706 completion.song = function album(context, artist, album) {
707 context.title = ["Song"];
708 context.completions = [[v, ""] for ([, v] in Iterator(library.getTracks(artist, album)))];
711 mappings: function initMappings() {
712 mappings.add([modes.PLAYER],
714 function () { ex.playerplay(); });
716 mappings.add([modes.PLAYER],
717 ["z"], "Previous track",
718 function ({ count }) { ex.playerprev({ "#": count }); },
721 mappings.add([modes.PLAYER],
722 ["c"], "Pause/unpause track",
723 function () { ex.playerpause(); });
725 mappings.add([modes.PLAYER],
727 function ({ count }) { ex.playernext({ "#": count }); },
730 mappings.add([modes.PLAYER],
732 function () { ex.playerstop(); });
734 mappings.add([modes.PLAYER],
735 ["Q"], "Queue tracks by artist/album/track",
736 function () { commandline.open(":", "queue ", modes.EX); });
738 mappings.add([modes.PLAYER],
739 ["f"], "Loads current view filtered by the keywords",
740 function () { commandline.open(":", "filter ", modes.EX); });
742 mappings.add([modes.PLAYER],
743 ["i"], "Select current track",
744 function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); });
746 mappings.add([modes.PLAYER],
747 ["s"], "Toggle shuffle",
748 function () { player.toggleShuffle(); });
750 mappings.add([modes.PLAYER],
751 ["r"], "Toggle repeat",
752 function () { player.toggleRepeat(); });
754 mappings.add([modes.PLAYER],
755 ["h", "<Left>"], "Seek -10s",
756 function ({ count} ) { player.seekBackward(Math.max(1, count) * 10000); },
759 mappings.add([modes.PLAYER],
760 ["l", "<Right>"], "Seek +10s",
761 function ({ count} ) { player.seekForward(Math.max(1, count) * 10000); },
764 mappings.add([modes.PLAYER],
765 ["H", "<S-Left>"], "Seek -1m",
766 function ({ count }) { player.seekBackward(Math.max(1, count) * 60000); },
769 mappings.add([modes.PLAYER],
770 ["L", "<S-Right>"], "Seek +1m",
771 function ({ count }) { player.seekForward(Math.max(1, count) * 60000); },
774 mappings.add([modes.PLAYER],
775 ["=", "+"], "Increase volume by 5% of the maximum",
776 function () { player.increaseVolume(); });
778 mappings.add([modes.PLAYER],
779 ["-"], "Decrease volume by 5% of the maximum",
780 function () { player.decreaseVolume(); });
782 mappings.add([modes.PLAYER],
783 ["/"], "Search forward for a track",
784 function () { player.CommandMode(modes.SEARCH_VIEW_FORWARD).open(); });
786 mappings.add([modes.PLAYER],
787 ["n"], "Find the next track",
788 function () { player.searchViewAgain(false); });
790 mappings.add([modes.PLAYER],
791 ["N"], "Find the previous track",
792 function () { player.searchViewAgain(true); });
794 for (let i in util.range(0, 6)) {
796 mappings.add([modes.PLAYER],
797 ["<C-" + rating + ">"], "Rate the current media item " + rating,
799 let item = gMM.sequencer.currentItem || this._currentView.selection.currentMediaItem; // XXX: a bit too magic
801 player.rateMediaItem(item, rating);
809 options: function initOptions() {
810 options.add(["repeat"],
811 "Set the playback repeat mode",
814 setter: function (value) gMM.sequencer.repeatMode = value,
815 getter: function () gMM.sequencer.repeatMode,
816 completer: function (context) [
817 ["0", "Repeat none"],
823 options.add(["shuffle"],
824 "Play tracks in shuffled order",
827 setter: function (value) gMM.sequencer.mode = value ? gMM.sequencer.MODE_SHUFFLE : gMM.sequencer.MODE_FORWARD,
828 getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE
833 // vim: set fdm=marker sw=4 sts=4 ts=8 et: