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