1# Copyright (C) 2008-2010 Adam Olsen 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, or (at your option) 6# any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# 18# The developers of the Exaile media player hereby grant permission 19# for non-GPL compatible GStreamer and Exaile plugins to be used and 20# distributed together with GStreamer and Exaile. This permission is 21# above and beyond the permissions granted by the GPL license by which 22# Exaile is covered. If you modify this code, you may extend this 23# exception to your version of the code, but you are not obligated to 24# do so. If you do not wish to do so, delete this exception statement 25# from your version. 26 27# Here's where it all begins..... 28# 29# Holds the main Exaile class, whose instantiation starts up the entirety 30# of Exaile and which also handles Exaile shutdown. 31# 32# Also takes care of parsing commandline options. 33 34import os 35import platform 36import sys 37import threading 38 39from xl import logger_setup 40from xl.externals.sigint import InterruptibleLoopContext 41from xl.nls import gettext as _ 42 43# Imported later to avoid PyGObject imports just for --help. 44GLib = Gio = Gtk = common = xdg = None 45 46 47def _do_heavy_imports(): 48 global GLib, Gio, Gtk, common, xdg 49 50 import gi 51 52 gi.require_version('Gdk', '3.0') 53 gi.require_version('Gtk', '3.0') 54 gi.require_version('Gst', '1.0') 55 gi.require_version('GIRepository', '2.0') 56 gi.require_version('GstPbutils', '1.0') 57 58 from gi.repository import GLib, Gio, Gtk 59 from xl import common, xdg 60 61 62# placeholder, - xl.version can be slow to import, which would slow down 63# cli args. Thus we import __version__ later. 64__version__ = None 65 66logger = None 67 68 69def create_argument_parser(): 70 """Create command-line argument parser for Exaile""" 71 72 import argparse 73 74 # argparse hard-codes "usage:" uncapitalized. We replace this with an 75 # empty string and put "Usage:" in the actual usage string instead. 76 77 class Formatter(argparse.HelpFormatter): 78 def _format_usage(self, usage, actions, groups, prefix): 79 return super(self.__class__, self)._format_usage(usage, actions, groups, "") 80 81 p = argparse.ArgumentParser( 82 usage=_("Usage: exaile [OPTION...] [LOCATION...]"), 83 description=_( 84 "Launch Exaile, optionally adding tracks specified by" 85 " LOCATION to the active playlist." 86 " If Exaile is already running, this attempts to use the existing" 87 " instance instead of creating a new one." 88 ), 89 add_help=False, 90 formatter_class=Formatter, 91 ) 92 93 p.add_argument('locs', nargs='*', help=argparse.SUPPRESS) 94 95 group = p.add_argument_group(_('Playback Options')) 96 group.add_argument( 97 "-n", 98 "--next", 99 dest="Next", 100 action="store_true", 101 default=False, 102 help=_("Play the next track"), 103 ) 104 group.add_argument( 105 "-p", 106 "--prev", 107 dest="Prev", 108 action="store_true", 109 default=False, 110 help=_("Play the previous track"), 111 ) 112 group.add_argument( 113 "-s", 114 "--stop", 115 dest="Stop", 116 action="store_true", 117 default=False, 118 help=_("Stop playback"), 119 ) 120 group.add_argument( 121 "-a", "--play", dest="Play", action="store_true", default=False, help=_("Play") 122 ) 123 group.add_argument( 124 "-u", 125 "--pause", 126 dest="Pause", 127 action="store_true", 128 default=False, 129 help=_("Pause"), 130 ) 131 group.add_argument( 132 "-t", 133 "--play-pause", 134 dest="PlayPause", 135 action="store_true", 136 default=False, 137 help=_("Pause or resume playback"), 138 ) 139 group.add_argument( 140 "--stop-after-current", 141 dest="StopAfterCurrent", 142 action="store_true", 143 default=False, 144 help=_("Stop playback after current track"), 145 ) 146 147 group = p.add_argument_group(_('Collection Options')) 148 group.add_argument( 149 "--add", 150 dest="Add", 151 # TRANSLATORS: Meta variable for --add and --export-playlist 152 metavar=_("LOCATION"), 153 help=_("Add tracks from LOCATION to the collection"), 154 ) 155 156 group = p.add_argument_group(_('Playlist Options')) 157 group.add_argument( 158 "--export-playlist", 159 dest="ExportPlaylist", 160 # TRANSLATORS: Meta variable for --add and --export-playlist 161 metavar=_("LOCATION"), 162 help=_('Export the current playlist to LOCATION'), 163 ) 164 165 group = p.add_argument_group(_('Track Options')) 166 group.add_argument( 167 "-q", 168 "--query", 169 dest="Query", 170 action="store_true", 171 default=False, 172 help=_("Query player"), 173 ) 174 group.add_argument( 175 "--format-query", 176 dest="FormatQuery", 177 # TRANSLATORS: Meta variable for --format-query 178 metavar=_('FORMAT'), 179 help=_('Retrieve the current playback state and track information as FORMAT'), 180 ) 181 group.add_argument( 182 "--format-query-tags", 183 dest="FormatQueryTags", 184 # TRANSLATORS: Meta variable for --format-query-tags 185 metavar=_('TAGS'), 186 help=_('Tags to retrieve from the current track; use with --format-query'), 187 ) 188 group.add_argument( 189 "--gui-query", 190 dest="GuiQuery", 191 action="store_true", 192 default=False, 193 help=_("Show a popup with data of the current track"), 194 ) 195 group.add_argument( 196 "--get-title", 197 dest="GetTitle", 198 action="store_true", 199 default=False, 200 help=_("Print the title of current track"), 201 ) 202 group.add_argument( 203 "--get-album", 204 dest="GetAlbum", 205 action="store_true", 206 default=False, 207 help=_("Print the album of current track"), 208 ) 209 group.add_argument( 210 "--get-artist", 211 dest="GetArtist", 212 action="store_true", 213 default=False, 214 help=_("Print the artist of current track"), 215 ) 216 group.add_argument( 217 "--get-length", 218 dest="GetLength", 219 action="store_true", 220 default=False, 221 help=_("Print the length of current track"), 222 ) 223 group.add_argument( 224 '--set-rating', 225 dest="SetRating", 226 type=int, 227 # TRANSLATORS: Variable for command line options with arguments 228 metavar=_('N'), 229 help=_('Set rating for current track to N%').replace("%", "%%"), 230 ) 231 group.add_argument( 232 '--get-rating', 233 dest='GetRating', 234 action='store_true', 235 default=False, 236 help=_('Get rating for current track'), 237 ) 238 group.add_argument( 239 "--current-position", 240 dest="CurrentPosition", 241 action="store_true", 242 default=False, 243 help=_("Print the current playback position as time"), 244 ) 245 group.add_argument( 246 "--current-progress", 247 dest="CurrentProgress", 248 action="store_true", 249 default=False, 250 help=_("Print the current playback progress as percentage"), 251 ) 252 253 group = p.add_argument_group(_('Volume Options')) 254 group.add_argument( 255 "-i", 256 "--increase-vol", 257 dest="IncreaseVolume", 258 type=int, 259 # TRANSLATORS: Meta variable for --increase-vol and--decrease-vol 260 metavar=_("N"), 261 help=_("Increase the volume by N%").replace("%", "%%"), 262 ) 263 group.add_argument( 264 "-l", 265 "--decrease-vol", 266 dest="DecreaseVolume", 267 type=int, 268 # TRANSLATORS: Meta variable for --increase-vol and--decrease-vol 269 metavar=_("N"), 270 help=_("Decrease the volume by N%").replace("%", "%%"), 271 ) 272 group.add_argument( 273 "-m", 274 "--toggle-mute", 275 dest="ToggleMute", 276 action="store_true", 277 default=False, 278 help=_("Mute or unmute the volume"), 279 ) 280 group.add_argument( 281 "--get-volume", 282 dest="GetVolume", 283 action="store_true", 284 default=False, 285 help=_("Print the current volume percentage"), 286 ) 287 288 group = p.add_argument_group(_('Other Options')) 289 group.add_argument( 290 "--new", 291 dest="NewInstance", 292 action="store_true", 293 default=False, 294 help=_("Start new instance"), 295 ) 296 group.add_argument( 297 "-h", "--help", action="help", help=_("Show this help message and exit") 298 ) 299 group.add_argument( 300 "--version", 301 dest="ShowVersion", 302 action="store_true", 303 help=_("Show program's version number and exit."), 304 ) 305 group.add_argument( 306 "--start-minimized", 307 dest="StartMinimized", 308 action="store_true", 309 default=False, 310 help=_("Start minimized (to tray, if possible)"), 311 ) 312 group.add_argument( 313 "--toggle-visible", 314 dest="GuiToggleVisible", 315 action="store_true", 316 default=False, 317 help=_("Toggle visibility of the GUI (if possible)"), 318 ) 319 group.add_argument( 320 "--safemode", 321 dest="SafeMode", 322 action="store_true", 323 default=False, 324 help=_( 325 "Start in safe mode - sometimes" " useful when you're running into problems" 326 ), 327 ) 328 group.add_argument( 329 "--force-import", 330 dest="ForceImport", 331 action="store_true", 332 default=False, 333 help=_( 334 "Force import of old data" " from version 0.2.x (overwrites current data)" 335 ), 336 ) 337 group.add_argument( 338 "--no-import", 339 dest="NoImport", 340 action="store_true", 341 default=False, 342 help=_("Do not import old data" " from version 0.2.x"), 343 ) 344 group.add_argument( 345 "--start-anyway", 346 dest="StartAnyway", 347 action="store_true", 348 default=False, 349 help=_("Make control options like" " --play start Exaile if it is not running"), 350 ) 351 352 group = p.add_argument_group(_('Development/Debug Options')) 353 group.add_argument( 354 "--datadir", 355 dest="UseDataDir", 356 metavar=_('DIRECTORY'), 357 help=_("Set data directory"), 358 ) 359 group.add_argument( 360 "--all-data-dir", 361 dest="UseAllDataDir", 362 metavar=_('DIRECTORY'), 363 help=_("Set data and config directory"), 364 ) 365 group.add_argument( 366 "--modulefilter", 367 dest="ModuleFilter", 368 metavar=_('MODULE'), 369 help=_('Limit log output to MODULE'), 370 ) 371 group.add_argument( 372 "--levelfilter", 373 dest="LevelFilter", 374 metavar=_('LEVEL'), 375 help=_('Limit log output to LEVEL'), 376 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], 377 ) 378 group.add_argument( 379 "--debug", 380 dest="Debug", 381 action="store_true", 382 default=False, 383 help=_("Show debugging output"), 384 ) 385 group.add_argument( 386 "--eventdebug", 387 dest="DebugEvent", 388 action="store_true", 389 default=False, 390 help=_("Enable debugging of" " xl.event. Generates lots of output"), 391 ) 392 group.add_argument( 393 "--eventdebug-full", 394 dest="DebugEventFull", 395 action="store_true", 396 default=False, 397 help=_("Enable full debugging of" " xl.event. Generates LOTS of output"), 398 ) 399 group.add_argument( 400 "--threaddebug", 401 dest="DebugThreads", 402 action="store_true", 403 default=False, 404 help=_("Add thread name to logging" " messages."), 405 ) 406 group.add_argument( 407 "--eventfilter", 408 dest="EventFilter", 409 metavar=_('TYPE'), 410 help=_("Limit xl.event debug to output of TYPE"), 411 ) 412 group.add_argument( 413 "--quiet", 414 dest="Quiet", 415 action="store_true", 416 default=False, 417 help=_("Reduce level of output"), 418 ) 419 group.add_argument( 420 '--startgui', dest='StartGui', action='store_true', default=False 421 ) 422 group.add_argument( 423 '--no-dbus', 424 dest='Dbus', 425 action='store_false', 426 default=True, 427 help=_("Disable D-Bus support"), 428 ) 429 group.add_argument( 430 '--no-hal', 431 dest='Hal', 432 action='store_false', 433 default=True, 434 help=_("Disable HAL support."), 435 ) 436 437 return p 438 439 440class Exaile: 441 _exaile = None 442 443 def __init__(self): 444 """ 445 Initializes Exaile. 446 """ 447 self.quitting = False 448 self.loading = True 449 450 # NOTE: This automatically exits on --help. 451 self.options = create_argument_parser().parse_args() 452 453 if self.options.ShowVersion: 454 self.version() 455 return 456 457 _do_heavy_imports() 458 459 # Set program name for matching with .desktop file in Plasma 460 # under wayland (see #653); should be done before splash screen 461 # is displayed. 462 GLib.set_prgname('exaile') 463 464 if self.options.UseDataDir: 465 xdg.data_dirs.insert(1, self.options.UseDataDir) 466 467 # this is useful on Win32, because you cannot set these directories 468 # via environment variables 469 if self.options.UseAllDataDir: 470 alldatadir = self.options.UseAllDataDir 471 472 # TODO: is this still necessary? Python3 does not seem to 473 # have issue with UTF-8 characters in path (in contrast 474 # to Python2, os.path.join() does not fail). 475 # For now, we replace the UTF-8 characters with ? to keep 476 # the behavior consistent with the old version... 477 if not os.path.supports_unicode_filenames: 478 try: 479 alldatadir.encode('ascii') 480 except UnicodeEncodeError: 481 # Replace non-ASCII characters with ? 482 alldatadir = alldatadir.encode('ascii', 'replace').decode('ascii') 483 print( 484 "WARNING : converted non-ASCII data dir %s to ascii: %s" 485 % (self.options.UseAllDataDir, alldatadir) 486 ) 487 xdg.data_home = alldatadir 488 xdg.data_dirs.insert(0, xdg.data_home) 489 xdg.config_home = alldatadir 490 xdg.config_dirs.insert(0, xdg.config_home) 491 xdg.cache_home = alldatadir 492 493 try: 494 xdg._make_missing_dirs() 495 except OSError as e: 496 print( 497 'ERROR: Could not create configuration directories: %s' % e, 498 file=sys.stderr, 499 ) 500 return 501 502 # Make event debug imply debug 503 if self.options.DebugEventFull: 504 self.options.DebugEvent = True 505 506 if self.options.DebugEvent: 507 self.options.Debug = True 508 509 try: 510 logger_setup.start_logging( 511 self.options.Debug, 512 self.options.Quiet, 513 self.options.DebugThreads, 514 self.options.ModuleFilter, 515 self.options.LevelFilter, 516 ) 517 except OSError as e: 518 print('ERROR: could not setup logging: %s' % e, file=sys.stderr) 519 return 520 521 global logger 522 import logging 523 524 logger = logging.getLogger(__name__) 525 526 try: 527 # Late import ensures xl.event uses correct logger 528 from xl import event 529 530 if self.options.EventFilter: 531 event.EVENT_MANAGER.logger_filter = self.options.EventFilter 532 self.options.DebugEvent = True 533 534 if self.options.DebugEvent: 535 event.EVENT_MANAGER.use_logger = True 536 537 if self.options.DebugEventFull: 538 event.EVENT_MANAGER.use_verbose_logger = True 539 540 # initial mainloop setup. The actual loop is started later, 541 # if necessary 542 self.mainloop_init() 543 544 # initialize DbusManager 545 if self.options.StartGui and self.options.Dbus: 546 from xl import xldbus 547 548 exit = xldbus.check_exit(self.options, self.options.locs) 549 if exit == "exit": 550 sys.exit(0) 551 elif exit == "command": 552 if not self.options.StartAnyway: 553 sys.exit(0) 554 self.dbus = xldbus.DbusManager(self) 555 556 # import version, see note above 557 global __version__ 558 from xl.version import __version__ 559 560 # load the rest. 561 self.__init() 562 563 # handle delayed commands 564 if ( 565 self.options.StartGui 566 and self.options.Dbus 567 and self.options.StartAnyway 568 and exit == "command" 569 ): 570 xldbus.run_commands(self.options, self.dbus) 571 572 # connect dbus signals 573 if self.options.StartGui and self.options.Dbus: 574 self.dbus._connect_signals() 575 576 # On SIGTERM, quit normally. 577 import signal 578 579 signal.signal(signal.SIGTERM, (lambda sig, stack: self.quit())) 580 581 # run the GUIs mainloop, if needed 582 if self.options.StartGui: 583 # Handle keyboard interruptions 584 with InterruptibleLoopContext(self.quit): 585 Gtk.main() # mainloop 586 except Exception: 587 logger.exception("Unhandled exception") 588 589 def __init(self): 590 """ 591 Initializes Exaile 592 """ 593 594 logger.info("Loading Exaile %s...", __version__) 595 596 from gi.repository import GObject 597 from .version import register 598 599 register('Python', platform.python_version()) 600 register('PyGObject', '%d.%d.%d' % GObject.pygobject_version) 601 602 logger.info("Loading settings...") 603 try: 604 from xl import settings 605 except common.VersionError: 606 logger.exception("Error loading settings") 607 sys.exit(1) 608 609 logger.debug("Settings loaded from %s", settings.location) 610 611 # display locale information if available 612 try: 613 import locale 614 615 lc, enc = locale.getlocale() 616 if enc is not None: 617 locale_str = '%s %s' % (lc, enc) 618 else: 619 locale_str = _('Unknown') 620 621 register('Locale', locale_str) 622 except Exception: 623 pass 624 625 splash = None 626 627 if self.options.StartGui: 628 if settings.get_option('gui/use_splash', True): 629 from xlgui.widgets.info import Splash 630 631 splash = Splash() 632 splash.show() 633 634 firstrun = settings.get_option("general/first_run", True) 635 636 # Migrate old rating options 637 from xl.migrations.settings import rating 638 639 rating.migrate() 640 641 # Migrate builtin OSD to plugin 642 from xl.migrations.settings import osd 643 644 osd.migrate() 645 646 # Migrate engines 647 from xl.migrations.settings import engine 648 649 engine.migrate() 650 651 # TODO: enable audio plugins separately from normal 652 # plugins? What about plugins that use the player? 653 654 # Gstreamer doesn't initialize itself automatically, and fails 655 # miserably when you try to inherit from something and GST hasn't 656 # been initialized yet. So this is here. 657 from gi.repository import Gst 658 659 Gst.init(None) 660 661 # Initialize plugin manager 662 from xl import plugins 663 664 self.plugins = plugins.PluginsManager(self) 665 666 if not self.options.SafeMode: 667 logger.info("Loading plugins...") 668 self.plugins.load_enabled() 669 else: 670 logger.info("Safe mode enabled, not loading plugins.") 671 672 # Initialize the collection 673 logger.info("Loading collection...") 674 from xl import collection 675 676 try: 677 self.collection = collection.Collection( 678 "Collection", location=os.path.join(xdg.get_data_dir(), 'music.db') 679 ) 680 except common.VersionError: 681 logger.exception("VersionError loading collection") 682 sys.exit(1) 683 684 # Migrate covers.db. This can only be done after the collection is loaded. 685 import xl.migrations.database.covers_1to2 as mig 686 687 mig.migrate() 688 689 from xl import event 690 691 # Set up the player and playback queue 692 from xl import player 693 694 event.log_event("player_loaded", player.PLAYER, None) 695 696 # Initalize playlist manager 697 from xl import playlist 698 699 self.playlists = playlist.PlaylistManager() 700 self.smart_playlists = playlist.SmartPlaylistManager( 701 'smart_playlists', collection=self.collection 702 ) 703 if firstrun: 704 self._add_default_playlists() 705 event.log_event("playlists_loaded", self, None) 706 707 # Initialize dynamic playlist support 708 from xl import dynamic 709 710 dynamic.MANAGER.collection = self.collection 711 712 # Initalize device manager 713 logger.info("Loading devices...") 714 from xl import devices 715 716 self.devices = devices.DeviceManager() 717 event.log_event("device_manager_ready", self, None) 718 719 # Initialize dynamic device discovery interface 720 # -> if initialized and connected, then the object is not None 721 722 self.udisks2 = None 723 724 if self.options.Hal: 725 from xl import hal 726 727 udisks2 = hal.UDisks2(self.devices) 728 if udisks2.connect(): 729 self.udisks2 = udisks2 730 731 # Radio Manager 732 from xl import radio 733 734 self.stations = playlist.PlaylistManager('radio_stations') 735 self.radio = radio.RadioManager() 736 737 self.gui = None 738 # Setup GUI 739 if self.options.StartGui: 740 logger.info("Loading interface...") 741 742 import xlgui 743 744 self.gui = xlgui.Main(self) 745 self.gui.main.window.show_all() 746 event.log_event("gui_loaded", self, None) 747 748 if splash is not None: 749 splash.destroy() 750 751 if firstrun: 752 settings.set_option("general/first_run", False) 753 754 self.loading = False 755 Exaile._exaile = self 756 event.log_event("exaile_loaded", self, None) 757 758 restore = True 759 760 if self.gui: 761 # Find out if the user just passed in a list of songs 762 # TODO: find a better place to put this 763 764 songs = [Gio.File.new_for_path(arg).get_uri() for arg in self.options.locs] 765 if len(songs) > 0: 766 restore = False 767 self.gui.open_uri(songs[0], play=True) 768 for arg in songs[1:]: 769 self.gui.open_uri(arg) 770 771 # kick off autoscan of libraries 772 # -> don't do it in command line mode, since that isn't expected 773 self.gui.rescan_collection_with_progress(True) 774 775 if restore: 776 player.QUEUE._restore_player_state( 777 os.path.join(xdg.get_data_dir(), 'player.state') 778 ) 779 780 # pylint: enable-msg=W0201 781 782 def version(self): 783 from xl.version import __version__ 784 785 print("Exaile", __version__) 786 sys.exit(0) 787 788 def _add_default_playlists(self): 789 """ 790 Adds some default smart playlists to the playlist manager 791 """ 792 from xl import playlist 793 794 # entire playlist 795 entire_lib = playlist.SmartPlaylist( 796 _("Entire Library"), collection=self.collection 797 ) 798 self.smart_playlists.save_playlist(entire_lib, overwrite=True) 799 800 # random playlists 801 for count in (100, 300, 500): 802 pl = playlist.SmartPlaylist( 803 _("Random %d") % count, collection=self.collection 804 ) 805 pl.set_return_limit(count) 806 pl.set_random_sort(True) 807 self.smart_playlists.save_playlist(pl, overwrite=True) 808 809 # rating based playlists 810 for item in (3, 4): 811 pl = playlist.SmartPlaylist( 812 _("Rating > %d") % item, collection=self.collection 813 ) 814 pl.add_param('__rating', '>', item) 815 self.smart_playlists.save_playlist(pl, overwrite=True) 816 817 def mainloop_init(self): 818 from gi.repository import GObject 819 820 MIN_VER = (3, 10, 2) 821 ver = GObject.pygobject_version 822 823 if ver < MIN_VER: 824 # Probably should exit? 825 logger.warning( 826 "Exaile requires PyGObject %d.%d.%d or greater! (got %d.%d.%d)", 827 *(MIN_VER + ver) 828 ) 829 830 if self.options.Dbus: 831 import dbus 832 import dbus.mainloop.glib 833 834 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 835 dbus.mainloop.glib.threads_init() 836 dbus.mainloop.glib.gthreads_init() 837 838 if not self.options.StartGui: 839 from gi.repository import GLib 840 841 loop = GLib.MainLoop() 842 context = loop.get_context() 843 t = threading.Thread(target=self.__mainloop, args=(context,)) 844 t.daemon = True 845 t.start() 846 847 def __mainloop(self, context): 848 while True: 849 try: 850 context.iteration(True) 851 except Exception: 852 pass 853 854 def get_version(self): 855 """ 856 Returns the current version 857 """ 858 return __version__ 859 860 def get_user_agent_string(self, plugin_name=None): 861 """ 862 Returns an appropriately formatted User-agent string for 863 web requests. When possible, plugins should use this to 864 format user agent strings. 865 866 Users can control this agent string by manually setting 867 general/user_agent and general/user_agent_w_plugin in settings.ini 868 869 :param plugin_name: the name of the plugin 870 """ 871 872 version = __version__ 873 if '+' in version: # strip out revision identifier 874 version = version[: version.index('+')] 875 876 fmt = {'version': version} 877 878 if not hasattr(self, '_user_agent_no_plugin'): 879 880 from xl import settings 881 882 default_no_plugin = 'Exaile/%(version)s (+https://www.exaile.org)' 883 default_plugin = 'Exaile/%(version)s %(plugin_name)s/%(plugin_version)s (+https://www.exaile.org)' 884 885 self._user_agent_no_plugin = settings.get_option( 886 'general/user_agent', default_no_plugin 887 ) 888 self._user_agent_w_plugin = settings.get_option( 889 'general/user_agent_w_plugin', default_plugin 890 ) 891 892 if plugin_name is not None: 893 plugin_info = self.plugins.get_plugin_info(plugin_name) 894 895 fmt['plugin_name'] = plugin_info['Name'].replace(' ', '') 896 fmt['plugin_version'] = plugin_info['Version'] 897 898 return self._user_agent_w_plugin % fmt 899 else: 900 return self._user_agent_no_plugin % fmt 901 902 def quit(self, restart=False): 903 """ 904 Exits Exaile normally. Takes care of saving 905 preferences, databases, etc. 906 907 :param restart: Whether to directly restart 908 :type restart: bool 909 """ 910 if self.quitting: 911 return 912 self.quitting = True 913 logger.info("Exaile is shutting down...") 914 915 logger.info("Tearing down plugins...") 916 self.plugins.teardown(self) 917 918 from xl import event 919 920 # this event should be used by modules that dont need 921 # to be saved in any particular order. modules that might be 922 # touched by events triggered here should be added statically 923 # below. 924 event.log_event("quit_application", self, None) 925 926 logger.info("Saving state...") 927 self.plugins.save_enabled() 928 929 if self.gui: 930 self.gui.quit() 931 932 from xl import covers 933 934 covers.MANAGER.save() 935 936 self.collection.save_to_location() 937 938 # Save order of custom playlists 939 self.playlists.save_order() 940 self.stations.save_order() 941 942 # save player, queue 943 from xl import player 944 945 player.QUEUE._save_player_state( 946 os.path.join(xdg.get_data_dir(), 'player.state') 947 ) 948 player.QUEUE.save_to_location(os.path.join(xdg.get_data_dir(), 'queue.state')) 949 player.PLAYER.stop() 950 951 from xl import settings 952 953 settings.MANAGER.save() 954 955 if restart: 956 logger.info("Restarting...") 957 logger_setup.stop_logging() 958 python = sys.executable 959 if sys.platform == 'win32': 960 # Python Win32 bug: it does not quote individual command line 961 # arguments. Here we do it ourselves and pass the whole thing 962 # as one string. 963 # See https://bugs.python.org/issue436259 (closed wontfix). 964 import subprocess 965 966 cmd = [python] + sys.argv 967 cmd = subprocess.list2cmdline(cmd) 968 os.execl(python, cmd) 969 else: 970 os.execl(python, python, *sys.argv) 971 972 logger.info("Bye!") 973 logger_setup.stop_logging() 974 sys.exit(0) 975 976 977def exaile(): 978 if not Exaile._exaile: 979 raise AttributeError( 980 _( 981 "Exaile is not yet finished loading" 982 ". Perhaps you should listen for the exaile_loaded" 983 " signal?" 984 ) 985 ) 986 987 return Exaile._exaile 988 989 990# vim: et sts=4 sw=4 991