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)) 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
class Grilo(GObject.GObject): __gsignals__ = { 'ready': (GObject.SIGNAL_RUN_FIRST, None, ()), 'changes-pending': (GObject.SIGNAL_RUN_FIRST, None, ()), 'new-source-added': (GObject.SIGNAL_RUN_FIRST, None, (Grl.Source, )) } METADATA_KEYS = [ Grl.METADATA_KEY_ID, Grl.METADATA_KEY_TITLE, Grl.METADATA_KEY_ARTIST, Grl.METADATA_KEY_ALBUM, Grl.METADATA_KEY_DURATION, Grl.METADATA_KEY_CREATION_DATE, Grl.METADATA_KEY_URL, Grl.METADATA_KEY_LYRICS, Grl.METADATA_KEY_THUMBNAIL] METADATA_THUMBNAIL_KEYS = [ Grl.METADATA_KEY_ID, Grl.METADATA_KEY_THUMBNAIL, ] CHANGED_MEDIA_MAX_ITEMS = 500 CHANGED_MEDIA_SIGNAL_TIMEOUT = 2000 @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)) self.options = Grl.OperationOptions() self.options.set_flags(Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY) self.full_options = Grl.OperationOptions() self.full_options.set_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.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() except GLib.GError: logger.error('Failed to load plugins.') if self.tracker is not None: logger.debug("tracker found") @log def _on_content_changed(self, mediaSource, changedMedias, changeType, locationUnknown): try: for media in changedMedias: media_id = media.get_id() if changeType == Grl.SourceChangeType.ADDED: # Check that this media is an audio file query = "select DISTINCT rdf:type nie:mimeType(?urn) as mime-type" +\ " { ?urn rdf:type nie:InformationElement . FILTER (tracker:id(?urn) = %s) }" % media_id mimeType = grilo.tracker.query_sync(query, [Grl.METADATA_KEY_MIME], grilo.options)[0].get_mime() if mimeType and mimeType.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 self.changed_media_ids.append(media.get_id()) 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) @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): 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.tracker.connect('content-changed', self._on_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 (id not in self.blacklist) and (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): self.populate_items(Query.all_artists(), offset, callback, count) @log def populate_albums(self, offset, callback, count=-1): self.populate_items(Query.all_albums(), offset, callback, count) @log def populate_songs(self, offset, callback, count=-1): self.populate_items(Query.all_songs(), offset, callback, count) @log def populate_playlists(self, offset, callback, count=-1): 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': 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): 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): # TODO: change "bool(song_item.get_lyrics())" --> song_item.get_favourite() once query works properly # TODO: when .set/get_favourite work, set_favourite outside loop: item.set_favourite(!item.get_favourite()) if bool(song_item.get_lyrics()): # is favorite self.sparqltracker.update(Query.remove_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, None) song_item.set_lyrics("") else: # not favorite self.sparqltracker.update(Query.add_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, None) song_item.set_lyrics("i'm truthy") @log def search(self, q, callback, data=None): options = self.options.copy() @log def _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, _search_callback, data) @log def get_album_art_for_item(self, item, callback, data=None): item_id = item.get_id() query = None if isinstance(item, Grl.MediaAudio): 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, data) @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)
def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker
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 )
def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker self._static_playlists = StaticPlaylists() grilo.connect('ready', self._on_grilo_ready)
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
def __init__(self): GObject.GObject.__init__(self) self.tracker = TrackerWrapper().tracker StaticPlaylists()
from gi.repository import Gtk, Gdk, Gio, GLib, Gd from gettext import gettext as _, ngettext from gnomemusic import TrackerWrapper from gnomemusic.toolbar import Toolbar, ToolbarState from gnomemusic.player import Player, SelectionToolbar from gnomemusic.query import Query import gnomemusic.view as Views import gnomemusic.widgets as Widgets from gnomemusic.playlists import Playlists from gnomemusic.grilo import grilo from gnomemusic import log import logging logger = logging.getLogger(__name__) tracker = TrackerWrapper().tracker playlist = Playlists.get_default() class Window(Gtk.ApplicationWindow): def __repr__(self): return '<Window>' @log def __init__(self, app): Gtk.ApplicationWindow.__init__(self, application=app, title=_("Music")) self.connect('focus-in-event', self._windows_focus_cb) self.settings = Gio.Settings.new('org.gnome.Music') self.add_action(self.settings.create_action('repeat')) selectAll = Gio.SimpleAction.new('selectAll', None) app.add_accelerator('<Primary>a', 'win.selectAll', None)
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): super().__init__() 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', 'grl-spotify-cover' ] 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 and media.is_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: logger.warning("Skipping {}".format(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 error: logger.warning( "Exception in _on_content_changed: {}".format(error)) 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 {}".format( 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 {}: exception {}".format(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): """Asynchronously get playlists (user and smart ones) :param int offset: start index :param function callback: callback function :param int count: limit number of results """ if self.tracker: GLib.idle_add(self.populate_items, Query.all_playlists(), offset, callback, count) @log def populate_user_playlists(self, offset, callback, count=-1): """Asynchronously get user playlists :param int offset: start index :param function callback: callback function :param int count: limit number of results """ if self.tracker: GLib.idle_add(self.populate_items, Query.all_user_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): if error: logger.warning("Error {}: {}".format(error.domain, error.message)) 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 try: self.tracker.store_metadata_sync(song_item, [Grl.METADATA_KEY_FAVOURITE], Grl.WriteFlags.NORMAL) except GLib.Error as error: logger.warning("Error {}: {}".format(error.domain, error.message)) @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): if error: logger.warning("Error {}: {}".format(error.domain, error.message)) callback(source, param, item, remaining, data) self._search_callback_counter += 1 @log def _multiple_search_callback(source, param, item, remaining, data, error): if error: logger.warning("Error {}: {}".format(error.domain, error.message)) 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 try: self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_PLAY_COUNT], Grl.WriteFlags.NORMAL) except GLib.Error as error: logger.warning("Error {}: {}".format(error.domain, error.message)) @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 try: self.tracker.store_metadata_sync(media, [Grl.METADATA_KEY_LAST_PLAYED], Grl.WriteFlags.NORMAL) except GLib.Error as error: logger.warning("Error {}: {}".format(error.domain, error.message)) @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.warning("Error: {}, {}".format(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.warning("Error: {}, {}".format(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 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_ID, Grl.METADATA_KEY_TITLE, Grl.METADATA_KEY_ARTIST, Grl.METADATA_KEY_ALBUM, Grl.METADATA_KEY_DURATION, Grl.METADATA_KEY_CREATION_DATE, Grl.METADATA_KEY_URL, Grl.METADATA_KEY_LYRICS, Grl.METADATA_KEY_THUMBNAIL ] 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 query = "select DISTINCT rdf:type nie:mimeType(?urn) as mime-type" +\ " { ?urn rdf:type nie:InformationElement . FILTER (tracker:id(?urn) = %s) }" % media_id mimeType = grilo.tracker.query_sync( query, [Grl.METADATA_KEY_MIME], grilo.options)[0].get_mime() if mimeType and mimeType.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): self.populate_items(Query.all_artists(), offset, callback, count) @log def populate_albums(self, offset, callback, count=-1): self.populate_items(Query.all_albums(), offset, callback, count) @log def populate_songs(self, offset, callback, count=-1): self.populate_items(Query.all_songs(), offset, callback, count) @log def populate_playlists(self, offset, callback, count=-1): 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': 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): 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): # TODO: change "bool(song_item.get_lyrics())" --> song_item.get_favourite() once query works properly # TODO: when .set/get_favourite work, set_favourite outside loop: item.set_favourite(!item.get_favourite()) if bool(song_item.get_lyrics()): # is favorite self.sparqltracker.update( Query.remove_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, None) song_item.set_lyrics("") else: # not favorite self.sparqltracker.update(Query.add_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, None) song_item.set_lyrics("i'm truthy") @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, data=None): item_id = item.get_id() query = None 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, data) @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)