Exemplo n.º 1
0
class Application(Gtk.Application, ApplicationActions):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """
    def __init__(self, version):
        """
            Create application
            @param version as str
        """
        Gtk.Application.__init__(
            self,
            application_id="org.gnome.Lollypop",
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.__version = version
        self.set_property("register-session", True)
        signal(SIGINT, lambda a, b: self.quit())
        signal(SIGTERM, lambda a, b: self.quit())
        # Set main thread name
        # We force it to current python 3.6 name, to be sure in case of
        # change in python
        current_thread().setName("MainThread")
        set_proxy_from_gnome()
        GLib.setenv("PULSE_PROP_media.role", "music", True)
        GLib.setenv("PULSE_PROP_application.icon_name", "org.gnome.Lollypop",
                    True)
        # Fix proxy for python
        proxy = GLib.environ_getenv(GLib.get_environ(), "all_proxy")
        if proxy is not None and proxy.startswith("socks://"):
            proxy = proxy.replace("socks://", "socks4://")
            from os import environ
            environ["all_proxy"] = proxy
            environ["ALL_PROXY"] = proxy
        # Ideally, we will be able to delete this once Flatpak has a solution
        # for SSL certificate management inside of applications.
        if GLib.file_test("/app", GLib.FileTest.EXISTS):
            paths = [
                "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/cert.pem",
                "/etc/ssl/cert.pem"
            ]
            for path in paths:
                if GLib.file_test(path, GLib.FileTest.EXISTS):
                    GLib.setenv("SSL_CERT_FILE", path, True)
                    break
        self.cursors = {}
        self.notify = None
        self.scrobblers = []
        self.debug = False
        self.shown_sidebar_tooltip = False
        self.__window = None
        self.__fs_window = None
        self.__scanner_timeout_id = None
        self.__scanner_uris = []
        GLib.set_application_name("Lollypop")
        GLib.set_prgname("lollypop")
        self.add_main_option("play-ids", b"a", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play ids", None)
        self.add_main_option("debug", b"d", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug Lollypop", None)
        self.add_main_option("set-rating", b"r", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b"t", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback", None)
        self.add_main_option("stop", b"s", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Stop playback", None)
        self.add_main_option("next", b"n", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track", None)
        self.add_main_option("prev", b"p", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track", None)
        ## anhsirk0 edits
        self.add_main_option("set-next", b"m", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Set next track by track id",
                             None)
        ## anhsirk0 edits ends

        self.add_main_option("emulate-phone", b"e", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Emulate a Librem phone",
                             None)
        self.add_main_option("version", b"v", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Lollypop version", None)
        self.connect("command-line", self.__on_command_line)
        self.connect("handle-local-options", self.__on_handle_local_options)
        self.connect("activate", self.__on_activate)
        self.connect("shutdown", lambda a: self.__save_state())
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()

    def init(self):
        """
            Init main application
        """
        self.settings = Settings.new()
        # Mount enclosing volume as soon as possible
        uris = self.settings.get_music_uris()
        try:
            for uri in uris:
                if uri.startswith("file:/"):
                    continue
                f = Gio.File.new_for_uri(uri)
                f.mount_enclosing_volume(Gio.MountMountFlags.NONE, None, None,
                                         None)
        except Exception as e:
            Logger.error("Application::init(): %s" % e)

        cssProviderFile = Gio.File.new_for_uri(
            "resource:///org/gnome/Lollypop/application.css")
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.db = Database()
        self.playlists = Playlists()
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.inhibitor = Inhibitor()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.notify = NotificationManager()
        self.art.update_art_size()
        self.task_helper = TaskHelper()
        self.art_helper = ArtHelper()
        self.spotify = SpotifyHelper()
        if not self.settings.get_value("disable-mpris"):
            from lollypop.mpris import MPRIS
            MPRIS(self)

        settings = Gtk.Settings.get_default()
        self.__gtk_dark = settings.get_property(
            "gtk-application-prefer-dark-theme")
        if not self.__gtk_dark:
            dark = self.settings.get_value("dark-ui")
            settings.set_property("gtk-application-prefer-dark-theme", dark)
        ApplicationActions.__init__(self)
        startup_one_ids = self.settings.get_value("startup-one-ids")
        startup_two_ids = self.settings.get_value("startup-two-ids")
        if startup_one_ids:
            self.settings.set_value("state-one-ids", startup_one_ids)
            self.settings.set_value("state-three-ids", GLib.Variant("ai", []))
        if startup_two_ids:
            self.settings.set_value("state-two-ids", startup_two_ids)

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)

        if self.__window is None:
            self.init()
            self.__window = Window()
            self.__window.connect("delete-event", self.__hide_on_delete)
            self.__window.show()
            self.player.restore_state()

    def quit(self, vacuum=False):
        """
            Quit Lollypop
            @param vacuum as bool
        """
        if self.settings.get_value("save-state"):
            # Special case, we can't handle this earlier
            if self.window.is_adaptive:
                visible = self.window.container.stack.get_visible_child()
                if visible == self.window.container.list_one:
                    self.settings.set_value("state-one-ids",
                                            GLib.Variant("ai", []))
                    self.settings.set_value("state-two-ids",
                                            GLib.Variant("ai", []))
                elif visible == self.window.container.list_two:
                    selected_ids = self.window.container.list_one.selected_ids
                    self.settings.set_value("state-one-ids",
                                            GLib.Variant("ai", selected_ids))
                    self.settings.set_value("state-two-ids",
                                            GLib.Variant("ai", []))
        else:
            self.settings.set_value("state-one-ids", GLib.Variant("ai", []))
            self.settings.set_value("state-two-ids", GLib.Variant("ai", []))
        # Then vacuum db
        if vacuum:
            self.__vacuum()
            self.art.clean_web()
        self.__window.hide()
        for scrobbler in self.scrobblers:
            scrobbler.save()
        Gio.Application.quit(self)

    def set_mini(self):
        """
            Set mini player on/off
        """
        if self.__window is not None:
            self.__window.set_mini()

    def load_listenbrainz(self):
        """
            Load listenbrainz support if needed
        """
        if self.settings.get_value("listenbrainz-user-token").get_string():
            from lollypop.listenbrainz import ListenBrainz
            for scrobbler in self.scrobblers:
                if isinstance(scrobbler, ListenBrainz):
                    return
            listenbrainz = ListenBrainz()
            self.scrobblers.append(listenbrainz)
            self.settings.bind("listenbrainz-user-token", listenbrainz,
                               "user_token", 0)

    def fullscreen(self):
        """
            Go fullscreen
        """
        def on_destroy(window):
            self.__fs_window = None
            self.__window.show()

        if self.__fs_window is None:
            self.__window.hide()
            from lollypop.fullscreen import FullScreen
            self.__fs_window = FullScreen(self)
            self.__fs_window.show()
            self.__fs_window.connect("destroy", on_destroy)

    @property
    def devices(self):
        """
            Get available devices
            Merge connected and known
            @return [str]
        """
        devices = self.__window.toolbar.end.devices_popover.devices
        devices += list(self.settings.get_value("devices"))
        result = []
        # Do not use set() + filter() because we want to keep order
        for device in devices:
            if device not in result and device != "":
                result.append(device)
        return result

    @property
    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__fs_window is not None

    @property
    def lastfm(self):
        """
            Get lastfm provider from scrobbler
            @return LastFM/None
        """
        if LastFM is None:
            return None
        from pylast import LastFMNetwork
        for scrobbler in self.scrobblers:
            if isinstance(scrobbler, LastFMNetwork):
                return scrobbler
        return None

    @property
    def main_window(self):
        """
            Get main window
        """
        return self.__window

    @property
    def window(self):
        """
            Get current application window
            @return Gtk.Window
        """
        if self.__fs_window is not None:
            return self.__fs_window
        else:
            return self.__window

    @property
    def gtk_application_prefer_dark_theme(self):
        """
            Return default gtk value
            @return bool
        """
        return self.__gtk_dark

#######################
# PRIVATE             #
#######################

    def __save_state(self):
        """
            Save window position and view
        """
        if not self.settings.get_value("save-state"):
            return

        if self.player.current_track.id is None or\
                self.player.current_track.mtime == 0:
            track_id = None
        elif self.player.current_track.id == Type.RADIOS:
            from lollypop.radios import Radios
            radios = Radios()
            track_id = radios.get_id(self.player.current_track.radio_name)
        else:
            track_id = self.player.current_track.id
            # Save albums context
            try:
                with open(LOLLYPOP_DATA_PATH + "/Albums.bin", "wb") as f:
                    dump(list(self.player.albums), f)
            except Exception as e:
                Logger.error("Application::__save_state(): %s" % e)
        dump(track_id, open(LOLLYPOP_DATA_PATH + "/track_id.bin", "wb"))
        dump([self.player.is_playing, self.player.is_party],
             open(LOLLYPOP_DATA_PATH + "/player.bin", "wb"))
        dump(self.player.queue, open(LOLLYPOP_DATA_PATH + "/queue.bin", "wb"))
        # Save current playlist
        if self.player.current_track.id == Type.RADIOS:
            playlist_ids = [Type.RADIOS]
        elif not self.player.playlist_ids:
            playlist_ids = []
        else:
            playlist_ids = self.player.playlist_ids
        dump(playlist_ids, open(LOLLYPOP_DATA_PATH + "/playlist_ids.bin",
                                "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(LOLLYPOP_DATA_PATH + "/position.bin", "wb"))
        self.player.stop_all()
        self.__window.container.stop_all()

    def __vacuum(self):
        """
            VACUUM DB
        """
        try:
            if self.scanner.is_locked():
                self.scanner.stop()
                GLib.idle_add(self.__vacuum)
                return
            self.tracks.del_non_persistent()
            self.tracks.clean()
            self.albums.clean()
            self.artists.clean()
            self.genres.clean()

            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
            with SqlCursor(self.playlists) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
            with SqlCursor(Radios()) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
        except Exception as e:
            Logger.error("Application::__vacuum(): %s" % e)

    def __on_handle_local_options(self, app, options):
        """
            Handle local options
            @param app as Gio.Application
            @param options as GLib.VariantDict
        """
        if options.contains("version"):
            print("Lollypop %s" % self.__version)
            exit(0)
        return -1

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        try:
            args = app_cmd_line.get_arguments()
            options = app_cmd_line.get_options_dict()
            if options.contains("debug"):
                self.debug = True
            # We are forced to enable scrobblers here if we want full debug
            if not self.scrobblers:
                if LastFM is not None:
                    self.scrobblers = [LastFM("lastfm"), LastFM("librefm")]
                self.load_listenbrainz()
            if options.contains("set-rating"):
                value = options.lookup_value("set-rating").get_string()
                try:
                    value = min(max(0, int(value)), 5)
                    if self.player.current_track.id is not None:
                        self.player.current_track.set_rate(value)
                except Exception as e:
                    Logger.error("Application::__on_command_line(): %s", e)
                    pass
            elif options.contains("play-pause"):
                self.player.play_pause()
            elif options.contains("stop"):
                self.player.stop()
            ## anhsirk0 edits
            elif options.contains("set-next"):
                try:
                    track_id = int(args[1])
                    try:
                        self.player.append_to_queue(track_id, notify=True)
                    except:
                        pass
                except:
                    pass

            ## anhsirk0 edits ends

            elif options.contains("play-ids"):
                try:
                    value = options.lookup_value("play-ids").get_string()
                    ids = value.split(";")
                    tracks = []
                    for id in ids:
                        if id[0:2] == "a:":
                            album = Album(int(id[2:]))
                            tracks += album.tracks
                        else:
                            tracks.append(Track(int(id[2:])))
                    self.player.load(tracks[0])
                    self.player.populate_playlist_by_tracks(
                        tracks, [Type.SEARCH])
                except Exception as e:
                    Logger.error("Application::__on_command_line(): %s", e)
                    pass
            elif options.contains("next"):
                self.player.next()
            elif options.contains("prev"):
                self.player.prev()
            elif options.contains("emulate-phone"):
                self.__window.toolbar.end.devices_popover.add_fake_phone()
            elif len(args) > 1:
                uris = []
                pls = []
                for uri in args[1:]:
                    try:
                        uri = GLib.filename_to_uri(uri)
                    except:
                        pass
                    f = Gio.File.new_for_uri(uri)
                    if not f.query_exists():
                        uri = GLib.filename_to_uri(
                            "%s/%s" % (GLib.get_current_dir(), uri))
                        f = Gio.File.new_for_uri(uri)
                    if is_audio(f):
                        uris.append(uri)
                    elif is_pls(f):
                        pls.append(uri)
                    else:
                        info = f.query_info(Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
                                            Gio.FileQueryInfoFlags.NONE, None)
                        if info.get_file_type() == Gio.FileType.DIRECTORY:
                            uris.append(uri)
                if pls:
                    from gi.repository import TotemPlParser
                    parser = TotemPlParser.Parser.new()
                    parser.connect("entry-parsed", self.__on_entry_parsed,
                                   uris)
                    parser.parse_async(uri, True, None,
                                       self.__on_parse_finished, uris)
                else:
                    self.__on_parse_finished(None, None, uris)
            elif self.__window is not None:
                if not self.__window.is_visible():
                    self.__window.present()
                    self.player.emit("status-changed")
                    self.player.emit("current-changed")
            Gdk.notify_startup_complete()
        except Exception as e:
            Logger.error("Application::__on_command_line(): %s", e)
        return 0

    def __on_parse_finished(self, parser, result, uris):
        """
            Play stream
            @param parser as TotemPlParser.Parser
            @param result as Gio.AsyncResult
            @param uris as [str]
        """
        def scanner_update():
            self.__scanner_timeout_id = None
            self.player.play_uris(self.__scanner_uris)
            self.scanner.update(ScanType.EPHEMERAL, self.__scanner_uris)
            self.__scanner_uris = []

        if self.__scanner_timeout_id is not None:
            GLib.source_remove(self.__scanner_timeout_id)
        self.__scanner_uris += uris
        self.__scanner_timeout_id = GLib.timeout_add(500, scanner_update)

    def __on_entry_parsed(self, parser, uri, metadata, uris):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param uri as str
            @param metadata as GLib.HastTable
            @param uris as str
        """
        uris.append(uri)

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        # Quit if background mode is on but player is off
        if not self.settings.get_value("background-mode") or\
                not self.player.is_playing:
            GLib.idle_add(self.quit, True)
        return widget.hide_on_delete()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.__window.present()
Exemplo n.º 2
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """
    def __init__(self, version):
        """
            Create application
            @param version as str
        """
        Gtk.Application.__init__(
            self,
            application_id="org.gnome.Lollypop",
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.__version = version
        self.set_property("register-session", True)
        GLib.setenv("PULSE_PROP_media.role", "music", True)
        GLib.setenv("PULSE_PROP_application.icon_name", "org.gnome.Lollypop",
                    True)

        # Ideally, we will be able to delete this once Flatpak has a solution
        # for SSL certificate management inside of applications.
        if GLib.file_test("/app", GLib.FileTest.EXISTS):
            paths = [
                "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/cert.pem",
                "/etc/ssl/cert.pem"
            ]
            for path in paths:
                if GLib.file_test(path, GLib.FileTest.EXISTS):
                    GLib.setenv("SSL_CERT_FILE", path, True)
                    break

        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.librefm = None
        self.debug = False
        self.__fs = None
        self.__externals_count = 0
        self.__init_proxy()
        GLib.set_application_name("Lollypop")
        GLib.set_prgname("lollypop")
        self.add_main_option("play-ids", b"a", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play ids", None)
        self.add_main_option("debug", b"d", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug lollypop", None)
        self.add_main_option("set-rating", b"r", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b"t", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback", None)
        self.add_main_option("next", b"n", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track", None)
        self.add_main_option("prev", b"p", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track", None)
        self.add_main_option("emulate-phone", b"e", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Emulate an Android Phone",
                             None)
        self.add_main_option("version", b"V", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Lollypop version", None)
        self.connect("command-line", self.__on_command_line)
        self.connect("handle-local-options", self.__on_handle_local_options)
        self.connect("activate", self.__on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()
        self.__listen_to_gnome_sm()

    def init(self):
        """
            Init main application
        """
        self.settings = Settings.new()
        # Mount enclosing volume as soon as possible
        uris = self.settings.get_music_uris()
        try:
            for uri in uris:
                if uri.startswith("file:/"):
                    continue
                f = Gio.File.new_for_uri(uri)
                f.mount_enclosing_volume(Gio.MountMountFlags.NONE, None, None,
                                         None)
        except Exception as e:
            print("Application::init():", e)

        cssProviderFile = Lio.File.new_for_uri(
            "resource:///org/gnome/Lollypop/application.css")
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.inhibitor = Inhibitor()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.notify = NotificationManager()
        self.art.update_art_size()
        if self.settings.get_value("artist-artwork"):
            GLib.timeout_add(5000, self.art.cache_artists_info)
        if LastFM is not None:
            self.lastfm = LastFM("lastfm")
            self.librefm = LastFM("librefm")
        if not self.settings.get_value("disable-mpris"):
            from lollypop.mpris import MPRIS
            MPRIS(self)

        settings = Gtk.Settings.get_default()
        self.__gtk_dark = settings.get_property(
            "gtk-application-prefer-dark-theme")
        if not self.__gtk_dark:
            dark = self.settings.get_value("dark-ui")
            settings.set_property("gtk-application-prefer-dark-theme", dark)

        # Map some settings to actions
        self.add_action(self.settings.create_action("playback"))
        self.add_action(self.settings.create_action("shuffle"))

        self.db.upgrade()

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        if not self.window:
            self.init()
            menu = self.__setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect("delete-event", self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            self.charts = None
            if self.settings.get_value("show-charts"):
                if GLib.find_program_in_path("youtube-dl") is not None:
                    from lollypop.charts import Charts
                    self.charts = Charts()
                    if get_network_available():
                        self.charts.start()
                else:
                    self.settings.set_value("network-search",
                                            GLib.Variant("b", False))
            self.__preload_portal()

    def quit(self, vacuum=False):
        """
            Quit Lollypop
            @param vacuum as bool
        """
        # First save state
        self.__save_state()
        # Then vacuum db
        if vacuum:
            self.__vacuum()
        self.window.destroy()
        Gio.Application.quit(self)

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__fs is not None

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

    @property
    def gtk_application_prefer_dark_theme(self):
        """
            Return default gtk value
            @return bool
        """
        return self.__gtk_dark

#######################
# PRIVATE             #
#######################

    def __save_state(self):
        """
            Save window position and view
        """
        if self.is_fullscreen():
            return
        if self.settings.get_value("save-state"):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                from lollypop.radios import Radios
                radios = Radios()
                track_id = radios.get_id(
                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::__save_state()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            dump([self.player.is_playing, self.player.is_party],
                 open(DataPath + "/player.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids, open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        self.window.stop_all()
        if self.charts is not None:
            self.charts.stop()

    def __vacuum(self):
        """
            VACUUM DB
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.__vacuum)
            return
        self.db.del_tracks(self.tracks.get_non_persistent())
        try:
            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
            with SqlCursor(self.playlists) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
            with SqlCursor(Radios()) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
        except Exception as e:
            print("Application::__vacuum(): ", e)

    def __preload_portal(self):
        """
            Preload lollypop portal
        """
        try:
            bus = self.get_dbus_connection()
            Gio.DBusProxy.new(bus, Gio.DBusProxyFlags.NONE, None,
                              "org.gnome.Lollypop.Portal",
                              "/org/gnome/LollypopPortal",
                              "org.gnome.Lollypop.Portal", None, None)
        except Exception as e:
            print("You are missing lollypop-portal: "
                  "https://github.com/gnumdk/lollypop-portal")
            print("Application::__preload_portal():", e)

    def __init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            proxy = Gio.Settings.new("org.gnome.system.proxy")
            https = Gio.Settings.new("org.gnome.system.proxy.https")
            mode = proxy.get_value("mode").get_string()
            if mode != "none":
                h = https.get_value("host").get_string()
                p = https.get_value("port").get_int32()
                GLib.setenv("http_proxy", "http://%s:%s" % (h, p), True)
                GLib.setenv("https_proxy", "http://%s:%s" % (h, p), True)
        except Exception as e:
            print("Application::__init_proxy()", e)

    def __on_handle_local_options(self, app, options):
        """
            Handle local options
            @param app as Gio.Application
            @param options as GLib.VariantDict
        """
        if options.contains("version"):
            print("Lollypop %s" % self.__version)
            return 0
        return -1

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self.__externals_count = 0
        args = app_cmd_line.get_arguments()
        options = app_cmd_line.get_options_dict()
        if options.contains("debug"):
            self.debug = True
        if options.contains("set-rating"):
            value = options.lookup_value("set-rating").get_string()
            try:
                value = min(max(0, int(value)), 5)
                if self.player.current_track.id is not None:
                    self.player.current_track.set_rate(value)
            except Exception as e:
                print(e)
                pass
        elif options.contains("play-pause"):
            self.player.play_pause()
        elif options.contains("play-ids"):
            try:
                value = options.lookup_value("play-ids").get_string()
                ids = value.split(";")
                track_ids = []
                for id in ids:
                    if id[0:2] == "a:":
                        album = Album(int(id[2:]))
                        track_ids += album.track_ids
                    else:
                        track_ids.append(int(id[2:]))
                track = Track(track_ids[0])
                self.player.load(track)
                self.player.populate_user_playlist_by_tracks(
                    track_ids, [Type.SEARCH])
            except Exception as e:
                print(e)
                pass
        elif options.contains("next"):
            self.player.next()
        elif options.contains("prev"):
            self.player.prev()
        elif options.contains("emulate-phone"):
            self.window.add_fake_phone()
        elif len(args) > 1:
            self.player.clear_externals()
            for uri in args[1:]:
                try:
                    uri = GLib.filename_to_uri(uri)
                except:
                    pass
                parser = TotemPlParser.Parser.new()
                parser.connect("entry-parsed", self.__on_entry_parsed)
                parser.parse_async(uri, True, None, None)
        elif self.window is not None and self.window.is_visible():
            self.window.present_with_time(Gtk.get_current_event_time())
        elif self.window is not None:
            # self.window.setup_window()
            # self.window.present()
            # Horrible HACK: https://bugzilla.gnome.org/show_bug.cgi?id=774130
            self.window.save_view_state()
            self.window.destroy()
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                menu = self.__setup_app_menu()
                self.window.setup_menu(menu)
            self.window.connect("delete-event", self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.emit("status-changed")
            self.player.emit("current-changed")
        return 0

    def __on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self.__externals_count == 0:
            if self.player.is_party:
                self.player.set_party(False)
            self.player.play_first_external()
        self.__externals_count += 1

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value("background-mode") or\
                not self.player.is_playing:
            GLib.timeout_add(500, self.quit, True)
        return widget.hide_on_delete()

    def __update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def __set_network(self, action, param):
        """
            Enable/disable network
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        action.set_state(param)
        self.settings.set_value("network-access", param)
        if self.charts is not None:
            if param.get_boolean():
                self.charts.start()
            else:
                self.charts.stop()
        self.window.reload_view()

    def __fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist information
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self.is_fullscreen():
            from lollypop.fullscreen import FullScreen
            self.__fs = FullScreen(self, self.window)
            self.__fs.connect("destroy", self.__on_fs_destroyed)
            self.__fs.show()
        elif self.window and self.is_fullscreen():
            self.__fs.destroy()

    def __on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self.__fs = None
        if not self.window.is_visible():
            self.quit(True)

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def __listen_to_gnome_sm(self):
        """
            Save state on EndSession signal
        """
        try:
            bus = self.get_dbus_connection()
            bus.signal_subscribe(None,
                                 "org.gnome.SessionManager.EndSessionDialog",
                                 "ConfirmedLogout",
                                 "/org/gnome/SessionManager/EndSessionDialog",
                                 None, Gio.DBusSignalFlags.NONE,
                                 lambda a, b, c, d, e, f: self.__save_state())
        except Exception as e:
            print("Application::__listen_to_gnome_sm():", e)

    def __settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def __about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/Lollypop/AboutDialog.ui")
        about = builder.get_object("about_dialog")
        about.set_transient_for(self.window)
        about.connect("response", self.__about_response)
        about.show()

    def __shortcuts(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            builder = Gtk.Builder()
            builder.add_from_resource("/org/gnome/Lollypop/Shortcuts.ui")
            builder.get_object("shortcuts").set_transient_for(self.window)
            builder.get_object("shortcuts").show()
        except:  # GTK < 3.20
            self.__help(action, param)

    def __help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def __about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def __setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/Lollypop/Appmenu.ui")
        menu = builder.get_object("app-menu")

        settingsAction = Gio.SimpleAction.new("settings", None)
        settingsAction.connect("activate", self.__settings_dialog)
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new("update_db", None)
        updateAction.connect("activate", self.__update_db)
        self.add_action(updateAction)

        networkAction = Gio.SimpleAction.new_stateful(
            "network", None,
            GLib.Variant.new_boolean(
                self.settings.get_value("network-access")))
        networkAction.connect("change-state", self.__set_network)
        self.add_action(networkAction)

        fsAction = Gio.SimpleAction.new("fullscreen", None)
        fsAction.connect("activate", self.__fullscreen)
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new("mini", None)
        mini_action.connect("activate", self.set_mini)
        self.add_action(mini_action)

        aboutAction = Gio.SimpleAction.new("about", None)
        aboutAction.connect("activate", self.__about)
        self.add_action(aboutAction)

        shortcutsAction = Gio.SimpleAction.new("shortcuts", None)
        shortcutsAction.connect("activate", self.__shortcuts)
        self.add_action(shortcutsAction)

        helpAction = Gio.SimpleAction.new("help", None)
        helpAction.connect("activate", self.__help)
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new("quit", None)
        quitAction.connect("activate", lambda x, y: self.quit(True))
        self.add_action(quitAction)

        return menu
Exemplo n.º 3
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """

    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
                            self,
                            application_id='org.gnome.Lollypop',
                            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        GLib.setenv('PULSE_PROP_media.role', 'music', True)
        GLib.setenv('PULSE_PROP_application.icon_name', 'lollypop', True)
        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self.__externals_count = 0
        self.__init_proxy()
        GLib.set_application_name('lollypop')
        GLib.set_prgname('lollypop')
        # TODO: Remove this test later
        if Gtk.get_minor_version() > 12:
            self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Debug lollypop", None)
            self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.INT, "Rate the current track",
                                 None)
            self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Toggle playback",
                                 None)
            self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to next track",
                                 None)
            self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to prev track",
                                 None)
            self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE,
                                 "Emulate an Android Phone",
                                 None)
        self.connect('command-line', self.__on_command_line)
        self.connect('activate', self.__on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()

    def init(self):
        """
            Init main application
        """
        self.__is_fs = False
        if Gtk.get_minor_version() > 18:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application.css')
        else:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application-legacy.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.art.update_art_size()
        if self.settings.get_value('artist-artwork'):
            GLib.timeout_add(5000, self.art.cache_artists_info)
        if LastFM is not None:
            self.lastfm = LastFM()
        if not self.settings.get_value('disable-mpris'):
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.mpris import MPRIS
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.mpris_legacy import MPRIS
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            from lollypop.notification import NotificationManager
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self.__parser = TotemPlParser.Parser.new()
        self.__parser.connect('entry-parsed', self.__on_entry_parsed)

        self.add_action(self.settings.create_action('shuffle'))

        self.db.upgrade()

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        # Check locale, we want unicode!
        (code, encoding) = getlocale()
        if encoding is None or encoding != "UTF-8":
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Unicode.ui')
            self.window = builder.get_object('unicode')
            self.window.set_application(self)
            self.window.show()
        elif not self.window:
            self.init()
            menu = self.__setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window(self)
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            # Will not start sooner
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.inhibitor import Inhibitor
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.inhibitor_legacy import Inhibitor
            self.inhibitor = Inhibitor()
            self.charts = None
            if self.settings.get_value('network-search'):
                if GLib.find_program_in_path("youtube-dl") is not None:
                    from lollypop.charts import Charts
                    self.charts = Charts()
                    if get_network_available():
                        self.charts.update()
                    cs_api_key = self.settings.get_value(
                                                     'cs-api-key').get_string()
                    default_cs_api_key = self.settings.get_default_value(
                                                     'cs-api-key').get_string()
                    if (not cs_api_key or
                        cs_api_key == default_cs_api_key) and\
                            self.notify is not None:
                        self.notify.send(
                         _("Google Web Services need a custom API key"),
                         _("Lollypop needs this to search artwork and music."))
                else:
                    self.settings.set_value('network-search',
                                            GLib.Variant('b', False))

    def prepare_to_exit(self, action=None, param=None):
        """
            Save window position and view
        """
        if self.__is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                from lollypop.radios import Radios
                radios = Radios()
                track_id = radios.get_id(
                                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::prepare_to_exit()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids,
                 open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        if self.window:
            self.window.stop_all()
        self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        self.db.del_tracks(self.tracks.get_non_persistent())
        try:
            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.execute('VACUUM')
            with SqlCursor(self.playlists) as sql:
                sql.execute('VACUUM')
            with SqlCursor(Radios()) as sql:
                sql.execute('VACUUM')
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__is_fs

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

#######################
# PRIVATE             #
#######################
    def __init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            settings = Gio.Settings.new('org.gnome.system.proxy.http')
            h = settings.get_value('host').get_string()
            p = settings.get_value('port').get_int32()
            if h != '' and p != 0:
                GLib.setenv('http_proxy', "%s:%s" % (h, p), True)
                GLib.setenv('https_proxy', "%s:%s" % (h, p), True)
        except:
            pass

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self.__externals_count = 0
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_popularity(value)
        if options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        args = app_cmd_line.get_arguments()
        if len(args) > 1:
            self.player.clear_externals()
            for f in args[1:]:
                try:
                    f = GLib.filename_to_uri(f)
                except:
                    pass
                self.__parser.parse_async(f, True,
                                          None, None)
        if self.window is not None and not self.window.is_visible():
            self.window.setup_window()
            self.window.present()
        return 0

    def __on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self.__externals_count == 0:
            if self.player.is_party:
                self.player.set_party(False)
            self.player.play_first_external()
        self.__externals_count += 1

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode'):
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def __update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def __set_network(self, action, param):
        """
            Enable/disable network
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        action.set_state(param)
        self.settings.set_value('network-access', param)
        if self.charts is not None:
            if param.get_boolean():
                self.charts.update()
            else:
                self.charts.stop()
        self.window.reload_view()

    def __fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self.__is_fs:
            from lollypop.fullscreen import FullScreen
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self.__on_fs_destroyed)
            self.__is_fs = True
            fs.show()

    def __on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self.__is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def __settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def __about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self.__about_response)
        about.show()

    def __shortcuts(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Shortcuts.ui')
            builder.get_object('shortcuts').set_transient_for(self.window)
            builder.get_object('shortcuts').show()
        except:  # GTK < 3.20
            self.__help(action, param)

    def __help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def __about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def __setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')
        menu = builder.get_object('app-menu')

        settingsAction = Gio.SimpleAction.new('settings', None)
        settingsAction.connect('activate', self.__settings_dialog)
        self.set_accels_for_action('app.settings', ["<Control>s"])
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self.__update_db)
        self.set_accels_for_action('app.update_db', ["<Control>u"])
        self.add_action(updateAction)

        networkAction = Gio.SimpleAction.new_stateful(
           'network',
           None,
           GLib.Variant.new_boolean(self.settings.get_value('network-access')))
        networkAction.connect('change-state', self.__set_network)
        self.add_action(networkAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self.__fullscreen)
        self.set_accels_for_action('app.fullscreen', ["F11", "F7"])
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)
        self.set_accels_for_action("app.mini", ["<Control>m"])

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self.__about)
        self.set_accels_for_action('app.about', ["F3"])
        self.add_action(aboutAction)

        shortcutsAction = Gio.SimpleAction.new('shortcuts', None)
        shortcutsAction.connect('activate', self.__shortcuts)
        self.set_accels_for_action('app.shortcuts', ["F2"])
        self.add_action(shortcutsAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self.__help)
        self.set_accels_for_action('app.help', ["F1"])
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.set_accels_for_action('app.quit', ["<Control>q"])
        self.add_action(quitAction)

        return menu
Exemplo n.º 4
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """
    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
            self,
            application_id='org.gnome.Lollypop',
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.set_property('register-session', True)
        GLib.setenv('PULSE_PROP_media.role', 'music', True)
        GLib.setenv('PULSE_PROP_application.icon_name', 'lollypop', True)

        # Ideally, we will be able to delete this once Flatpak has a solution
        # for SSL certificate management inside of applications.
        if GLib.file_test("/app", GLib.FileTest.EXISTS):
            paths = [
                "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/cert.pem",
                "/etc/ssl/cert.pem"
            ]
            for path in paths:
                if GLib.file_test(path, GLib.FileTest.EXISTS):
                    GLib.setenv('SSL_CERT_FILE', path, True)
                    break

        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self.__externals_count = 0
        self.__init_proxy()
        GLib.set_application_name('Lollypop')
        GLib.set_prgname('lollypop')
        self.add_main_option("play-ids", b'a', GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play ids", None)
        self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug lollypop", None)
        self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                             GLib.OptionArg.INT, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback", None)
        self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track", None)
        self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track", None)
        self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Emulate an Android Phone",
                             None)
        self.connect('command-line', self.__on_command_line)
        self.connect('activate', self.__on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()
        self.__listen_to_gnome_sm()

    def init(self):
        """
            Init main application
        """
        self.__is_fs = False
        if Gtk.get_minor_version() > 18:
            cssProviderFile = Lio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application.css')
        else:
            cssProviderFile = Lio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application-legacy.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.art.update_art_size()
        if self.settings.get_value('artist-artwork'):
            GLib.timeout_add(5000, self.art.cache_artists_info)
        if LastFM is not None:
            self.lastfm = LastFM()
        if not self.settings.get_value('disable-mpris'):
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.mpris import MPRIS
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.mpris_legacy import MPRIS
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            from lollypop.notification import NotificationManager
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self.add_action(self.settings.create_action('playback'))
        self.add_action(self.settings.create_action('shuffle'))

        self.db.upgrade()

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        if not self.window:
            self.init()
            menu = self.__setup_app_menu()
            if self.prefers_app_menu():
                self.set_app_menu(menu)
                self.window = Window()
            else:
                self.window = Window()
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            self.charts = None
            if self.settings.get_value('show-charts'):
                if GLib.find_program_in_path("youtube-dl") is not None:
                    from lollypop.charts import Charts
                    self.charts = Charts()
                    if get_network_available():
                        self.charts.update()
                else:
                    self.settings.set_value('network-search',
                                            GLib.Variant('b', False))
            t = Thread(target=self.__preload_portal)
            t.daemon = True
            t.start()

    def prepare_to_exit(self, action=None, param=None, exit=True):
        """
            Save window position and view
        """
        if self.__is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                from lollypop.radios import Radios
                radios = Radios()
                track_id = radios.get_id(
                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::prepare_to_exit()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            dump([self.player.is_playing, self.player.is_party],
                 open(DataPath + "/player.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids, open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        self.window.stop_all()
        if self.charts is not None:
            self.charts.stop()
        if exit:
            self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        self.db.del_tracks(self.tracks.get_non_persistent())
        try:
            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.execute('VACUUM')
            with SqlCursor(self.playlists) as sql:
                sql.execute('VACUUM')
            with SqlCursor(Radios()) as sql:
                sql.execute('VACUUM')
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__is_fs

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

#######################
# PRIVATE             #
#######################

    def __preload_portal(self):
        """
            Preload lollypop portal
        """
        try:
            bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
            Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None,
                                   'org.gnome.Lollypop.Portal',
                                   '/org/gnome/LollypopPortal',
                                   'org.gnome.Lollypop.Portal', None)
        except:
            pass

    def __init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            proxy = Gio.Settings.new('org.gnome.system.proxy')
            https = Gio.Settings.new('org.gnome.system.proxy.https')
            mode = proxy.get_value('mode').get_string()
            if mode != 'none':
                h = https.get_value('host').get_string()
                p = https.get_value('port').get_int32()
                GLib.setenv('http_proxy', "http://%s:%s" % (h, p), True)
                GLib.setenv('https_proxy', "http://%s:%s" % (h, p), True)
        except Exception as e:
            print("Application::__init_proxy()", e)

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self.__externals_count = 0
        args = app_cmd_line.get_arguments()
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_rate(value)
        elif options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('play-ids'):
            try:
                value = options.lookup_value('play-ids').get_string()
                ids = value.split(';')
                track_ids = []
                for id in ids:
                    if id[0:2] == "a:":
                        album = Album(int(id[2:]))
                        track_ids += album.track_ids
                    else:
                        track_ids.append(int(id[2:]))
                track = Track(track_ids[0])
                self.player.load(track)
                self.player.populate_user_playlist_by_tracks(
                    track_ids, [Type.SEARCH])
            except Exception as e:
                print(e)
                pass
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        elif len(args) > 1:
            self.player.clear_externals()
            for uri in args[1:]:
                try:
                    uri = GLib.filename_to_uri(uri)
                except:
                    pass
                parser = TotemPlParser.Parser.new()
                parser.connect('entry-parsed', self.__on_entry_parsed)
                parser.parse_async(uri, True, None, None)
        elif self.window is not None and self.window.is_visible():
            self.window.present()
        elif self.window is not None:
            # self.window.setup_window()
            # self.window.present()
            # Horrible HACK: https://bugzilla.gnome.org/show_bug.cgi?id=774130
            self.window.save_view_state()
            self.window.destroy()
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                menu = self.__setup_app_menu()
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.emit('status-changed')
            self.player.emit('current-changed')
        return 0

    def __on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self.__externals_count == 0:
            if self.player.is_party:
                self.player.set_party(False)
            self.player.play_first_external()
        self.__externals_count += 1

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode') or\
                self.player.current_track.id is None:
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def __update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def __set_network(self, action, param):
        """
            Enable/disable network
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        action.set_state(param)
        self.settings.set_value('network-access', param)
        if self.charts is not None:
            if param.get_boolean():
                self.charts.update()
            else:
                self.charts.stop()
        self.window.reload_view()

    def __fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self.__is_fs:
            from lollypop.fullscreen import FullScreen
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self.__on_fs_destroyed)
            self.__is_fs = True
            fs.show()

    def __on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self.__is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def __on_sm_listener_ok(self, proxy, task):
        """
            Connect signals
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        try:
            proxy.call('GetClients', None, Gio.DBusCallFlags.NO_AUTO_START,
                       500, None, self.__on_get_clients)
        except:
            pass

    def __on_sm_client_listener_ok(self, proxy, task, client):
        """
            Get app id
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
            @param client as str
        """
        try:
            proxy.call('GetAppId', None, Gio.DBusCallFlags.NO_AUTO_START, 500,
                       None, self.__on_get_app_id, client)
        except:
            pass

    def __on_sm_client_private_listener_ok(self, proxy, task):
        """
            Connect signals
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        # Needed or object will be destroyed
        self.__proxy = proxy
        proxy.connect('g-signal', self.__on_signals)

    def __on_get_clients(self, proxy, task):
        """
            Search us in clients
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        try:
            for client in proxy.call_finish(task)[0]:
                Gio.DBusProxy.new(self.get_dbus_connection(),
                                  Gio.DBusProxyFlags.NONE, None,
                                  'org.gnome.SessionManager', client,
                                  'org.gnome.SessionManager.Client', None,
                                  self.__on_sm_client_listener_ok, client)
        except:
            pass

    def __on_get_app_id(self, proxy, task, client):
        """
            Connect signals if we are this client
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
            @param client as str
        """
        try:
            if proxy.call_finish(task)[0] == "org.gnome.Lollypop":
                Gio.DBusProxy.new(self.get_dbus_connection(),
                                  Gio.DBusProxyFlags.NONE, None,
                                  'org.gnome.SessionManager', client,
                                  'org.gnome.SessionManager.ClientPrivate',
                                  None,
                                  self.__on_sm_client_private_listener_ok)
        except:
            pass

    def __on_signals(self, proxy, sender, signal, parameters):
        """
            Connect to Session Manager QueryEndSession signal
        """
        if signal == "EndSession":
            # Save session, do not quit as we may be killed to quickly
            # to be able to VACUUM database
            self.prepare_to_exit(False)

    def __listen_to_gnome_sm(self):
        """
            Connect to GNOME session manager
        """
        try:
            Gio.DBusProxy.new(self.get_dbus_connection(),
                              Gio.DBusProxyFlags.NONE, None,
                              'org.gnome.SessionManager',
                              '/org/gnome/SessionManager',
                              'org.gnome.SessionManager', None,
                              self.__on_sm_listener_ok)
        except:
            pass

    def __settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def __about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self.__about_response)
        about.show()

    def __shortcuts(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Shortcuts.ui')
            builder.get_object('shortcuts').set_transient_for(self.window)
            builder.get_object('shortcuts').show()
        except:  # GTK < 3.20
            self.__help(action, param)

    def __help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def __about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def __setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')
        menu = builder.get_object('app-menu')

        settingsAction = Gio.SimpleAction.new('settings', None)
        settingsAction.connect('activate', self.__settings_dialog)
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self.__update_db)
        self.add_action(updateAction)

        networkAction = Gio.SimpleAction.new_stateful(
            'network', None,
            GLib.Variant.new_boolean(
                self.settings.get_value('network-access')))
        networkAction.connect('change-state', self.__set_network)
        self.add_action(networkAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self.__fullscreen)
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self.__about)
        self.add_action(aboutAction)

        shortcutsAction = Gio.SimpleAction.new('shortcuts', None)
        shortcutsAction.connect('activate', self.__shortcuts)
        self.add_action(shortcutsAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self.__help)
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.add_action(quitAction)

        return menu
Exemplo n.º 5
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """

    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
                            self,
                            application_id='org.gnome.Lollypop',
                            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        os.environ['PULSE_PROP_media.role'] = 'music'
        os.environ['PULSE_PROP_application.icon_name'] = 'lollypop'
        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self._externals_count = 0
        self._init_proxy()
        GLib.set_application_name('lollypop')
        GLib.set_prgname('lollypop')
        # TODO: Remove this test later
        if Gtk.get_minor_version() > 12:
            self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Debug lollypop", None)
            self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.INT, "Rate the current track",
                                 None)
            self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Toggle playback",
                                 None)
            self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to next track",
                                 None)
            self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to prev track",
                                 None)
            self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE,
                                 "Emulate an Android Phone",
                                 None)
        self.connect('command-line', self._on_command_line)
        self.connect('activate', self._on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()

    def init(self):
        """
            Init main application
        """
        if Gtk.get_minor_version() > 18:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application.css')
        else:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application-legacy.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        ArtSize.BIG = self.settings.get_value('cover-size').get_int32()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        if self.settings.get_value('artist-artwork'):
            GLib.timeout_add(5000, self.art.cache_artists_art)
        if LastFM is not None:
            self.lastfm = LastFM()
        if not self.settings.get_value('disable-mpris'):
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self._parser = TotemPlParser.Parser.new()
        self._parser.connect('entry-parsed', self._on_entry_parsed)

        self.add_action(self.settings.create_action('shuffle'))

        self._is_fs = False

    def do_startup(self):
        """
            Add startup notification and
            build gnome-shell menu after Gtk.Application startup
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        # Check locale, we want unicode!
        (code, encoding) = getlocale()
        if encoding is None or encoding != "UTF-8":
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Unicode.ui')
            self.window = builder.get_object('unicode')
            self.window.set_application(self)
            self.window.show()
        elif not self.window:
            self.init()
            menu = self._setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window(self)
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self._hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            # Will not start sooner
            self.inhibitor = Inhibitor()

    def prepare_to_exit(self, action=None, param=None):
        """
            Save window position and view
        """
        if self._is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                radios = Radios()
                track_id = radios.get_id(
                                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::prepare_to_exit()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids,
                 open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.is_playing():
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        if self.window:
            self.window.stop_all()
        self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        try:
            with SqlCursor(self.db) as sql:
                sql.execute('VACUUM')
            with SqlCursor(self.playlists) as sql:
                sql.execute('VACUUM')
            with SqlCursor(Radios()) as sql:
                sql.execute('VACUUM')
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()
        Gst.deinit()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self._is_fs

#######################
# PRIVATE             #
#######################
    def _init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            settings = Gio.Settings.new('org.gnome.system.proxy.http')
            h = settings.get_value('host').get_string()
            p = settings.get_value('port').get_int32()
            if h != '' and p != 0:
                os.environ['HTTP_PROXY'] = "%s:%s" % (h, p)
        except:
            pass

    def _on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self._externals_count = 0
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_popularity(value)
        if options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        args = app_cmd_line.get_arguments()
        if len(args) > 1:
            self.player.clear_externals()
            for f in args[1:]:
                try:
                    f = GLib.filename_to_uri(f)
                except:
                    pass
                self._parser.parse_async(f, True,
                                         None, None)
        if self.window is not None and not self.window.is_visible():
            self.window.setup_window()
            self.window.present()
        return 0

    def _on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self._externals_count == 0:
            if self.player.is_party():
                self.player.set_party(False)
            self.player.play_first_external()
        self._externals_count += 1

    def _hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode'):
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def _update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def _fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self._is_fs:
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self._on_fs_destroyed)
            self._is_fs = True
            fs.show()

    def _on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self._is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def _on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def _settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def _about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        artists = self.artists.count()
        albums = self.albums.count()
        tracks = self.tracks.count()
        builder.get_object('artists').set_text(
                        ngettext("%d artist", "%d artists", artists) % artists)
        builder.get_object('albums').set_text(
                            ngettext("%d album", "%d albums", albums) % albums)
        builder.get_object('tracks').set_text(
                            ngettext("%d track", "%d tracks", tracks) % tracks)
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self._about_response)
        about.show()

    def _help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def _about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

    def _setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()

        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')

        menu = builder.get_object('app-menu')

        settingsAction = Gio.SimpleAction.new('settings', None)
        settingsAction.connect('activate', self._settings_dialog)
        self.set_accels_for_action('app.settings', ["<Control>s"])
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self._update_db)
        self.set_accels_for_action('app.update_db', ["<Control>u"])
        self.add_action(updateAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self._fullscreen)
        self.set_accels_for_action('app.fullscreen', ["F11", "F7"])
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)
        self.set_accels_for_action("app.mini", ["<Control>m"])

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self._about)
        self.set_accels_for_action('app.about', ["F2"])
        self.add_action(aboutAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self._help)
        self.set_accels_for_action('app.help', ["F1"])
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.set_accels_for_action('app.quit', ["<Control>q"])
        self.add_action(quitAction)

        return menu
Exemplo n.º 6
0
class Application(Gtk.Application, ApplicationActions):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """
    def __init__(self, version, data_dir):
        """
            Create application
            @param version as str
            @param data_dir as str
        """
        Gtk.Application.__init__(
            self,
            application_id="org.gnome.Lollypop",
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.__version = version
        self.__data_dir = data_dir
        self.set_property("register-session", True)
        signal(SIGINT, lambda a, b: self.quit())
        signal(SIGTERM, lambda a, b: self.quit())
        # Set main thread name
        # We force it to current python 3.6 name, to be sure in case of
        # change in python
        current_thread().setName("MainThread")
        (self.__proxy_host, self.__proxy_port) = init_proxy_from_gnome()
        GLib.setenv("PULSE_PROP_media.role", "music", True)
        GLib.setenv("PULSE_PROP_application.icon_name", "org.gnome.Lollypop",
                    True)
        # Ideally, we will be able to delete this once Flatpak has a solution
        # for SSL certificate management inside of applications.
        if GLib.file_test("/app", GLib.FileTest.EXISTS):
            paths = [
                "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/cert.pem",
                "/etc/ssl/cert.pem"
            ]
            for path in paths:
                if GLib.file_test(path, GLib.FileTest.EXISTS):
                    GLib.setenv("SSL_CERT_FILE", path, True)
                    break
        self.cursors = {}
        self.debug = False
        self.shown_sidebar_tooltip = False
        self.__window = None
        self.__fs_window = None
        settings = Gio.Settings.new("org.gnome.desktop.interface")
        self.animations = settings.get_value("enable-animations").get_boolean()
        GLib.set_application_name("Lollypop")
        GLib.set_prgname("lollypop")
        self.add_main_option("play-ids", b"a", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play ids", None)
        self.add_main_option("debug", b"d", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug Lollypop", None)
        self.add_main_option("set-rating", b"r", GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b"t", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback", None)
        self.add_main_option("stop", b"s", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Stop playback", None)
        self.add_main_option("next", b"n", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track", None)
        self.add_main_option("prev", b"p", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track", None)
        self.add_main_option("emulate-phone", b"e", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Emulate a Librem phone",
                             None)
        self.add_main_option("version", b"v", GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Lollypop version", None)
        self.connect("command-line", self.__on_command_line)
        self.connect("handle-local-options", self.__on_handle_local_options)
        self.connect("activate", self.__on_activate)
        self.connect("shutdown", lambda a: self.__save_state())
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()
        if GLib.environ_getenv(GLib.get_environ(), "DEBUG_LEAK") is not None:
            import gc
            gc.set_debug(gc.DEBUG_LEAK)

    def init(self):
        """
            Init main application
        """
        self.settings = Settings.new()
        # Mount enclosing volume as soon as possible
        uris = self.settings.get_music_uris()
        try:
            for uri in uris:
                if uri.startswith("file:/"):
                    continue
                f = Gio.File.new_for_uri(uri)
                f.mount_enclosing_volume(Gio.MountMountFlags.NONE, None, None,
                                         None)
        except Exception as e:
            Logger.error("Application::init(): %s" % e)

        cssProviderFile = Gio.File.new_for_uri(
            "resource:///org/gnome/Lollypop/application.css")
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.db = Database()
        self.cache = CacheDatabase()
        self.playlists = Playlists()
        self.albums = AlbumsDatabase(self.db)
        self.artists = ArtistsDatabase(self.db)
        self.genres = GenresDatabase(self.db)
        self.tracks = TracksDatabase(self.db)
        self.player = Player()
        self.inhibitor = Inhibitor()
        self.scanner = CollectionScanner()
        self.notify = NotificationManager()
        self.task_helper = TaskHelper()
        self.art_helper = ArtHelper()
        self.art = Art()
        self.art.update_art_size()
        self.ws_director = DirectorWebService()
        self.ws_director.start()
        if not self.settings.get_value("disable-mpris"):
            from lollypop.mpris import MPRIS
            MPRIS(self)

        settings = Gtk.Settings.get_default()
        self.__gtk_dark = settings.get_property(
            "gtk-application-prefer-dark-theme")
        if not self.__gtk_dark:
            dark = self.settings.get_value("dark-ui")
            settings.set_property("gtk-application-prefer-dark-theme", dark)
        ApplicationActions.__init__(self)
        monitor = Gio.NetworkMonitor.get_default()
        if monitor.get_network_available() and\
                not monitor.get_network_metered() and\
                self.settings.get_value("recent-youtube-dl"):
            self.task_helper.run(install_youtube_dl)

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Handy.init()
        if self.__window is None:
            from lollypop.window import Window
            self.init()
            self.__window = Window()
            self.__window.connect("delete-event", self.__hide_on_delete)
            self.__window.setup()
            self.__window.show()
            self.player.restore_state()

    def quit(self, vacuum=False):
        """
            Quit Lollypop
            @param vacuum as bool
        """
        self.__window.container.stop()
        self.__window.hide()
        if not self.ws_director.stop():
            GLib.timeout_add(100, self.quit, vacuum)
            return
        if self.settings.get_value("save-state"):
            self.__window.container.stack.save_history()
        # Then vacuum db
        if vacuum:
            self.__vacuum()
            self.art.clean_artwork()
        Gio.Application.quit(self)
        if GLib.environ_getenv(GLib.get_environ(), "DEBUG_LEAK") is not None:
            import gc
            gc.collect()
            for x in gc.garbage:
                s = str(x)
                print(type(x), "\n  ", s)

    def fullscreen(self):
        """
            Go fullscreen
        """
        def on_destroy(window):
            self.__fs_window = None
            self.__window.show()

        if self.__fs_window is None:
            from lollypop.fullscreen import FullScreen
            self.__fs_window = FullScreen()
            self.__fs_window.delayed_init()
            self.__fs_window.show()
            self.__fs_window.connect("destroy", on_destroy)
            self.__window.hide()
        else:
            self.__fs_window.destroy()

    @property
    def proxy_host(self):
        """
            Get proxy host
            @return str
        """
        return self.__proxy_host

    @property
    def proxy_port(self):
        """
            Get proxy port
            @return int
        """
        return self.__proxy_port

    @property
    def devices(self):
        """
            Get available devices
            Merge connected and known
            @return [str]
        """
        devices = self.__window.toolbar.end.devices_popover.devices
        devices += list(self.settings.get_value("devices"))
        result = []
        # Do not use set() + filter() because we want to keep order
        for device in devices:
            if device not in result and device != "":
                result.append(device)
        return result

    @property
    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__fs_window is not None

    @property
    def main_window(self):
        """
            Get main window
        """
        return self.__window

    @property
    def window(self):
        """
            Get current application window
            @return Gtk.Window
        """
        if self.__fs_window is not None:
            return self.__fs_window
        else:
            return self.__window

    @property
    def data_dir(self):
        """
            Get data dir
            @return str
        """
        return self.__data_dir

    @property
    def gtk_application_prefer_dark_theme(self):
        """
            Return default gtk value
            @return bool
        """
        return self.__gtk_dark

    @property
    def version(self):
        """
            Get Lollypop version
            @return srt
        """
        return self.__version

#######################
# PRIVATE             #
#######################

    def __save_state(self):
        """
            Save player state
        """
        if self.settings.get_value("save-state"):
            if self.player.current_track.id is None or\
                    self.player.current_track.storage_type &\
                    StorageType.EPHEMERAL:
                track_id = None
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    with open(LOLLYPOP_DATA_PATH + "/Albums.bin", "wb") as f:
                        dump(self.player.albums, f)
                except Exception as e:
                    Logger.error("Application::__save_state(): %s" % e)
            dump(track_id, open(LOLLYPOP_DATA_PATH + "/track_id.bin", "wb"))
            dump([self.player.is_playing, self.player.is_party],
                 open(LOLLYPOP_DATA_PATH + "/player.bin", "wb"))
            dump(self.player.queue,
                 open(LOLLYPOP_DATA_PATH + "/queue.bin", "wb"))
            if self.player.current_track.id is not None:
                position = self.player.position
            else:
                position = 0
            dump(position, open(LOLLYPOP_DATA_PATH + "/position.bin", "wb"))
        self.player.stop_all()

    def __vacuum(self):
        """
            VACUUM DB
        """
        try:
            if self.scanner.is_locked():
                self.scanner.stop()
                GLib.idle_add(self.__vacuum)
                return
            SqlCursor.add(self.db)
            self.tracks.del_non_persistent(False)
            self.tracks.clean(False)
            self.albums.clean(False)
            self.artists.clean(False)
            self.genres.clean(False)
            SqlCursor.remove(self.db)
            self.cache.clean(True)

            with SqlCursor(self.db) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
            with SqlCursor(self.playlists) as sql:
                sql.isolation_level = None
                sql.execute("VACUUM")
                sql.isolation_level = ""
        except Exception as e:
            Logger.error("Application::__vacuum(): %s" % e)

    def __parse_uris(self, playlist_uris, audio_uris):
        """
            Parse playlist uris
            @param playlist_uris as [str]
            @param audio_uris as [str]
        """
        from gi.repository import TotemPlParser
        playlist_uri = playlist_uris.pop(0)
        parser = TotemPlParser.Parser.new()
        parser.connect("entry-parsed", self.__on_entry_parsed, audio_uris)
        parser.parse_async(playlist_uri, True, None, self.__on_parse_finished,
                           playlist_uris, audio_uris)

    def __on_handle_local_options(self, app, options):
        """
            Handle local options
            @param app as Gio.Application
            @param options as GLib.VariantDict
        """
        if options.contains("version"):
            print("Lollypop %s" % self.__version)
            exit(0)
        return -1

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        try:
            args = app_cmd_line.get_arguments()
            options = app_cmd_line.get_options_dict()
            if options.contains("debug"):
                self.debug = True
            if options.contains("set-rating"):
                value = options.lookup_value("set-rating").get_string()
                try:
                    value = min(max(0, int(value)), 5)
                    if self.player.current_track.id is not None:
                        self.player.current_track.set_rate(value)
                except Exception as e:
                    Logger.error("Application::__on_command_line(): %s", e)
                    pass
            elif options.contains("play-pause"):
                self.player.play_pause()
            elif options.contains("stop"):
                self.player.stop()
            elif options.contains("play-ids"):
                try:
                    value = options.lookup_value("play-ids").get_string()
                    ids = value.split(";")
                    albums = []
                    for id in ids:
                        if id[0:2] == "a:":
                            album = Album(int(id[2:]))
                            self.player.add_album(album)
                            albums.append(album)
                        else:
                            track = Track(int(id[2:]))
                            self.player.add_album(track.album)
                            albums.append(track.album)
                    if albums and albums[0].tracks:
                        self.player.load(albums[0].tracks[0])
                except Exception as e:
                    Logger.error("Application::__on_command_line(): %s", e)
                    pass
            elif options.contains("next"):
                self.player.next()
            elif options.contains("prev"):
                self.player.prev()
            elif options.contains("emulate-phone"):
                self.__window.toolbar.end.devices_popover.add_fake_phone()
            elif len(args) > 1:
                audio_uris = []
                playlist_uris = []
                for uri in args[1:]:
                    parsed = urlparse(uri)
                    if parsed.scheme not in ["http", "https"]:
                        try:
                            uri = GLib.filename_to_uri(uri)
                        except:
                            pass
                        f = Gio.File.new_for_uri(uri)
                        # Try ./filename
                        if not f.query_exists():
                            uri = GLib.filename_to_uri(
                                "%s/%s" % (GLib.get_current_dir(), uri))
                            print(uri)
                            f = Gio.File.new_for_uri(uri)
                    file_type = get_file_type(uri)
                    if file_type == FileType.PLS:
                        playlist_uris.append(uri)
                    else:
                        audio_uris.append(uri)
                if playlist_uris:
                    self.__parse_uris(playlist_uris, audio_uris)
                else:
                    self.__on_parse_finished(None, None, [], audio_uris)
            elif self.__window is not None:
                if not self.__window.is_visible():
                    self.__window.present()
                    emit_signal(self.player, "status-changed")
                    emit_signal(self.player, "current-changed")
            Gdk.notify_startup_complete()
        except Exception as e:
            Logger.error("Application::__on_command_line(): %s", e)
        return 0

    def __on_parse_finished(self, source, result, playlist_uris, audio_uris):
        """
            Play stream
            @param source as None
            @param result as Gio.AsyncResult
            @param uris as ([str], [str])
        """
        if playlist_uris:
            self.__parse_uris(playlist_uris, audio_uris)
        else:
            self.scanner.update(ScanType.EXTERNAL, audio_uris)

    def __on_entry_parsed(self, parser, uri, metadata, audio_uris):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param uri as str
            @param metadata as GLib.HastTable
            @param audio_uris as str
        """
        audio_uris.append(uri)

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        # Quit if background mode is on but player is off
        if not self.settings.get_value("background-mode") or\
                not self.player.is_playing:
            GLib.idle_add(self.quit, True)
        return widget.hide_on_delete()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.__window.present()
Exemplo n.º 7
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """

    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
                            self,
                            application_id='org.gnome.Lollypop',
                            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        GLib.setenv('PULSE_PROP_media.role', 'music', True)
        GLib.setenv('PULSE_PROP_application.icon_name', 'lollypop', True)
        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self.__externals_count = 0
        self.__init_proxy()
        GLib.set_application_name('Lollypop')
        GLib.set_prgname('lollypop')
        self.add_main_option("album", b'a', GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play album", None)
        self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug lollypop", None)
        self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                             GLib.OptionArg.INT, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback",
                             None)
        self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track",
                             None)
        self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track",
                             None)
        self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE,
                             "Emulate an Android Phone",
                             None)
        self.connect('command-line', self.__on_command_line)
        self.connect('activate', self.__on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()

    def init(self):
        """
            Init main application
        """
        self.__is_fs = False
        if Gtk.get_minor_version() > 18:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application.css')
        else:
            cssProviderFile = Gio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application-legacy.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.art.update_art_size()
        if self.settings.get_value('artist-artwork'):
            GLib.timeout_add(5000, self.art.cache_artists_info)
        if LastFM is not None:
            self.lastfm = LastFM()
        if not self.settings.get_value('disable-mpris'):
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.mpris import MPRIS
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.mpris_legacy import MPRIS
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            from lollypop.notification import NotificationManager
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self.add_action(self.settings.create_action('playback'))
        self.add_action(self.settings.create_action('shuffle'))

        self.db.upgrade()

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        # Check locale, we want unicode!
        (code, encoding) = getlocale()
        if encoding is None or encoding != "UTF-8":
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Unicode.ui')
            self.window = builder.get_object('unicode')
            self.window.set_application(self)
            self.window.show()
        elif not self.window:
            self.init()
            menu = self.__setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            # Will not start sooner
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.inhibitor import Inhibitor
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.inhibitor_legacy import Inhibitor
            self.inhibitor = Inhibitor()
            self.charts = None
            if self.settings.get_value('show-charts'):
                if GLib.find_program_in_path("youtube-dl") is not None:
                    from lollypop.charts import Charts
                    self.charts = Charts()
                    if get_network_available():
                        self.charts.update()
                else:
                    self.settings.set_value('network-search',
                                            GLib.Variant('b', False))

    def prepare_to_exit(self, action=None, param=None):
        """
            Save window position and view
        """
        if self.__is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                from lollypop.radios import Radios
                radios = Radios()
                track_id = radios.get_id(
                                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::prepare_to_exit()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids,
                 open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        if self.window:
            self.window.stop_all()
        self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        self.db.del_tracks(self.tracks.get_non_persistent())
        try:
            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.execute('VACUUM')
            with SqlCursor(self.playlists) as sql:
                sql.execute('VACUUM')
            with SqlCursor(Radios()) as sql:
                sql.execute('VACUUM')
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__is_fs

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

#######################
# PRIVATE             #
#######################
    def __init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            settings = Gio.Settings.new('org.gnome.system.proxy.http')
            h = settings.get_value('host').get_string()
            p = settings.get_value('port').get_int32()
            if h != '' and p != 0:
                GLib.setenv('http_proxy', "%s:%s" % (h, p), True)
                GLib.setenv('https_proxy', "%s:%s" % (h, p), True)
        except:
            pass

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self.__externals_count = 0
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_popularity(value)
        if options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('album'):
            try:
                value = options.lookup_value('album').get_string()
                album_ids = value.split(';')
                album = Album(int(album_ids.pop(0)))
                self.player.play_album(album)
                for album_id in album_ids:
                    self.player.add_album(Album(int(album_id)))
            except:
                pass
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        args = app_cmd_line.get_arguments()
        if len(args) > 1:
            self.player.clear_externals()
            for uri in args[1:]:
                try:
                    uri = GLib.filename_to_uri(uri)
                except:
                    pass
                parser = TotemPlParser.Parser.new()
                parser.connect('entry-parsed', self.__on_entry_parsed)
                parser.parse_async(uri, True, None, None)
        if self.window is not None and not self.window.is_visible():
            # self.window.setup_window()
            # self.window.present()
            # Horrible HACK: https://bugzilla.gnome.org/show_bug.cgi?id=774130
            self.window.save_view_state()
            self.window.destroy()
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                menu = self.__setup_app_menu()
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.emit('status-changed')
            self.player.emit('current-changed')
        return 0

    def __on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self.__externals_count == 0:
            if self.player.is_party:
                self.player.set_party(False)
            self.player.play_first_external()
        self.__externals_count += 1

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode') or\
                self.player.current_track.id is None:
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def __update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def __set_network(self, action, param):
        """
            Enable/disable network
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        action.set_state(param)
        self.settings.set_value('network-access', param)
        if self.charts is not None:
            if param.get_boolean():
                self.charts.update()
            else:
                self.charts.stop()
        self.window.reload_view()

    def __fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self.__is_fs:
            from lollypop.fullscreen import FullScreen
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self.__on_fs_destroyed)
            self.__is_fs = True
            fs.show()

    def __on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self.__is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def __settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def __about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self.__about_response)
        about.show()

    def __shortcuts(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Shortcuts.ui')
            builder.get_object('shortcuts').set_transient_for(self.window)
            builder.get_object('shortcuts').show()
        except:  # GTK < 3.20
            self.__help(action, param)

    def __help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def __about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def __setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')
        menu = builder.get_object('app-menu')

        settingsAction = Gio.SimpleAction.new('settings', None)
        settingsAction.connect('activate', self.__settings_dialog)
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self.__update_db)
        self.add_action(updateAction)

        networkAction = Gio.SimpleAction.new_stateful(
           'network',
           None,
           GLib.Variant.new_boolean(self.settings.get_value('network-access')))
        networkAction.connect('change-state', self.__set_network)
        self.add_action(networkAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self.__fullscreen)
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self.__about)
        self.add_action(aboutAction)

        shortcutsAction = Gio.SimpleAction.new('shortcuts', None)
        shortcutsAction.connect('activate', self.__shortcuts)
        self.add_action(shortcutsAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self.__help)
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.add_action(quitAction)

        return menu
Exemplo n.º 8
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """

    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
                            self,
                            application_id='org.gnome.Lollypop',
                            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.set_property('register-session', True)
        GLib.setenv('PULSE_PROP_media.role', 'music', True)
        GLib.setenv('PULSE_PROP_application.icon_name', 'lollypop', True)

        # Ideally, we will be able to delete this once Flatpak has a solution
        # for SSL certificate management inside of applications.
        if GLib.file_test("/app", GLib.FileTest.EXISTS):
            paths = ["/etc/ssl/certs/ca-certificates.crt",
                     "/etc/pki/tls/cert.pem",
                     "/etc/ssl/cert.pem"]
            for path in paths:
                if GLib.file_test(path, GLib.FileTest.EXISTS):
                    GLib.setenv('SSL_CERT_FILE', path, True)
                    break

        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self.__externals_count = 0
        self.__init_proxy()
        GLib.set_application_name('Lollypop')
        GLib.set_prgname('lollypop')
        self.add_main_option("play-ids", b'a', GLib.OptionFlags.NONE,
                             GLib.OptionArg.STRING, "Play ids", None)
        self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Debug lollypop", None)
        self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                             GLib.OptionArg.INT, "Rate the current track",
                             None)
        self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Toggle playback",
                             None)
        self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to next track",
                             None)
        self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE, "Go to prev track",
                             None)
        self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                             GLib.OptionArg.NONE,
                             "Emulate an Android Phone",
                             None)
        self.connect('command-line', self.__on_command_line)
        self.connect('activate', self.__on_activate)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()
        self.__listen_to_gnome_sm()

    def init(self):
        """
            Init main application
        """
        self.__is_fs = False
        if Gtk.get_minor_version() > 18:
            cssProviderFile = Lio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application.css')
        else:
            cssProviderFile = Lio.File.new_for_uri(
                'resource:///org/gnome/Lollypop/application-legacy.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        self.art.update_art_size()
        if self.settings.get_value('artist-artwork'):
            GLib.timeout_add(5000, self.art.cache_artists_info)
        if LastFM is not None:
            self.lastfm = LastFM()
        if not self.settings.get_value('disable-mpris'):
            # Ubuntu > 16.04
            if Gtk.get_minor_version() > 18:
                from lollypop.mpris import MPRIS
            # Ubuntu <= 16.04, Debian Jessie, ElementaryOS
            else:
                from lollypop.mpris_legacy import MPRIS
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            from lollypop.notification import NotificationManager
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self.add_action(self.settings.create_action('playback'))
        self.add_action(self.settings.create_action('shuffle'))

        self.db.upgrade()

    def do_startup(self):
        """
            Init application
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        if not self.window:
            self.init()
            menu = self.__setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # We add to mainloop as we want to run
            # after player::restore_state() signals
            GLib.idle_add(self.window.toolbar.set_mark)
            self.charts = None
            if self.settings.get_value('show-charts'):
                if GLib.find_program_in_path("youtube-dl") is not None:
                    from lollypop.charts import Charts
                    self.charts = Charts()
                    if get_network_available():
                        self.charts.start()
                else:
                    self.settings.set_value('network-search',
                                            GLib.Variant('b', False))
            t = Thread(target=self.__preload_portal)
            t.daemon = True
            t.start()

    def prepare_to_exit(self, action=None, param=None, exit=True):
        """
            Save window position and view
        """
        if self.__is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                from lollypop.radios import Radios
                radios = Radios()
                track_id = radios.get_id(
                                    self.player.current_track.album_artists[0])
            else:
                track_id = self.player.current_track.id
                # Save albums context
                try:
                    dump(self.player.context.genre_ids,
                         open(DataPath + "/genre_ids.bin", "wb"))
                    dump(self.player.context.artist_ids,
                         open(DataPath + "/artist_ids.bin", "wb"))
                    self.player.shuffle_albums(False)
                    dump(self.player.get_albums(),
                         open(DataPath + "/albums.bin", "wb"))
                except Exception as e:
                    print("Application::prepare_to_exit()", e)
            dump(track_id, open(DataPath + "/track_id.bin", "wb"))
            dump([self.player.is_playing, self.player.is_party],
                 open(DataPath + "/player.bin", "wb"))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            dump(playlist_ids,
                 open(DataPath + "/playlist_ids.bin", "wb"))
        if self.player.current_track.id is not None:
            position = self.player.position
        else:
            position = 0
        dump(position, open(DataPath + "/position.bin", "wb"))
        self.player.stop_all()
        self.window.stop_all()
        if self.charts is not None:
            self.charts.stop()
        if exit:
            self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        self.db.del_tracks(self.tracks.get_non_persistent())
        try:
            from lollypop.radios import Radios
            with SqlCursor(self.db) as sql:
                sql.isolation_level = None
                sql.execute('VACUUM')
                sql.isolation_level = ''
            with SqlCursor(self.playlists) as sql:
                sql.isolation_level = None
                sql.execute('VACUUM')
                sql.isolation_level = ''
            with SqlCursor(Radios()) as sql:
                sql.isolation_level = None
                sql.execute('VACUUM')
                sql.isolation_level = ''
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self.__is_fs

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

#######################
# PRIVATE             #
#######################
    def __preload_portal(self):
        """
            Preload lollypop portal
        """
        try:
            bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
            Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None,
                                   'org.gnome.Lollypop.Portal',
                                   '/org/gnome/LollypopPortal',
                                   'org.gnome.Lollypop.Portal', None)
        except:
            pass

    def __init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            proxy = Gio.Settings.new('org.gnome.system.proxy')
            https = Gio.Settings.new('org.gnome.system.proxy.https')
            mode = proxy.get_value('mode').get_string()
            if mode != 'none':
                h = https.get_value('host').get_string()
                p = https.get_value('port').get_int32()
                GLib.setenv('http_proxy', "http://%s:%s" % (h, p), True)
                GLib.setenv('https_proxy', "http://%s:%s" % (h, p), True)
        except Exception as e:
            print("Application::__init_proxy()", e)

    def __on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self.__externals_count = 0
        args = app_cmd_line.get_arguments()
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_rate(value)
        elif options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('play-ids'):
            try:
                value = options.lookup_value('play-ids').get_string()
                ids = value.split(';')
                track_ids = []
                for id in ids:
                    if id[0:2] == "a:":
                        album = Album(int(id[2:]))
                        track_ids += album.track_ids
                    else:
                        track_ids.append(int(id[2:]))
                track = Track(track_ids[0])
                self.player.load(track)
                self.player.populate_user_playlist_by_tracks(track_ids,
                                                             [Type.SEARCH])
            except Exception as e:
                print(e)
                pass
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        elif len(args) > 1:
            self.player.clear_externals()
            for uri in args[1:]:
                try:
                    uri = GLib.filename_to_uri(uri)
                except:
                    pass
                parser = TotemPlParser.Parser.new()
                parser.connect('entry-parsed', self.__on_entry_parsed)
                parser.parse_async(uri, True, None, None)
        elif self.window is not None and self.window.is_visible():
            self.window.present()
        elif self.window is not None:
            # self.window.setup_window()
            # self.window.present()
            # Horrible HACK: https://bugzilla.gnome.org/show_bug.cgi?id=774130
            self.window.save_view_state()
            self.window.destroy()
            self.window = Window()
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                menu = self.__setup_app_menu()
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self.__hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.emit('status-changed')
            self.player.emit('current-changed')
        return 0

    def __on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self.__externals_count == 0:
            if self.player.is_party:
                self.player.set_party(False)
            self.player.play_first_external()
        self.__externals_count += 1

    def __hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode') or\
                self.player.current_track.id is None:
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def __update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def __set_network(self, action, param):
        """
            Enable/disable network
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        action.set_state(param)
        self.settings.set_value('network-access', param)
        if self.charts is not None:
            if param.get_boolean():
                self.charts.start()
            else:
                self.charts.stop()
        self.window.reload_view()

    def __fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self.__is_fs:
            from lollypop.fullscreen import FullScreen
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self.__on_fs_destroyed)
            self.__is_fs = True
            fs.show()

    def __on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self.__is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def __on_activate(self, application):
        """
            Call default handler
            @param application as Gio.Application
        """
        self.window.present()

    def __on_sm_listener_ok(self, proxy, task):
        """
            Connect signals
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        try:
            proxy.call('GetClients', None,
                       Gio.DBusCallFlags.NO_AUTO_START,
                       500, None, self.__on_get_clients)
        except:
            pass

    def __on_sm_client_listener_ok(self, proxy, task, client):
        """
            Get app id
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
            @param client as str
        """
        try:
            proxy.call('GetAppId', None,
                       Gio.DBusCallFlags.NO_AUTO_START,
                       500, None, self.__on_get_app_id, client)
        except:
            pass

    def __on_sm_client_private_listener_ok(self, proxy, task):
        """
            Connect signals
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        # Needed or object will be destroyed
        self.__proxy = proxy
        proxy.connect('g-signal', self.__on_signals)

    def __on_get_clients(self, proxy, task):
        """
            Search us in clients
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
        """
        try:
            for client in proxy.call_finish(task)[0]:
                Gio.DBusProxy.new(self.get_dbus_connection(),
                                  Gio.DBusProxyFlags.NONE, None,
                                  'org.gnome.SessionManager',
                                  client,
                                  'org.gnome.SessionManager.Client', None,
                                  self.__on_sm_client_listener_ok, client)
        except:
            pass

    def __on_get_app_id(self, proxy, task, client):
        """
            Connect signals if we are this client
            @param proxy as Gio.DBusProxy
            @param task as Gio.Task
            @param client as str
        """
        try:
            if proxy.call_finish(task)[0] == "org.gnome.Lollypop":
                Gio.DBusProxy.new(self.get_dbus_connection(),
                                  Gio.DBusProxyFlags.NONE, None,
                                  'org.gnome.SessionManager',
                                  client,
                                  'org.gnome.SessionManager.ClientPrivate',
                                  None,
                                  self.__on_sm_client_private_listener_ok)
        except:
            pass

    def __on_signals(self, proxy, sender, signal, parameters):
        """
            Connect to Session Manager QueryEndSession signal
        """
        if signal == "EndSession":
            # Save session, do not quit as we may be killed to quickly
            # to be able to VACUUM database
            self.prepare_to_exit(False)

    def __listen_to_gnome_sm(self):
        """
            Connect to GNOME session manager
        """
        try:
            Gio.DBusProxy.new(self.get_dbus_connection(),
                              Gio.DBusProxyFlags.NONE, None,
                              'org.gnome.SessionManager',
                              '/org/gnome/SessionManager',
                              'org.gnome.SessionManager', None,
                              self.__on_sm_listener_ok)
        except:
            pass

    def __settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def __about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self.__about_response)
        about.show()

    def __shortcuts(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Shortcuts.ui')
            builder.get_object('shortcuts').set_transient_for(self.window)
            builder.get_object('shortcuts').show()
        except:  # GTK < 3.20
            self.__help(action, param)

    def __help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def __about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def __setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')
        menu = builder.get_object('app-menu')

        settingsAction = Gio.SimpleAction.new('settings', None)
        settingsAction.connect('activate', self.__settings_dialog)
        self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self.__update_db)
        self.add_action(updateAction)

        networkAction = Gio.SimpleAction.new_stateful(
           'network',
           None,
           GLib.Variant.new_boolean(self.settings.get_value('network-access')))
        networkAction.connect('change-state', self.__set_network)
        self.add_action(networkAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self.__fullscreen)
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self.__about)
        self.add_action(aboutAction)

        shortcutsAction = Gio.SimpleAction.new('shortcuts', None)
        shortcutsAction.connect('activate', self.__shortcuts)
        self.add_action(shortcutsAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self.__help)
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.add_action(quitAction)

        return menu
Exemplo n.º 9
0
class Application(Gtk.Application):
    """
        Lollypop application:
            - Handle appmenu
            - Handle command line
            - Create main window
    """
    def __init__(self):
        """
            Create application
        """
        Gtk.Application.__init__(
            self,
            application_id='org.gnome.Lollypop',
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        os.environ['PULSE_PROP_media.role'] = 'music'
        os.environ['PULSE_PROP_application.icon_name'] = 'lollypop'
        self.cursors = {}
        self.window = None
        self.notify = None
        self.lastfm = None
        self.debug = False
        self._externals_count = 0
        self._init_proxy()
        GLib.set_application_name('lollypop')
        GLib.set_prgname('lollypop')
        # TODO: Remove this test later
        if Gtk.get_minor_version() > 12:
            self.add_main_option("debug", b'd', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Debug lollypop", None)
            self.add_main_option("set-rating", b'r', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.INT, "Rate the current track",
                                 None)
            self.add_main_option("play-pause", b't', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Toggle playback", None)
            self.add_main_option("next", b'n', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to next track", None)
            self.add_main_option("prev", b'p', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE, "Go to prev track", None)
            self.add_main_option("emulate-phone", b'e', GLib.OptionFlags.NONE,
                                 GLib.OptionArg.NONE,
                                 "Emulate an Android Phone", None)
        self.connect('command-line', self._on_command_line)
        self.register(None)
        if self.get_is_remote():
            Gdk.notify_startup_complete()

    def init(self):
        """
            Init main application
        """
        cssProviderFile = Gio.File.new_for_uri(
            'resource:///org/gnome/Lollypop/application.css')
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)
        self.settings = Settings.new()
        ArtSize.BIG = self.settings.get_value('cover-size').get_int32()
        if LastFM is not None:
            self.lastfm = LastFM()
        self.db = Database()
        self.playlists = Playlists()
        # We store cursors for main thread
        SqlCursor.add(self.db)
        SqlCursor.add(self.playlists)
        self.albums = AlbumsDatabase()
        self.artists = ArtistsDatabase()
        self.genres = GenresDatabase()
        self.tracks = TracksDatabase()
        self.player = Player()
        self.scanner = CollectionScanner()
        self.art = Art()
        if not self.settings.get_value('disable-mpris'):
            MPRIS(self)
        if not self.settings.get_value('disable-notifications'):
            self.notify = NotificationManager()

        settings = Gtk.Settings.get_default()
        dark = self.settings.get_value('dark-ui')
        settings.set_property('gtk-application-prefer-dark-theme', dark)

        self._parser = TotemPlParser.Parser.new()
        self._parser.connect('entry-parsed', self._on_entry_parsed)

        self.add_action(self.settings.create_action('shuffle'))

        self._is_fs = False

    def do_startup(self):
        """
            Add startup notification and
            build gnome-shell menu after Gtk.Application startup
        """
        Gtk.Application.do_startup(self)
        Notify.init("Lollypop")

        # Check locale, we want unicode!
        (code, encoding) = getlocale()
        if encoding is None or encoding != "UTF-8":
            builder = Gtk.Builder()
            builder.add_from_resource('/org/gnome/Lollypop/Unicode.ui')
            self.window = builder.get_object('unicode')
            self.window.set_application(self)
            self.window.show()
        elif not self.window:
            self.init()
            menu = self._setup_app_menu()
            # If GNOME/Unity, add appmenu
            if is_gnome() or is_unity():
                self.set_app_menu(menu)
            self.window = Window(self)
            # If not GNOME/Unity add menu to toolbar
            if not is_gnome() and not is_unity():
                self.window.setup_menu(menu)
            self.window.connect('delete-event', self._hide_on_delete)
            self.window.init_list_one()
            self.window.show()
            self.player.restore_state()
            # Will not start sooner
            self.inhibitor = Inhibitor()

    def prepare_to_exit(self, action=None, param=None):
        """
            Save window position and view
        """
        if self._is_fs:
            return
        if self.settings.get_value('save-state'):
            self.window.save_view_state()
            # Save current track
            if self.player.current_track.id is None:
                track_id = -1
            elif self.player.current_track.id == Type.RADIOS:
                radios = Radios()
                track_id = radios.get_id(
                    self.player.current_track.album_artist)
            else:
                track_id = self.player.current_track.id
            self.settings.set_value('track-id', GLib.Variant('i', track_id))
            # Save current playlist
            if self.player.current_track.id == Type.RADIOS:
                playlist_ids = [Type.RADIOS]
            elif not self.player.get_user_playlist_ids():
                playlist_ids = []
            else:
                playlist_ids = self.player.get_user_playlist_ids()
            self.settings.set_value('playlist-ids',
                                    GLib.Variant('ai', playlist_ids))
        self.player.stop_all()
        if self.window:
            self.window.stop_all()
        self.quit()

    def quit(self):
        """
            Quit lollypop
        """
        if self.scanner.is_locked():
            self.scanner.stop()
            GLib.idle_add(self.quit)
            return
        try:
            with SqlCursor(self.db) as sql:
                sql.execute('VACUUM')
            with SqlCursor(self.playlists) as sql:
                sql.execute('VACUUM')
            with SqlCursor(Radios()) as sql:
                sql.execute('VACUUM')
        except Exception as e:
            print("Application::quit(): ", e)
        self.window.destroy()
        Gst.deinit()

    def is_fullscreen(self):
        """
            Return True if application is fullscreen
        """
        return self._is_fs

#######################
# PRIVATE             #
#######################

    def _init_proxy(self):
        """
            Init proxy setting env
        """
        try:
            settings = Gio.Settings.new('org.gnome.system.proxy.http')
            h = settings.get_value('host').get_string()
            p = settings.get_value('port').get_int32()
            if h != '' and p != 0:
                os.environ['HTTP_PROXY'] = "%s:%s" % (h, p)
        except:
            pass

    def _on_command_line(self, app, app_cmd_line):
        """
            Handle command line
            @param app as Gio.Application
            @param options as Gio.ApplicationCommandLine
        """
        self._externals_count = 0
        options = app_cmd_line.get_options_dict()
        if options.contains('debug'):
            self.debug = True
        if options.contains('set-rating'):
            value = options.lookup_value('set-rating').get_int32()
            if value > 0 and value < 6 and\
                    self.player.current_track.id is not None:
                self.player.current_track.set_popularity(value)
        if options.contains('play-pause'):
            self.player.play_pause()
        elif options.contains('next'):
            self.player.next()
        elif options.contains('prev'):
            self.player.prev()
        elif options.contains('emulate-phone'):
            self.window.add_fake_phone()
        args = app_cmd_line.get_arguments()
        if len(args) > 1:
            self.player.clear_externals()
            for f in args[1:]:
                try:
                    f = GLib.filename_to_uri(f)
                except:
                    pass
                self._parser.parse_async(f, True, None, None)
        if self.window is not None and not self.window.is_visible():
            self.window.setup_window()
            self.window.present()
        return 0

    def _on_entry_parsed(self, parser, uri, metadata):
        """
            Add playlist entry to external files
            @param parser as TotemPlParser.Parser
            @param track uri as str
            @param metadata as GLib.HastTable
        """
        self.player.load_external(uri)
        if self._externals_count == 0:
            if self.player.is_party():
                self.player.set_party(False)
            self.player.play_first_external()
        self._externals_count += 1

    def _hide_on_delete(self, widget, event):
        """
            Hide window
            @param widget as Gtk.Widget
            @param event as Gdk.Event
        """
        if not self.settings.get_value('background-mode'):
            GLib.timeout_add(500, self.prepare_to_exit)
            self.scanner.stop()
        return widget.hide_on_delete()

    def _update_db(self, action=None, param=None):
        """
            Search for new music
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window:
            t = Thread(target=self.art.clean_all_cache)
            t.daemon = True
            t.start()
            self.window.update_db()

    def _fullscreen(self, action=None, param=None):
        """
            Show a fullscreen window with cover and artist informations
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        if self.window and not self._is_fs:
            fs = FullScreen(self, self.window)
            fs.connect("destroy", self._on_fs_destroyed)
            self._is_fs = True
            fs.show()

    def _on_fs_destroyed(self, widget):
        """
            Mark fullscreen as False
            @param widget as Fullscreen
        """
        self._is_fs = False
        if not self.window.is_visible():
            self.prepare_to_exit()

    def _settings_dialog(self, action=None, param=None):
        """
            Show settings dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        dialog = SettingsDialog()
        dialog.show()

    def _about(self, action, param):
        """
            Setup about dialog
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/AboutDialog.ui')
        artists = self.artists.count()
        albums = self.albums.count()
        tracks = self.tracks.count()
        builder.get_object('artists').set_text(
            ngettext("%d artist", "%d artists", artists) % artists)
        builder.get_object('albums').set_text(
            ngettext("%d album", "%d albums", albums) % albums)
        builder.get_object('tracks').set_text(
            ngettext("%d track", "%d tracks", tracks) % tracks)
        about = builder.get_object('about_dialog')
        about.set_transient_for(self.window)
        about.connect("response", self._about_response)
        about.show()

    def _help(self, action, param):
        """
            Show help in yelp
            @param action as Gio.SimpleAction
            @param param as GLib.Variant
        """
        try:
            Gtk.show_uri(None, "help:lollypop", Gtk.get_current_event_time())
        except:
            print(_("Lollypop: You need to install yelp."))

    def _about_response(self, dialog, response_id):
        """
            Destroy about dialog when closed
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        dialog.destroy()

    def set_mini(self, action, param):
        """
            Set mini player on/off
            @param dialog as Gtk.Dialog
            @param response id as int
        """
        if self.window is not None:
            self.window.set_mini()

    def _setup_app_menu(self):
        """
            Setup application menu
            @return menu as Gio.Menu
        """
        builder = Gtk.Builder()

        builder.add_from_resource('/org/gnome/Lollypop/Appmenu.ui')

        menu = builder.get_object('app-menu')

        # TODO: Remove this test later
        if Gtk.get_minor_version() > 12:
            settingsAction = Gio.SimpleAction.new('settings', None)
            settingsAction.connect('activate', self._settings_dialog)
            self.set_accels_for_action('app.settings', ["<Control>s"])
            self.add_action(settingsAction)

        updateAction = Gio.SimpleAction.new('update_db', None)
        updateAction.connect('activate', self._update_db)
        self.set_accels_for_action('app.update_db', ["<Control>u"])
        self.add_action(updateAction)

        fsAction = Gio.SimpleAction.new('fullscreen', None)
        fsAction.connect('activate', self._fullscreen)
        self.set_accels_for_action('app.fullscreen', ["F11", "F7"])
        self.add_action(fsAction)

        mini_action = Gio.SimpleAction.new('mini', None)
        mini_action.connect('activate', self.set_mini)
        self.add_action(mini_action)
        self.set_accels_for_action("app.mini", ["<Control>m"])

        aboutAction = Gio.SimpleAction.new('about', None)
        aboutAction.connect('activate', self._about)
        self.set_accels_for_action('app.about', ["F2"])
        self.add_action(aboutAction)

        helpAction = Gio.SimpleAction.new('help', None)
        helpAction.connect('activate', self._help)
        self.set_accels_for_action('app.help', ["F1"])
        self.add_action(helpAction)

        quitAction = Gio.SimpleAction.new('quit', None)
        quitAction.connect('activate', self.prepare_to_exit)
        self.set_accels_for_action('app.quit', ["<Control>q"])
        self.add_action(quitAction)

        return menu