예제 #1
0
    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
예제 #2
0
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)
예제 #3
0
 def __init__(self):
     GObject.GObject.__init__(self)
     self.tracker = TrackerWrapper().tracker
예제 #4
0
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
            )
예제 #5
0
    def __init__(self):
        GObject.GObject.__init__(self)
        self.tracker = TrackerWrapper().tracker
        self._static_playlists = StaticPlaylists()

        grilo.connect('ready', self._on_grilo_ready)
예제 #6
0
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)
예제 #7
0
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
예제 #8
0
 def __init__(self):
     GObject.GObject.__init__(self)
     self.tracker = TrackerWrapper().tracker
     StaticPlaylists()
예제 #9
0
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)
예제 #10
0
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)
예제 #11
0
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)