class Playlists(GObject.GObject): __gsignals__ = { 'playlist-created': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, )), 'playlist-deleted': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, )), 'playlist-updated': (GObject.SignalFlags.RUN_FIRST, None, (int, )), 'song-added-to-playlist': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media)), 'song-removed-from-playlist': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media)), } instance = None tracker = None def __repr__(self): return '<Playlists>' @classmethod def get_default(cls, tracker=None): if cls.instance: return cls.instance else: cls.instance = Playlists() return cls.instance @log def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker self._static_playlists = StaticPlaylists() grilo.connect('ready', self._on_grilo_ready) @log def _on_grilo_ready(self, data=None): """For all static playlists: get ID, if exists; if not, create the playlist and get ID.""" def playlist_id_fetched_cb(cursor, res, playlist): """ Called after the playlist id is fetched """ try: cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist.ID = cursor.get_integer(1) if not playlist.ID: # Create the static playlist self._create_static_playlist(playlist) else: # Update playlist self.update_static_playlist(playlist) def callback(obj, result, playlist): """ Starts retrieving the playlist id """ try: cursor = obj.query_finish(result) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return # Search for the playlist ID cursor.next_async(None, playlist_id_fetched_cb, playlist) # Start fetching all the static playlists for playlist in self._static_playlists.get_all(): self.tracker.query_async( Query.get_playlist_with_tag(playlist.TAG_TEXT), None, callback, playlist) @log def _create_static_playlist(self, playlist): """ Create the tag and the static playlist, and fetch the newly created playlist's songs. """ title = playlist.TITLE tag_text = playlist.TAG_TEXT def playlist_next_async_cb(cursor, res, playlist): """ Called after we finished moving the Tracker cursor, and ready to retrieve the playlist id""" # Update the playlist ID try: cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist.ID = cursor.get_integer(0) # Fetch the playlist contents self.update_static_playlist(playlist) def playlist_queried_cb(obj, res, playlist): """ Called after the playlist is created and the ID is fetched """ try: cursor = obj.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return cursor.next_async(None, playlist_next_async_cb, playlist) def playlist_created_cb(obj, res, playlist): """ Called when the static playlist is created """ data = obj.update_blank_finish(res) playlist_urn = data.get_child_value(0).get_child_value(0).\ get_child_value(0).get_child_value(1).get_string() query = Query.get_playlist_with_urn(playlist_urn) # Start fetching the playlist self.tracker.query_async(query, None, playlist_queried_cb, playlist) def tag_created_cb(obj, res, playlist): """ Called when the tag is created """ creation_query = Query.create_playlist_with_tag(title, tag_text) # Start creating the playlist itself self.tracker.update_blank_async(creation_query, GLib.PRIORITY_LOW, None, playlist_created_cb, playlist) # Start the playlist creation by creating the tag self.tracker.update_blank_async(Query.create_tag(tag_text), GLib.PRIORITY_LOW, None, tag_created_cb, playlist) @log def update_static_playlist(self, playlist): """Given a static playlist (subclass of StaticPlaylists), updates according to its query.""" # Clear the playlist self.clear_playlist(playlist) @log def clear_playlist(self, playlist): """Starts cleaning the playlist""" query = Query.clear_playlist_with_id(playlist.ID) self.tracker.update_async(query, GLib.PRIORITY_LOW, None, self._static_playlist_cleared_cb, playlist) @log def _static_playlist_cleared_cb(self, connection, res, playlist): """After clearing the playlist, start querying the playlist's songs""" # Get a list of matching songs self.tracker.query_async(playlist.QUERY, None, self._static_playlist_query_cb, playlist) @log def _static_playlist_query_cb(self, connection, res, playlist): """Fetch the playlist's songs""" final_query = '' # Get a list of matching songs try: cursor = connection.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return def callback(cursor, res, final_query): uri = cursor.get_string(0)[0] final_query += Query.add_song_to_playlist(playlist.ID, uri) try: has_next = cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) has_next = False # Only perform the update when the cursor reached the end if has_next: cursor.next_async(None, callback, final_query) return self.tracker.update_blank_async(final_query, GLib.PRIORITY_LOW, None, None, None) # tell system we updated the playlist so playlist is reloaded self.emit('playlist-updated', playlist.ID) # Asynchronously form the playlist's final query cursor.next_async(None, callback, final_query) @log def update_all_static_playlists(self): for playlist in self._static_playlists.get_all(): self.update_static_playlist(playlist) @log def create_playlist(self, title): def get_callback(source, param, item, count, data, error): if item: self.emit('playlist-created', item) def cursor_callback(cursor, res, data): try: has_next = cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist_id = cursor.get_integer(0) grilo.get_playlist_with_id(playlist_id, get_callback) def query_callback(conn, res, data): try: cursor = conn.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return if not cursor: return cursor.next_async(None, cursor_callback, data) def update_callback(conn, res, data): playlist_urn = conn.update_blank_finish(res)[0][0]['playlist'] self.tracker.query_async(Query.get_playlist_with_urn(playlist_urn), None, query_callback, None) self.tracker.update_blank_async(Query.create_playlist(title), GLib.PRIORITY_LOW, None, update_callback, None) @log def delete_playlist(self, item): def update_callback(conn, res, data): conn.update_finish(res) self.emit('playlist-deleted', item) self.tracker.update_async(Query.delete_playlist(item.get_id()), GLib.PRIORITY_LOW, None, update_callback, None) @log def add_to_playlist(self, playlist, items): def get_callback(source, param, item, count, data, error): if item: self.emit('song-added-to-playlist', playlist, item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return entry_id = cursor.get_integer(0) grilo.get_playlist_song_with_id(playlist.get_id(), entry_id, get_callback) def update_callback(conn, res, data): entry_urn = conn.update_blank_finish(res)[0][0]['entry'] self.tracker.query_async( Query.get_playlist_song_with_urn(entry_urn), None, query_callback, None) for item in items: uri = item.get_url() if not uri: continue self.tracker.update_blank_async( Query.add_song_to_playlist(playlist.get_id(), uri), GLib.PRIORITY_LOW, None, update_callback, None) @log def remove_from_playlist(self, playlist, items): def update_callback(conn, res, data): conn.update_finish(res) self.emit('song-removed-from-playlist', playlist, data) for item in items: self.tracker.update_async( Query.remove_song_from_playlist(playlist.get_id(), item.get_id()), GLib.PRIORITY_LOW, None, update_callback, item) @log def is_static_playlist(self, playlist): """Checks whether the given playlist is static or not :return: True if the playlist is static :rtype: bool """ for static_playlist_id in self._static_playlists.get_ids(): if playlist.get_id() == static_playlist_id: return True return False
class Grilo(GObject.GObject): __gsignals__ = { 'ready': (GObject.SignalFlags.RUN_FIRST, None, ()), 'changes-pending': (GObject.SignalFlags.RUN_FIRST, None, ()), 'new-source-added': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Source, )) } METADATA_KEYS = [ Grl.METADATA_KEY_ALBUM, Grl.METADATA_KEY_ALBUM_ARTIST, Grl.METADATA_KEY_ALBUM_DISC_NUMBER, Grl.METADATA_KEY_ARTIST, Grl.METADATA_KEY_CREATION_DATE, Grl.METADATA_KEY_COMPOSER, Grl.METADATA_KEY_DURATION, Grl.METADATA_KEY_FAVOURITE, Grl.METADATA_KEY_ID, Grl.METADATA_KEY_LYRICS, Grl.METADATA_KEY_PLAY_COUNT, Grl.METADATA_KEY_THUMBNAIL, Grl.METADATA_KEY_TITLE, Grl.METADATA_KEY_TRACK_NUMBER, Grl.METADATA_KEY_URL ] METADATA_THUMBNAIL_KEYS = [ Grl.METADATA_KEY_ID, Grl.METADATA_KEY_THUMBNAIL, ] CHANGED_MEDIA_MAX_ITEMS = 500 CHANGED_MEDIA_SIGNAL_TIMEOUT = 2000 def __repr__(self): return '<Grilo>' @log def __init__(self): GObject.GObject.__init__(self) self.playlist_path = GLib.build_filenamev( [GLib.get_user_data_dir(), "gnome-music", "playlists"]) if not (GLib.file_test(self.playlist_path, GLib.FileTest.IS_DIR)): GLib.mkdir_with_parents(self.playlist_path, int("0755", 8)) Grl.init(None) self.options = Grl.OperationOptions() self.options.set_resolution_flags(Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY) self.full_options = Grl.OperationOptions() self.full_options.set_resolution_flags( Grl.ResolutionFlags.FULL | Grl.ResolutionFlags.IDLE_RELAY) self.sources = {} self.blacklist = [ 'grl-filesystem', 'grl-bookmarks', 'grl-metadata-store', 'grl-podcasts' ] self.tracker = None self.changed_media_ids = [] self.pending_event_id = 0 self.changes_pending = { 'Albums': False, 'Artists': False, 'Songs': False } self.pending_changed_medias = [] self.registry = Grl.Registry.get_default() self.sparqltracker = TrackerWrapper().tracker @log def _find_sources(self): self.registry.connect('source_added', self._on_source_added) self.registry.connect('source_removed', self._on_source_removed) try: self.registry.load_all_plugins(True) except GLib.GError: logger.error('Failed to load plugins.') if self.tracker is not None: logger.debug("tracker found") def _rate_limited_content_changed(self, mediaSource, changedMedias, changeType, locationUnknown): [self.pending_changed_medias.append(media) for media in changedMedias] if self.content_changed_timeout is None: self.content_changed_timeout = GLib.timeout_add( 500, self._on_content_changed, mediaSource, self.pending_changed_medias, changeType, locationUnknown) @log def _on_content_changed(self, mediaSource, changedMedias, changeType, locationUnknown): try: with self.tracker.handler_block(self.notification_handler): for media in changedMedias: media_id = media.get_id() if changeType == Grl.SourceChangeType.ADDED: # Check that this media is an audio file mime_type = self.tracker.query_sync( Query.is_audio(media_id), [Grl.METADATA_KEY_MIME], self.options)[0].get_mime() if mime_type and mime_type.startswith("audio"): self.changed_media_ids.append(media_id) if changeType == Grl.SourceChangeType.REMOVED: # There is no way to check that removed item is a media # so always do the refresh # todo: remove one single url try: self.changed_media_ids.append(media.get_id()) except Exception as e: logger.warn("Skipping %s", media) if self.changed_media_ids == []: self.pending_changed_medias = [] if self.content_changed_timeout is not None: GLib.source_remove(self.content_changed_timeout) self.content_changed_timeout = None return False self.changed_media_ids = list(set(self.changed_media_ids)) logger.debug("Changed medias: %s", self.changed_media_ids) if len(self.changed_media_ids) >= self.CHANGED_MEDIA_MAX_ITEMS: self.emit_change_signal() elif self.changed_media_ids != []: if self.pending_event_id > 0: GLib.Source.remove(self.pending_event_id) self.pending_event_id = 0 self.pending_event_id = GLib.timeout_add( self.CHANGED_MEDIA_SIGNAL_TIMEOUT, self.emit_change_signal) except Exception as e: logger.warn("Exception in _on_content_changed: %s", e) finally: self.pending_changed_medias = [] if self.content_changed_timeout is not None: GLib.source_remove(self.content_changed_timeout) self.content_changed_timeout = None return False @log def emit_change_signal(self): self.changed_media_ids = [] self.pending_event_id = 0 self.changes_pending['Albums'] = True self.changes_pending['Artists'] = True self.changes_pending['Songs'] = True self.emit('changes-pending') return False @log def _on_source_added(self, pluginRegistry, mediaSource): if ("net:plaintext" in mediaSource.get_tags() or mediaSource.get_id() in self.blacklist): try: pluginRegistry.unregister_source(mediaSource) except GLib.GError: logger.error("Failed to unregister %s.", mediaSource.get_id()) return id = mediaSource.get_id() logger.debug("new grilo source %s was added", id) try: ops = mediaSource.supported_operations() if id == 'grl-tracker-source': if ops & Grl.SupportedOps.SEARCH: logger.debug("found searchable tracker source") self.sources[id] = mediaSource self.tracker = mediaSource self.search_source = mediaSource if self.tracker is not None: self.emit('ready') self.tracker.notify_change_start() self.content_changed_timeout = None self.notification_handler = self.tracker.connect( 'content-changed', self._rate_limited_content_changed) elif (id.startswith('grl-upnp')): logger.debug("found upnp source %s", id) self.sources[id] = mediaSource self.emit('new-source-added', mediaSource) elif (ops & Grl.SupportedOps.SEARCH and mediaSource.get_supported_media() & Grl.MediaType.AUDIO): logger.debug("source %s is searchable", id) self.sources[id] = mediaSource self.emit('new-source-added', mediaSource) except Exception as e: logger.debug("Source %s: exception %s", id, e) @log def _on_source_removed(self, pluginRegistry, mediaSource): pass @log def populate_artists(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_artists(), offset, callback, count) @log def populate_albums(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_albums(), offset, callback, count) @log def populate_songs(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_songs(), offset, callback, count) @log def populate_playlists(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_playlists(), offset, callback, count) @log def populate_album_songs(self, album, callback, count=-1): if album.get_source() == 'grl-tracker-source': GLib.idle_add(self.populate_items, Query.album_songs(album.get_id()), 0, callback, count) else: source = self.sources[album.get_source()] length = len(album.songs) for i, track in enumerate(album.songs): callback(source, None, track, length - (i + 1), None) callback(source, None, None, 0, None) @log def populate_playlist_songs(self, playlist, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.playlist_songs(playlist.get_id()), 0, callback, count) @log def populate_custom_query(self, query, callback, count=-1, data=None): self.populate_items(query, 0, callback, count, data) @log def populate_items(self, query, offset, callback, count=-1, data=None): options = self.options.copy() options.set_skip(offset) if count != -1: options.set_count(count) def _callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) self.tracker.query(query, self.METADATA_KEYS, options, _callback, data) @log def toggle_favorite(self, song_item): """Toggles favorite status for media item Toggles favorite status and writes it back to the tracker store :param song_item: A Grilo media item """ if song_item.get_favourite(): # For now keep unsetting the lyrics to deal with how # previous versions dealt with favorites. song_item.set_lyrics("") song_item.set_favourite(False) else: song_item.set_favourite(True) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(song_item, [Grl.METADATA_KEY_FAVOURITE], Grl.WriteFlags.NORMAL) @log def set_favorite(self, song_item, favorite): """Set the favorite status of a media item :param song_item: A Grilo media item :param bool favorite: Set favorite status """ if song_item.get_favourite() != favorite: self.toggle_favorite(song_item) @log def search(self, q, callback, data=None): options = self.options.copy() self._search_callback_counter = 0 @log def _search_callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) self._search_callback_counter += 1 @log def _multiple_search_callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) if self.search_source: if self.search_source.get_id().startswith('grl-upnp'): options.set_type_filter(Grl.TypeFilter.AUDIO) self.search_source.search(q, self.METADATA_KEYS, options, _search_callback, data) else: Grl.multiple_search([ self.sources[key] for key in self.sources if key != 'grl-tracker-source' ], q, self.METADATA_KEYS, options, _multiple_search_callback, data) @log def get_album_art_for_item(self, item, callback): item_id = item.get_id() if item.is_audio(): query = Query.get_album_for_song_id(item_id) else: query = Query.get_album_for_album_id(item_id) options = self.full_options.copy() options.set_count(1) self.search_source.query(query, self.METADATA_THUMBNAIL_KEYS, options, callback) @log def get_playlist_with_id(self, playlist_id, callback): options = self.options.copy() query = Query.get_playlist_with_id(playlist_id) self.tracker.query(query, self.METADATA_KEYS, options, callback, None) @log def get_playlist_song_with_id(self, playlist_id, entry_id, callback): options = self.options.copy() query = Query.get_playlist_song_with_id(playlist_id, entry_id) self.tracker.query(query, self.METADATA_KEYS, options, callback, None) @log def bump_play_count(self, media): """Bumps the play count of a song Adds one to the playcount and adds it to the tracker store :param media: A Grilo media item """ count = media.get_play_count() media.set_play_count(count + 1) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_PLAY_COUNT], Grl.WriteFlags.NORMAL) @log def set_last_played(self, media): """Sets the date-time when the media was last played Sets the last played date-time for the media. :param media: A Grilo media item """ media.set_last_played(GLib.DateTime.new_now_utc()) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_LAST_PLAYED], Grl.WriteFlags.NORMAL) @log def songs_available(self, callback): """Checks if there are any songs available Calls a callback function with True or False depending on the availability of songs. :param callback: Function to call on result """ def cursor_next_cb(conn, res, data): try: has_next = conn.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return if has_next: count = conn.get_integer(0) if count > 0: callback(True) return callback(False) def songs_query_cb(conn, res, data): try: cursor = conn.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return cursor.next_async(None, cursor_next_cb, None) # TODO: currently just checks tracker, should work with any # queryable supported Grilo source. self.sparqltracker.query_async(Query.all_songs_count(), None, songs_query_cb, None) @log def playlists_available(self, callback): """Checks if there are any non-static playlists available Calls a callback function with True or False depending on the availability of playlists. :param callback: Function to call on result """ def cursor_next_cb(conn, res, data): try: has_next = conn.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return if has_next: count = conn.get_integer(0) if count > 0: callback(True) return callback(False) def playlists_query_cb(conn, res, data): try: cursor = conn.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return cursor.next_async(None, cursor_next_cb, None) # TODO: currently just checks tracker, should work with any # queryable supported Grilo source. self.sparqltracker.query_async(Query.all_non_static_playlists_count(), None, playlists_query_cb, None)
class Playlists(GObject.GObject): __gsignals__ = { 'playlist-created': (GObject.SIGNAL_RUN_FIRST, None, (Grl.Media,)), 'playlist-deleted': (GObject.SIGNAL_RUN_FIRST, None, (Grl.Media,)), 'playlist-updated': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 'song-added-to-playlist': ( GObject.SIGNAL_RUN_FIRST, None, (Grl.Media, Grl.Media) ), 'song-removed-from-playlist': ( GObject.SIGNAL_RUN_FIRST, None, (Grl.Media, Grl.Media) ), } instance = None tracker = None @classmethod def get_default(self, tracker=None): if self.instance: return self.instance else: self.instance = Playlists() return self.instance @log def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker @log def fetch_or_create_static_playlists(self): """For all static playlists: get ID, if exists; if not, create the playlist and get ID.""" playlists = [cls for name, cls in inspect.getmembers(StaticPlaylists) if inspect.isclass(cls) and not (name == "__class__")] # hacky def callback(obj, result, playlist): cursor = obj.query_finish(result) while (cursor.next(None)): playlist.ID = cursor.get_integer(1) if not playlist.ID: # create the playlist playlist.ID = self.create_playlist_and_return_id(playlist.TITLE, playlist.TAG_TEXT) self.update_static_playlist(playlist) for playlist in playlists: self.tracker.query_async( Query.get_playlist_with_tag(playlist.TAG_TEXT), None, callback, playlist) @log def clear_playlist_with_id(self, playlist_id): query = Query.clear_playlist_with_id(playlist_id) self.tracker.update(query, GLib.PRIORITY_DEFAULT, None) @log def update_playcount(self, song_url): query = Query.update_playcount(song_url) self.tracker.update(query, GLib.PRIORITY_DEFAULT, None) @log def update_last_played(self, song_url): cur_time = time.strftime(sparql_dateTime_format, time.gmtime()) query = Query.update_last_played(song_url, cur_time) self.tracker.update(query, GLib.PRIORITY_DEFAULT, None) @log def update_static_playlist(self, playlist): """Given a static playlist (subclass of StaticPlaylists), updates according to its query.""" # Clear the playlist self.clear_playlist_with_id(playlist.ID) final_query = '' # Get a list of matching songs cursor = self.tracker.query(playlist.QUERY, None) if not cursor: return # For each song run 'add song to playlist' while cursor.next(): uri = cursor.get_string(0)[0] final_query += Query.add_song_to_playlist(playlist.ID, uri) self.tracker.update_blank_async(final_query, GLib.PRIORITY_DEFAULT, None, None, None) # tell system we updated the playlist so playlist is reloaded self.emit('playlist-updated', playlist.ID) @log def update_all_static_playlists(self): playlists = [cls for name, cls in inspect.getmembers(StaticPlaylists) if inspect.isclass(cls) and not (name == "__class__")] # hacky for playlist in playlists: self.update_static_playlist(playlist) @log def create_playlist_and_return_id(self, title, tag_text): self.tracker.update_blank(Query.create_tag(tag_text), GLib.PRIORITY_DEFAULT, None) data = self.tracker.update_blank( Query.create_playlist_with_tag(title, tag_text), GLib.PRIORITY_DEFAULT, None) playlist_urn = data.get_child_value(0).get_child_value(0).\ get_child_value(0).get_child_value(1).get_string() cursor = self.tracker.query( Query.get_playlist_with_urn(playlist_urn), None) if not cursor or not cursor.next(): return return cursor.get_integer(0) @log def create_playlist(self, title): def get_callback(source, param, item, count, data, error): if item: self.emit('playlist-created', item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return playlist_id = cursor.get_integer(0) grilo.get_playlist_with_id(playlist_id, get_callback) def update_callback(conn, res, data): playlist_urn = conn.update_blank_finish(res)[0][0]['playlist'] self.tracker.query_async( Query.get_playlist_with_urn(playlist_urn), None, query_callback, None ) self.tracker.update_blank_async( Query.create_playlist(title), GLib.PRIORITY_DEFAULT, None, update_callback, None ) @log def delete_playlist(self, item): def update_callback(conn, res, data): conn.update_finish(res) self.emit('playlist-deleted', item) self.tracker.update_async( Query.delete_playlist(item.get_id()), GLib.PRIORITY_DEFAULT, None, update_callback, None ) @log def add_to_playlist(self, playlist, items): def get_callback(source, param, item, count, data, error): if item: self.emit('song-added-to-playlist', playlist, item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return entry_id = cursor.get_integer(0) grilo.get_playlist_song_with_id( playlist.get_id(), entry_id, get_callback ) def update_callback(conn, res, data): entry_urn = conn.update_blank_finish(res)[0][0]['entry'] self.tracker.query_async( Query.get_playlist_song_with_urn(entry_urn), None, query_callback, None ) for item in items: uri = item.get_url() if not uri: continue self.tracker.update_blank_async( Query.add_song_to_playlist(playlist.get_id(), uri), GLib.PRIORITY_DEFAULT, None, update_callback, None ) @log def remove_from_playlist(self, playlist, items): def update_callback(conn, res, data): conn.update_finish(res) self.emit('song-removed-from-playlist', playlist, data) for item in items: self.tracker.update_async( Query.remove_song_from_playlist( playlist.get_id(), item.get_id() ), GLib.PRIORITY_DEFAULT, None, update_callback, item )
class Grilo(GObject.GObject): __gsignals__ = { 'ready': (GObject.SignalFlags.RUN_FIRST, None, ()), 'changes-pending': (GObject.SignalFlags.RUN_FIRST, None, ()), 'new-source-added': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Source, )) } METADATA_KEYS = [ Grl.METADATA_KEY_ALBUM, Grl.METADATA_KEY_ALBUM_ARTIST, Grl.METADATA_KEY_ALBUM_DISC_NUMBER, Grl.METADATA_KEY_ARTIST, Grl.METADATA_KEY_CREATION_DATE, Grl.METADATA_KEY_COMPOSER, Grl.METADATA_KEY_DURATION, Grl.METADATA_KEY_FAVOURITE, Grl.METADATA_KEY_ID, Grl.METADATA_KEY_LYRICS, Grl.METADATA_KEY_PLAY_COUNT, Grl.METADATA_KEY_THUMBNAIL, Grl.METADATA_KEY_TITLE, Grl.METADATA_KEY_TRACK_NUMBER, Grl.METADATA_KEY_URL ] METADATA_THUMBNAIL_KEYS = [ Grl.METADATA_KEY_ID, Grl.METADATA_KEY_THUMBNAIL, ] CHANGED_MEDIA_MAX_ITEMS = 500 CHANGED_MEDIA_SIGNAL_TIMEOUT = 2000 def __repr__(self): return '<Grilo>' @log def __init__(self): GObject.GObject.__init__(self) self.playlist_path = GLib.build_filenamev([GLib.get_user_data_dir(), "gnome-music", "playlists"]) if not (GLib.file_test(self.playlist_path, GLib.FileTest.IS_DIR)): GLib.mkdir_with_parents(self.playlist_path, int("0755", 8)) Grl.init(None) self.options = Grl.OperationOptions() self.options.set_resolution_flags(Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY) self.full_options = Grl.OperationOptions() self.full_options.set_resolution_flags(Grl.ResolutionFlags.FULL | Grl.ResolutionFlags.IDLE_RELAY) self.sources = {} self.blacklist = ['grl-filesystem', 'grl-bookmarks', 'grl-metadata-store', 'grl-podcasts'] self.tracker = None self.changed_media_ids = [] self.pending_event_id = 0 self.changes_pending = {'Albums': False, 'Artists': False, 'Songs': False} self.pending_changed_medias = [] self.registry = Grl.Registry.get_default() self.sparqltracker = TrackerWrapper().tracker @log def _find_sources(self): self.registry.connect('source_added', self._on_source_added) self.registry.connect('source_removed', self._on_source_removed) try: self.registry.load_all_plugins(True) except GLib.GError: logger.error('Failed to load plugins.') if self.tracker is not None: logger.debug("tracker found") def _rate_limited_content_changed(self, mediaSource, changedMedias, changeType, locationUnknown): [self.pending_changed_medias.append(media) for media in changedMedias] if self.content_changed_timeout is None: self.content_changed_timeout = GLib.timeout_add( 500, self._on_content_changed, mediaSource, self.pending_changed_medias, changeType, locationUnknown) @log def _on_content_changed(self, mediaSource, changedMedias, changeType, locationUnknown): try: with self.tracker.handler_block(self.notification_handler): for media in changedMedias: media_id = media.get_id() if changeType == Grl.SourceChangeType.ADDED: # Check that this media is an audio file mime_type = self.tracker.query_sync( Query.is_audio(media_id), [Grl.METADATA_KEY_MIME], self.options)[0].get_mime() if mime_type and mime_type.startswith("audio"): self.changed_media_ids.append(media_id) if changeType == Grl.SourceChangeType.REMOVED: # There is no way to check that removed item is a media # so always do the refresh # todo: remove one single url try: self.changed_media_ids.append(media.get_id()) except Exception as e: logger.warn("Skipping %s", media) if self.changed_media_ids == []: self.pending_changed_medias = [] if self.content_changed_timeout is not None: GLib.source_remove(self.content_changed_timeout) self.content_changed_timeout = None return False self.changed_media_ids = list(set(self.changed_media_ids)) logger.debug("Changed medias: %s", self.changed_media_ids) if len(self.changed_media_ids) >= self.CHANGED_MEDIA_MAX_ITEMS: self.emit_change_signal() elif self.changed_media_ids != []: if self.pending_event_id > 0: GLib.Source.remove(self.pending_event_id) self.pending_event_id = 0 self.pending_event_id = GLib.timeout_add(self.CHANGED_MEDIA_SIGNAL_TIMEOUT, self.emit_change_signal) except Exception as e: logger.warn("Exception in _on_content_changed: %s", e) finally: self.pending_changed_medias = [] if self.content_changed_timeout is not None: GLib.source_remove(self.content_changed_timeout) self.content_changed_timeout = None return False @log def emit_change_signal(self): self.changed_media_ids = [] self.pending_event_id = 0 self.changes_pending['Albums'] = True self.changes_pending['Artists'] = True self.changes_pending['Songs'] = True self.emit('changes-pending') return False @log def _on_source_added(self, pluginRegistry, mediaSource): if ("net:plaintext" in mediaSource.get_tags() or mediaSource.get_id() in self.blacklist): try: pluginRegistry.unregister_source(mediaSource) except GLib.GError: logger.error("Failed to unregister %s.", mediaSource.get_id()) return id = mediaSource.get_id() logger.debug("new grilo source %s was added", id) try: ops = mediaSource.supported_operations() if id == 'grl-tracker-source': if ops & Grl.SupportedOps.SEARCH: logger.debug("found searchable tracker source") self.sources[id] = mediaSource self.tracker = mediaSource self.search_source = mediaSource if self.tracker is not None: self.emit('ready') self.tracker.notify_change_start() self.content_changed_timeout = None self.notification_handler = self.tracker.connect( 'content-changed', self._rate_limited_content_changed) elif (id.startswith('grl-upnp')): logger.debug("found upnp source %s", id) self.sources[id] = mediaSource self.emit('new-source-added', mediaSource) elif (ops & Grl.SupportedOps.SEARCH and mediaSource.get_supported_media() & Grl.MediaType.AUDIO): logger.debug("source %s is searchable", id) self.sources[id] = mediaSource self.emit('new-source-added', mediaSource) except Exception as e: logger.debug("Source %s: exception %s", id, e) @log def _on_source_removed(self, pluginRegistry, mediaSource): pass @log def populate_artists(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_artists(), offset, callback, count) @log def populate_albums(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_albums(), offset, callback, count) @log def populate_songs(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_songs(), offset, callback, count) @log def populate_playlists(self, offset, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.all_playlists(), offset, callback, count) @log def populate_album_songs(self, album, callback, count=-1): if album.get_source() == 'grl-tracker-source': GLib.idle_add(self.populate_items, Query.album_songs(album.get_id()), 0, callback, count) else: source = self.sources[album.get_source()] length = len(album.tracks) for i, track in enumerate(album.tracks): callback(source, None, track, length - (i + 1), None) callback(source, None, None, 0, None) @log def populate_playlist_songs(self, playlist, callback, count=-1): if self.tracker: GLib.idle_add(self.populate_items, Query.playlist_songs(playlist.get_id()), 0, callback, count) @log def populate_custom_query(self, query, callback, count=-1, data=None): self.populate_items(query, 0, callback, count, data) @log def populate_items(self, query, offset, callback, count=-1, data=None): options = self.options.copy() options.set_skip(offset) if count != -1: options.set_count(count) def _callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) self.tracker.query(query, self.METADATA_KEYS, options, _callback, data) @log def toggle_favorite(self, song_item): """Toggles favorite status for media item Toggles favorite status and writes it back to the tracker store :param song_item: A Grilo media item """ if song_item.get_favourite(): # For now keep unsetting the lyrics to deal with how # previous versions dealt with favorites. song_item.set_lyrics("") song_item.set_favourite(False) else: song_item.set_favourite(True) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(song_item, [Grl.METADATA_KEY_FAVOURITE], Grl.WriteFlags.NORMAL) @log def set_favorite(self, song_item, favorite): """Set the favorite status of a media item :param song_item: A Grilo media item :param bool favorite: Set favorite status """ if song_item.get_favourite() != favorite: self.toggle_favorite(song_item) @log def search(self, q, callback, data=None): options = self.options.copy() self._search_callback_counter = 0 @log def _search_callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) self._search_callback_counter += 1 @log def _multiple_search_callback(source, param, item, remaining, data, error): callback(source, param, item, remaining, data) if self.search_source: if self.search_source.get_id().startswith('grl-upnp'): options.set_type_filter(Grl.TypeFilter.AUDIO) self.search_source.search(q, self.METADATA_KEYS, options, _search_callback, data) else: Grl.multiple_search([self.sources[key] for key in self.sources if key != 'grl-tracker-source'], q, self.METADATA_KEYS, options, _multiple_search_callback, data) @log def get_album_art_for_item(self, item, callback): item_id = item.get_id() if item.is_audio(): query = Query.get_album_for_song_id(item_id) else: query = Query.get_album_for_album_id(item_id) options = self.full_options.copy() options.set_count(1) self.search_source.query(query, self.METADATA_THUMBNAIL_KEYS, options, callback) @log def get_playlist_with_id(self, playlist_id, callback): options = self.options.copy() query = Query.get_playlist_with_id(playlist_id) self.tracker.query(query, self.METADATA_KEYS, options, callback, None) @log def get_playlist_song_with_id(self, playlist_id, entry_id, callback): options = self.options.copy() query = Query.get_playlist_song_with_id(playlist_id, entry_id) self.tracker.query(query, self.METADATA_KEYS, options, callback, None) @log def bump_play_count(self, media): """Bumps the play count of a song Adds one to the playcount and adds it to the tracker store :param media: A Grilo media item """ count = media.get_play_count() media.set_play_count(count + 1) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_PLAY_COUNT], Grl.WriteFlags.NORMAL) @log def set_last_played(self, media): """Sets the date-time when the media was last played Sets the last played date-time for the media. :param media: A Grilo media item """ media.set_last_played(GLib.DateTime.new_now_utc()) # FIXME: We assume this is the tracker plugin. # FIXME: Doing this async crashes self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_LAST_PLAYED], Grl.WriteFlags.NORMAL) @log def songs_available(self, callback): """Checks if there are any songs available Calls a callback function with True or False depending on the availability of songs. :param callback: Function to call on result """ def cursor_next_cb(conn, res, data): try: has_next = conn.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return if has_next: count = conn.get_integer(0) if count > 0: callback(True) return callback(False) def songs_query_cb(conn, res, data): try: cursor = conn.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) callback(False) return cursor.next_async(None, cursor_next_cb, None) # TODO: currently just checks tracker, should work with any # queryable supported Grilo source. self.sparqltracker.query_async(Query.all_songs_count(), None, songs_query_cb, None)
class Playlists(GObject.GObject): __gsignals__ = { 'playlist-created': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media,)), 'playlist-deleted': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media,)), 'playlist-updated': (GObject.SignalFlags.RUN_FIRST, None, (int,)), 'song-added-to-playlist': ( GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media) ), 'song-removed-from-playlist': ( GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media) ), } instance = None tracker = None def __repr__(self): return '<Playlists>' @classmethod def get_default(cls, tracker=None): if cls.instance: return cls.instance else: cls.instance = Playlists() return cls.instance @log def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker self._static_playlists = StaticPlaylists() grilo.connect('ready', self._on_grilo_ready) @log def _on_grilo_ready(self, data=None): """For all static playlists: get ID, if exists; if not, create the playlist and get ID.""" def playlist_id_fetched_cb(cursor, res, playlist): """ Called after the playlist id is fetched """ try: cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist.ID = cursor.get_integer(1) if not playlist.ID: # Create the static playlist self._create_static_playlist(playlist) else: # Update playlist self.update_static_playlist(playlist) def callback(obj, result, playlist): """ Starts retrieving the playlist id """ try: cursor = obj.query_finish(result) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return # Search for the playlist ID cursor.next_async(None, playlist_id_fetched_cb, playlist) # Start fetching all the static playlists for playlist in self._static_playlists.get_all(): self.tracker.query_async( Query.get_playlist_with_tag(playlist.TAG_TEXT), None, callback, playlist) @log def _create_static_playlist(self, playlist): """ Create the tag and the static playlist, and fetch the newly created playlist's songs. """ title = playlist.TITLE tag_text = playlist.TAG_TEXT def playlist_next_async_cb(cursor, res, playlist): """ Called after we finished moving the Tracker cursor, and ready to retrieve the playlist id""" # Update the playlist ID try: cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist.ID = cursor.get_integer(0) # Fetch the playlist contents self.update_static_playlist(playlist) def playlist_queried_cb(obj, res, playlist): """ Called after the playlist is created and the ID is fetched """ try: cursor = obj.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return cursor.next_async(None, playlist_next_async_cb, playlist) def playlist_created_cb(obj, res, playlist): """ Called when the static playlist is created """ data = obj.update_blank_finish(res) playlist_urn = data.get_child_value(0).get_child_value(0).\ get_child_value(0).get_child_value(1).get_string() query = Query.get_playlist_with_urn(playlist_urn) # Start fetching the playlist self.tracker.query_async(query, None, playlist_queried_cb, playlist) def tag_created_cb(obj, res, playlist): """ Called when the tag is created """ creation_query = Query.create_playlist_with_tag(title, tag_text) # Start creating the playlist itself self.tracker.update_blank_async(creation_query, GLib.PRIORITY_LOW, None, playlist_created_cb, playlist) # Start the playlist creation by creating the tag self.tracker.update_blank_async(Query.create_tag(tag_text), GLib.PRIORITY_LOW, None, tag_created_cb, playlist) @log def update_static_playlist(self, playlist): """Given a static playlist (subclass of StaticPlaylists), updates according to its query.""" # Clear the playlist self.clear_playlist(playlist) @log def clear_playlist(self, playlist): """Starts cleaning the playlist""" query = Query.clear_playlist_with_id(playlist.ID) self.tracker.update_async(query, GLib.PRIORITY_LOW, None, self._static_playlist_cleared_cb, playlist) @log def _static_playlist_cleared_cb(self, connection, res, playlist): """After clearing the playlist, start querying the playlist's songs""" # Get a list of matching songs self.tracker.query_async(playlist.QUERY, None, self._static_playlist_query_cb, playlist) @log def _static_playlist_query_cb(self, connection, res, playlist): """Fetch the playlist's songs""" final_query = '' # Get a list of matching songs try: cursor = connection.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return def callback(cursor, res, final_query): uri = cursor.get_string(0)[0] final_query += Query.add_song_to_playlist(playlist.ID, uri) try: has_next = cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) has_next = False # Only perform the update when the cursor reached the end if has_next: cursor.next_async(None, callback, final_query) return self.tracker.update_blank_async(final_query, GLib.PRIORITY_LOW, None, None, None) # tell system we updated the playlist so playlist is reloaded self.emit('playlist-updated', playlist.ID) # Asynchronously form the playlist's final query cursor.next_async(None, callback, final_query) @log def update_all_static_playlists(self): for playlist in self._static_playlists.get_all(): self.update_static_playlist(playlist) @log def create_playlist(self, title): def get_callback(source, param, item, count, data, error): if item: self.emit('playlist-created', item) def cursor_callback(cursor, res, data): try: has_next = cursor.next_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return playlist_id = cursor.get_integer(0) grilo.get_playlist_with_id(playlist_id, get_callback) def query_callback(conn, res, data): try: cursor = conn.query_finish(res) except GLib.Error as err: logger.warn("Error: %s, %s", err.__class__, err) return if not cursor: return cursor.next_async(None, cursor_callback, data) def update_callback(conn, res, data): playlist_urn = conn.update_blank_finish(res)[0][0]['playlist'] self.tracker.query_async( Query.get_playlist_with_urn(playlist_urn), None, query_callback, None ) self.tracker.update_blank_async( Query.create_playlist(title), GLib.PRIORITY_LOW, None, update_callback, None ) @log def delete_playlist(self, item): def update_callback(conn, res, data): conn.update_finish(res) self.emit('playlist-deleted', item) self.tracker.update_async( Query.delete_playlist(item.get_id()), GLib.PRIORITY_LOW, None, update_callback, None ) @log def add_to_playlist(self, playlist, items): def get_callback(source, param, item, count, data, error): if item: self.emit('song-added-to-playlist', playlist, item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return entry_id = cursor.get_integer(0) grilo.get_playlist_song_with_id( playlist.get_id(), entry_id, get_callback ) def update_callback(conn, res, data): entry_urn = conn.update_blank_finish(res)[0][0]['entry'] self.tracker.query_async( Query.get_playlist_song_with_urn(entry_urn), None, query_callback, None ) for item in items: uri = item.get_url() if not uri: continue self.tracker.update_blank_async( Query.add_song_to_playlist(playlist.get_id(), uri), GLib.PRIORITY_LOW, None, update_callback, None ) @log def remove_from_playlist(self, playlist, items): def update_callback(conn, res, data): conn.update_finish(res) self.emit('song-removed-from-playlist', playlist, data) for item in items: self.tracker.update_async( Query.remove_song_from_playlist( playlist.get_id(), item.get_id() ), GLib.PRIORITY_LOW, None, update_callback, item ) @log def is_static_playlist(self, playlist): """Checks whether the given playlist is static or not :return: True if the playlist is static :rtype: bool """ for static_playlist_id in self._static_playlists.get_ids(): if playlist.get_id() == static_playlist_id: return True return False
class Playlists(GObject.GObject): __gsignals__ = { 'playlist-created': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media,)), 'playlist-deleted': (GObject.SignalFlags.RUN_FIRST, None, (Grl.Media,)), 'playlist-updated': (GObject.SignalFlags.RUN_FIRST, None, (int,)), 'song-added-to-playlist': ( GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media) ), 'song-removed-from-playlist': ( GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media) ), } instance = None tracker = None def __repr__(self): return '<Playlists>' @classmethod def get_default(cls, tracker=None): if cls.instance: return cls.instance else: cls.instance = Playlists() return cls.instance @log def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker StaticPlaylists() @log def fetch_or_create_static_playlists(self): """For all static playlists: get ID, if exists; if not, create the playlist and get ID.""" playlists = [cls for name, cls in inspect.getmembers(StaticPlaylists) if inspect.isclass(cls) and not (name == "__class__")] # hacky def callback(obj, result, playlist): cursor = obj.query_finish(result) while (cursor.next(None)): playlist.ID = cursor.get_integer(1) if not playlist.ID: # create the playlist playlist.ID = self.create_playlist_and_return_id(playlist.TITLE, playlist.TAG_TEXT) self.update_static_playlist(playlist) for playlist in playlists: self.tracker.query_async( Query.get_playlist_with_tag(playlist.TAG_TEXT), None, callback, playlist) @log def clear_playlist_with_id(self, playlist_id): query = Query.clear_playlist_with_id(playlist_id) self.tracker.update(query, GLib.PRIORITY_LOW, None) @log def update_playcount(self, song_url): query = Query.update_playcount(song_url) self.tracker.update(query, GLib.PRIORITY_LOW, None) @log def update_last_played(self, song_url): cur_time = time.strftime(sparql_dateTime_format, time.gmtime()) query = Query.update_last_played(song_url, cur_time) self.tracker.update(query, GLib.PRIORITY_LOW, None) @log def update_static_playlist(self, playlist): """Given a static playlist (subclass of StaticPlaylists), updates according to its query.""" # Clear the playlist self.clear_playlist_with_id(playlist.ID) final_query = '' # Get a list of matching songs cursor = self.tracker.query(playlist.QUERY, None) if not cursor: return # For each song run 'add song to playlist' while cursor.next(): uri = cursor.get_string(0)[0] final_query += Query.add_song_to_playlist(playlist.ID, uri) self.tracker.update_blank_async(final_query, GLib.PRIORITY_LOW, None, None, None) # tell system we updated the playlist so playlist is reloaded self.emit('playlist-updated', playlist.ID) @log def update_all_static_playlists(self): playlists = [cls for name, cls in inspect.getmembers(StaticPlaylists) if inspect.isclass(cls) and not (name == "__class__")] # hacky for playlist in playlists: self.update_static_playlist(playlist) @log def create_playlist_and_return_id(self, title, tag_text): self.tracker.update_blank(Query.create_tag(tag_text), GLib.PRIORITY_LOW, None) data = self.tracker.update_blank( Query.create_playlist_with_tag(title, tag_text), GLib.PRIORITY_LOW, None) playlist_urn = data.get_child_value(0).get_child_value(0).\ get_child_value(0).get_child_value(1).get_string() cursor = self.tracker.query( Query.get_playlist_with_urn(playlist_urn), None) if not cursor or not cursor.next(): return return cursor.get_integer(0) @log def create_playlist(self, title): def get_callback(source, param, item, count, data, error): if item: self.emit('playlist-created', item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return playlist_id = cursor.get_integer(0) grilo.get_playlist_with_id(playlist_id, get_callback) def update_callback(conn, res, data): playlist_urn = conn.update_blank_finish(res)[0][0]['playlist'] self.tracker.query_async( Query.get_playlist_with_urn(playlist_urn), None, query_callback, None ) self.tracker.update_blank_async( Query.create_playlist(title), GLib.PRIORITY_LOW, None, update_callback, None ) @log def delete_playlist(self, item): def update_callback(conn, res, data): conn.update_finish(res) self.emit('playlist-deleted', item) self.tracker.update_async( Query.delete_playlist(item.get_id()), GLib.PRIORITY_LOW, None, update_callback, None ) @log def add_to_playlist(self, playlist, items): def get_callback(source, param, item, count, data, error): if item: self.emit('song-added-to-playlist', playlist, item) def query_callback(conn, res, data): cursor = conn.query_finish(res) if not cursor or not cursor.next(): return entry_id = cursor.get_integer(0) grilo.get_playlist_song_with_id( playlist.get_id(), entry_id, get_callback ) def update_callback(conn, res, data): entry_urn = conn.update_blank_finish(res)[0][0]['entry'] self.tracker.query_async( Query.get_playlist_song_with_urn(entry_urn), None, query_callback, None ) for item in items: uri = item.get_url() if not uri: continue self.tracker.update_blank_async( Query.add_song_to_playlist(playlist.get_id(), uri), GLib.PRIORITY_LOW, None, update_callback, None ) @log def remove_from_playlist(self, playlist, items): def update_callback(conn, res, data): conn.update_finish(res) self.emit('song-removed-from-playlist', playlist, data) for item in items: self.tracker.update_async( Query.remove_song_from_playlist( playlist.get_id(), item.get_id() ), GLib.PRIORITY_LOW, None, update_callback, item )