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