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, ApplicationActions): """ Lollypop application: - Handle appmenu - Handle command line - Create main window """ def __init__(self, version, data_dir): """ Create application @param version as str @param data_dir as str """ Gtk.Application.__init__( self, application_id="org.gnome.Lollypop", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) self.__version = version self.__data_dir = data_dir self.set_property("register-session", True) signal(SIGINT, lambda a, b: self.quit()) signal(SIGTERM, lambda a, b: self.quit()) # Set main thread name # We force it to current python 3.6 name, to be sure in case of # change in python current_thread().setName("MainThread") (self.__proxy_host, self.__proxy_port) = init_proxy_from_gnome() GLib.setenv("PULSE_PROP_media.role", "music", True) GLib.setenv("PULSE_PROP_application.icon_name", "org.gnome.Lollypop", True) # Ideally, we will be able to delete this once Flatpak has a solution # for SSL certificate management inside of applications. if GLib.file_test("/app", GLib.FileTest.EXISTS): paths = [ "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/cert.pem", "/etc/ssl/cert.pem" ] for path in paths: if GLib.file_test(path, GLib.FileTest.EXISTS): GLib.setenv("SSL_CERT_FILE", path, True) break self.cursors = {} self.debug = False self.shown_sidebar_tooltip = False self.__window = None self.__fs_window = None settings = Gio.Settings.new("org.gnome.desktop.interface") self.animations = settings.get_value("enable-animations").get_boolean() GLib.set_application_name("Lollypop") GLib.set_prgname("lollypop") self.add_main_option("play-ids", b"a", GLib.OptionFlags.NONE, GLib.OptionArg.STRING, "Play ids", None) self.add_main_option("debug", b"d", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Debug Lollypop", None) self.add_main_option("set-rating", b"r", GLib.OptionFlags.NONE, GLib.OptionArg.STRING, "Rate the current track", None) self.add_main_option("play-pause", b"t", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Toggle playback", None) self.add_main_option("stop", b"s", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Stop playback", None) self.add_main_option("next", b"n", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Go to next track", None) self.add_main_option("prev", b"p", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Go to prev track", None) self.add_main_option("emulate-phone", b"e", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Emulate a Librem phone", None) self.add_main_option("version", b"v", GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Lollypop version", None) self.connect("command-line", self.__on_command_line) self.connect("handle-local-options", self.__on_handle_local_options) self.connect("activate", self.__on_activate) self.connect("shutdown", lambda a: self.__save_state()) self.register(None) if self.get_is_remote(): Gdk.notify_startup_complete() if GLib.environ_getenv(GLib.get_environ(), "DEBUG_LEAK") is not None: import gc gc.set_debug(gc.DEBUG_LEAK) def init(self): """ Init main application """ self.settings = Settings.new() # Mount enclosing volume as soon as possible uris = self.settings.get_music_uris() try: for uri in uris: if uri.startswith("file:/"): continue f = Gio.File.new_for_uri(uri) f.mount_enclosing_volume(Gio.MountMountFlags.NONE, None, None, None) except Exception as e: Logger.error("Application::init(): %s" % e) cssProviderFile = Gio.File.new_for_uri( "resource:///org/gnome/Lollypop/application.css") cssProvider = Gtk.CssProvider() cssProvider.load_from_file(cssProviderFile) screen = Gdk.Screen.get_default() styleContext = Gtk.StyleContext() styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) self.db = Database() self.cache = CacheDatabase() self.playlists = Playlists() self.albums = AlbumsDatabase(self.db) self.artists = ArtistsDatabase(self.db) self.genres = GenresDatabase(self.db) self.tracks = TracksDatabase(self.db) self.player = Player() self.inhibitor = Inhibitor() self.scanner = CollectionScanner() self.notify = NotificationManager() self.task_helper = TaskHelper() self.art_helper = ArtHelper() self.art = Art() self.art.update_art_size() self.ws_director = DirectorWebService() self.ws_director.start() if not self.settings.get_value("disable-mpris"): from lollypop.mpris import MPRIS MPRIS(self) settings = Gtk.Settings.get_default() self.__gtk_dark = settings.get_property( "gtk-application-prefer-dark-theme") if not self.__gtk_dark: dark = self.settings.get_value("dark-ui") settings.set_property("gtk-application-prefer-dark-theme", dark) ApplicationActions.__init__(self) monitor = Gio.NetworkMonitor.get_default() if monitor.get_network_available() and\ not monitor.get_network_metered() and\ self.settings.get_value("recent-youtube-dl"): self.task_helper.run(install_youtube_dl) def do_startup(self): """ Init application """ Gtk.Application.do_startup(self) Handy.init() if self.__window is None: from lollypop.window import Window self.init() self.__window = Window() self.__window.connect("delete-event", self.__hide_on_delete) self.__window.setup() self.__window.show() self.player.restore_state() def quit(self, vacuum=False): """ Quit Lollypop @param vacuum as bool """ self.__window.container.stop() self.__window.hide() if not self.ws_director.stop(): GLib.timeout_add(100, self.quit, vacuum) return if self.settings.get_value("save-state"): self.__window.container.stack.save_history() # Then vacuum db if vacuum: self.__vacuum() self.art.clean_artwork() Gio.Application.quit(self) if GLib.environ_getenv(GLib.get_environ(), "DEBUG_LEAK") is not None: import gc gc.collect() for x in gc.garbage: s = str(x) print(type(x), "\n ", s) def fullscreen(self): """ Go fullscreen """ def on_destroy(window): self.__fs_window = None self.__window.show() if self.__fs_window is None: from lollypop.fullscreen import FullScreen self.__fs_window = FullScreen() self.__fs_window.delayed_init() self.__fs_window.show() self.__fs_window.connect("destroy", on_destroy) self.__window.hide() else: self.__fs_window.destroy() @property def proxy_host(self): """ Get proxy host @return str """ return self.__proxy_host @property def proxy_port(self): """ Get proxy port @return int """ return self.__proxy_port @property def devices(self): """ Get available devices Merge connected and known @return [str] """ devices = self.__window.toolbar.end.devices_popover.devices devices += list(self.settings.get_value("devices")) result = [] # Do not use set() + filter() because we want to keep order for device in devices: if device not in result and device != "": result.append(device) return result @property def is_fullscreen(self): """ Return True if application is fullscreen """ return self.__fs_window is not None @property def main_window(self): """ Get main window """ return self.__window @property def window(self): """ Get current application window @return Gtk.Window """ if self.__fs_window is not None: return self.__fs_window else: return self.__window @property def data_dir(self): """ Get data dir @return str """ return self.__data_dir @property def gtk_application_prefer_dark_theme(self): """ Return default gtk value @return bool """ return self.__gtk_dark @property def version(self): """ Get Lollypop version @return srt """ return self.__version ####################### # PRIVATE # ####################### def __save_state(self): """ Save player state """ if self.settings.get_value("save-state"): if self.player.current_track.id is None or\ self.player.current_track.storage_type &\ StorageType.EPHEMERAL: track_id = None else: track_id = self.player.current_track.id # Save albums context try: with open(LOLLYPOP_DATA_PATH + "/Albums.bin", "wb") as f: dump(self.player.albums, f) except Exception as e: Logger.error("Application::__save_state(): %s" % e) dump(track_id, open(LOLLYPOP_DATA_PATH + "/track_id.bin", "wb")) dump([self.player.is_playing, self.player.is_party], open(LOLLYPOP_DATA_PATH + "/player.bin", "wb")) dump(self.player.queue, open(LOLLYPOP_DATA_PATH + "/queue.bin", "wb")) if self.player.current_track.id is not None: position = self.player.position else: position = 0 dump(position, open(LOLLYPOP_DATA_PATH + "/position.bin", "wb")) self.player.stop_all() def __vacuum(self): """ VACUUM DB """ try: if self.scanner.is_locked(): self.scanner.stop() GLib.idle_add(self.__vacuum) return SqlCursor.add(self.db) self.tracks.del_non_persistent(False) self.tracks.clean(False) self.albums.clean(False) self.artists.clean(False) self.genres.clean(False) SqlCursor.remove(self.db) self.cache.clean(True) with SqlCursor(self.db) as sql: sql.isolation_level = None sql.execute("VACUUM") sql.isolation_level = "" with SqlCursor(self.playlists) as sql: sql.isolation_level = None sql.execute("VACUUM") sql.isolation_level = "" except Exception as e: Logger.error("Application::__vacuum(): %s" % e) def __parse_uris(self, playlist_uris, audio_uris): """ Parse playlist uris @param playlist_uris as [str] @param audio_uris as [str] """ from gi.repository import TotemPlParser playlist_uri = playlist_uris.pop(0) parser = TotemPlParser.Parser.new() parser.connect("entry-parsed", self.__on_entry_parsed, audio_uris) parser.parse_async(playlist_uri, True, None, self.__on_parse_finished, playlist_uris, audio_uris) def __on_handle_local_options(self, app, options): """ Handle local options @param app as Gio.Application @param options as GLib.VariantDict """ if options.contains("version"): print("Lollypop %s" % self.__version) exit(0) return -1 def __on_command_line(self, app, app_cmd_line): """ Handle command line @param app as Gio.Application @param options as Gio.ApplicationCommandLine """ try: args = app_cmd_line.get_arguments() options = app_cmd_line.get_options_dict() if options.contains("debug"): self.debug = True if options.contains("set-rating"): value = options.lookup_value("set-rating").get_string() try: value = min(max(0, int(value)), 5) if self.player.current_track.id is not None: self.player.current_track.set_rate(value) except Exception as e: Logger.error("Application::__on_command_line(): %s", e) pass elif options.contains("play-pause"): self.player.play_pause() elif options.contains("stop"): self.player.stop() elif options.contains("play-ids"): try: value = options.lookup_value("play-ids").get_string() ids = value.split(";") albums = [] for id in ids: if id[0:2] == "a:": album = Album(int(id[2:])) self.player.add_album(album) albums.append(album) else: track = Track(int(id[2:])) self.player.add_album(track.album) albums.append(track.album) if albums and albums[0].tracks: self.player.load(albums[0].tracks[0]) except Exception as e: Logger.error("Application::__on_command_line(): %s", e) pass elif options.contains("next"): self.player.next() elif options.contains("prev"): self.player.prev() elif options.contains("emulate-phone"): self.__window.toolbar.end.devices_popover.add_fake_phone() elif len(args) > 1: audio_uris = [] playlist_uris = [] for uri in args[1:]: parsed = urlparse(uri) if parsed.scheme not in ["http", "https"]: try: uri = GLib.filename_to_uri(uri) except: pass f = Gio.File.new_for_uri(uri) # Try ./filename if not f.query_exists(): uri = GLib.filename_to_uri( "%s/%s" % (GLib.get_current_dir(), uri)) print(uri) f = Gio.File.new_for_uri(uri) file_type = get_file_type(uri) if file_type == FileType.PLS: playlist_uris.append(uri) else: audio_uris.append(uri) if playlist_uris: self.__parse_uris(playlist_uris, audio_uris) else: self.__on_parse_finished(None, None, [], audio_uris) elif self.__window is not None: if not self.__window.is_visible(): self.__window.present() emit_signal(self.player, "status-changed") emit_signal(self.player, "current-changed") Gdk.notify_startup_complete() except Exception as e: Logger.error("Application::__on_command_line(): %s", e) return 0 def __on_parse_finished(self, source, result, playlist_uris, audio_uris): """ Play stream @param source as None @param result as Gio.AsyncResult @param uris as ([str], [str]) """ if playlist_uris: self.__parse_uris(playlist_uris, audio_uris) else: self.scanner.update(ScanType.EXTERNAL, audio_uris) def __on_entry_parsed(self, parser, uri, metadata, audio_uris): """ Add playlist entry to external files @param parser as TotemPlParser.Parser @param uri as str @param metadata as GLib.HastTable @param audio_uris as str """ audio_uris.append(uri) def __hide_on_delete(self, widget, event): """ Hide window @param widget as Gtk.Widget @param event as Gdk.Event """ # Quit if background mode is on but player is off if not self.settings.get_value("background-mode") or\ not self.player.is_playing: GLib.idle_add(self.quit, True) return widget.hide_on_delete() def __on_activate(self, application): """ Call default handler @param application as Gio.Application """ self.__window.present()
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