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) self.__is_fs = False 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.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() 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 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 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 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-mpd'): self.mpd = MpdServerDaemon( self.settings.get_value('mpd-eth').get_string(), self.settings.get_value('mpd-port').get_int32()) 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 __init__(self): """ Create application """ Gtk.Application.__init__(self, application_id='org.gnome.Lollypop', flags=Gio.ApplicationFlags.FLAGS_NONE) self._init_proxy() GLib.set_application_name('lollypop') GLib.set_prgname('lollypop') self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN) # 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.connect('handle-local-options', self._on_handle_local_options) 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) Lp.settings = Settings.new() if LastFM is not None: Lp.lastfm = LastFM() Lp.db = Database() # We store a cursor for the main thread Lp.sql = Lp.db.get_cursor() Lp.player = Player() Lp.albums = AlbumsDatabase() Lp.artists = ArtistsDatabase() Lp.genres = GenresDatabase() Lp.tracks = TracksDatabase() Lp.playlists = PlaylistsManager() Lp.scanner = CollectionScanner() Lp.art = Art() if not Lp.settings.get_value('disable-mpris'): MPRIS(self) if not Lp.settings.get_value('disable-notifications'): Lp.notify = NotificationManager() settings = Gtk.Settings.get_default() dark = Lp.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(Lp.settings.create_action('shuffle')) self._externals_count = 0 self._is_fs = False 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 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() # For a 200 album artwork, we want a 60 artist artwork ArtSize.ARTIST_SMALL = ArtSize.BIG * 60 / 200 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 __init__(self, app): Gtk.ApplicationWindow.__init__(self, application=app, title=_("Lollypop")) self._timeout = None self._scanner = CollectionScanner() self._scanner.connect("scan-finished", self._setup_list_one, True) self._setup_window() self._setup_view() self._setup_media_keys() party_settings = Objects["settings"].get_value('party-ids') ids = [] for setting in party_settings: if isinstance(setting, int): ids.append(setting) Objects["player"].set_party_ids(ids) self.connect("destroy", self._on_destroyed_window)
def __init__(self, app, db, player): Gtk.ApplicationWindow.__init__(self, application=app, title=_("Lollypop")) self._db = db self._player = player self._scanner = CollectionScanner() self._settings = Gio.Settings.new('org.gnome.Lollypop') self._artist_signal_id = 0 self._setup_window() self._setup_view() self._setup_media_keys() party_settings = self._settings.get_value('party-ids') ids = [] for setting in party_settings: if isinstance(setting, int): ids.append(setting) self._player.set_party_ids(ids) self.connect("map-event", self._on_mapped_window)
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.cursors = {} self.window = None self.notify = None self.mpd = 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.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-mpd'): self.mpd = MpdServerDaemon( self.settings.get_value('mpd-eth').get_string(), self.settings.get_value('mpd-port').get_int32()) 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 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() def prepare_to_exit(self, action=None, param=None): """ Save window position and view """ if self.settings.get_value('save-state'): self.window.save_view_state() if self.player.current_track.id is None: track_id = -1 else: track_id = self.player.current_track.id self.settings.set_value('track-id', GLib.Variant('i', track_id)) self.player.stop() if self.window: self.window.stop_all() self.quit() def quit(self): """ Quit lollypop """ if self.mpd is not None: self.mpd.quit() 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() 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: 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 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') builder.get_object('artists').set_text( _("%s artist(s)") % self.artists.count()) builder.get_object('albums').set_text( _("%s album(s)") % self.albums.count()) builder.get_object('tracks').set_text( _("%s track(s)") % self.tracks.count()) 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 _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", "<Control>m"]) self.add_action(fsAction) 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
class Container: def __init__(self): # Index will start at -VOLUMES self._devices = {} self._devices_index = Navigation.DEVICES self._show_genres = Objects.settings.get_value('show-genres') self._stack = ViewContainer(500) self._stack.show() self._setup_view() self._setup_scanner() self._list_one_restore = Navigation.POPULARS self._list_two_restore = None if Objects.settings.get_value('save-state'): self._restore_view_state() # Volume manager self._vm = Gio.VolumeMonitor.get() self._vm.connect('mount-added', self._on_mount_added) self._vm.connect('mount-removed', self._on_mount_removed) Objects.playlists.connect("playlists-changed", self.update_lists) """ Update db at startup only if needed @param force as bool to force update (if possible) """ def update_db(self, force=False): if not self._progress.is_visible(): if force or Objects.tracks.is_empty(): self._scanner.update(False) elif Objects.settings.get_value('startup-scan') or\ Objects.settings.get_value('force-scan'): self._scanner.update(True) """ Save view state """ def save_view_state(self): Objects.settings.set_value("list-one", GLib.Variant( 'i', self._list_one.get_selected_id())) Objects.settings.set_value("list-two", GLib.Variant( 'i', self._list_two.get_selected_id())) """ Show playlist manager for object_id Current view stay present in ViewContainer @param object id as int @param genre id as int @param is_album as bool """ def show_playlist_manager(self, object_id, genre_id, is_album): old_view = self._stack.get_visible_child() view = PlaylistManageView(object_id, genre_id, is_album, self._stack.get_allocated_width()/2) view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) # Keep previous view, if isinstance(old_view, PlaylistManageView): old_view.destroy() """ Show playlist editor for playlist Current view stay present in ViewContainer @param playlist name as str """ def show_playlist_editor(self, playlist_name): old_view = self._stack.get_visible_child() view = PlaylistEditView(playlist_name, self._stack.get_allocated_width()/2) view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) # Keep previous view, if isinstance(old_view, PlaylistEditView): old_view.destroy() """ Update lists @param updater as GObject """ def update_lists(self, updater=None): self._update_list_one(updater) self._update_list_two(updater) """ Load external files @param files as [Gio.Files] """ def load_external(self, files): # We wait as selection list is threaded, # we don't want to insert item before populated if self._list_one_restore is None: start_new_thread(self._scanner.add, (files,)) else: GLib.timeout_add(250, self.load_external, files) """ Get main widget @return Gtk.HPaned """ def main_widget(self): return self._paned_main_list """ Stop current view from processing """ def stop_all(self): view = self._stack.get_visible_child() if view is not None: self._stack.clean_old_views(None) """ Show/Hide genres @param bool """ def show_genres(self, show): self._show_genres = show self._update_list_one(None) """ Destroy current view """ def destroy_current_view(self): view = self._stack.get_visible_child() view.hide() GLib.timeout_add(2000, view.destroy) ############ # Private # ############ """ Setup window main view: - genre list - artist list - main view as artist view or album view """ def _setup_view(self): self._paned_main_list = Gtk.HPaned() self._paned_list_view = Gtk.HPaned() vgrid = Gtk.Grid() vgrid.set_orientation(Gtk.Orientation.VERTICAL) self._list_one = SelectionList() self._list_one.widget.show() self._list_two = SelectionList() self._list_one.connect('item-selected', self._on_list_one_selected) self._list_one.connect('populated', self._on_list_one_populated) self._list_two.connect('item-selected', self._on_list_two_selected) self._list_two.connect('populated', self._on_list_two_populated) self._progress = Gtk.ProgressBar() vgrid.add(self._stack) vgrid.add(self._progress) vgrid.show() separator = Gtk.Separator() separator.show() self._paned_list_view.add1(self._list_two.widget) self._paned_list_view.add2(vgrid) self._paned_main_list.add1(self._list_one.widget) self._paned_main_list.add2(self._paned_list_view) self._paned_main_list.set_position( Objects.settings.get_value( "paned-mainlist-width").get_int32()) self._paned_list_view.set_position( Objects.settings.get_value( "paned-listview-width").get_int32()) self._paned_main_list.show() self._paned_list_view.show() """ Restore saved view """ def _restore_view_state(self): position = Objects.settings.get_value('list-one').get_int32() if position != -1: self._list_one_restore = position else: self._list_one_restore = Navigation.POPULARS position = Objects.settings.get_value('list-two').get_int32() if position != -1: self._list_two_restore = position """ Add genre to genre list @param scanner as CollectionScanner @param genre id as int """ def _add_genre(self, scanner, genre_id): if self._show_genres: genre_name = Objects.genres.get_name(genre_id) self._list_one.add((genre_id, genre_name)) """ Add artist to artist list @param scanner as CollectionScanner @param artist id as int @param album id as int """ def _add_artist(self, scanner, artist_id, album_id): artist_name = Objects.artists.get_name(artist_id) if self._show_genres: genre_ids = Objects.albums.get_genre_ids(album_id) if self._list_one.get_selected_id() in genre_ids: self._list_two.add((artist_id, artist_name)) else: self._list_one.add((artist_id, artist_name)) """ Run collection update if needed @return True if hard scan is running """ def _setup_scanner(self): self._scanner = CollectionScanner(self._progress) self._scanner.connect("scan-finished", self._on_scan_finished) self._scanner.connect("genre-update", self._add_genre) self._scanner.connect("artist-update", self._add_artist) self._scanner.connect("add-finished", self._play_tracks) """ Update list one @param updater as GObject """ def _update_list_one(self, updater): update = updater is not None # Do not update if updater is PlaylistsManager if not isinstance(updater, PlaylistsManager): if self._show_genres: start_new_thread(self._setup_list_genres, (self._list_one, update)) else: start_new_thread(self._setup_list_artists, (self._list_one, Navigation.ALL, update)) """ Update list two @param updater as GObject """ def _update_list_two(self, updater): if self._show_genres: object_id = self._list_one.get_selected_id() if object_id == Navigation.PLAYLISTS: start_new_thread(self._setup_list_playlists, (True,)) elif isinstance(updater, CollectionScanner): start_new_thread(self._setup_list_artists, (self._list_two, object_id, True)) """ Return list one headers """ def _get_headers(self): items = [] items.append((Navigation.POPULARS, _("Popular albums"))) items.append((Navigation.PLAYLISTS, _("Playlists"))) if self._show_genres: items.append((Navigation.ALL, _("All artists"))) else: items.append((Navigation.ALL, _("All albums"))) return items """ Setup list for genres @param list as SelectionList @param update as bool, if True, just update entries @thread safe """ def _setup_list_genres(self, selection_list, update): sql = Objects.db.get_cursor() selection_list.mark_as_artists(False) items = self._get_headers() + Objects.genres.get(sql) if update: GLib.idle_add(selection_list.update, items) else: selection_list.populate(items) sql.close() """ Hide list two base on current artist list """ def _pre_setup_list_artists(self, selection_list): if selection_list == self._list_one: if self._list_two.widget.is_visible(): self._list_two.widget.hide() self._list_two_restore = None """ Setup list for artists @param list as SelectionList @param update as bool, if True, just update entries @thread safe """ def _setup_list_artists(self, selection_list, genre_id, update): GLib.idle_add(self._pre_setup_list_artists, selection_list) sql = Objects.db.get_cursor() items = [] selection_list.mark_as_artists(True) if selection_list == self._list_one: items = self._get_headers() if len(Objects.albums.get_compilations(genre_id, sql)) > 0: items.append((Navigation.COMPILATIONS, _("Compilations"))) items += Objects.artists.get(genre_id, sql) if update: GLib.idle_add(selection_list.update, items) else: selection_list.populate(items) sql.close() """ Setup list for playlists @param update as bool """ def _setup_list_playlists(self, update): playlists = Objects.playlists.get() if update: self._list_two.update(playlists) else: self._list_two.mark_as_artists(False) self._list_two.populate(playlists) GLib.idle_add(self._update_view_playlists, None) """ Update current view with device view, Use existing view if available @param object id as int """ def _update_view_device(self, object_id): device = self._devices[object_id] if device and device.view: view = device.view else: view = DeviceView(device, self._progress, self._stack.get_allocated_width()/2) device.view = view view.show() start_new_thread(view.populate, ()) self._stack.add(view) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with artists view @param object id as int @param genre id as int """ def _update_view_artists(self, object_id, genre_id): view = ArtistView(object_id, True) self._stack.add(view) view.show() start_new_thread(view.populate, (genre_id,)) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with albums view @param object id as int @param genre id as int """ def _update_view_albums(self, object_id, genre_id): view = AlbumView(object_id) self._stack.add(view) view.show() start_new_thread(view.populate, (genre_id,)) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with playlist view @param playlist id as int """ def _update_view_playlists(self, playlist_id): view = None if playlist_id is not None: for (p_id, p_str) in Objects.playlists.get(): if p_id == playlist_id: view = PlaylistView(p_str, self._stack) break else: view = PlaylistManageView(-1, None, False, self._stack.get_allocated_width()/2) if view: view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) self._stack.clean_old_views(view) """ Add volume to device list @param volume as Gio.Volume """ def _add_device(self, volume): if volume is None: return root = volume.get_activation_root() if root is None: return path = root.get_path() if path and path.find('mtp:') != -1: self._devices_index -= 1 dev = Device() dev.id = self._devices_index dev.name = volume.get_name() dev.path = path self._devices[self._devices_index] = dev self._list_one.add_device(dev.name, dev.id) """ Remove volume from device list @param volume as Gio.Volume """ def _remove_device(self, volume): for dev in self._devices.values(): if not os.path.exists(dev.path): self._list_one.remove(dev.id) device = self._devices[dev.id] if device.view: device.view.destroy() del self._devices[dev.id] break """ Update view based on selected object @param list as SelectionList @param object id as int """ def _on_list_one_selected(self, selection_list, object_id): if object_id == Navigation.PLAYLISTS: start_new_thread(self._setup_list_playlists, (False,)) self._list_two.widget.show() elif object_id < Navigation.DEVICES: self._list_two.widget.hide() self._update_view_device(object_id) elif object_id == Navigation.POPULARS: self._list_two.widget.hide() self._update_view_albums(object_id, None) elif selection_list.is_marked_as_artists(): self._list_two.widget.hide() if object_id == Navigation.ALL or\ object_id == Navigation.COMPILATIONS: self._update_view_albums(object_id, None) else: self._update_view_artists(object_id, None) else: start_new_thread(self._setup_list_artists, (self._list_two, object_id, False)) self._list_two.widget.show() if self._list_two_restore is None: self._update_view_albums(object_id, None) """ Restore previous state @param selection list as SelectionList """ def _on_list_one_populated(self, selection_list): if self._list_one_restore is not None: self._list_one.select_id(self._list_one_restore) self._list_one_restore = None for dev in self._devices.values(): self._list_one.add_device(dev.name, dev.id) """ Update view based on selected object @param list as SelectionList @param object id as int """ def _on_list_two_selected(self, selection_list, object_id): selected_id = self._list_one.get_selected_id() if selected_id == Navigation.PLAYLISTS: self._update_view_playlists(object_id) elif object_id == Navigation.COMPILATIONS: self._update_view_albums(object_id, selected_id) else: self._update_view_artists(object_id, selected_id) """ Restore previous state @param selection list as SelectionList """ def _on_list_two_populated(self, selection_list): if self._list_two_restore is not None: self._list_two.select_id(self._list_two_restore) self._list_two_restore = None """ Play tracks as user playlist @param scanner as collection scanner @param outdb as bool (tracks not present in db) """ def _play_tracks(self, scanner, outdb): ids = scanner.get_added() if ids: if not Objects.player.is_party(): Objects.player.set_user_playlist(ids, ids[0]) if outdb: Objects.settings.set_value('force-scan', GLib.Variant('b', True)) Objects.player.load(ids[0]) """ Mark force scan as False, update lists @param scanner as CollectionScanner """ def _on_scan_finished(self, scanner): Objects.settings.set_value('force-scan', GLib.Variant('b', False)) self.update_lists(scanner) """ On volume mounter @param vm as Gio.VolumeMonitor @param mnt as Gio.Mount """ def _on_mount_added(self, vm, mnt): self._add_device(mnt.get_volume()) """ On volume removed, clean selection list @param vm as Gio.VolumeMonitor @param mnt as Gio.Mount """ def _on_mount_removed(self, vm, mnt): self._remove_device(mnt.get_volume())
def _setup_scanner(self): self._scanner = CollectionScanner(self._progress) self._scanner.connect("scan-finished", self._on_scan_finished) self._scanner.connect("genre-update", self._add_genre) self._scanner.connect("artist-update", self._add_artist) self._scanner.connect("add-finished", self._play_tracks)
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
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
class Window(Gtk.ApplicationWindow): """ Init window objects """ def __init__(self, app): Gtk.ApplicationWindow.__init__(self, application=app, title=_("Lollypop")) self._timeout = None self._scanner = CollectionScanner() self._scanner.connect("scan-finished", self._setup_list_one, True) self._setup_window() self._setup_view() self._setup_media_keys() party_settings = Objects["settings"].get_value('party-ids') ids = [] for setting in party_settings: if isinstance(setting, int): ids.append(setting) Objects["player"].set_party_ids(ids) self.connect("destroy", self._on_destroyed_window) """ Run collection update if needed """ def setup_view(self): if Objects["tracks"].is_empty(): self._scanner.update(self._progress, False) return elif Objects["settings"].get_value('startup-scan'): self._scanner.update(self._progress, True) self._setup_list_one() self._update_view_albums(POPULARS) """ Update music database """ def update_db(self): self._list_one.widget.hide() self._list_two.widget.hide() old_view = self._stack.get_visible_child() self._loading = True view = LoadingView() self._stack.add(view) self._stack.set_visible_child(view) self._scanner.update(self._progress, False) if old_view: self._stack.remove(old_view) old_view.remove_signals() """ Update view class @param bool """ def update_view_class(self, dark): current_view = self._stack.get_visible_child() if dark: current_view.get_style_context().add_class('black') else: current_view.get_style_context().remove_class('black') ############ # Private # ############ """ Setup media player keys """ def _setup_media_keys(self): self._proxy = Gio.DBusProxy.new_sync(Gio.bus_get_sync(Gio.BusType.SESSION, None), Gio.DBusProxyFlags.NONE, None, 'org.gnome.SettingsDaemon', '/org/gnome/SettingsDaemon/MediaKeys', 'org.gnome.SettingsDaemon.MediaKeys', None) self._grab_media_player_keys() try: self._proxy.connect('g-signal', self._handle_media_keys) except GLib.GError: # We cannot grab media keys if no settings daemon is running pass """ Do key grabbing """ def _grab_media_player_keys(self): try: self._proxy.call_sync('GrabMediaPlayerKeys', GLib.Variant('(su)', ('Lollypop', 0)), Gio.DBusCallFlags.NONE, -1, None) except GLib.GError: # We cannot grab media keys if no settings daemon is running pass """ Do player actions in response to media key pressed """ def _handle_media_keys(self, proxy, sender, signal, parameters): if signal != 'MediaPlayerKeyPressed': print('Received an unexpected signal \'%s\' from media player'.format(signal)) return response = parameters.get_child_value(1).get_string() if 'Play' in response: Objects["player"].play_pause() elif 'Stop' in response: Objects["player"].stop() elif 'Next' in response: Objects["player"].next() elif 'Previous' in response: Objects["player"].prev() """ Setup window icon, position and size, callback for updating this values """ def _setup_window(self): self.set_icon_name('lollypop') size_setting = Objects["settings"].get_value('window-size') if isinstance(size_setting[0], int) and isinstance(size_setting[1], int): self.resize(size_setting[0], size_setting[1]) else: self.set_size_request(800, 600) position_setting = Objects["settings"].get_value('window-position') if len(position_setting) == 2 \ and isinstance(position_setting[0], int) \ and isinstance(position_setting[1], int): self.move(position_setting[0], position_setting[1]) if Objects["settings"].get_value('window-maximized'): self.maximize() self.connect("window-state-event", self._on_window_state_event) self.connect("configure-event", self._on_configure_event) """ Setup window main view: - genre list - artist list - main view as artist view or album view """ def _setup_view(self): self._paned_main_list = Gtk.HPaned() self._paned_list_view = Gtk.HPaned() vgrid = Gtk.Grid() vgrid.set_orientation(Gtk.Orientation.VERTICAL) self._toolbar = Toolbar() self._toolbar.header_bar.show() self._toolbar.get_view_genres_btn().connect("toggled", self._setup_list_one) self._list_one = SelectionList("Genre") self._list_two = SelectionList("Artist") self._list_one_signal = None self._list_two_signal = None self._loading = True loading_view = LoadingView() self._stack = Gtk.Stack() self._stack.add(loading_view) self._stack.set_visible_child(loading_view) self._stack.set_transition_duration(500) self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) self._stack.show() self._progress = Gtk.ProgressBar() vgrid.add(self._stack) vgrid.add(self._progress) vgrid.show() DESKTOP = environ.get("XDG_CURRENT_DESKTOP") if DESKTOP and ("GNOME" in DESKTOP or "Pantheon" in DESKTOP): self.set_titlebar(self._toolbar.header_bar) self._toolbar.header_bar.set_show_close_button(True) self.add(self._paned_main_list) else: hgrid = Gtk.Grid() hgrid.set_orientation(Gtk.Orientation.VERTICAL) hgrid.add(self._toolbar.header_bar) hgrid.add(self._paned_main_list) hgrid.show() self.add(hgrid) separator = Gtk.Separator() separator.show() self._paned_list_view.add1(self._list_two.widget) self._paned_list_view.add2(vgrid) self._paned_main_list.add1(self._list_one.widget) self._paned_main_list.add2(self._paned_list_view) self._paned_main_list.set_position(Objects["settings"].get_value("paned-mainlist-width").get_int32()) self._paned_list_view.set_position(Objects["settings"].get_value("paned-listview-width").get_int32()) self._paned_main_list.show() self._paned_list_view.show() self.show() """ Init the filter list @param widget as unused """ def _init_main_list(self, widget): if self._list_one.widget.is_visible(): self._update_genres() else: self._init_genres() """ Init list with genres or artist If update, only update list content @param obj as unused, bool """ def _setup_list_one(self, obj = None, update = None): if self._list_one_signal: self._list_one.disconnect(self._list_one_signal) active = self._toolbar.get_view_genres_btn().get_active() if active: items = Objects["genres"].get_ids() else: self._list_two.widget.hide() items = Objects["artists"].get_ids(ALL) if len(Objects["albums"].get_compilations(ALL)) > 0: items.insert(0, (COMPILATIONS, _("Compilations"))) items.insert(0, (ALL, _("All artists"))) items.insert(0, (POPULARS, _("Popular albums"))) if update: self._list_one.update(items, not active) else: self._list_one.populate(items, not active) # If was empty if not self._list_one_signal: self._list_one.select_first() if self._loading: self._stack.get_visible_child().hide() self._list_one.select_first() self._update_view_albums(POPULARS) self._loading = False self._list_one.widget.show() if active: self._list_one_signal = self._list_one.connect('item-selected', self._setup_list_two) else: self._list_one_signal = self._list_one.connect('item-selected', self._update_view_artists, None) """ Init list two with artist based on genre @param obj as unused, genre id as int """ def _setup_list_two(self, obj, genre_id): if self._list_two_signal: self._list_two.disconnect(self._list_two_signal) if genre_id == POPULARS: self._list_two.widget.hide() self._list_two_signal = None else: values = Objects["artists"].get_ids(genre_id) if len(Objects["albums"].get_compilations(genre_id)) > 0: values.insert(0, (COMPILATIONS, _("Compilations"))) self._list_two.populate(values, True) self._list_two.widget.show() self._list_two_signal = self._list_two.connect('item-selected', self._update_view_artists, genre_id) self._update_view_albums(genre_id) """ Update artist view @param artist id as int, genre id as int """ def _update_view_artists(self, obj, artist_id, genre_id): if artist_id == ALL or artist_id == POPULARS: self._update_view_albums(artist_id) else: old_view = self._stack.get_visible_child() view = ArtistView(artist_id, genre_id, False) self._stack.add(view) start_new_thread(view.populate, ()) self._stack.set_visible_child(view) if old_view: self._stack.remove(old_view) old_view.remove_signals() """ Update albums view @param genre id as int """ def _update_view_albums(self, genre_id): old_view = self._stack.get_visible_child() view = AlbumView(genre_id) self._stack.add(view) start_new_thread(view.populate, ()) self._stack.set_visible_child(view) if old_view: self._stack.remove(old_view) old_view.remove_signals() """ Delay event @param: widget as Gtk.Window @param: event as Gtk.Event """ def _on_configure_event(self, widget, event): if self._timeout: GLib.source_remove(self._timeout) self._timeout = GLib.timeout_add(500, self._save_size_position, widget) """ Save window state, update current view content size @param: widget as Gtk.Window """ def _save_size_position(self, widget): self._timeout = None size = widget.get_size() Objects["settings"].set_value('window-size', GLib.Variant('ai', [size[0], size[1]])) position = widget.get_position() Objects["settings"].set_value('window-position', GLib.Variant('ai', [position[0], position[1]])) """ Save maximised state """ def _on_window_state_event(self, widget, event): Objects["settings"].set_boolean('window-maximized', 'GDK_WINDOW_STATE_MAXIMIZED' in event.new_window_state.value_names) """ Save paned widget width @param widget as unused, data as unused """ def _on_destroyed_window(self, widget): Objects["settings"].set_value("paned-mainlist-width", GLib.Variant('i', self._paned_main_list.get_position())) Objects["settings"].set_value("paned-listview-width", GLib.Variant('i', self._paned_list_view.get_position()))
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()
class Container: def __init__(self): # Index will start at -VOLUMES self._devices = {} self._devices_index = Navigation.DEVICES self._show_genres = Objects.settings.get_value('show-genres') self._stack = ViewContainer(500) self._stack.show() self._setup_view() self._setup_scanner() self._list_one_restore = Navigation.POPULARS self._list_two_restore = None if Objects.settings.get_value('save-state'): self._restore_view_state() # Volume manager self._vm = Gio.VolumeMonitor.get() self._vm.connect('mount-added', self._on_mount_added) self._vm.connect('mount-removed', self._on_mount_removed) Objects.playlists.connect("playlists-changed", self.update_lists) """ Update db at startup only if needed @param force as bool to force update (if possible) """ def update_db(self, force=False): if not self._progress.is_visible(): if force or Objects.tracks.is_empty(): self._scanner.update(False) elif Objects.settings.get_value('startup-scan') or\ Objects.settings.get_value('force-scan'): self._scanner.update(True) """ Save view state """ def save_view_state(self): Objects.settings.set_value( "list-one", GLib.Variant('i', self._list_one.get_selected_id())) Objects.settings.set_value( "list-two", GLib.Variant('i', self._list_two.get_selected_id())) """ Show playlist manager for object_id Current view stay present in ViewContainer @param object id as int @param genre id as int @param is_album as bool """ def show_playlist_manager(self, object_id, genre_id, is_album): old_view = self._stack.get_visible_child() view = PlaylistManageView(object_id, genre_id, is_album, self._stack.get_allocated_width() / 2) view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) # Keep previous view, if isinstance(old_view, PlaylistManageView): old_view.destroy() """ Show playlist editor for playlist Current view stay present in ViewContainer @param playlist name as str """ def show_playlist_editor(self, playlist_name): old_view = self._stack.get_visible_child() view = PlaylistEditView(playlist_name, self._stack.get_allocated_width() / 2) view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) # Keep previous view, if isinstance(old_view, PlaylistEditView): old_view.destroy() """ Update lists @param updater as GObject """ def update_lists(self, updater=None): self._update_list_one(updater) self._update_list_two(updater) """ Load external files @param files as [Gio.Files] """ def load_external(self, files): # We wait as selection list is threaded, # we don't want to insert item before populated if self._list_one_restore is None: start_new_thread(self._scanner.add, (files, )) else: GLib.timeout_add(250, self.load_external, files) """ Get main widget @return Gtk.HPaned """ def main_widget(self): return self._paned_main_list """ Stop current view from processing """ def stop_all(self): view = self._stack.get_visible_child() if view is not None: self._stack.clean_old_views(None) """ Show/Hide genres @param bool """ def show_genres(self, show): self._show_genres = show self._update_list_one(None) """ Destroy current view """ def destroy_current_view(self): view = self._stack.get_visible_child() view.hide() GLib.timeout_add(2000, view.destroy) ############ # Private # ############ """ Setup window main view: - genre list - artist list - main view as artist view or album view """ def _setup_view(self): self._paned_main_list = Gtk.HPaned() self._paned_list_view = Gtk.HPaned() vgrid = Gtk.Grid() vgrid.set_orientation(Gtk.Orientation.VERTICAL) self._list_one = SelectionList() self._list_one.widget.show() self._list_two = SelectionList() self._list_one.connect('item-selected', self._on_list_one_selected) self._list_one.connect('populated', self._on_list_one_populated) self._list_two.connect('item-selected', self._on_list_two_selected) self._list_two.connect('populated', self._on_list_two_populated) self._progress = Gtk.ProgressBar() vgrid.add(self._stack) vgrid.add(self._progress) vgrid.show() separator = Gtk.Separator() separator.show() self._paned_list_view.add1(self._list_two.widget) self._paned_list_view.add2(vgrid) self._paned_main_list.add1(self._list_one.widget) self._paned_main_list.add2(self._paned_list_view) self._paned_main_list.set_position( Objects.settings.get_value("paned-mainlist-width").get_int32()) self._paned_list_view.set_position( Objects.settings.get_value("paned-listview-width").get_int32()) self._paned_main_list.show() self._paned_list_view.show() """ Restore saved view """ def _restore_view_state(self): position = Objects.settings.get_value('list-one').get_int32() if position != -1: self._list_one_restore = position else: self._list_one_restore = Navigation.POPULARS position = Objects.settings.get_value('list-two').get_int32() if position != -1: self._list_two_restore = position """ Add genre to genre list @param scanner as CollectionScanner @param genre id as int """ def _add_genre(self, scanner, genre_id): if self._show_genres: genre_name = Objects.genres.get_name(genre_id) self._list_one.add((genre_id, genre_name)) """ Add artist to artist list @param scanner as CollectionScanner @param artist id as int @param album id as int """ def _add_artist(self, scanner, artist_id, album_id): artist_name = Objects.artists.get_name(artist_id) if self._show_genres: genre_ids = Objects.albums.get_genre_ids(album_id) if self._list_one.get_selected_id() in genre_ids: self._list_two.add((artist_id, artist_name)) else: self._list_one.add((artist_id, artist_name)) """ Run collection update if needed @return True if hard scan is running """ def _setup_scanner(self): self._scanner = CollectionScanner(self._progress) self._scanner.connect("scan-finished", self._on_scan_finished) self._scanner.connect("genre-update", self._add_genre) self._scanner.connect("artist-update", self._add_artist) self._scanner.connect("add-finished", self._play_tracks) """ Update list one @param updater as GObject """ def _update_list_one(self, updater): update = updater is not None # Do not update if updater is PlaylistsManager if not isinstance(updater, PlaylistsManager): if self._show_genres: start_new_thread(self._setup_list_genres, (self._list_one, update)) else: start_new_thread(self._setup_list_artists, (self._list_one, Navigation.ALL, update)) """ Update list two @param updater as GObject """ def _update_list_two(self, updater): if self._show_genres: object_id = self._list_one.get_selected_id() if object_id == Navigation.PLAYLISTS: start_new_thread(self._setup_list_playlists, (True, )) elif isinstance(updater, CollectionScanner): start_new_thread(self._setup_list_artists, (self._list_two, object_id, True)) """ Return list one headers """ def _get_headers(self): items = [] items.append((Navigation.POPULARS, _("Popular albums"))) items.append((Navigation.PLAYLISTS, _("Playlists"))) if self._show_genres: items.append((Navigation.ALL, _("All artists"))) else: items.append((Navigation.ALL, _("All albums"))) return items """ Setup list for genres @param list as SelectionList @param update as bool, if True, just update entries @thread safe """ def _setup_list_genres(self, selection_list, update): sql = Objects.db.get_cursor() selection_list.mark_as_artists(False) items = self._get_headers() + Objects.genres.get(sql) if update: GLib.idle_add(selection_list.update, items) else: selection_list.populate(items) sql.close() """ Hide list two base on current artist list """ def _pre_setup_list_artists(self, selection_list): if selection_list == self._list_one: if self._list_two.widget.is_visible(): self._list_two.widget.hide() self._list_two_restore = None """ Setup list for artists @param list as SelectionList @param update as bool, if True, just update entries @thread safe """ def _setup_list_artists(self, selection_list, genre_id, update): GLib.idle_add(self._pre_setup_list_artists, selection_list) sql = Objects.db.get_cursor() items = [] selection_list.mark_as_artists(True) if selection_list == self._list_one: items = self._get_headers() if len(Objects.albums.get_compilations(genre_id, sql)) > 0: items.append((Navigation.COMPILATIONS, _("Compilations"))) items += Objects.artists.get(genre_id, sql) if update: GLib.idle_add(selection_list.update, items) else: selection_list.populate(items) sql.close() """ Setup list for playlists @param update as bool """ def _setup_list_playlists(self, update): playlists = Objects.playlists.get() if update: self._list_two.update(playlists) else: self._list_two.mark_as_artists(False) self._list_two.populate(playlists) GLib.idle_add(self._update_view_playlists, None) """ Update current view with device view, Use existing view if available @param object id as int """ def _update_view_device(self, object_id): device = self._devices[object_id] if device and device.view: view = device.view else: view = DeviceView(device, self._progress, self._stack.get_allocated_width() / 2) device.view = view view.show() start_new_thread(view.populate, ()) self._stack.add(view) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with artists view @param object id as int @param genre id as int """ def _update_view_artists(self, object_id, genre_id): view = ArtistView(object_id, True) self._stack.add(view) view.show() start_new_thread(view.populate, (genre_id, )) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with albums view @param object id as int @param genre id as int """ def _update_view_albums(self, object_id, genre_id): view = AlbumView(object_id) self._stack.add(view) view.show() start_new_thread(view.populate, (genre_id, )) self._stack.set_visible_child(view) self._stack.clean_old_views(view) """ Update current view with playlist view @param playlist id as int """ def _update_view_playlists(self, playlist_id): view = None if playlist_id is not None: for (p_id, p_str) in Objects.playlists.get(): if p_id == playlist_id: view = PlaylistView(p_str, self._stack) break else: view = PlaylistManageView(-1, None, False, self._stack.get_allocated_width() / 2) if view: view.show() self._stack.add(view) self._stack.set_visible_child(view) start_new_thread(view.populate, ()) self._stack.clean_old_views(view) """ Add volume to device list @param volume as Gio.Volume """ def _add_device(self, volume): if volume is None: return root = volume.get_activation_root() if root is None: return path = root.get_path() if path and path.find('mtp:') != -1: self._devices_index -= 1 dev = Device() dev.id = self._devices_index dev.name = volume.get_name() dev.path = path self._devices[self._devices_index] = dev self._list_one.add_device(dev.name, dev.id) """ Remove volume from device list @param volume as Gio.Volume """ def _remove_device(self, volume): for dev in self._devices.values(): if not os.path.exists(dev.path): self._list_one.remove(dev.id) device = self._devices[dev.id] if device.view: device.view.destroy() del self._devices[dev.id] break """ Update view based on selected object @param list as SelectionList @param object id as int """ def _on_list_one_selected(self, selection_list, object_id): if object_id == Navigation.PLAYLISTS: start_new_thread(self._setup_list_playlists, (False, )) self._list_two.widget.show() elif object_id < Navigation.DEVICES: self._list_two.widget.hide() self._update_view_device(object_id) elif object_id == Navigation.POPULARS: self._list_two.widget.hide() self._update_view_albums(object_id, None) elif selection_list.is_marked_as_artists(): self._list_two.widget.hide() if object_id == Navigation.ALL or\ object_id == Navigation.COMPILATIONS: self._update_view_albums(object_id, None) else: self._update_view_artists(object_id, None) else: start_new_thread(self._setup_list_artists, (self._list_two, object_id, False)) self._list_two.widget.show() if self._list_two_restore is None: self._update_view_albums(object_id, None) """ Restore previous state @param selection list as SelectionList """ def _on_list_one_populated(self, selection_list): if self._list_one_restore is not None: self._list_one.select_id(self._list_one_restore) self._list_one_restore = None for dev in self._devices.values(): self._list_one.add_device(dev.name, dev.id) """ Update view based on selected object @param list as SelectionList @param object id as int """ def _on_list_two_selected(self, selection_list, object_id): selected_id = self._list_one.get_selected_id() if selected_id == Navigation.PLAYLISTS: self._update_view_playlists(object_id) elif object_id == Navigation.COMPILATIONS: self._update_view_albums(object_id, selected_id) else: self._update_view_artists(object_id, selected_id) """ Restore previous state @param selection list as SelectionList """ def _on_list_two_populated(self, selection_list): if self._list_two_restore is not None: self._list_two.select_id(self._list_two_restore) self._list_two_restore = None """ Play tracks as user playlist @param scanner as collection scanner @param outdb as bool (tracks not present in db) """ def _play_tracks(self, scanner, outdb): ids = scanner.get_added() if ids: if not Objects.player.is_party(): Objects.player.set_user_playlist(ids, ids[0]) if outdb: Objects.settings.set_value('force-scan', GLib.Variant('b', True)) Objects.player.load(ids[0]) """ Mark force scan as False, update lists @param scanner as CollectionScanner """ def _on_scan_finished(self, scanner): Objects.settings.set_value('force-scan', GLib.Variant('b', False)) self.update_lists(scanner) """ On volume mounter @param vm as Gio.VolumeMonitor @param mnt as Gio.Mount """ def _on_mount_added(self, vm, mnt): self._add_device(mnt.get_volume()) """ On volume removed, clean selection list @param vm as Gio.VolumeMonitor @param mnt as Gio.Mount """ def _on_mount_removed(self, vm, mnt): self._remove_device(mnt.get_volume())
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
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
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
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
class Window(Gtk.ApplicationWindow): """ Init window objects """ def __init__(self, app, db, player): Gtk.ApplicationWindow.__init__(self, application=app, title=_("Lollypop")) self._db = db self._player = player self._scanner = CollectionScanner() self._settings = Gio.Settings.new('org.gnome.Lollypop') self._artist_signal_id = 0 self._setup_window() self._setup_view() self._setup_media_keys() party_settings = self._settings.get_value('party-ids') ids = [] for setting in party_settings: if isinstance(setting, int): ids.append(setting) self._player.set_party_ids(ids) self.connect("map-event", self._on_mapped_window) def edit_party(self): builder = Gtk.Builder() builder.add_from_resource('/org/gnome/Lollypop/PartyDialog.ui') self._party_dialog = builder.get_object('party_dialog') self._party_dialog.set_transient_for(self) self._party_dialog.set_title(_("Select what will be available in party mode")) party_button = builder.get_object('button1') party_button.connect("clicked", self._edit_party_close) scrolled = builder.get_object('scrolledwindow1') genres = self._db.get_all_genres() genres.insert(0, (-1, "Populars")) self._party_grid = Gtk.Grid() self._party_grid.set_orientation(Gtk.Orientation.VERTICAL) self._party_grid.set_property("column-spacing", 10) ids = self._player.get_party_ids() i = 0 x = 0 for genre_id, genre in genres: label = Gtk.Label() label.set_text(genre) switch = Gtk.Switch() if genre_id in ids: switch.set_state(True) switch.connect("state-set", self._party_switch_state, genre_id) self._party_grid.attach(label, x, i, 1, 1) self._party_grid.attach(switch, x+1, i, 1, 1) if x == 0: x += 2 else: i += 1 x = 0 scrolled.add(self._party_grid) self._party_dialog.show_all() """ Update music database Empty database if reinit True """ def update_db(self, reinit): if reinit: self._player.stop() self._player.clear_albums() self._toolbar.update_toolbar(None, None) self._db.reset() self._list_genres.widget.hide() self._list_artists.widget.hide() self._box.remove(self._view) self._view = LoadingView() self._box.add(self._view) self._scanner.update(self._update_genres) ############ # Private # ############ """ Update party ids when use change a switch in dialog """ def _party_switch_state(self, widget, state, genre_id): ids = self._player.get_party_ids() if state: try: ids.append(genre_id) except: pass else: try: ids.remove(genre_id) except: pass self._player.set_party_ids(ids) self._settings.set_value('party-ids', GLib.Variant('ai', ids)) """ Close edit party dialog """ def _edit_party_close(self, widget): self._party_dialog.hide() self._party_dialog.destroy() """ Setup media player keys """ def _setup_media_keys(self): self._proxy = Gio.DBusProxy.new_sync(Gio.bus_get_sync(Gio.BusType.SESSION, None), Gio.DBusProxyFlags.NONE, None, 'org.gnome.SettingsDaemon', '/org/gnome/SettingsDaemon/MediaKeys', 'org.gnome.SettingsDaemon.MediaKeys', None) self._grab_media_player_keys() try: self._proxy.connect('g-signal', self._handle_media_keys) except GLib.GError: # We cannot grab media keys if no settings daemon is running pass """ Do key grabbing """ def _grab_media_player_keys(self): try: self._proxy.call_sync('GrabMediaPlayerKeys', GLib.Variant('(su)', ('Lollypop', 0)), Gio.DBusCallFlags.NONE, -1, None) except GLib.GError: # We cannot grab media keys if no settings daemon is running pass """ Do player actions in response to media key pressed """ def _handle_media_keys(self, proxy, sender, signal, parameters): if signal != 'MediaPlayerKeyPressed': print('Received an unexpected signal \'%s\' from media player'.format(signal)) return response = parameters.get_child_value(1).get_string() if 'Play' in response: self._player.play_pause() elif 'Stop' in response: self._player.stop() elif 'Next' in response: self._player.next() elif 'Previous' in response: self._player.prev() """ Setup window icon, position and size, callback for updating this values """ def _setup_window(self): self.set_size_request(200, 100) self.set_icon_name('lollypop') size_setting = self._settings.get_value('window-size') if isinstance(size_setting[0], int) and isinstance(size_setting[1], int): self.resize(size_setting[0], size_setting[1]) position_setting = self._settings.get_value('window-position') if len(position_setting) == 2 \ and isinstance(position_setting[0], int) \ and isinstance(position_setting[1], int): self.move(position_setting[0], position_setting[1]) if self._settings.get_value('window-maximized'): self.maximize() self.connect("window-state-event", self._on_window_state_event) self.connect("configure-event", self._on_configure_event) """ Setup window main view: - genre list - artist list - main view as artist view or album view """ def _setup_view(self): self._box = Gtk.Grid() self._toolbar = Toolbar(self._db, self._player) self.set_titlebar(self._toolbar.header_bar) self._toolbar.header_bar.show() self._toolbar.get_infobox().connect("button-press-event", self._show_current_album) self._list_genres = SelectionList("Genre", 150) self._list_artists = SelectionList("Artist", 200) self._view = LoadingView() separator = Gtk.Separator() separator.show() self._box.add(self._list_genres.widget) self._box.add(separator) self._box.add(self._list_artists.widget) self._box.add(self._view) self.add(self._box) self._box.show() self.show() """ Run collection update on mapped window Pass _update_genre() as collection scanned callback """ def _on_mapped_window(self, obj, data): if self._db.is_empty(): self._scanner.update(self._update_genres) else: genres = self._db.get_all_genres() self._update_genres(genres) """ Update genres list with genres """ def _update_genres(self, genres): genres.insert(0, (-1, _("All genres"))) genres.insert(0, (-2, _("Populars albums"))) self._list_genres.populate(genres) self._list_genres.connect('item-selected', self._update_artists) self._list_genres.widget.show() self._list_genres.select_first() """ Update artist list for genre_id """ def _update_artists(self, obj, genre_id): if self._artist_signal_id: self._list_artists.disconnect(self._artist_signal_id) if genre_id == -1: self._list_artists.populate(self._db.get_all_artists(), True) self._update_view_albums(self, -1) self._list_artists.widget.show() elif genre_id == -2: self._update_view_populars_albums() self._list_artists.widget.hide() else: self._list_artists.populate(self._db.get_artists_by_genre_id(genre_id), True) self._update_view_albums(self, genre_id) self._list_artists.widget.show() self._artist_signal_id = self._list_artists.connect('item-selected', self._update_view_artist) self._genre_id = genre_id """ Update artist view for artist_id """ def _update_view_artist(self, obj, artist_id): self._box.remove(self._view) self._view.destroy() self._view = ArtistView(self._db, self._player, self._genre_id, artist_id) self._box.add(self._view) self._view.populate() """ Update albums view with populars albums """ def _update_view_populars_albums(self): self._box.remove(self._view) self._view.destroy() self._view = AlbumView(self._db, self._player, None) self._box.add(self._view) self._view.populate_popular() """ Update albums view for genre_id """ def _update_view_albums(self, obj, genre_id): self._box.remove(self._view) self._view.destroy() self._view = AlbumView(self._db, self._player, genre_id) self._box.add(self._view) self._view.populate() """ Save new window size/position """ def _on_configure_event(self, widget, event): size = widget.get_size() self._settings.set_value('window-size', GLib.Variant('ai', [size[0], size[1]])) position = widget.get_position() self._settings.set_value('window-position', GLib.Variant('ai', [position[0], position[1]])) """ Save maximised state """ def _on_window_state_event(self, widget, event): self._settings.set_boolean('window-maximized', 'GDK_WINDOW_STATE_MAXIMIZED' in event.new_window_state.value_names) """ Show current album context/content """ def _show_current_album(self, obj, data): track_id = self._player.get_current_track_id() if track_id != -1: self._view.current_changed(False, track_id)