1# Copyright 2004-2005 Joe Wreschnig, Michael Urman, Iñigo Serna,
2#           2011-2013,2016 Nick Boultbee
3#           2014 Christoph Reiter
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10import os
11
12from senf import uri2fsn, fsnative, fsn2text, text2fsn
13
14from quodlibet.util.string import split_escape
15
16from quodlibet import browsers
17
18from quodlibet import util
19from quodlibet.util import print_d, print_e, copool
20
21from quodlibet.qltk.browser import LibraryBrowser
22from quodlibet.qltk.properties import SongProperties
23from quodlibet.util.library import scan_library
24
25from quodlibet.order.repeat import RepeatListForever, RepeatSongForever, \
26        OneSong
27from quodlibet.order.reorder import OrderWeighted, OrderShuffle
28
29from quodlibet.config import RATINGS
30
31
32class CommandError(Exception):
33    pass
34
35
36class CommandRegistry(object):
37    """Knows about all commands and handles parsing/executing them"""
38
39    def __init__(self):
40        self._commands = {}
41
42    def register(self, name, args=0, optional=0):
43        """Register a new command function
44
45        The functions gets zero or more arguments as `fsnative`
46        and should return `None` or `fsnative`. In case an error
47        occurred the command should raise `CommandError`.
48
49        Args:
50            name (str): the command name
51            args (int): amount of required arguments
52            optional (int): amount of additional optional arguments
53        Returns:
54            Callable
55        """
56
57        def wrap(func):
58            self._commands[name] = (func, args, optional)
59            return func
60        return wrap
61
62    def handle_line(self, app, line):
63        """Parses a command line and executes the command.
64
65        Can not fail.
66
67        Args:
68            app (Application)
69            line (fsnative)
70        Returns:
71            fsnative or None
72        """
73
74        assert isinstance(line, fsnative)
75
76        # only one arg supported atm
77        parts = line.split(" ", 1)
78        command = parts[0]
79        args = parts[1:]
80
81        print_d("command: %r(*%r)" % (command, args))
82
83        try:
84            return self.run(app, command, *args)
85        except CommandError as e:
86            print_e(e)
87        except:
88            util.print_exc()
89
90    def run(self, app, name, *args):
91        """Execute the command `name` passing args
92
93        May raise CommandError
94        """
95
96        if name not in self._commands:
97            raise CommandError("Unknown command %r" % name)
98
99        cmd, argcount, optcount = self._commands[name]
100        if len(args) < argcount:
101            raise CommandError("Not enough arguments for %r" % name)
102        if len(args) > argcount + optcount:
103            raise CommandError("Too many arguments for %r" % name)
104
105        print_d("Running %r with params %s " % (cmd.__name__, args))
106
107        try:
108            result = cmd(app, *args)
109        except CommandError as e:
110            raise CommandError("%s: %s" % (name, str(e)))
111        else:
112            if result is not None and not isinstance(result, fsnative):
113                raise CommandError(
114                    "%s: returned %r which is not fsnative" % (name, result))
115            return result
116
117
118def arg2text(arg):
119    """Like fsn2text but is strict by default and raises CommandError"""
120
121    try:
122        return fsn2text(arg, strict=True)
123    except ValueError as e:
124        raise CommandError(e)
125
126
127registry = CommandRegistry()
128
129
130@registry.register("previous")
131def _previous(app):
132    app.player.previous()
133
134
135@registry.register("force-previous")
136def _force_previous(app):
137    app.player.previous(True)
138
139
140@registry.register("next")
141def _next(app):
142    app.player.next()
143
144
145@registry.register("pause")
146def _pause(app):
147    app.player.paused = True
148
149
150@registry.register("play")
151def _play(app):
152    app.player.play()
153
154
155@registry.register("play-pause")
156def _play_pause(app):
157    app.player.playpause()
158
159
160@registry.register("stop")
161def _stop(app):
162    app.player.stop()
163
164
165@registry.register("focus")
166def _focus(app):
167    app.present()
168
169
170@registry.register("volume", args=1)
171def _volume(app, value):
172    if not value:
173        raise CommandError("invalid arg")
174
175    if value[0] in ('+', '-'):
176        if len(value) > 1:
177            try:
178                change = (float(value[1:]) / 100.0)
179            except ValueError:
180                return
181        else:
182            change = 0.05
183        if value[0] == '-':
184            change = -change
185        volume = app.player.volume + change
186    else:
187        try:
188            volume = (float(value) / 100.0)
189        except ValueError:
190            return
191    app.player.volume = min(1.0, max(0.0, volume))
192
193
194@registry.register("stop-after", args=1)
195def _stop_after(app, value):
196    po = app.player_options
197    if value == "0":
198        po.stop_after = False
199    elif value == "1":
200        po.stop_after = True
201    elif value == "t":
202        po.stop_after = not po.stop_after
203    else:
204        raise CommandError("Invalid value %r" % value)
205
206
207@registry.register("shuffle", args=1)
208def _shuffle(app, value):
209    po = app.player_options
210    if value in ["0", "off"]:
211        po.shuffle = False
212    elif value in ["1", "on"]:
213        po.shuffle = True
214    elif value in ["t", "toggle"]:
215        po.shuffle = not po.shuffle
216
217
218@registry.register("shuffle-type", args=1)
219def _shuffle_type(app, value):
220    if value in ["random", "weighted"]:
221        app.player_options.shuffle = True
222        if value == "random":
223            app.window.order.shuffler = OrderShuffle
224        elif value == "weighted":
225            app.window.order.shuffler = OrderWeighted
226    elif value in ["off", "0"]:
227        app.player_options.shuffle = False
228
229
230@registry.register("repeat", args=1)
231def _repeat(app, value):
232    po = app.player_options
233    if value in ["0", "off"]:
234        po.repeat = False
235    elif value in ["1", "on"]:
236        print_d("Enabling repeat")
237        po.repeat = True
238    elif value in ["t", "toggle"]:
239        po.repeat = not po.repeat
240
241
242@registry.register("repeat-type", args=1)
243def _repeat_type(app, value):
244    if value in ["current", "all", "one"]:
245        app.player_options.repeat = True
246        if value == "current":
247            app.window.order.repeater = RepeatSongForever
248        elif value == "all":
249            app.window.order.repeater = RepeatListForever
250        elif value == "one":
251            app.window.order.repeater = OneSong
252    elif value in ["off", "0"]:
253        app.player_options.repeat = False
254
255
256@registry.register("seek", args=1)
257def _seek(app, time):
258    player = app.player
259    if not player.song:
260        return
261    seek_to = player.get_position()
262    if time[0] == "+":
263        seek_to += util.parse_time(time[1:]) * 1000
264    elif time[0] == "-":
265        seek_to -= util.parse_time(time[1:]) * 1000
266    else:
267        seek_to = util.parse_time(time) * 1000
268    seek_to = min(player.song.get("~#length", 0) * 1000 - 1,
269                  max(0, seek_to))
270    player.seek(seek_to)
271
272
273@registry.register("play-file", args=1)
274def _play_file(app, value):
275    app.window.open_file(value)
276
277
278@registry.register("add-location", args=1)
279def _add_location(app, value):
280    if os.path.isfile(value):
281        ret = app.library.add_filename(value)
282        if not ret:
283            print_e("Couldn't add file to library")
284    elif os.path.isdir(value):
285        copool.add(app.library.scan, [value], cofuncid="library",
286                   funcid="library")
287    else:
288        print_e("Invalid location")
289
290
291@registry.register("toggle-window")
292def _toggle_window(app):
293    if app.window.get_property('visible'):
294        app.hide()
295    else:
296        app.show()
297
298
299@registry.register("hide-window")
300def _hide_window(app):
301    app.hide()
302
303
304@registry.register("show-window")
305def _show_window(app):
306    app.show()
307
308
309@registry.register("rating", args=1)
310def _rating(app, value):
311    song = app.player.song
312    if not song:
313        return
314
315    if value[0] in ('+', '-'):
316        if len(value) > 1:
317            try:
318                change = float(value[1:])
319            except ValueError:
320                return
321        else:
322            change = (1 / RATINGS.number)
323        if value[0] == '-':
324            change = -change
325        rating = song["~#rating"] + change
326    else:
327        try:
328            rating = float(value)
329        except (ValueError, TypeError):
330            return
331    song["~#rating"] = max(0.0, min(1.0, rating))
332    app.library.changed([song])
333
334
335@registry.register("dump-browsers")
336def _dump_browsers(app):
337    response = u""
338    for i, b in enumerate(browsers.browsers):
339        response += u"%d. %s\n" % (i, browsers.name(b))
340    return text2fsn(response)
341
342
343@registry.register("set-browser", args=1)
344def _set_browser(app, value):
345    if not app.window.select_browser(value, app.library, app.player):
346        raise CommandError("Unknown browser %r" % value)
347
348
349@registry.register("open-browser", args=1)
350def _open_browser(app, value):
351    value = arg2text(value)
352
353    try:
354        Kind = browsers.get(value)
355    except ValueError:
356        raise CommandError("Unknown browser %r" % value)
357    LibraryBrowser.open(Kind, app.library, app.player)
358
359
360@registry.register("random", args=1)
361def _random(app, tag):
362    tag = arg2text(tag)
363    if app.browser.can_filter(tag):
364        app.browser.filter_random(tag)
365
366
367@registry.register("filter", args=1)
368def _filter(app, value):
369    value = arg2text(value)
370
371    try:
372        tag, value = value.split('=', 1)
373    except ValueError:
374        raise CommandError("invalid argument")
375
376    if app.browser.can_filter(tag):
377        app.browser.filter(tag, [value])
378
379
380@registry.register("query", args=1)
381def _query(app, value):
382    value = arg2text(value)
383
384    if app.browser.can_filter_text():
385        app.browser.filter_text(value)
386
387
388@registry.register("unfilter")
389def _unfilter(app):
390    app.browser.unfilter()
391
392
393@registry.register("properties", optional=1)
394def _properties(app, value=None):
395    library = app.library
396    player = app.player
397    window = app.window
398
399    if value is not None:
400        value = arg2text(value)
401        if value in library:
402            songs = [library[value]]
403        else:
404            songs = library.query(value)
405    else:
406        songs = [player.song]
407
408    songs = list(filter(None, songs))
409
410    if songs:
411        window = SongProperties(library, songs, parent=window)
412        window.show()
413
414
415@registry.register("enqueue", args=1)
416def _enqueue(app, value):
417    playlist = app.window.playlist
418    library = app.library
419    if value in library:
420        songs = [library[value]]
421    elif os.path.isfile(value):
422        songs = [library.add_filename(os.path.realpath(value))]
423    else:
424        songs = library.query(arg2text(value))
425    songs.sort()
426    playlist.enqueue(songs)
427
428
429@registry.register("enqueue-files", args=1)
430def _enqueue_files(app, value):
431    """Enqueues comma-separated filenames or song names.
432    Commas in filenames should be backslash-escaped"""
433
434    library = app.library
435    window = app.window
436    songs = []
437    for param in split_escape(value, ","):
438        try:
439            song_path = uri2fsn(param)
440        except ValueError:
441            song_path = param
442        if song_path in library:
443            songs.append(library[song_path])
444        elif os.path.isfile(song_path):
445            songs.append(library.add_filename(os.path.realpath(value)))
446    if songs:
447        window.playlist.enqueue(songs)
448
449
450@registry.register("unqueue", args=1)
451def _unqueue(app, value):
452    window = app.window
453    library = app.library
454    playlist = window.playlist
455    if value in library:
456        songs = [library[value]]
457    else:
458        songs = library.query(arg2text(value))
459    playlist.unqueue(songs)
460
461
462@registry.register("quit")
463def _quit(app):
464    app.quit()
465
466
467@registry.register("status")
468def _status(app):
469    player = app.player
470
471    if player.paused:
472        strings = ["paused"]
473    else:
474        strings = ["playing"]
475    strings.append(type(app.browser).__name__)
476    po = app.player_options
477    strings.append("%0.3f" % player.volume)
478    strings.append("shuffle" if po.shuffle else "inorder")
479    strings.append("on" if po.repeat else "off")
480    progress = 0
481    if player.info:
482        length = player.info.get("~#length", 0)
483        if length:
484            progress = player.get_position() / (length * 1000.0)
485    strings.append("%0.3f" % progress)
486    status = u" ".join(strings) + u"\n"
487
488    return text2fsn(status)
489
490
491@registry.register("queue", args=1)
492def _queue(app, value):
493    window = app.window
494    value = arg2text(value)
495
496    if value.startswith("t"):
497        value = not window.qexpander.get_property('visible')
498    else:
499        value = value not in ['0', 'off', 'false']
500    window.qexpander.set_property('visible', value)
501
502
503@registry.register("dump-playlist")
504def _dump_playlist(app):
505    window = app.window
506    uris = []
507    for song in window.playlist.pl.get():
508        uris.append(song("~uri"))
509    return text2fsn(u"\n".join(uris) + u"\n")
510
511
512@registry.register("dump-queue")
513def _dump_queue(app):
514    window = app.window
515    uris = []
516    for song in window.playlist.q.get():
517        uris.append(song("~uri"))
518    return text2fsn(u"\n".join(uris) + u"\n")
519
520
521@registry.register("refresh")
522def _refresh(app):
523    scan_library(app.library, False)
524
525
526@registry.register("print-query", args=1)
527def _print_query(app, query):
528    """Queries library, dumping filenames of matches to stdout
529    See Issue 716
530    """
531
532    query = arg2text(query)
533    songs = app.library.query(query)
534    return "\n".join([song("~filename") for song in songs]) + "\n"
535
536
537@registry.register("print-query-text")
538def _print_query_text(app):
539    if app.browser.can_filter_text():
540        return text2fsn(str(app.browser.get_filter_text()) + u"\n")
541
542
543@registry.register("print-playing", optional=1)
544def _print_playing(app, fstring=None):
545    from quodlibet.formats import AudioFile
546    from quodlibet.pattern import Pattern
547
548    if fstring is None:
549        fstring = u"<artist~album~tracknumber~title>"
550    else:
551        fstring = arg2text(fstring)
552
553    song = app.player.info
554    if song is None:
555        song = AudioFile({"~filename": fsnative(u"/")})
556        song.sanitize()
557
558    return text2fsn(Pattern(fstring).format(song) + u"\n")
559
560
561@registry.register("uri-received", args=1)
562def _uri_received(app, uri):
563    uri = arg2text(uri)
564    app.browser.emit("uri-received", uri)
565