1# Copyright 2014,2016 Nick Boultbee
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8import os
9
10from senf import fsn2text, uri2fsn
11
12from quodlibet import C_, _
13from quodlibet.util.dprint import print_, print_e
14from quodlibet.remote import Remote, RemoteError
15
16
17def exit_(status=None, notify_startup=False):
18    """Call this to abort the startup before any mainloop starts.
19
20    notify_startup needs to be true if QL could potentially have been
21    called from the desktop file.
22    """
23
24    if notify_startup:
25        import gi
26        gi.require_version("Gdk", "3.0")
27        from gi.repository import Gdk
28        Gdk.notify_startup_complete()
29    raise SystemExit(status)
30
31
32def is_running():
33    """If maybe is another instance running"""
34
35    return Remote.remote_exists()
36
37
38def control(command, arg=None, ignore_error=False):
39    """Sends command to the existing instance if possible and exits.
40
41    Will print any response it gets to stdout.
42
43    Does not return except if ignore_error is True and sending
44    the command failed.
45    """
46
47    if not is_running():
48        if ignore_error:
49            return
50        exit_(_("Quod Libet is not running (add '--run' to start it)"),
51              notify_startup=True)
52        return
53
54    message = command
55    if arg is not None:
56        message += " " + arg
57
58    try:
59        response = Remote.send_message(message)
60    except RemoteError as e:
61        if ignore_error:
62            return
63        exit_(str(e), notify_startup=True)
64    else:
65        if response is not None:
66            print_(response, end="", flush=True)
67        exit_(notify_startup=True)
68
69
70def process_arguments(argv):
71    from quodlibet.util.path import uri_is_valid
72    from quodlibet import util
73    from quodlibet import const
74
75    actions = []
76    controls = ["next", "previous", "play", "pause", "play-pause", "stop",
77                "hide-window", "show-window", "toggle-window",
78                "focus", "quit", "unfilter", "refresh", "force-previous"]
79    controls_opt = ["seek", "repeat", "query", "volume", "filter",
80                    "rating", "set-browser", "open-browser", "shuffle",
81                    "queue", "stop-after", "random", "repeat-type",
82                    "shuffle-type", "add-location"]
83
84    options = util.OptionParser(
85        "Quod Libet", const.VERSION,
86        _("a music library and player"),
87        _("[option]"))
88
89    options.add("print-playing", help=_("Print the playing song and exit"))
90    options.add("start-playing", help=_("Begin playing immediately"))
91    options.add("start-hidden", help=_("Don't show any windows on start"))
92
93    for opt, help in [
94        ("next", _("Jump to next song")),
95        ("previous",
96            _("Jump to previous song or restart if near the beginning")),
97        ("force-previous", _("Jump to previous song")),
98        ("play", _("Start playback")),
99        ("pause", _("Pause playback")),
100        ("play-pause", _("Toggle play/pause mode")),
101        ("stop", _("Stop playback")),
102        ("volume-up", _("Turn up volume")),
103        ("volume-down", _("Turn down volume")),
104        ("rating-up", _("Increase rating of playing song by one star")),
105        ("rating-down", _("Decrease rating of playing song by one star")),
106        ("status", _("Print player status")),
107        ("hide-window", _("Hide main window")),
108        ("show-window", _("Show main window")),
109        ("toggle-window", _("Toggle main window visibility")),
110        ("focus", _("Focus the running player")),
111        ("unfilter", _("Remove active browser filters")),
112        ("refresh", _("Refresh and rescan library")),
113        ("list-browsers", _("List available browsers")),
114        ("print-playlist", _("Print the current playlist")),
115        ("print-queue", _("Print the contents of the queue")),
116        ("print-query-text", _("Print the active text query")),
117        ("no-plugins", _("Start without plugins")),
118        ("run", _("Start Quod Libet if it isn't running")),
119        ("quit", _("Exit Quod Libet")),
120            ]:
121        options.add(opt, help=help)
122
123    for opt, help, arg in [
124        ("seek", _("Seek within the playing song"), _("[+|-][HH:]MM:SS")),
125        ("shuffle", _("Set or toggle shuffle mode"), "0|1|t"),
126        ("shuffle-type", _("Set shuffle mode type"), "random|weighted|off"),
127        ("repeat", _("Turn repeat off, on, or toggle it"), "0|1|t"),
128        ("repeat-type", _("Set repeat mode type"), "current|all|one|off"),
129        ("volume", _("Set the volume"), "[+|-]0..100"),
130        ("query", _("Search your audio library"), _("query")),
131        ("play-file", _("Play a file"), C_("command", "filename")),
132        ("rating", _("Set rating of playing song"), "[+|-]0.0..1.0"),
133        ("set-browser", _("Set the current browser"), "BrowserName"),
134        ("stop-after", _("Stop after the playing song"), "0|1|t"),
135        ("open-browser", _("Open a new browser"), "BrowserName"),
136        ("queue", _("Show or hide the queue"), "on|off|t"),
137        ("random", _("Filter on a random value"), C_("command", "tag")),
138        ("filter", _("Filter on a tag value"), _("tag=value")),
139        ("enqueue", _("Enqueue a file or query"), "%s|%s" % (
140            C_("command", "filename"), _("query"))),
141        ("enqueue-files", _("Enqueue comma-separated files"), "%s[,%s..]" % (
142            _("filename"), _("filename"))),
143        ("print-query", _("Print filenames of results of query to stdout"),
144            _("query")),
145        ("unqueue", _("Unqueue a file or query"), "%s|%s" % (
146            C_("command", "filename"), _("query"))),
147        ("add-location", _("Add a file or directory to the library"),
148            _("location")),
149            ]:
150        options.add(opt, help=help, arg=arg)
151
152    options.add("sm-config-prefix", arg="dummy")
153    options.add("sm-client-id", arg="prefix")
154    options.add("screen", arg="dummy")
155
156    def is_vol(str):
157        if len(str) == 1 and str[0] in '+-':
158            return True
159        return is_float(str)
160
161    def is_rate(str):
162        if len(str) == 1 and str[0] in '+-':
163            return True
164        return is_float(str)
165
166    def is_time(str):
167        if str[0] not in "+-0123456789":
168            return False
169        elif str[0] in "+-":
170            str = str[1:]
171        parts = str.split(":")
172        if len(parts) > 3:
173            return False
174        else:
175            return not (False in [p.isdigit() for p in parts])
176
177    def is_float(str):
178        try:
179            float(str)
180        except ValueError:
181            return False
182        else:
183            return True
184
185    validators = {
186        "shuffle": ["0", "1", "t", "on", "off", "toggle"].__contains__,
187        "shuffle-type": ["random", "weighted", "off", "0"].__contains__,
188        "repeat": ["0", "1", "t", "on", "off", "toggle"].__contains__,
189        "repeat-type": ["current", "all", "one", "off", "0"].__contains__,
190        "volume": is_vol,
191        "seek": is_time,
192        "rating": is_rate,
193        "stop-after": ["0", "1", "t"].__contains__,
194        }
195
196    cmds_todo = []
197
198    def queue(*args):
199        cmds_todo.append(args)
200
201    # XXX: to make startup work in case the desktop file isn't passed
202    # a file path/uri
203    if argv[-1] == "--play-file":
204        argv = argv[:-1]
205
206    opts, args = options.parse(argv[1:])
207
208    for command, arg in opts.items():
209        if command in controls:
210            queue(command)
211        elif command in controls_opt:
212            if command in validators and not validators[command](arg):
213                print_e(_("Invalid argument for '%s'.") % command)
214                print_e(_("Try %s --help.") % fsn2text(argv[0]))
215                exit_(True, notify_startup=True)
216            else:
217                queue(command, arg)
218        elif command == "status":
219            queue("status")
220        elif command == "print-playlist":
221            queue("dump-playlist")
222        elif command == "print-queue":
223            queue("dump-queue")
224        elif command == "list-browsers":
225            queue("dump-browsers")
226        elif command == "volume-up":
227            queue("volume +")
228        elif command == "volume-down":
229            queue("volume -")
230        elif command == "rating-up":
231            queue("rating +")
232        elif command == "rating-down":
233            queue("rating -")
234        elif command == "enqueue" or command == "unqueue":
235            try:
236                filename = uri2fsn(arg)
237            except ValueError:
238                filename = arg
239            queue(command, filename)
240        elif command == "enqueue-files":
241            queue(command, arg)
242        elif command == "play-file":
243            if uri_is_valid(arg) and arg.startswith("quodlibet://"):
244                # TODO: allow handling of URIs without --play-file
245                queue("uri-received", arg)
246            else:
247                try:
248                    filename = uri2fsn(arg)
249                except ValueError:
250                    filename = arg
251                filename = os.path.abspath(util.path.expanduser(arg))
252                queue("play-file", filename)
253        elif command == 'add-location':
254            try:
255                path = uri2fsn(arg)
256            except ValueError:
257                path = arg
258            path = os.path.abspath(util.path.expanduser(arg))
259            queue("add-location", path)
260        elif command == "print-playing":
261            try:
262                queue("print-playing", args[0])
263            except IndexError:
264                queue("print-playing")
265        elif command == "print-query":
266            queue(command, arg)
267        elif command == "print-query-text":
268            queue(command)
269        elif command == "start-playing":
270            actions.append(command)
271        elif command == "start-hidden":
272            actions.append(command)
273        elif command == "no-plugins":
274            actions.append(command)
275        elif command == "run":
276            actions.append(command)
277
278    if cmds_todo:
279        for cmd in cmds_todo:
280            control(*cmd, **{"ignore_error": "run" in actions})
281    else:
282        # this will exit if it succeeds
283        control('focus', ignore_error=True)
284
285    return actions, cmds_todo
286