def __remove_old_tracks(self, uris, scan_type): """ Remove non existent tracks from DB @param scan_type as ScanType """ if scan_type != ScanType.EXTERNAL 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 uris: # Handle a stop request if self.__thread is None: raise Exception("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: Logger.warning( "Removed, not in collection anymore: %s -> %s", uri, collections) self.del_from_db(uri, True) elif not f.query_exists(): Logger.warning("Removed, file has been deleted: %s", uri) self.del_from_db(uri, True)
def _get_audiodb_artist_artwork_uri(self, artist, cancellable=None): """ Get artist artwork using AutdioDB @param artist as str @param cancellable as Gio.Cancellable @return uri as str @thread safe """ if not get_network_available("AUDIODB"): return [] try: artist = GLib.uri_escape_string(artist, None, True) uri = "https://theaudiodb.com/api/v1/json/" uri += "%s/search.php?s=%s" % (AUDIODB_CLIENT_ID, artist) (status, data) = App().task_helper.load_uri_content_sync( uri, cancellable) if status: decode = json.loads(data.decode("utf-8")) uri = None for item in decode["artists"]: for key in ["strArtistFanart", "strArtistThumb"]: uri = item[key] if uri is not None: return [uri] except Exception as e: Logger.warning("%s %s", e, artist) Logger.warning( "DownloaderArt::_get_audiodb_artist_artwork_uri: %s", data) return []
def _get_fanarttv_artist_artwork_uri(self, artist, cancellable=None): """ Get artist artwork using FanartTV @param artist as str @param cancellable as Gio.Cancellable @return uri as str @thread safe """ if not get_network_available("FANARTTV"): return [] uris = [] try: mbid = self.__get_musicbrainz_mbid("artist", artist, cancellable) if mbid is None: return [] uri = "http://webservice.fanart.tv/v3/music/%s?api_key=%s" (status, data) = App().task_helper.load_uri_content_sync( uri % (mbid, FANARTTV_ID), cancellable) if status: decode = json.loads(data.decode("utf-8")) for item in decode["artistbackground"]: uris.append(item["url"]) except Exception as e: Logger.warning("%s %s", e, artist) Logger.warning( "DownloaderArt::_get_fanarttv_artist_artwork_uri: %s", data) return uris
def __set_popularity(self, pop): """ Set popularity as kid3 is installed @param pop as int """ try: if App().art.kid3_available: if pop == 0: value = 0 elif pop == 1: value = 1 elif pop == 2: value = 64 elif pop == 3: value = 128 elif pop == 4: value = 196 else: value = 255 path = GLib.filename_from_uri(self.__object.uri)[0] if GLib.find_program_in_path("flatpak-spawn") is not None: argv = ["flatpak-spawn", "--host", "kid3-cli", "-c", "set POPM %s" % value, path] else: argv = ["kid3-cli", "-c", "set POPM %s" % value, path] (pid, stdin, stdout, stderr) = GLib.spawn_async( argv, flags=GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.STDOUT_TO_DEV_NULL, standard_input=False, standard_output=False, standard_error=False ) except Exception as e: Logger.error("RatingWidget::__on_can_set_popularity(): %s" % e)
def __get_similar_artists_from_deezer_id(self, deezer_id, cancellable): """ Get similar artists from deezer id @param deezer_id as str @param cancellable as Gio.Cancellable @return [(str, str, str)] : list of (deezer_id, artist, cover_uri) """ artists = [] try: uri = "https://api.deezer.com/artist/%s/related" % deezer_id (status, data) = App().task_helper.load_uri_content_sync(uri, cancellable) if status: decode = json.loads(data.decode("utf-8")) for artist in decode["data"]: if "picture_xl" in artist.keys(): artwork_uri = artist["picture_xl"] elif "picture_big" in artist.keys(): artwork_uri = artist["picture_big"] elif "picture_medium" in artist.keys(): artwork_uri = artist["picture_medium"] else: artwork_uri = None artists.append((artist["id"], artist["name"], artwork_uri)) except: Logger.error( "DeezerSimilars::__get_similar_artists_from_deezer_id(): %s", deezer_id) return artists
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 load_uri_content_sync(self, uri, cancellable=None): """ Load uri @param uri as str @param cancellable as Gio.Cancellable @return (loaded as bool, content as bytes) """ try: session = Soup.Session.new() # Post message if self.__headers: msg = Soup.Message.new("GET", uri) headers = msg.get_property("request-headers") for header in self.__headers: headers.append(header[0], header[1]) session.send_message(msg) body = msg.get_property("response-body") bytes = body.flatten().get_data() # Get message else: request = session.request(uri) stream = request.send(cancellable) bytes = bytearray(0) buf = stream.read_bytes(1024, cancellable).get_data() while buf: bytes += buf buf = stream.read_bytes(1024, cancellable).get_data() stream.close() return (True, bytes) except Exception as e: Logger.error("TaskHelper::load_uri_content_sync(): %s" % e) return (False, b"")
def __listen(self, track, timestamp): """ Scrobble track @param track as Track @param timestamp as int """ tracks = self.__queue + [(track, timestamp)] self.__queue = [] try: for (track, timestamp) in tracks: payload = self.__get_payload(track) payload[0]["listened_at"] = timestamp post_data = { "listen_type": "single", "payload": payload } body = json.dumps(post_data).encode("utf-8") msg = Soup.Message.new("POST", self.__uri) msg.set_request("application/json", Soup.MemoryUse.STATIC, body) msg.request_headers.append("Accept-Charset", "utf-8") msg.request_headers.append("Authorization", "Token %s" % self.user_token) msg.request_headers.append("Content-Type", "application/json") data = App().task_helper.send_message_sync(msg, self.__cancellable) if data is not None: Logger.debug("%s: %s", self.__uri, data) except Exception as e: Logger.error("ListenBrainzWebService::__listen(): %s" % e)
def __playing_now(self, track): """ Now playing track @param track as Track """ try: payload = self.__get_payload(track) post_data = { "listen_type": "playing_now", "payload": payload } body = json.dumps(post_data).encode("utf-8") msg = Soup.Message.new("POST", self.__uri) msg.set_request("application/json", Soup.MemoryUse.STATIC, body) msg.request_headers.append("Accept-Charset", "utf-8") msg.request_headers.append("Authorization", "Token %s" % self.user_token) data = App().task_helper.send_message_sync(msg, self.__cancellable) if data is not None: Logger.debug("%s: %s", self.__uri, data) except Exception as e: Logger.error("ListenBrainzWebService::__playing_now(): %s" % e)
def __on_load_uri_content(self, uri, loaded, content, api, uris): """ Add loaded pixbuf @param uri as str @param loaded as bool @param content as bytes @param uris as [str] @param api as str @param last as bool """ try: if loaded: self.__add_pixbuf(content, api) if uris: (uri, api) = uris.pop(0) App().task_helper.load_uri_content(uri, self._cancellable, self.__on_load_uri_content, api, uris) else: self.__loaders -= 1 except Exception as e: self.__loaders -= 1 Logger.warning("ArtworkSearchWidget::__on_load_uri_content(): %s", e) if self.__loaders == 0: self.__spinner.stop()
def populate(self, bytes, art_size): """ Populate images with bytes @param bytes as bytes @param art_size as int @return bool if success """ try: scale_factor = self.get_scale_factor() gbytes = GLib.Bytes.new(bytes) stream = Gio.MemoryInputStream.new_from_bytes(gbytes) if stream is not None: pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, None) if self.__api is None: text = "%sx%s" % (pixbuf.get_width(), pixbuf.get_height()) else: text = "%s: %sx%s" % (self.__api, pixbuf.get_width(), pixbuf.get_height()) self.__label.set_text(text) pixbuf = App().art.load_behaviour(pixbuf, None, art_size * scale_factor, art_size * scale_factor, ArtBehaviour.CROP) stream.close() self.__bytes = bytes surface = Gdk.cairo_surface_create_from_pixbuf( pixbuf, scale_factor, None) del pixbuf self.__image.set_from_surface(surface) del surface return True except Exception as e: Logger.error("ArtworkSearch::__get_image: %s" % e) return False
def populate(self): """ Populate view """ try: grid = Gtk.Grid() grid.set_orientation(Gtk.Orientation.VERTICAL) grid.show() grid.set_row_spacing(5) image = Gtk.Image.new_from_icon_name("edit-clear-all-symbolic", Gtk.IconSize.INVALID) image.set_pixel_size(self._art_size) context = image.get_style_context() context.add_class("cover-frame") padding = context.get_padding(Gtk.StateFlags.NORMAL) border = context.get_border(Gtk.StateFlags.NORMAL) image.set_size_request( self._art_size + padding.left + padding.right + border.left + border.right, self._art_size + padding.top + padding.bottom + border.top + border.bottom) image.show() label = Gtk.Label.new(_("Remove")) label.show() grid.add(image) grid.add(label) grid.set_property("valign", Gtk.Align.CENTER) grid.set_property("halign", Gtk.Align.CENTER) self._flowbox.add(grid) self.__search_for_artwork() except Exception as e: Logger.error("ArtworkSearchWidget::populate(): %s", e)
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: Logger.error("Application::__vacuum(): %s" % e)
def update(self, scan_type, uris=[]): """ Update database @param scan_type as ScanType @param uris as [str] """ self.__disable_compilations = not App().settings.get_value( "show-compilations") App().lookup_action("update_db").set_enabled(False) # Stop previous scan if self.is_locked() and scan_type != ScanType.EXTERNAL: self.stop() GLib.timeout_add(250, self.update, scan_type, uris) elif App().ws_director.collection_ws is not None and\ not App().ws_director.collection_ws.stop(): GLib.timeout_add(250, self.update, scan_type, uris) else: if scan_type == ScanType.FULL: uris = App().settings.get_music_uris() if not uris: return # Register to progressbar if scan_type != ScanType.EXTERNAL: App().window.container.progress.add(self) App().window.container.progress.set_fraction(0, self) Logger.info("Scan started") # Launch scan in a separate thread self.__thread = App().task_helper.run(self.__scan, scan_type, uris)
def __on_filesystem_info(self, source, result): """ Show available space on disk @param source as GObject.Object @param result as Gio.AsyncResult """ try: info = source.query_filesystem_info_finish(result) size = info.get_attribute_uint64(FILE_ATTRIBUTE_FILESYSTEM_SIZE) free = info.get_attribute_uint64(FILE_ATTRIBUTE_FILESYSTEM_FREE) if size == 0: return used = size - free fraction = 1 * used / size self.__progressbar.set_fraction(fraction) style_context = self.__progressbar.get_style_context() style_context.remove_class("usagebar-green") style_context.remove_class("usagebar-orange") style_context.remove_class("usagebar-red") if fraction < 0.6: style_context.add_class("usagebar-green") elif fraction < 0.8: style_context.add_class("usagebar-orange") else: style_context.add_class("usagebar-red") except Exception as e: Logger.error("DeviceWiget::__on_filesystem_info(): %s", e)
def get_year(self, tags): """ Return track year for tags @param tags as Gst.TagList @return year and timestamp (int, int) """ try: (exists, date) = tags.get_date_index("date", 0) dt = year = timestamp = None if exists: year = date.get_year() d = Gst.DateTime.new_local_time(year, 1, 1, 0, 0, 0) dt = d.to_g_date_time() timestamp = dt.to_unix() else: (exists, date) = tags.get_date_time_index("datetime", 0) if exists: dt = date.to_g_date_time() if dt is None: year = date.get_year() d = Gst.DateTime.new_local_time(year, 1, 1, 0, 0, 0) dt = d.to_g_date_time() timestamp = dt.to_unix() return (year, timestamp) except Exception as e: error = "" if tags is None else tags.to_string() Logger.error("TagReader::get_year(): %s, %s", e, error) return (None, None)
def _get_audiodb_artist_info(self, artist): """ Get artist info from audiodb @param artist as str @return info as bytes """ if not get_network_available("AUDIODB"): return None try: artist = GLib.uri_escape_string(artist, None, True) uri = "https://theaudiodb.com/api/v1/json/" uri += "%s/search.php?s=%s" % (AUDIODB_CLIENT_ID, artist) (status, data) = App().task_helper.load_uri_content_sync(uri, None) if status: decode = json.loads(data.decode("utf-8")) language = getdefaultlocale()[0][-2:] for item in decode["artists"]: for key in ["strBiography%s" % language, "strBiographyEN"]: info = item[key] if info is not None: return info.encode("utf-8") except Exception as e: Logger.error("InfoDownloader::_get_audiodb_artist_info: %s, %s" % (e, artist)) return None
def get_artist_id(self, artist_name, cancellable): """ Get artist id @param artist_name as str @param cancellable as Gio.Cancellable @return str/None """ try: artist_name = GLib.uri_escape_string(artist_name, None, True).replace(" ", "+") token = App().ws_director.token_ws.get_token( "SPOTIFY", cancellable) bearer = "Bearer %s" % token headers = [("Authorization", bearer)] uri = "https://api.spotify.com/v1/search?q=%s&type=artist" %\ artist_name (status, data) = App().task_helper.load_uri_content_sync_with_headers( uri, headers, cancellable) if status: decode = json.loads(data.decode("utf-8")) for item in decode["artists"]["items"]: return item["id"] except Exception as e: Logger.error("SpotifyWebHelper::get_artist_id(): %s", e) return None
def load_uri_content(self, uri, cancellable, callback, *args): """ Load uri with libsoup @param uri as str @param cancellable as Gio.Cancellable @param callback as a function @callback (uri as str, status as bool, content as bytes, args) """ try: session = Soup.Session.new() session.set_property('accept-language-auto', True) # Post message if self.__headers: msg = Soup.Message.new("GET", uri) headers = msg.get_property("request-headers") for header in self.__headers: headers.append(header[0], header[1]) session.send_async(msg, cancellable, self.__on_load_uri_content, callback, cancellable, uri, *args) # Get message else: request = session.request(uri) request.send_async(cancellable, self.__on_request_send_async, callback, cancellable, uri, *args) except Exception as e: Logger.error("HelperTask::load_uri_content(): %s" % e) callback(uri, False, b"", *args)
def get_artist_top_tracks(self, spotify_id, cancellable): """ Get top tracks for spotify id @param spotify_id as str @param cancellable as Gio.Cancellable @return str """ try: locale = getdefaultlocale()[0][0:2] track_ids = [] token = App().ws_director.token_ws.get_token( "SPOTIFY", cancellable) bearer = "Bearer %s" % token headers = [("Authorization", bearer)] uri = "https://api.spotify.com/v1/artists/%s/top-tracks" %\ spotify_id uri += "?country=%s" % locale (status, data) = App().task_helper.load_uri_content_sync_with_headers( uri, headers, cancellable) if status: decode = json.loads(data.decode("utf-8")) for item in decode["tracks"]: track_ids.append(item["id"]) except Exception as e: Logger.error("SpotifyWebHelper::get_artist_top_tracks(): %s", e) return track_ids
def __update_fm_settings(self, name): """ Update *fm settings @param name as str (librefm/lastfm) """ if App().lastfm is None: return from pylast import LastFMNetwork, LibreFMNetwork fm = None for scrobbler in App().scrobblers: if (isinstance(scrobbler, LibreFMNetwork) and name == "librefm") or\ (isinstance(scrobbler, LastFMNetwork) and name != "librefm"): fm = scrobbler break if name == "librefm": callback = self.__test_librefm_connection login = self.__librefm_login.get_text() password = self.__librefm_password.get_text() elif App().lastfm is not None: callback = self.__test_lastfm_connection login = self.__lastfm_login.get_text() password = self.__lastfm_password.get_text() try: if fm is not None and login and password: from lollypop.helper_passwords import PasswordsHelper helper = PasswordsHelper() helper.clear(name, helper.store, name, login, password, self.__on_password_store, fm, callback) except Exception as e: Logger.error("SettingsDialog::__update_fm_settings(): %s" % e)
def install_youtube_dl(): try: path = GLib.get_user_data_dir() + "/lollypop/python" argv = ["pip3", "install", "-t", path, "-U", "youtube-dl"] GLib.spawn_sync(None, argv, [], GLib.SpawnFlags.SEARCH_PATH, None) except Exception as e: Logger.error("install_youtube_dl: %s" % e)
def load_similars(self, artist_ids, storage_type, cancellable): """ Load similar artists for artist ids @param artist_ids as int @param storage_type as StorageType @param cancellable as Gio.Cancellable """ for artist_id in artist_ids: artist_name = App().artists.get_name(artist_id) deezer_id = self.get_artist_id(artist_name, cancellable) try: uri = "https://api.deezer.com/artist/%s/radio" % deezer_id (status, data) = App().task_helper.load_uri_content_sync( uri, cancellable) if status: decode = json.loads(data.decode("utf-8")) for payload in decode["data"]: track_payload = self.get_track_payload( payload["id"], cancellable) album_payload = self.get_album_payload( payload["album"]["id"], cancellable) lollypop_payload = self.lollypop_album_payload( album_payload) item = self.save_album_payload_to_db( lollypop_payload, storage_type, True, cancellable) lollypop_payload = self.lollypop_track_payload( track_payload) self.save_track_payload_to_db(lollypop_payload, item, storage_type, True, cancellable) except Exception as e: Logger.error("DeezerSimilars::load_similars(): %s", e) emit_signal(self, "finished")
def __init__(self): """ Create database tables or manage update if needed """ self.thread_lock = MyLock() f = Gio.File.new_for_path(self.DB_PATH) upgrade = DatabaseAlbumsUpgrade() if not f.query_exists(): try: d = Gio.File.new_for_path(self.__LOCAL_PATH) if not d.query_exists(): d.make_directory_with_parents() # Create db schema with SqlCursor(self, True) as sql: sql.execute(self.__create_albums) sql.execute(self.__create_artists) sql.execute(self.__create_genres) sql.execute(self.__create_album_genres) sql.execute(self.__create_album_artists) sql.execute(self.__create_tracks) sql.execute(self.__create_track_artists) sql.execute(self.__create_track_genres) sql.execute(self.__create_album_artists_idx) sql.execute(self.__create_track_artists_idx) sql.execute(self.__create_album_genres_idx) sql.execute(self.__create_track_genres_idx) sql.execute("PRAGMA user_version=%s" % upgrade.version) except Exception as e: Logger.error("Database::__init__(): %s" % e) else: upgrade.upgrade(self)
def __on_get_youtube_id(self, uri, status, content, track, cancellable, methods): """ Get youtube id or run another method if not found @param uri as str @param status as bool @param content as bytes @param track as Track @param cancellable as Gio.Cancellable @param methods as [function] """ try: youtube_id = None if status: decode = json.loads(content.decode("utf-8")) dic = {} best = self.__BAD_SCORE for i in decode["items"]: score = get_page_score(i["snippet"]["title"], track.name, track.artists[0], track.album.name) if score == -1 or score == best: continue elif score < best: best = score dic[score] = i["id"]["videoId"] # Return url from first dic item if best != self.__BAD_SCORE: youtube_id = dic[best] except: Logger.warning("BaseWebHelper::__on_get_youtube_id(): %s", content) self.__emit_uri_loaded(youtube_id, track, cancellable, methods)
def __request(self, listen_type, payload, retry=0): """ Submit payload to service @param listen_type as str @param payload as [] @param retry as int (internal) """ self.__wait_for_ratelimit() Logger.debug("ListenBrainz %s: %r" % (listen_type, payload)) data = { "listen_type": listen_type, "payload": payload } body = json.dumps(data).encode("utf-8") session = Soup.Session.new() uri = "https://%s%s" % (HOST_NAME, PATH_SUBMIT) msg = Soup.Message.new("POST", uri) msg.set_request("application/json", Soup.MemoryUse.STATIC, body) msg.request_headers.append("Authorization", "Token %s" % self.user_token) try: status = session.send_message(msg) response_headers = msg.get_property("response-headers") self.__handle_ratelimit(response_headers) # Too Many Requests if status == 429 and retry < 5: self.__request(listen_type, payload, retry + 1) except Exception as e: print("ListenBrainz::__submit():", e)
def _get_deezer_artist_artwork_uri(self, artist, cancellable=None): """ Get artist artwork using Deezer @param artist as str @param cancellable as Gio.Cancellable @return uri as str @tread safe """ if not get_network_available("DEEZER"): return [] try: artist_formated = GLib.uri_escape_string( artist, None, True).replace(" ", "+") uri = "https://api.deezer.com/search/artist/?" +\ "q=%s&output=json&index=0&limit=1" % artist_formated (status, data) = App().task_helper.load_uri_content_sync( uri, cancellable) if status: uri = None decode = json.loads(data.decode("utf-8")) uri = decode["data"][0]["picture_xl"] return [uri] except Exception as e: Logger.warning("%s %s", e, artist) Logger.warning( "DownloaderArt::_get_deezer_artist_artwork_uri(): %s", data) return []
def __get_basename_for_sync(self): """ Get basename base on device content @return str """ names = [] try: if not self.__uri.startswith("mtp://") and\ self.__name != "Librem phone": return None # Search for previous sync d = Gio.File.new_for_uri(self.__uri) infos = d.enumerate_children("standard::name,standard::type", Gio.FileQueryInfoFlags.NONE, None) for info in infos: if info.get_file_type() != Gio.FileType.DIRECTORY: continue f = infos.get_child(info) uri = f.get_uri() + "/Music" previous_sync = Gio.File.new_for_uri("%s/unsync" % uri) if previous_sync.query_exists(): names.insert(0, info.get_name()) else: names.append(info.get_name()) infos.close(None) except Exception as e: Logger.error("DeviceWidget::__get_best_uri_for_sync: %s: %s" % (self.__uri, e)) return names
def _get_spotify_artist_artwork_uri(self, artist, cancellable=None): """ Get artist artwork using Spotify @param artist as str @param cancellable as Gio.Cancellable @return uri as str @tread safe """ if not get_network_available("SPOTIFY"): return [] try: artist_formated = GLib.uri_escape_string( artist, None, True).replace(" ", "+") uri = "https://api.spotify.com/v1/search?q=%s" % artist_formated +\ "&type=artist" token = App().ws_director.token_ws.get_token("SPOTIFY", cancellable) bearer = "Bearer %s" % token headers = [("Authorization", bearer)] (status, data) = App().task_helper.load_uri_content_sync_with_headers( uri, headers, cancellable) if status: uri = None decode = json.loads(data.decode("utf-8")) for item in decode["artists"]["items"]: if noaccents(item["name"].lower()) ==\ noaccents(artist.lower()): uri = item["images"][0]["url"] return [uri] except Exception as e: Logger.warning("%s %s", e, artist) Logger.warning( "DownloaderArt::_get_spotify_artist_artwork_uri(): %s", data) return []
def __save_in_db(self, storage_type): """ Save current tags into DB @param storage_type as StorageType @return [CollectionItem] """ items = [] notify_index = 0 previous_album_id = None for uri in list(self.__tags.keys()): # Handle a stop request if self.__thread is None: raise Exception("cancelled") Logger.debug("Adding file: %s" % uri) tags = self.__tags[uri] item = self.__add2db(uri, *tags, storage_type) items.append(item) self.__progress_count += 1 self.__update_progress(self.__progress_count, self.__progress_total, 0.001) if previous_album_id != item.album_id: self.__notify_ui(items[notify_index:]) notify_index = len(items) previous_album_id = item.album_id del self.__tags[uri] # Handle a stop request if self.__thread is None: raise Exception("cancelled") self.__notify_ui(items) return items