]> git.donarmstrong.com Git - dactyl.git/blob - melodactyl/content/player.js
8eb8f1171092fa8326f05e1b06b6e6dcb3f45b9c
[dactyl.git] / melodactyl / content / player.js
1 // Copyright (c) 2009 by Prathyush Thota <prathyushthota@gmail.com>
2 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 const Player = Module("player", {
9     init: function init() {
10         this._lastSearchString = "";
11         this._lastSearchIndex = 0;
12         this._lastSearchView = this._currentView; //XXX
13
14         // Get the focus to the visible playlist first
15         //window._SBShowMainLibrary();
16
17         gMM.addListener(this._mediaCoreListener);
18     },
19
20     destroy: function destroy() {
21         gMM.removeListener(this._mediaCoreListener);
22     },
23
24     /**
25      * Moves the track position *interval* milliseconds forwards or backwards.
26      *
27      * @param {number} interval The time interval (ms) to move the track
28      *     position.
29      * @param {boolean} direction The direction in which to move the track
30      *     position, forward if true otherwise backwards.
31      * @private
32      */
33     _seek: function _seek(interval, direction) {
34         let position = gMM.playbackControl ? gMM.playbackControl.position : 0;
35         player.seekTo(position + (direction ? interval : -interval));
36     },
37
38     /**
39      * Listens for media core events and in response dispatches the appropriate
40      * autocommand events.
41      * @private
42      */
43     _mediaCoreListener: {
44         onMediacoreEvent: function (event) {
45             switch (event.type) {
46                 case Ci.sbIMediacoreEvent.BEFORE_TRACK_CHANGE:
47                     dactyl.log("Before track changed: " + event.data);
48                     autocommands.trigger("TrackChangePre", { track: event.data });
49                     break;
50                 case Ci.sbIMediacoreEvent.TRACK_CHANGE:
51                     autocommands.trigger("TrackChange", { track: event.data });
52                     break;
53                 case Ci.sbIMediacoreEvent.BEFORE_VIEW_CHANGE:
54                     dactyl.log("Before view changed: " + event.data);
55                     autocommands.trigger("ViewChangePre", { view: event.data });
56                     break;
57                 case Ci.sbIMediacoreEvent.VIEW_CHANGE:
58                     dactyl.log("View changed: " + event.data);
59                     autocommands.trigger("ViewChange", { view: event.data });
60                     break;
61                 case Ci.sbIMediacoreEvent.STREAM_START:
62                     dactyl.log("Track started: " + gMM.sequencer.currentItem);
63                     autocommands.trigger("StreamStart", { track: gMM.sequencer.currentItem });
64                     break;
65                 case Ci.sbIMediacoreEvent.STREAM_PAUSE:
66                     dactyl.log("Track paused: " + gMM.sequencer.currentItem);
67                     autocommands.trigger("StreamPause", { track: gMM.sequencer.currentItem });
68                     break;
69                 case Ci.sbIMediacoreEvent.STREAM_END:
70                     dactyl.log("Track ended: " + gMM.sequencer.currentItem);
71                     autocommands.trigger("StreamEnd", { track: gMM.sequencer.currentItem });
72                     break;
73                 case Ci.sbIMediacoreEvent.STREAM_STOP:
74                     dactyl.log("Track stopped: " + gMM.sequencer.currentItem);
75                     autocommands.trigger("StreamStop", { track: gMM.sequencer.currentItem });
76                     break;
77             }
78         }
79     },
80
81     /** @property {sbIMediaListView} The current media list view. @private */
82     get _currentView() SBGetBrowser().currentMediaListView,
83
84     /**
85      * @property {number} The player volume in the range 0.0-1.0.
86      */
87     get volume() gMM.volumeControl.volume,
88     set volume(value) {
89         gMM.volumeControl.volume = value;
90     },
91
92     /**
93      * Focuses the specified media item in the current media list view.
94      *
95      * @param {sbIMediaItem} mediaItem The media item to focus.
96      */
97     focusTrack: function focusTrack(mediaItem) {
98         SBGetBrowser().mediaTab.mediaPage.highlightItem(this._currentView.getIndexForItem(mediaItem));
99     },
100
101     /**
102      * Plays the currently selected media item. If no item is selected the
103      * first item in the current media view is played.
104      */
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));
111         else
112             gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0);
113
114         this.focusTrack(gMM.sequencer.currentItem);
115     },
116
117     /**
118      * Stops playback of the currently playing media item.
119      */
120     stop: function stop() {
121         gMM.sequencer.stop();
122     },
123
124     /**
125      * Plays the *count*th next media item in the current media view.
126      *
127      * @param {number} count
128      */
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");
133     },
134
135     /**
136      * Plays the *count*th previous media item in the current media view.
137      *
138      * @param {number} count
139      */
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");
144     },
145
146     /**
147      * Toggles the play/pause status of the current media item.
148      */
149     togglePlayPause: function togglePlayPause() {
150         ["cmd_control_playpause", "cmd_find_current_track"].forEach(gSongbirdWindowController.doCommand);
151     },
152
153     /**
154      * Toggles the shuffle status of the sequencer.
155      */
156     toggleShuffle: function toggleShuffle() {
157         if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE)
158             gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE;
159         else
160             gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD;
161     },
162
163     // FIXME: not really toggling (depending on your definition) - good enough for now.
164     /**
165      * Toggles between the sequencer's three repeat modes: Repeat-One,
166      * Repeat-All and Repeat-None.
167      */
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;
172                 break;
173             case gMM.sequencer.MODE_REPEAT_ONE:
174                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL;
175                 break;
176             case gMM.sequencer.MODE_REPEAT_ALL:
177                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
178                 break;
179             default:
180                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
181                 break;
182         }
183     },
184
185     /**
186      * Seeks forward *interval* milliseconds in the currently playing track.
187      *
188      * @param {number} interval The time interval (ms) to advance the
189      *     current track.
190      */
191     seekForward: function seekForward(interval) {
192         this._seek(interval, true);
193     },
194
195     /**
196      * Seeks backwards *interval* milliseconds in the currently playing track.
197      *
198      * @param {number} interval The time interval (ms) to rewind the
199      *     current track.
200      */
201     seekBackward: function seekBackward(interval) {
202         this._seek(interval, false);
203     },
204
205     /**
206      * Seeks to a specific position in the currently playing track.
207      *
208      * @param {number} The new position (ms) in the track.
209      */
210     seekTo: function seekTo(position) {
211         // FIXME: if not playing
212         if (!gMM.playbackControl)
213             this.play();
214
215         let min = 0;
216         let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable?
217
218         gMM.playbackControl.position = Math.constrain(position, min, max);
219     },
220
221     /**
222      * Increases the volume by 5% of the maximum volume.
223      */
224     increaseVolume: function increaseVolume() {
225         this.volume = Math.constrain(this.volume + 0.05, 0, 1);
226     },
227
228     /**
229      * Decreases the volume by 5% of the maximum volume.
230      */
231     decreaseVolume: function decreaseVolume() {
232         this.volume = Math.constrain(this.volume - 0.05, 0, 1);
233     },
234
235     // TODO: Document what this buys us over and above cmd_find_current_track
236     /**
237      * Focuses the currently playing track.
238      */
239     focusPlayingTrack: function focusPlayingTrack() {
240         this.focusTrack(gMM.sequencer.currentItem);
241     },
242
243     /**
244      * Searches the current media view for *str*
245      *
246      * @param {string} str The search string.
247      */
248     searchView: function searchView(str) {
249         let search = _getSearchString(this._currentView);
250         let searchString = "";
251
252         if (search != "") // XXX
253             searchString = str + " " + search;
254         else
255             searchString = str;
256
257         this._lastSearchString = searchString;
258
259         let searchView = LibraryUtils.createStandardMediaListView(this._currentView.mediaList, searchString);
260
261         if (searchView.length) {
262             this._lastSearchView = searchView;
263             this._lastSearchIndex = 0;
264             this.focusTrack(searchView.getItemByIndex(this._lastSearchIndex));
265         }
266         else
267             dactyl.echoerr(_("finder.notFound", searchString), commandline.FORCE_SINGLELINE);
268     },
269
270     /**
271      * Repeats the previous view search.
272      *
273      * @param {boolean} reverse Search in the reverse direction to the previous
274      *     search.
275      */
276     searchViewAgain: function searchViewAgain(reverse) {
277         function echo(str) {
278             this.timeout(function () {
279                 commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
280             }, 0);
281         }
282
283         if (reverse) {
284             if (this._lastSearchIndex == 0) {
285                 this._lastSearchIndex = this._lastSearchView.length - 1;
286                 echo(_("finder.atTop"));
287             }
288             else
289                 this._lastSearchIndex = this._lastSearchIndex - 1;
290         }
291         else {
292             if (this._lastSearchIndex == (this._lastSearchView.length - 1)) {
293                 this._lastSearchIndex = 0;
294                 echo(_("finder.atBottom"));
295             }
296             else
297                 this._lastSearchIndex = this._lastSearchIndex + 1;
298         }
299
300         // TODO: Implement for "?" --ken
301         commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE);
302         this.focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex));
303
304     },
305
306     /**
307      * The search dialog keypress callback.
308      *
309      * @param {string} str The contents of the search dialog.
310      */
311     onSearchKeyPress: function onSearchKeyPress(str) {
312         if (options["incsearch"])
313             this.searchView(str);
314     },
315
316     /**
317      * The search dialog submit callback.
318      *
319      * @param {string} str The contents of the search dialog.
320      */
321     onSearchSubmit: function onSearchSubmit(str) {
322         this.searchView(str);
323     },
324
325     /**
326      * The search dialog cancel callback.
327      */
328     onSearchCancel: function onSearchCancel() {
329         // TODO: restore the view state if altered by an 'incsearch' search
330     },
331
332     /**
333      * Returns an array of all available playlists.
334      *
335      * @returns {sbIMediaList[]}
336      */
337     getPlaylists: function getPlaylists() {
338         let mainLibrary = LibraryUtils.mainLibrary;
339         let playlists = [mainLibrary];
340         let listener = {
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;
348             }
349         };
350
351         mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener);
352
353         return playlists;
354     },
355
356     /**
357      * Plays the media item at *index* in *playlist*.
358      *
359      * @param {sbIMediaList} playlist
360      * @param {number} index
361      */
362     playPlaylist: function playPlaylist(playlist, index) {
363         gMM.sequencer.playView(playlist.createView(), index);
364     },
365
366     /**
367      * Returns an array of all available media pages.
368      *
369      * @returns {sbIMediaPageInfo[]}
370      */
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));
375     },
376
377     /**
378      * Loads the the specified media page into *view* with the given *list* of
379      * media items.
380      *
381      * @param {sbIMediaPage} page
382      * @param {sbIMediaList} list
383      * @param {sbIMediaView} view
384      */
385     loadMediaPage: function loadMediaPage(page, list, view) {
386         services.mediaPageManager.setPage(list, page);
387         SBGetBrowser().loadMediaList(list, null, null, view, null);
388     },
389
390     /**
391      * Applys the specified *rating* to *mediaItem*.
392      *
393      * @param {sbIMediaItem} mediaItem The media item to rate.
394      * @param {number} rating The star rating (1-5).
395      */
396     rateMediaItem: function rateMediaItem(mediaItem, rating) {
397         mediaItem.setProperty(SBProperties.rating, rating);
398     },
399
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
402     /**
403      * Sorts the current media view by *field* in the order specified by
404      * *ascending*.
405      *
406      * @param {string} field The sort field.
407      * @param {boolean} ascending If true sort in ascending order, otherwise in
408      *     descending order.
409      */
410     sortBy: function sortBy(field, ascending) {
411         let order = ascending ? "a" : "d";
412         let properties = services.MutablePropertyArray();
413         properties.strict = false;
414
415         switch (field) {
416             case "title":
417                 properties.appendProperty(SBProperties.trackName, order);
418                 break;
419             case "time":
420                 properties.appendProperty(SBProperties.duration, order);
421                 break;
422             case "artist":
423                 properties.appendProperty(SBProperties.artistName, order);
424                 break;
425             case "album":
426                 properties.appendProperty(SBProperties.albumName, order);
427                 break;
428             case "genre":
429                 properties.appendProperty(SBProperties.genre, order);
430                 break;
431             case "rating":
432                 properties.appendProperty(SBProperties.rating, order);
433                 break;
434             default:
435                 properties.appendProperty(SBProperties.trackName, order);
436                 break;
437         }
438
439         this._currentView.setSort(properties);
440     }
441 }, {
442 }, {
443     modes: function initModes(dactyl, modules, window) {
444         modes.addMode("SEARCH_VIEW", {
445             description: "Search View mode",
446             bases: [modes.COMMAND_LINE],
447         });
448         modes.addMode("SEARCH_VIEW_FORWARD", {
449             description: "Forward Search View mode",
450             bases: [modes.SEARCH_VIEW]
451         });
452         modes.addMode("SEARCH_VIEW_BACKWARD", {
453             description: "Backward Search View mode",
454             bases: [modes.SEARCH_VIEW]
455         });
456
457     },
458     commandline: function () {
459         player.CommandMode = Class("CommandSearchViewMode", modules.CommandMode, {
460             init: function init(mode) {
461                 this.mode = mode;
462                 init.supercall(this);
463             },
464
465             historyKey: "search-view",
466
467             get prompt() this.mode === modules.modes.SEARCH_VIEW_BACKWARD ? "?" : "/",
468
469             get onCancel() player.closure.onSearchCancel,
470             get onChange() player.closure.onSearchKeyPress,
471             get onSubmit() player.closure.onSearchSubmit
472         });
473     },
474     commands: function () {
475         commands.add(["f[ilter]"],
476                 "Filter tracks based on keywords {genre/artist/album/track}",
477                 function (args) {
478                     let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg);
479
480                     dactyl.assert(view.length, "No matching tracks");
481
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));
486                 },
487                 {
488                     argCount: "1",
489                     literal: 0
490                     //completer: function (context, args) completion.tracks(context, args);
491                 });
492
493         commands.add(["load"],
494             "Load a playlist",
495             function (args) {
496                 let arg = args.literalArg;
497
498                 if (arg) {
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));
504                             return;
505                         }
506                     }
507                     dactyl.echoerr(_("error.invalidArgument", arg));
508                 }
509                 else {
510                     // load main library if there are no args
511                     _SBShowMainLibrary();
512                 }
513             },
514             {
515                 argCount: "?",
516                 completer: function (context, args) completion.playlist(context),
517                 literal: 0
518             });
519
520         // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk
521         commands.add(["playerp[lay]"],
522             "Play track",
523             function () { player.play(); });
524
525         commands.add(["playerpa[use]"],
526             "Pause/unpause track",
527             function () { player.togglePlayPause(); });
528
529         commands.add(["playern[ext]"],
530             "Play next track",
531             function (args) { player.next(Math.max(args.count, 1)); },
532             { count: true });
533
534         commands.add(["playerpr[ev]"],
535             "Play previous track",
536             function (args) { player.previous(Math.max(args.count, 1)); },
537             { count: true });
538
539         commands.add(["players[top]"],
540             "Stop track",
541             function () { player.stop(); });
542
543         commands.add(["see[k]"],
544             "Seek to a track position",
545             function (args) {
546                 let arg = args[0];
547
548                 // intentionally supports 999:99:99
549                 dactyl.assert(/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg),
550                     _("error.invalidArgument", arg));
551
552                 function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m])
553
554                 if (/:/.test(arg)) {
555                     let [seconds, minutes, hours] = arg.split(":").reverse();
556                     hours = hours || 0;
557                     var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h");
558                 }
559                 else {
560                     if (!/[smh]/.test(arg.substr(-1)))
561                         arg += "s"; // default to seconds
562
563                     value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1));
564                 }
565
566                 if (/^[-+]/.test(arg))
567                     arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value);
568                 else
569                     player.seekTo(value);
570
571             },
572             { argCount: "1" });
573
574         commands.add(["mediav[iew]"],
575             "Change the current media view",
576             function (args) {
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
580
581                 let arg = args[0];
582
583                 if (arg) {
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);
588                             return;
589                         }
590                     }
591                     dactyl.echoerr(_("error.invalidArgument", arg));
592                 }
593             },
594             {
595                 argCount: "1",
596                 completer: function (context) completion.mediaView(context),
597                 literal: 0
598             });
599
600         commands.add(["sort[view]"],
601                 "Sort the current media view",
602                 function (args) {
603                     player.sortBy(args[0], args["-order"] == "up");
604                 },
605                 {
606                     argCount: "1",
607                     completer: function (context) completion.mediaListSort(context),
608                     options: [
609                         {
610                             names: ["-order", "-o"], type: CommandOption.STRING,
611                             default: "up",
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"]]
615                         }
616                     ]
617                 });
618
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",
622             function (args) {
623                 let properties = services.MutablePropertyArray();
624
625                 // args
626                 switch (args.length) {
627                     case 3:
628                         properties.appendProperty(SBProperties.trackName, args[2]);
629                     case 2:
630                         properties.appendProperty(SBProperties.albumName, args[1]);
631                     case 1:
632                         properties.appendProperty(SBProperties.artistName, args[0]);
633                         break;
634                     default:
635                         break;
636                 }
637
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();
643             },
644             {
645                 argCount: "+",
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]);
653                 }
654             });
655
656         // TODO: maybe :vol! could toggle mute on/off? --djk
657         commands.add(["vol[ume]"],
658             "Set the volume",
659             function (args) {
660                 let arg = args[0];
661
662                 dactyl.assert(arg, _("error.argumentRequired"));
663                 dactyl.assert(/^[+-]?\d+$/.test(arg), _("error.trailing"));
664
665                 let level = parseInt(arg, 10) / 100;
666
667                 if (/^[+-]/.test(arg))
668                     level = player.volume + level;
669
670                 player.volume = Math.constrain(level, 0, 1);
671             },
672             { argCount: "1" });
673     },
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)))];
678         };
679
680         completion.artist = function artist(context) {
681             context.title = ["Artist"];
682             context.completions = [[v, ""] for ([, v] in Iterator(library.getArtists()))];
683         };
684
685         completion.playlist = function playlist(context) {
686             context.title = ["Playlist", "Type"];
687             context.keys = { text: "name", description: "type" };
688             context.completions = player.getPlaylists();
689         };
690
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();
696         };
697
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
703         };
704
705         completion.song = function album(context, artist, album) {
706             context.title = ["Song"];
707             context.completions = [[v, ""] for ([, v] in Iterator(library.getTracks(artist, album)))];
708         };
709     },
710     mappings: function () {
711         mappings.add([modes.PLAYER],
712             ["x"], "Play track",
713             function () { ex.playerplay(); });
714
715         mappings.add([modes.PLAYER],
716             ["z"], "Previous track",
717             function (args) { ex.playerprev({ "#": args.count }); },
718             { count: true });
719
720         mappings.add([modes.PLAYER],
721             ["c"], "Pause/unpause track",
722             function () { ex.playerpause(); });
723
724         mappings.add([modes.PLAYER],
725             ["b"], "Next track",
726             function (args) { ex.playernext({ "#": args.count }); },
727             { count: true });
728
729         mappings.add([modes.PLAYER],
730             ["v"], "Stop track",
731             function () { ex.playerstop(); });
732
733         mappings.add([modes.PLAYER],
734             ["Q"], "Queue tracks by artist/album/track",
735             function () { commandline.open(":", "queue ", modes.EX); });
736
737         mappings.add([modes.PLAYER],
738             ["f"], "Loads current view filtered by the keywords",
739             function () { commandline.open(":", "filter ", modes.EX); });
740
741         mappings.add([modes.PLAYER],
742             ["i"], "Select current track",
743             function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); });
744
745         mappings.add([modes.PLAYER],
746             ["s"], "Toggle shuffle",
747             function () { player.toggleShuffle(); });
748
749         mappings.add([modes.PLAYER],
750             ["r"], "Toggle repeat",
751             function () { player.toggleRepeat(); });
752
753         mappings.add([modes.PLAYER],
754             ["h", "<Left>"], "Seek -10s",
755             function (args) { player.seekBackward(Math.max(1, args.count) * 10000); },
756             { count: true });
757
758         mappings.add([modes.PLAYER],
759             ["l", "<Right>"], "Seek +10s",
760             function (args) { player.seekForward(Math.max(1, args.count) * 10000); },
761             { count: true });
762
763         mappings.add([modes.PLAYER],
764             ["H", "<S-Left>"], "Seek -1m",
765             function (args) { player.seekBackward(Math.max(1, args.count) * 60000); },
766             { count: true });
767
768         mappings.add([modes.PLAYER],
769             ["L", "<S-Right>"], "Seek +1m",
770             function (args) { player.seekForward(Math.max(1, args.count) * 60000); },
771             { count: true });
772
773         mappings.add([modes.PLAYER],
774              ["=", "+"], "Increase volume by 5% of the maximum",
775              function () { player.increaseVolume(); });
776
777         mappings.add([modes.PLAYER],
778              ["-"], "Decrease volume by 5% of the maximum",
779              function () { player.decreaseVolume(); });
780
781         mappings.add([modes.PLAYER],
782              ["/"], "Search forward for a track",
783              function () { player.CommandMode(modes.SEARCH_VIEW_FORWARD).open(); });
784
785         mappings.add([modes.PLAYER],
786              ["n"], "Find the next track",
787              function () { player.searchViewAgain(false); });
788
789         mappings.add([modes.PLAYER],
790              ["N"], "Find the previous track",
791              function () { player.searchViewAgain(true); });
792
793         for (let i in util.range(0, 6)) {
794             let (rating = i) {
795                 mappings.add([modes.PLAYER],
796                      ["<C-" + rating + ">"], "Rate the current media item " + rating,
797                      function () {
798                          let item = gMM.sequencer.currentItem || this._currentView.selection.currentMediaItem; // XXX: a bit too magic
799                          if (item)
800                              player.rateMediaItem(item, rating);
801                          else
802                              dactyl.beep();
803                      }
804                 );
805             };
806         }
807     },
808     options: function () {
809         options.add(["repeat"],
810             "Set the playback repeat mode",
811             "number", 0,
812             {
813                 setter: function (value) gMM.sequencer.repeatMode = value,
814                 getter: function () gMM.sequencer.repeatMode,
815                 completer: function (context) [
816                     ["0", "Repeat none"],
817                     ["1", "Repeat one"],
818                     ["2", "Repeat all"]
819                 ]
820             });
821
822         options.add(["shuffle"],
823             "Play tracks in shuffled order",
824             "boolean", false,
825             {
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
828             });
829     }
830 });
831
832 // vim: set fdm=marker sw=4 ts=4 et: