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("Before track changed: " + event.data);
48 autocommands.trigger("TrackChangePre", { track: event.data });
50 case Ci.sbIMediacoreEvent.TRACK_CHANGE:
51 autocommands.trigger("TrackChange", { track: event.data });
53 case Ci.sbIMediacoreEvent.BEFORE_VIEW_CHANGE:
54 dactyl.log("Before view changed: " + event.data);
55 autocommands.trigger("ViewChangePre", { view: event.data });
57 case Ci.sbIMediacoreEvent.VIEW_CHANGE:
58 dactyl.log("View changed: " + event.data);
59 autocommands.trigger("ViewChange", { view: event.data });
61 case Ci.sbIMediacoreEvent.STREAM_START:
62 dactyl.log("Track started: " + gMM.sequencer.currentItem);
63 autocommands.trigger("StreamStart", { track: gMM.sequencer.currentItem });
65 case Ci.sbIMediacoreEvent.STREAM_PAUSE:
66 dactyl.log("Track paused: " + gMM.sequencer.currentItem);
67 autocommands.trigger("StreamPause", { track: gMM.sequencer.currentItem });
69 case Ci.sbIMediacoreEvent.STREAM_END:
70 dactyl.log("Track ended: " + gMM.sequencer.currentItem);
71 autocommands.trigger("StreamEnd", { track: gMM.sequencer.currentItem });
73 case Ci.sbIMediacoreEvent.STREAM_STOP:
74 dactyl.log("Track stopped: " + gMM.sequencer.currentItem);
75 autocommands.trigger("StreamStop", { track: gMM.sequencer.currentItem });
81 /** @property {sbIMediaListView} The current media list view. @private */
82 get _currentView() SBGetBrowser().currentMediaListView,
85 * @property {number} The player volume in the range 0.0-1.0.
87 get volume() gMM.volumeControl.volume,
89 gMM.volumeControl.volume = value;
93 * Focuses the specified media item in the current media list view.
95 * @param {sbIMediaItem} mediaItem The media item to focus.
97 focusTrack: function focusTrack(mediaItem) {
98 SBGetBrowser().mediaTab.mediaPage.highlightItem(this._currentView.getIndexForItem(mediaItem));
102 * Plays the currently selected media item. If no item is selected the
103 * first item in the current media view is played.
105 play: function play() {
106 // Check if there is any selection in place, else play first item of the visible view.
107 // TODO: this approach, or similar, should be generalised for all commands, PT? --djk
108 if (this._currentView.selection.count != 0)
109 gMM.sequencer.playView(this._currentView,
110 this._currentView.getIndexForItem(this._currentView.selection.currentMediaItem));
112 gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0);
114 this.focusTrack(gMM.sequencer.currentItem);
118 * Stops playback of the currently playing media item.
120 stop: function stop() {
121 gMM.sequencer.stop();
125 * Plays the *count*th next media item in the current media view.
127 * @param {number} count
129 next: function next(count) {
130 for (let i = 0; i < count; i++)
131 gSongbirdWindowController.doCommand("cmd_control_next");
132 gSongbirdWindowController.doCommand("cmd_find_current_track");
136 * Plays the *count*th previous media item in the current media view.
138 * @param {number} count
140 previous: function previous(count) {
141 for (let i = 0; i < count; i++)
142 gSongbirdWindowController.doCommand("cmd_control_previous");
143 gSongbirdWindowController.doCommand("cmd_find_current_track");
147 * Toggles the play/pause status of the current media item.
149 togglePlayPause: function togglePlayPause() {
150 ["cmd_control_playpause", "cmd_find_current_track"].forEach(gSongbirdWindowController.doCommand);
154 * Toggles the shuffle status of the sequencer.
156 toggleShuffle: function toggleShuffle() {
157 if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE)
158 gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE;
160 gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD;
163 // FIXME: not really toggling (depending on your definition) - good enough for now.
165 * Toggles between the sequencer's three repeat modes: Repeat-One,
166 * Repeat-All and Repeat-None.
168 toggleRepeat: function toggleRepeat() {
169 switch (gMM.sequencer.repeatMode) {
170 case gMM.sequencer.MODE_REPEAT_NONE:
171 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE;
173 case gMM.sequencer.MODE_REPEAT_ONE:
174 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL;
176 case gMM.sequencer.MODE_REPEAT_ALL:
177 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
180 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
186 * Seeks forward *interval* milliseconds in the currently playing track.
188 * @param {number} interval The time interval (ms) to advance the
191 seekForward: function seekForward(interval) {
192 this._seek(interval, true);
196 * Seeks backwards *interval* milliseconds in the currently playing track.
198 * @param {number} interval The time interval (ms) to rewind the
201 seekBackward: function seekBackward(interval) {
202 this._seek(interval, false);
206 * Seeks to a specific position in the currently playing track.
208 * @param {number} The new position (ms) in the track.
210 seekTo: function seekTo(position) {
211 // FIXME: if not playing
212 if (!gMM.playbackControl)
216 let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable?
218 gMM.playbackControl.position = Math.constrain(position, min, max);
222 * Increases the volume by 5% of the maximum volume.
224 increaseVolume: function increaseVolume() {
225 this.volume = Math.constrain(this.volume + 0.05, 0, 1);
229 * Decreases the volume by 5% of the maximum volume.
231 decreaseVolume: function decreaseVolume() {
232 this.volume = Math.constrain(this.volume - 0.05, 0, 1);
235 // TODO: Document what this buys us over and above cmd_find_current_track
237 * Focuses the currently playing track.
239 focusPlayingTrack: function focusPlayingTrack() {
240 this.focusTrack(gMM.sequencer.currentItem);
244 * Searches the current media view for *str*
246 * @param {string} str The search string.
248 searchView: function searchView(str) {
249 let search = _getSearchString(this._currentView);
250 let searchString = "";
252 if (search != "") // XXX
253 searchString = str + " " + search;
257 this._lastSearchString = searchString;
259 let searchView = LibraryUtils.createStandardMediaListView(this._currentView.mediaList, searchString);
261 if (searchView.length) {
262 this._lastSearchView = searchView;
263 this._lastSearchIndex = 0;
264 this.focusTrack(searchView.getItemByIndex(this._lastSearchIndex));
267 dactyl.echoerr(_("finder.notFound", searchString), commandline.FORCE_SINGLELINE);
271 * Repeats the previous view search.
273 * @param {boolean} reverse Search in the reverse direction to the previous
276 searchViewAgain: function searchViewAgain(reverse) {
278 this.timeout(function () {
279 commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
284 if (this._lastSearchIndex == 0) {
285 this._lastSearchIndex = this._lastSearchView.length - 1;
286 echo(_("finder.atTop"));
289 this._lastSearchIndex = this._lastSearchIndex - 1;
292 if (this._lastSearchIndex == (this._lastSearchView.length - 1)) {
293 this._lastSearchIndex = 0;
294 echo(_("finder.atBottom"));
297 this._lastSearchIndex = this._lastSearchIndex + 1;
300 // TODO: Implement for "?" --ken
301 commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE);
302 this.focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex));
307 * The search dialog keypress callback.
309 * @param {string} str The contents of the search dialog.
311 onSearchKeyPress: function onSearchKeyPress(str) {
312 if (options["incsearch"])
313 this.searchView(str);
317 * The search dialog submit callback.
319 * @param {string} str The contents of the search dialog.
321 onSearchSubmit: function onSearchSubmit(str) {
322 this.searchView(str);
326 * The search dialog cancel callback.
328 onSearchCancel: function onSearchCancel() {
329 // TODO: restore the view state if altered by an 'incsearch' search
333 * Returns an array of all available playlists.
335 * @returns {sbIMediaList[]}
337 getPlaylists: function getPlaylists() {
338 let mainLibrary = LibraryUtils.mainLibrary;
339 let playlists = [mainLibrary];
341 onEnumerationBegin: function () { },
342 onEnumerationEnd: function () { },
343 onEnumeratedItem: function (list, item) {
344 // FIXME: why are there null items and duplicates?
345 if (!playlists.some(function (list) list.name == item.name) && item.name != null)
346 playlists.push(item);
347 return Ci.sbIMediaListEnumerationListener.CONTINUE;
351 mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener);
357 * Plays the media item at *index* in *playlist*.
359 * @param {sbIMediaList} playlist
360 * @param {number} index
362 playPlaylist: function playPlaylist(playlist, index) {
363 gMM.sequencer.playView(playlist.createView(), index);
367 * Returns an array of all available media pages.
369 * @returns {sbIMediaPageInfo[]}
371 getMediaPages: function getMediaPages() {
372 let list = SBGetBrowser().currentMediaPage.mediaListView.mediaList;
373 let pages = services.mediaPageManager.getAvailablePages(list);
374 return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo));
378 * Loads the the specified media page into *view* with the given *list* of
381 * @param {sbIMediaPage} page
382 * @param {sbIMediaList} list
383 * @param {sbIMediaView} view
385 loadMediaPage: function loadMediaPage(page, list, view) {
386 services.mediaPageManager.setPage(list, page);
387 SBGetBrowser().loadMediaList(list, null, null, view, null);
391 * Applys the specified *rating* to *mediaItem*.
393 * @param {sbIMediaItem} mediaItem The media item to rate.
394 * @param {number} rating The star rating (1-5).
396 rateMediaItem: function rateMediaItem(mediaItem, rating) {
397 mediaItem.setProperty(SBProperties.rating, rating);
400 // TODO: add more fields, and generate the list dynamically. PT should the
401 // available fields reflect only the visible view fields or offer others? --djk
403 * Sorts the current media view by *field* in the order specified by
406 * @param {string} field The sort field.
407 * @param {boolean} ascending If true sort in ascending order, otherwise in
410 sortBy: function sortBy(field, ascending) {
411 let order = ascending ? "a" : "d";
412 let properties = services.MutablePropertyArray();
413 properties.strict = false;
417 properties.appendProperty(SBProperties.trackName, order);
420 properties.appendProperty(SBProperties.duration, order);
423 properties.appendProperty(SBProperties.artistName, order);
426 properties.appendProperty(SBProperties.albumName, order);
429 properties.appendProperty(SBProperties.genre, order);
432 properties.appendProperty(SBProperties.rating, order);
435 properties.appendProperty(SBProperties.trackName, order);
439 this._currentView.setSort(properties);
443 modes: function initModes(dactyl, modules, window) {
444 modes.addMode("SEARCH_VIEW", {
445 description: "Search View mode",
446 bases: [modes.COMMAND_LINE],
448 modes.addMode("SEARCH_VIEW_FORWARD", {
449 description: "Forward Search View mode",
450 bases: [modes.SEARCH_VIEW]
452 modes.addMode("SEARCH_VIEW_BACKWARD", {
453 description: "Backward Search View mode",
454 bases: [modes.SEARCH_VIEW]
458 commandline: function () {
459 player.CommandMode = Class("CommandSearchViewMode", modules.CommandMode, {
460 init: function init(mode) {
462 init.supercall(this);
465 historyKey: "search-view",
467 get prompt() this.mode === modules.modes.SEARCH_VIEW_BACKWARD ? "?" : "/",
469 get onCancel() player.closure.onSearchCancel,
470 get onChange() player.closure.onSearchKeyPress,
471 get onSubmit() player.closure.onSearchSubmit
474 commands: function () {
475 commands.add(["f[ilter]"],
476 "Filter tracks based on keywords {genre/artist/album/track}",
478 let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg);
480 dactyl.assert(view.length, "No matching tracks");
482 SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view,
483 "chrome://songbird/content/mediapages/filtersPage.xul");
484 // TODO: make this player.focusTrack work ?
485 player.focusTrack(view.getItemByIndex(0));
490 //completer: function (context, args) completion.tracks(context, args);
493 commands.add(["load"],
496 let arg = args.literalArg;
499 // load the selected playlist/smart playlist
500 for ([, playlist] in Iterator(player.getPlaylists())) {
501 if (util.compareIgnoreCase(arg, playlist.name) == 0) {
502 SBGetBrowser().loadMediaList(playlist);
503 player.focusTrack(player._currentView.getItemByIndex(0));
507 dactyl.echoerr(_("error.invalidArgument", arg));
510 // load main library if there are no args
511 _SBShowMainLibrary();
516 completer: function (context, args) completion.playlist(context),
520 // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk
521 commands.add(["playerp[lay]"],
523 function () { player.play(); });
525 commands.add(["playerpa[use]"],
526 "Pause/unpause track",
527 function () { player.togglePlayPause(); });
529 commands.add(["playern[ext]"],
531 function (args) { player.next(Math.max(args.count, 1)); },
534 commands.add(["playerpr[ev]"],
535 "Play previous track",
536 function (args) { player.previous(Math.max(args.count, 1)); },
539 commands.add(["players[top]"],
541 function () { player.stop(); });
543 commands.add(["see[k]"],
544 "Seek to a track position",
548 // intentionally supports 999:99:99
549 dactyl.assert(/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg),
550 _("error.invalidArgument", arg));
552 function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m])
555 let [seconds, minutes, hours] = arg.split(":").reverse();
557 var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h");
560 if (!/[smh]/.test(arg.substr(-1)))
561 arg += "s"; // default to seconds
563 value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1));
566 if (/^[-+]/.test(arg))
567 arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value);
569 player.seekTo(value);
574 commands.add(["mediav[iew]"],
575 "Change the current media view",
577 // FIXME: is this a SB restriction? --djk
578 dactyl.assert(SBGetBrowser().currentMediaPage,
579 "Exxx: Can only set the media view from the media tab"); // XXX
584 for ([, page] in Iterator(player.getMediaPages())) {
585 if (util.compareIgnoreCase(arg, page.contentTitle) == 0) {
586 player.loadMediaPage(page, SBGetBrowser().currentMediaListView.mediaList,
587 SBGetBrowser().currentMediaListView);
591 dactyl.echoerr(_("error.invalidArgument", arg));
596 completer: function (context) completion.mediaView(context),
600 commands.add(["sort[view]"],
601 "Sort the current media view",
603 player.sortBy(args[0], args["-order"] == "up");
607 completer: function (context) completion.mediaListSort(context),
610 names: ["-order", "-o"], type: CommandOption.STRING,
612 description: "Specify the sorting order of the given field",
613 validator: function (arg) /^(up|down)$/.test(arg),
614 completer: function () [["up", "Sort in ascending order"], ["down", "Sort in descending order"]]
619 // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk
620 commands.add(["qu[eue]"],
621 "Queue tracks by artist/album/track",
623 let properties = services.MutablePropertyArray();
626 switch (args.length) {
628 properties.appendProperty(SBProperties.trackName, args[2]);
630 properties.appendProperty(SBProperties.albumName, args[1]);
632 properties.appendProperty(SBProperties.artistName, args[0]);
638 let library = LibraryUtils.mainLibrary;
639 let mainView = library.createView();
640 gMM.sequencer.playView(mainView,
641 mainView.getIndexForItem(library.getItemsByProperties(properties).queryElementAt(0, Ci.sbIMediaItem)));
642 player.focusPlayingTrack();
646 completer: function (context, args) {
647 if (args.completeArg == 0)
648 completion.artist(context);
649 else if (args.completeArg == 1)
650 completion.album(context, args[0]);
651 else if (args.completeArg == 2)
652 completion.song(context, args[0], args[1]);
656 // TODO: maybe :vol! could toggle mute on/off? --djk
657 commands.add(["vol[ume]"],
662 dactyl.assert(arg, _("error.argumentRequired"));
663 dactyl.assert(/^[+-]?\d+$/.test(arg), _("error.trailing"));
665 let level = parseInt(arg, 10) / 100;
667 if (/^[+-]/.test(arg))
668 level = player.volume + level;
670 player.volume = Math.constrain(level, 0, 1);
674 completion: function () {
675 completion.album = function album(context, artist) {
676 context.title = ["Album"];
677 context.completions = [[v, ""] for ([, v] in Iterator(library.getAlbums(artist)))];
680 completion.artist = function artist(context) {
681 context.title = ["Artist"];
682 context.completions = [[v, ""] for ([, v] in Iterator(library.getArtists()))];
685 completion.playlist = function playlist(context) {
686 context.title = ["Playlist", "Type"];
687 context.keys = { text: "name", description: "type" };
688 context.completions = player.getPlaylists();
691 completion.mediaView = function mediaView(context) {
692 context.title = ["Media View", "URL"];
693 context.anchored = false;
694 context.keys = { text: "contentTitle", description: "contentUrl" };
695 context.completions = player.getMediaPages();
698 completion.mediaListSort = function mediaListSort(context) {
699 context.title = ["Media List Sort Field", "Description"];
700 context.anchored = false;
701 context.completions = [["title", "Track name"], ["time", "Duration"], ["artist", "Artist name"],
702 ["album", "Album name"], ["genre", "Genre"], ["rating", "Rating"]]; // FIXME: generate this list dynamically - see #sortBy
705 completion.song = function album(context, artist, album) {
706 context.title = ["Song"];
707 context.completions = [[v, ""] for ([, v] in Iterator(library.getTracks(artist, album)))];
710 mappings: function () {
711 mappings.add([modes.PLAYER],
713 function () { ex.playerplay(); });
715 mappings.add([modes.PLAYER],
716 ["z"], "Previous track",
717 function (args) { ex.playerprev({ "#": args.count }); },
720 mappings.add([modes.PLAYER],
721 ["c"], "Pause/unpause track",
722 function () { ex.playerpause(); });
724 mappings.add([modes.PLAYER],
726 function (args) { ex.playernext({ "#": args.count }); },
729 mappings.add([modes.PLAYER],
731 function () { ex.playerstop(); });
733 mappings.add([modes.PLAYER],
734 ["Q"], "Queue tracks by artist/album/track",
735 function () { commandline.open(":", "queue ", modes.EX); });
737 mappings.add([modes.PLAYER],
738 ["f"], "Loads current view filtered by the keywords",
739 function () { commandline.open(":", "filter ", modes.EX); });
741 mappings.add([modes.PLAYER],
742 ["i"], "Select current track",
743 function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); });
745 mappings.add([modes.PLAYER],
746 ["s"], "Toggle shuffle",
747 function () { player.toggleShuffle(); });
749 mappings.add([modes.PLAYER],
750 ["r"], "Toggle repeat",
751 function () { player.toggleRepeat(); });
753 mappings.add([modes.PLAYER],
754 ["h", "<Left>"], "Seek -10s",
755 function (args) { player.seekBackward(Math.max(1, args.count) * 10000); },
758 mappings.add([modes.PLAYER],
759 ["l", "<Right>"], "Seek +10s",
760 function (args) { player.seekForward(Math.max(1, args.count) * 10000); },
763 mappings.add([modes.PLAYER],
764 ["H", "<S-Left>"], "Seek -1m",
765 function (args) { player.seekBackward(Math.max(1, args.count) * 60000); },
768 mappings.add([modes.PLAYER],
769 ["L", "<S-Right>"], "Seek +1m",
770 function (args) { player.seekForward(Math.max(1, args.count) * 60000); },
773 mappings.add([modes.PLAYER],
774 ["=", "+"], "Increase volume by 5% of the maximum",
775 function () { player.increaseVolume(); });
777 mappings.add([modes.PLAYER],
778 ["-"], "Decrease volume by 5% of the maximum",
779 function () { player.decreaseVolume(); });
781 mappings.add([modes.PLAYER],
782 ["/"], "Search forward for a track",
783 function () { player.CommandMode(modes.SEARCH_VIEW_FORWARD).open(); });
785 mappings.add([modes.PLAYER],
786 ["n"], "Find the next track",
787 function () { player.searchViewAgain(false); });
789 mappings.add([modes.PLAYER],
790 ["N"], "Find the previous track",
791 function () { player.searchViewAgain(true); });
793 for (let i in util.range(0, 6)) {
795 mappings.add([modes.PLAYER],
796 ["<C-" + rating + ">"], "Rate the current media item " + rating,
798 let item = gMM.sequencer.currentItem || this._currentView.selection.currentMediaItem; // XXX: a bit too magic
800 player.rateMediaItem(item, rating);
808 options: function () {
809 options.add(["repeat"],
810 "Set the playback repeat mode",
813 setter: function (value) gMM.sequencer.repeatMode = value,
814 getter: function () gMM.sequencer.repeatMode,
815 completer: function (context) [
816 ["0", "Repeat none"],
822 options.add(["shuffle"],
823 "Play tracks in shuffled order",
826 setter: function (value) gMM.sequencer.mode = value ? gMM.sequencer.MODE_SHUFFLE : gMM.sequencer.MODE_FORWARD,
827 getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE
832 // vim: set fdm=marker sw=4 ts=4 et: