def __reset_database(self): """ Reset database """ def update_ui(): App().window.container.go_home() App().scanner.update(ScanType.FULL) App().player.stop() if App().ws_director.collection_ws is not None: App().ws_director.collection_ws.stop() uris = App().tracks.get_uris() i = 0 SqlCursor.add(App().db) SqlCursor.add(self.__history) count = len(uris) for uri in uris: self.del_from_db(uri, True) self.__update_progress(i, count, 0.01) i += 1 App().tracks.del_persistent(False) App().tracks.clean(False) App().albums.clean(False) App().artists.clean(False) App().genres.clean(False) App().cache.clear_table("duration") SqlCursor.commit(App().db) SqlCursor.remove(App().db) SqlCursor.commit(self.__history) SqlCursor.remove(self.__history) GLib.idle_add(update_ui)
def clean_old_albums(self, storage_types): """ Clean old albums from DB @param storage_types as [StorageType] """ SqlCursor.add(App().db) # Remove older albums for storage_type in storage_types: # If too many albums, do some cleanup count = App().albums.get_count_for_storage_type(storage_type) diff = count - self.MAX_ITEMS_PER_STORAGE_TYPE if diff > 0: album_ids = App().albums.get_oldest_for_storage_type( storage_type, diff) for album_id in album_ids: # EPHEMERAL with not tracks will be cleaned below App().albums.set_storage_type(album_id, StorageType.EPHEMERAL) App().tracks.remove_album(album_id, False) # On cancel, clean not needed, done in Application::quit() if not self.__cancellable.is_cancelled(): App().tracks.clean(False) App().albums.clean(False) App().artists.clean(False) SqlCursor.remove(App().db)
def del_tracks(self, track_ids): """ Delete tracks from db @param track_ids as [int] """ SqlCursor.add(Lp().playlists) with SqlCursor(self) as sql: all_album_ids = [] all_artist_ids = [] all_genre_ids = [] for track_id in track_ids: album_id = Lp().tracks.get_album_id(track_id) art_file = Lp().art.get_album_cache_name(Album(album_id)) genre_ids = Lp().tracks.get_genre_ids(track_id) album_artist_ids = Lp().albums.get_artist_ids(album_id) artist_ids = Lp().tracks.get_artist_ids(track_id) uri = Lp().tracks.get_uri(track_id) Lp().playlists.remove(uri) Lp().tracks.remove(track_id) Lp().tracks.clean(track_id) all_album_ids.append(album_id) all_artist_ids += album_artist_ids + artist_ids all_genre_ids += genre_ids for album_id in list(set(all_album_ids)): if Lp().albums.clean(album_id): Lp().art.clean_store(art_file) for artist_id in list(set(all_artist_ids)): Lp().artists.clean(artist_id) for genre_id in list(set(all_genre_ids)): Lp().genres.clean(genre_id) sql.commit() SqlCursor.remove(Lp().playlists)
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 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 upgrade(self, db): """ Upgrade db @param db as Database """ version = 0 SqlCursor.add(db) with SqlCursor(db, True) as sql: result = sql.execute("PRAGMA user_version") v = result.fetchone() if v is not None: version = v[0] if version < self.version: for i in range(version + 1, self.version + 1): try: if isinstance(self._UPGRADES[i], str): sql.execute(self._UPGRADES[i]) SqlCursor.commit(db) else: self._UPGRADES[i](db) SqlCursor.commit(db) except Exception as e: Logger.error("DB upgrade %s failed: %s" % (i, e)) sql.execute("PRAGMA user_version=%s" % self.version) SqlCursor.remove(db)
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 __upgrade_13(self): """ Convert tracks filepath column to uri """ with SqlCursor(self._db) as sql: sql.execute("ALTER TABLE tracks RENAME TO tmp_tracks") sql.execute('''CREATE TABLE tracks (id INTEGER PRIMARY KEY, name TEXT NOT NULL, uri TEXT NOT NULL, duration INT, tracknumber INT, discnumber INT, discname TEXT, album_id INT NOT NULL, year INT, popularity INT NOT NULL, ltime INT NOT NULL, mtime INT NOT NULL, persistent INT NOT NULL DEFAULT 1)''') sql.execute('''INSERT INTO tracks(id, name, uri, duration, tracknumber, discnumber, discname, album_id, year, popularity, ltime, mtime, persistent) SELECT id, name, filepath, duration, tracknumber, discnumber, discname, album_id, year, popularity, ltime, mtime, persistent FROM tmp_tracks''') sql.execute("DROP TABLE tmp_tracks") result = sql.execute("SELECT rowid FROM tracks") for track_id in list(itertools.chain(*result)): result = sql.execute("SELECT uri FROM tracks WHERE rowid=?", (track_id,)) v = result.fetchone() if v is not None: uri = v[0] if uri.startswith("/"): uri = GLib.filename_to_uri(uri) sql.execute("UPDATE tracks set uri=? WHERE rowid=?", (uri, track_id)) sql.commit() dumb = DumbPlaylists() SqlCursor.add(dumb) with SqlCursor(dumb) as sql: sql.execute("ALTER TABLE tracks RENAME TO tmp_tracks") sql.execute('''CREATE TABLE tracks (playlist_id INT NOT NULL, uri TEXT NOT NULL)''') sql.execute('''INSERT INTO tracks(playlist_id, uri) SELECT playlist_id, filepath FROM tmp_tracks''') sql.execute("DROP TABLE tmp_tracks") result = sql.execute("SELECT uri FROM tracks") for path in list(itertools.chain(*result)): if path.startswith("/"): uri = GLib.filename_to_uri(path) sql.execute("UPDATE tracks set uri=? WHERE uri=?", (uri, path)) sql.commit()
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): """ 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 insert_track(self, playlist_id, track, position): """ Insert track at position, will remove track first if exists @param playlist_id as int @param track as Track @param position as int """ SqlCursor.add(self) track_ids = self.get_track_ids(playlist_id) if track.id in track_ids: index = track_ids.index(track.id) track_ids.remove(track.id) if index < position: position -= 1 track_ids.insert(position, track.id) self.clear(playlist_id) tracks = [Track(track_id) for track_id in track_ids] self.add_tracks(playlist_id, tracks) SqlCursor.remove(self)
def __scan(self, uris, saved): """ Scan music collection for music files @param uris as [str] @param saved as bool @thread safe """ modifications = False if self.__history is None: self.__history = History() mtimes = App().tracks.get_mtimes() (new_tracks, new_dirs) = self.__get_objects_for_uris(uris) orig_tracks = App().tracks.get_uris() was_empty = len(orig_tracks) == 0 count = len(new_tracks) + len(orig_tracks) # Add monitors on dirs if self.__inotify is not None: for d in new_dirs: if d.startswith("file://"): self.__inotify.add_monitor(d) i = 0 # Look for new files/modified file SqlCursor.add(App().db) try: to_add = [] for uri in new_tracks: if self.__thread is None: SqlCursor.remove(App().db) return try: GLib.idle_add(self.__update_progress, i, count) f = Gio.File.new_for_uri(uri) # We do not use time::modified because many tag editors # just preserve this setting try: info = f.query_info("time::changed", Gio.FileQueryInfoFlags.NONE, None) mtime = int( info.get_attribute_as_string("time::changed")) except: # Fallback for remote fs info = f.query_info("time::modified", Gio.FileQueryInfoFlags.NONE, None) mtime = int( info.get_attribute_as_string("time::modified")) # If songs exists and mtime unchanged, continue, # else rescan if uri in orig_tracks: orig_tracks.remove(uri) i += 1 if not saved or mtime <= mtimes.get(uri, mtime + 1): i += 1 continue else: SqlCursor.allow_thread_execution(App().db) self.__del_from_db(uri) # If not saved, use 0 as mtime, easy delete on quit if not saved: mtime = 0 # On first scan, use modification time # Else, use current time elif not was_empty: mtime = int(time()) to_add.append((uri, mtime)) except Exception as e: Logger.error("CollectionScanner::__scan(mtime): %s" % e) if to_add or orig_tracks: modifications = True # Clean deleted files # Now because we need to populate history # Only if we are saving if saved: for uri in orig_tracks: i += 1 GLib.idle_add(self.__update_progress, i, count) self.__del_from_db(uri) SqlCursor.allow_thread_execution(App().db) # Add files to db for (uri, mtime) in to_add: try: Logger.debug("Adding file: %s" % uri) i += 1 GLib.idle_add(self.__update_progress, i, count) self.__add2db(uri, mtime) SqlCursor.allow_thread_execution(App().db) except Exception as e: Logger.error("CollectionScanner::__scan(add): %s, %s" % (e, uri)) except Exception as e: Logger.error("CollectionScanner::__scan(): %s" % e) SqlCursor.commit(App().db) SqlCursor.remove(App().db) GLib.idle_add(self.__finish, modifications and saved) if not saved: self.__play_new_tracks(new_tracks) del self.__history self.__history = None
def __scan(self, scan_type, uris): """ Scan music collection for music files @param scan_type as ScanType @param uris as [str] @thread safe """ try: SqlCursor.add(App().db) App().art.clean_rounded() (files, dirs, streams) = self.__get_objects_for_uris(scan_type, uris) if not files: App().notify.send("Lollypop", _("Scan disabled, missing collection")) return if scan_type == ScanType.NEW_FILES: db_uris = App().tracks.get_uris(uris) else: db_uris = App().tracks.get_uris() # Get mtime of all tracks to detect which has to be updated db_mtimes = App().tracks.get_mtimes() # * 2 => Scan + Save self.__progress_total = len(files) * 2 + len(streams) self.__progress_count = 0 self.__progress_fraction = 0 # Min: 1 thread, Max: 5 threads count = max(1, min(5, cpu_count() // 2)) split_files = split_list(files, count) self.__tags = {} self.__pending_new_artist_ids = [] threads = [] for files in split_files: thread = App().task_helper.run(self.__scan_files, files, db_mtimes, scan_type) threads.append(thread) if scan_type == ScanType.EXTERNAL: storage_type = StorageType.EXTERNAL else: storage_type = StorageType.COLLECTION # Start getting files and populating DB self.__items = [] i = 0 while threads: thread = threads[i] if not thread.is_alive(): threads.remove(thread) self.__items += self.__save_in_db(storage_type) if i >= len(threads) - 1: i = 0 else: i += 1 # Add streams to DB, only happening on command line/m3u files self.__items += self.__save_streams_in_db(streams, storage_type) self.__remove_old_tracks(db_uris, scan_type) if scan_type == ScanType.EXTERNAL: albums = tracks_to_albums( [Track(item.track_id) for item in self.__items]) App().player.play_albums(albums) else: self.__add_monitor(dirs) GLib.idle_add(self.__finish, self.__items) self.__tags = {} self.__items = [] self.__pending_new_artist_ids = [] except Exception as e: Logger.warning("CollectionScanner::__scan(): %s", e) SqlCursor.remove(App().db)
def __scan_files(self, files, db_uris, scan_type): """ Scan music collection for new audio files @param files as [str] @param db_uris as [str] @param scan_type as ScanType @return new track uris as [str] @thread safe """ SqlCursor.add(App().db) i = 0 # New tracks present in collection new_tracks = [] # Get mtime of all tracks to detect which has to be updated db_mtimes = App().tracks.get_mtimes() count = len(files) + 1 try: # Scan new files for (mtime, uri) in files: # Handle a stop request if self.__thread is None and scan_type != ScanType.EPHEMERAL: raise Exception("Scan add cancelled") try: if not self.__scan_to_handle(uri): continue if mtime > db_mtimes.get(uri, 0): # If not saved, use 0 as mtime, easy delete on quit if scan_type == ScanType.EPHEMERAL: mtime = 0 # Do not use mtime if not intial scan elif db_mtimes: mtime = int(time()) Logger.debug("Adding file: %s" % uri) self.__add2db(uri, mtime) SqlCursor.allow_thread_execution(App().db) new_tracks.append(uri) except Exception as e: Logger.error("CollectionScanner:: __scan_add_files: % s" % e) i += 1 self.__update_progress(i, count) if scan_type != ScanType.EPHEMERAL and self.__thread is not None: # We need to check files are always in collections if scan_type == ScanType.FULL: collections = App().settings.get_music_uris() else: collections = None for uri in db_uris: # Handle a stop request if self.__thread is None: raise Exception("Scan del cancelled") in_collection = True if collections is not None: in_collection = False for collection in collections: if collection in uri: in_collection = True break f = Gio.File.new_for_uri(uri) if not in_collection or not f.query_exists(): self.del_from_db(uri, True) SqlCursor.allow_thread_execution(App().db) except Exception as e: Logger.warning("CollectionScanner:: __scan_files: % s" % e) SqlCursor.commit(App().db) SqlCursor.remove(App().db) return new_tracks