Example #1
0
    def __init__(self, artist, album, player, model, header_bar, selectionModeAllowed):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.large)

        self.player = player
        self.album = album
        self.artist = artist
        self.model = model
        self.model.connect('row-changed', self._model_row_changed)
        self.header_bar = header_bar
        self.selectionMode = False
        self.selectionModeAllowed = selectionModeAllowed
        self.songs = []
        self.ui = Gtk.Builder()
        self.ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')

        GLib.idle_add(self._update_album_art)

        self.cover = self.ui.get_object('cover')
        self.cover.set_from_surface(self._loading_icon_surface)
        self.songsGrid = self.ui.get_object('grid1')
        self.ui.get_object('title').set_label(album.get_title())
        if album.get_creation_date():
            self.ui.get_object('year').set_markup(
                '<span color=\'grey\'>(%s)</span>' %
                str(album.get_creation_date().get_year())
            )
        self.tracks = []
        grilo.populate_album_songs(album, self.add_item)
        self.pack_start(self.ui.get_object('ArtistAlbumWidget'), True, True, 0)
    def __init__(self,
                 media,
                 player,
                 model,
                 header_bar,
                 selection_mode_allowed,
                 size_group=None,
                 cover_size_group=None):
        super().__init__(orientation=Gtk.Orientation.HORIZONTAL)

        self._size_group = size_group
        self._cover_size_group = cover_size_group
        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.MEDIUM)

        self._media = media
        self._player = player
        self._artist = utils.get_artist_name(self._media)
        self._album_title = utils.get_album_title(self._media)
        self._model = model
        self._header_bar = header_bar
        self._selection_mode = False
        self._selection_mode_allowed = selection_mode_allowed

        self._songs = []

        self._header_bar._select_button.connect(
            'toggled', self._on_header_select_button_toggled)

        ui = Gtk.Builder()
        ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')

        self.cover = ui.get_object('cover')
        self.cover.set_from_surface(self._loading_icon_surface)

        self._disc_listbox = ui.get_object('disclistbox')
        self._disc_listbox.set_selection_mode_allowed(
            self._selection_mode_allowed)

        ui.get_object('title').set_label(self._album_title)
        creation_date = self._media.get_creation_date()
        if creation_date:
            year = creation_date.get_year()
            ui.get_object('year').set_markup(
                '<span color=\'grey\'>{}</span>'.format(year))

        if self._size_group:
            self._size_group.add_widget(ui.get_object('box1'))

        if self._cover_size_group:
            self._cover_size_group.add_widget(self.cover)

        self.pack_start(ui.get_object('ArtistAlbumWidget'), True, True, 0)

        GLib.idle_add(self._update_album_art)
        grilo.populate_album_songs(self._media, self._add_item)
Example #3
0
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        self._songs = []

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._selection_mode = False

        self._builder = Gtk.Builder()
        self._builder.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
        self._create_model()
        self._album = None
        self._header_bar = None
        self._selection_mode_allowed = True

        self._composer_label = self._builder.get_object('composer_label')
        self._composer_info = self._builder.get_object('composer_info')

        view_box = self._builder.get_object('view')
        self._disc_listbox = DiscListBox()
        self._disc_listbox.set_selection_mode_allowed(True)
        # TODO: The top of the coverart is the same vertical
        # position as the top of the album songs, however
        # since we set a top margins for the discbox
        # subtract that margin here. A cleaner solution is
        # appreciated.
        self._disc_listbox.set_margin_top(64 - 16)
        self._disc_listbox.set_margin_bottom(64)
        self._disc_listbox.set_margin_end(32)
        self._disc_listbox.connect('selection-changed',
                                   self._on_selection_changed)
        view_box.add(self._disc_listbox)

        # FIXME: Assigned to appease searchview
        # _get_selected_songs
        self.view = self._disc_listbox

        self.add(self._builder.get_object('AlbumWidget'))
        self.get_style_context().add_class('view')
        self.get_style_context().add_class('content-view')

        self.show_all()
Example #4
0
    def __init__(self, parent_window):
        GObject.GObject.__init__(self)
        self._parent_window = parent_window
        self.playlist = None
        self.playlistType = None
        self.playlistId = None
        self.playlistField = None
        self.currentTrack = None
        self.currentTrackUri = None
        self._lastState = Gst.State.PAUSED
        scale = parent_window.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.xsmall)
        self._missingPluginMessages = []

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self.discoverer = GstPbutils.Discoverer()
        self.discoverer.connect('discovered', self._on_discovered)
        self.discoverer.start()
        self._discovering_urls = {}

        self.player = Gst.ElementFactory.make('playbin', 'player')
        self.bus = self.player.get_bus()
        self.bus.add_signal_watch()
        self.setup_replaygain()

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect('changed::repeat',
                               self._on_repeat_setting_changed)
        self._settings.connect('changed::replaygain',
                               self._on_replaygain_setting_changed)
        self.repeat = self._settings.get_enum('repeat')
        self.replaygain = self._settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

        self.bus.connect('message::state-changed', self._on_bus_state_changed)
        self.bus.connect('message::error', self._onBusError)
        self.bus.connect('message::element', self._on_bus_element)
        self.bus.connect('message::eos', self._on_bus_eos)
        self._setup_view()

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._check_last_fm()
Example #5
0
 def _get_active_playlist(self):
     playlist = self._get_playlist_from_id(self.player.playlistId) \
         if self.player.playlistType == 'Playlist' else None
     playlistName = AlbumArtCache.get_media_title(playlist) \
         if playlist else ''
     return (playlist is not None,
             (self._get_playlist_path(playlist), playlistName, ''))
Example #6
0
    def update_model(self, player, playlist, currentIter):
        # this is not our playlist, return
        if playlist != self.model:
            # TODO, only clean once, but that can wait util we have clean
            # the code a bit, and until the playlist refactoring.
            # the overhead is acceptable for now
            self.clean_model()
            return False

        currentSong = playlist.get_value(currentIter, 5)
        song_passed = False
        itr = playlist.get_iter_first()

        while itr:
            song = playlist.get_value(itr, 5)
            song_widget = song.song_widget

            if not song_widget.can_be_played:
                itr = playlist.iter_next(itr)
                continue

            escapedTitle = AlbumArtCache.get_media_title(song, True)
            if (song == currentSong):
                song_widget.now_playing_sign.show()
                song_widget.title.set_markup('<b>%s</b>' % escapedTitle)
                song_passed = True
            elif (song_passed):
                song_widget.now_playing_sign.hide()
                song_widget.title.set_markup('<span>%s</span>' % escapedTitle)
            else:
                song_widget.now_playing_sign.hide()
                song_widget.title\
                    .set_markup('<span color=\'grey\'>%s</span>' % escapedTitle)
            itr = playlist.iter_next(itr)
        return False
Example #7
0
    def _update_model(self, player, playlist, current_iter):
        """Player changed callback.

        :param player: The player object
        :param playlist: The current playlist
        :param current_iter: The current iter of the playlist model
        """
        # self is not our playlist, return
        if (playlist != self.model):
            return False

        current_song = playlist[current_iter][5]
        song_passed = False
        _iter = playlist.get_iter_first()
        self._duration = 0

        while _iter:
            song = playlist[_iter][5]
            self._duration += song.get_duration()
            escaped_title = AlbumArtCache.get_media_title(song, True)
            if (song == current_song):
                title = '<b>%s</b>' % escaped_title
                song_passed = True
            elif (song_passed):
                title = '<span>%s</span>' % escaped_title
            else:
                title = '<span color=\'grey\'>%s</span>' % escaped_title
            playlist[_iter][0] = title
            _iter = playlist.iter_next(_iter)
            self._ui.get_object('running_length_label_info').set_text(
                _("%d min") % (int(self._duration / 60) + 1))

        return False
Example #8
0
 def GetPlaylists(self, index, max_count, order, reverse):
     if order != 'Alphabetical':
         return []
     playlists = [(self._get_playlist_path(playlist),
                   AlbumArtCache.get_media_title(playlist) or '', '')
                  for playlist in self.playlists]
     return playlists[index:index + max_count] if not reverse \
         else playlists[index + max_count - 1:index - 1 if index - 1 >= 0 else None:-1]
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._ui = Gtk.Builder()
        self._ui.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
        self._create_model()
        self.view = Gd.MainView(shadow_type=Gtk.ShadowType.NONE)
        self.view.set_view_type(Gd.MainViewType.LIST)
        self._album = None
        self._header_bar = None
        self.view.connect('item-activated', self._on_item_activated)

        view_box = self._ui.get_object('view')
        self._ui.get_object('scrolledWindow').set_placement(
            Gtk.CornerType.TOP_LEFT)
        self.view.connect('selection-mode-request',
                          self._on_selection_mode_request)
        child_view = self.view.get_children()[0]
        child_view.set_margin_top(64)
        child_view.set_margin_bottom(64)
        child_view.set_margin_end(32)
        self.view.remove(child_view)
        view_box.add(child_view)

        self.add(self._ui.get_object('AlbumWidget'))
        self._star_handler = StarHandlerWidget(self, 9)
        self._add_list_renderers()
        self.get_style_context().add_class('view')
        self.get_style_context().add_class('content-view')
        self.view.get_generic_view().get_style_context().remove_class('view')
        self.show_all()
Example #10
0
 def clean_model(self):
     itr = self.model.get_iter_first()
     while itr:
         song = self.model.get_value(itr, 5)
         song_widget = song.song_widget
         escapedTitle = AlbumArtCache.get_media_title(song, True)
         if song_widget.can_be_played:
             song_widget.now_playing_sign.hide()
         song_widget.title.set_markup('<span>%s</span>' % escapedTitle)
         itr = self.model.iter_next(itr)
     return False
Example #11
0
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        self._tracks = []

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(DefaultIcon.Type.loading, ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._selection_mode = False

        self._builder = Gtk.Builder()
        self._builder.add_from_resource("/org/gnome/Music/AlbumWidget.ui")
        self._create_model()
        self._album = None
        self._header_bar = None
        self._selection_mode_allowed = True

        self._composer_label = self._builder.get_object("composer_label")
        self._composer_info = self._builder.get_object("composer_info")

        view_box = self._builder.get_object("view")
        self._disc_listbox = DiscListBox()
        self._disc_listbox.set_selection_mode_allowed(True)
        # TODO: The top of the coverart is the same vertical
        # position as the top of the album songs, however
        # since we set a top margins for the discbox
        # subtract that margin here. A cleaner solution is
        # appreciated.
        self._disc_listbox.set_margin_top(64 - 16)
        self._disc_listbox.set_margin_bottom(64)
        self._disc_listbox.set_margin_end(32)
        self._disc_listbox.connect("selection-changed", self._on_selection_changed)
        view_box.add(self._disc_listbox)

        # FIXME: Assigned to appease searchview
        # get_selected_tracks
        self.view = self._disc_listbox

        self.add(self._builder.get_object("AlbumWidget"))
        self.get_style_context().add_class("view")
        self.get_style_context().add_class("content-view")

        self.show_all()
Example #12
0
    def _add_item_to_model(self, item):
        """Adds (non-static only) playlists to the model"""

        # Don't show static playlists
        if self.playlist.is_static_playlist(item):
            return None

        new_iter = self.model.append()
        self.model.set(
            new_iter,
            [0, 1, 2],
            [AlbumArtCache.get_media_title(item), False, item]
        )
        return new_iter
Example #13
0
    def add_item(self, source, prefs, track, remaining, data=None):
        if remaining == 0:
            self.songsGrid.show_all()
            self.emit("tracks-loaded")

        if track:
            self.tracks.append(track)
        else:
            for i, track in enumerate(self.tracks):
                ui = Gtk.Builder()
                ui.add_from_resource('/org/gnome/Music/TrackWidget.ui')
                song_widget = ui.get_object('eventbox1')
                self.songs.append(song_widget)
                ui.get_object('num')\
                    .set_markup('<span color=\'grey\'>%d</span>'
                                % len(self.songs))
                title = AlbumArtCache.get_media_title(track)
                ui.get_object('title').set_text(title)
                ui.get_object('title').set_alignment(0.0, 0.5)
                ui.get_object('title').set_max_width_chars(MAX_TITLE_WIDTH)

                self.songsGrid.attach(
                    song_widget,
                    int(i / (len(self.tracks) / 2)),
                    int(i % (len(self.tracks) / 2)), 1, 1
                )
                track.song_widget = song_widget
                itr = self.model.append(None)
                song_widget._iter = itr
                song_widget.model = self.model
                song_widget.title = ui.get_object('title')
                song_widget.checkButton = ui.get_object('select')
                song_widget.checkButton.set_visible(self.selectionMode)
                song_widget.checkButton.connect(
                    'toggled', self._check_button_toggled, song_widget
                )
                self.model.set(itr,
                               [0, 1, 2, 3, 5],
                               [title, '', '', False, track])
                song_widget.now_playing_sign = ui.get_object('image1')
                song_widget.now_playing_sign.set_from_icon_name(
                    NOW_PLAYING_ICON_NAME,
                    Gtk.IconSize.SMALL_TOOLBAR)
                song_widget.now_playing_sign.set_no_show_all('True')
                song_widget.now_playing_sign.set_alignment(1, 0.6)
                song_widget.can_be_played = True
                song_widget.connect('button-release-event',
                                    self.track_selected)
Example #14
0
    def __init__(self, parent_window):
        GObject.GObject.__init__(self)
        self._parent_window = parent_window
        self.playlist = None
        self.playlistType = None
        self.playlistId = None
        self.playlistField = None
        self.currentTrack = None
        self.currentTrackUri = None
        self._lastState = Gst.State.PAUSED
        scale = parent_window.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.xsmall)
        self._missingPluginMessages = []

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self.discoverer = GstPbutils.Discoverer()
        self.discoverer.connect('discovered', self._on_discovered)
        self.discoverer.start()
        self._discovering_urls = {}

        self.player = Gst.ElementFactory.make('playbin', 'player')
        self.bus = self.player.get_bus()
        self.bus.add_signal_watch()
        self.setup_replaygain()

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect('changed::repeat', self._on_repeat_setting_changed)
        self._settings.connect('changed::replaygain', self._on_replaygain_setting_changed)
        self.repeat = self._settings.get_enum('repeat')
        self.replaygain = self._settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

        self.bus.connect('message::state-changed', self._on_bus_state_changed)
        self.bus.connect('message::error', self._onBusError)
        self.bus.connect('message::element', self._on_bus_element)
        self.bus.connect('message::eos', self._on_bus_eos)
        self._setup_view()

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._check_last_fm()
Example #15
0
    def _on_item_activated(self, widget, id, path):
        """List row activated."""
        if self._star_handler.star_renderer_click:
            self._star_handler.star_renderer_click = False
            return

        _iter = self.model.get_iter(path)

        if self.model[_iter][10] != DiscoveryStatus.FAILED:
            if (self._iter_to_clean
                    and self._player.playlistId == self._album):
                item = self.model[self._iter_to_clean][5]
                title = AlbumArtCache.get_media_title(item)
                self.model[self._iter_to_clean][0] = title
                # Hide now playing icon
                self.model[self._iter_to_clean][6] = False
            self._player.set_playlist('Album', self._album, self.model, _iter,
                                      5, 11)
            self._player.set_playing(True)
Example #16
0
    def load(self, media):
        self.progressScale.set_value(0)
        self._set_duration(media.get_duration())
        self.songTotalTimeLabel.set_label(self.seconds_to_string(media.get_duration()))
        self.progressScale.set_sensitive(True)

        self.playBtn.set_sensitive(True)
        self._sync_prev_next()

        artist = utils.get_artist_name(media)
        self.artistLabel.set_label(artist)
        self._currentArtist = artist

        album = _("Unknown Album")
        try:
            assert media.get_album() is not None
            album = media.get_album()
        except:
            self._currentAlbum = album

        self.coverImg.set_from_pixbuf(self._no_artwork_icon)
        self.cache.lookup(
            media, ART_SIZE, ART_SIZE, self._on_cache_lookup, None, artist, album)

        self._currentTitle = AlbumArtCache.get_media_title(media)
        self.titleLabel.set_label(self._currentTitle)

        self._currentTimestamp = int(time.time())

        url = media.get_url()
        if url != self.player.get_value('current-uri', 0):
            self.player.set_property('uri', url)

        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
            self.emit('playlist-item-changed', self.playlist, currentTrack)
            self.emit('current-changed')

        self._validate_next_track()
Example #17
0
    def add_item(self, source, prefs, track, remaining, data=None):
        """Add a song to the item to album list.

        :param source: The grilo source
        :param prefs:
        :param track: The grilo media object
        :param remaining: Remaining number of items to add
        :param data: User data
        """
        if track:
            self._duration = self._duration + track.get_duration()
            _iter = self.model.append()
            escapedTitle = AlbumArtCache.get_media_title(track, True)
            self.model[_iter][0, 1, 2, 3, 4, 5, 9] = [
                escapedTitle,
                self._player.seconds_to_string(track.get_duration()),
                '',
                '',
                None,
                track,
                bool(track.get_lyrics())
            ]
            self._ui.get_object('running_length_label_info').set_text(
                _("%d min") % (int(self._duration / 60) + 1))
Example #18
0
class AlbumWidget(Gtk.EventBox):
    """Album widget.

    The album widget consists of an image with the album art
    on the left and a list of songs on the right.
    """

    _duration = 0

    def __repr__(self):
        return "<AlbumWidget>"

    @log
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        self._tracks = []

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(DefaultIcon.Type.loading, ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._selection_mode = False

        self._builder = Gtk.Builder()
        self._builder.add_from_resource("/org/gnome/Music/AlbumWidget.ui")
        self._create_model()
        self._album = None
        self._header_bar = None
        self._selection_mode_allowed = True

        self._composer_label = self._builder.get_object("composer_label")
        self._composer_info = self._builder.get_object("composer_info")

        view_box = self._builder.get_object("view")
        self._disc_listbox = DiscListBox()
        self._disc_listbox.set_selection_mode_allowed(True)
        # TODO: The top of the coverart is the same vertical
        # position as the top of the album songs, however
        # since we set a top margins for the discbox
        # subtract that margin here. A cleaner solution is
        # appreciated.
        self._disc_listbox.set_margin_top(64 - 16)
        self._disc_listbox.set_margin_bottom(64)
        self._disc_listbox.set_margin_end(32)
        self._disc_listbox.connect("selection-changed", self._on_selection_changed)
        view_box.add(self._disc_listbox)

        # FIXME: Assigned to appease searchview
        # get_selected_tracks
        self.view = self._disc_listbox

        self.add(self._builder.get_object("AlbumWidget"))
        self.get_style_context().add_class("view")
        self.get_style_context().add_class("content-view")

        self.show_all()

    @log
    def _on_selection_mode_request(self, *args):
        """Selection mode toggled."""
        self._header_bar._select_button.clicked()

    @log
    def _create_model(self):
        """Create the ListStore model for this widget."""
        self._model = Gtk.ListStore(
            GObject.TYPE_STRING,  # title
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,  # icon
            GObject.TYPE_OBJECT,  # song object
            GObject.TYPE_BOOLEAN,  # item selected
            GObject.TYPE_STRING,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,  # icon shown
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,
        )

    @log
    def update(self, artist, album, item, header_bar, selection_toolbar):
        """Update the album widget.

        :param str artist: The artist name
        :param str album: The album name
        :param item: The grilo media item
        :param header_bar: The header bar object
        :param selection_toolbar: The selection toolbar object
        """
        # reset view
        self._tracks = []
        self._create_model()
        for widget in self._disc_listbox.get_children():
            self._disc_listbox.remove(widget)

        self.selection_toolbar = selection_toolbar
        self._header_bar = header_bar
        self._album = album
        self._builder.get_object("cover").set_from_surface(self._loading_icon_surface)
        self._cache.lookup(item, ArtSize.large, self._on_lookup, None)
        self._duration = 0

        GLib.idle_add(grilo.populate_album_songs, item, self.add_item)
        header_bar._select_button.connect("toggled", self._on_header_select_button_toggled)
        header_bar._cancel_button.connect("clicked", self._on_header_cancel_button_clicked)

        # FIXME: use utils
        escaped_artist = GLib.markup_escape_text(artist)
        escaped_album = GLib.markup_escape_text(album)
        self._builder.get_object("artist_label").set_markup(escaped_artist)
        self._builder.get_object("title_label").set_markup(escaped_album)

        if item.get_creation_date():
            self._builder.get_object("released_label_info").set_text(str(item.get_creation_date().get_year()))
        else:
            self._builder.get_object("released_label_info").set_text("----")

        self._set_composer_label(item)

        self._player.connect("playlist-item-changed", self._update_model)

    @log
    def _set_composer_label(self, item):
        composer = item.get_composer()
        show = False

        if composer:
            self._composer_info.set_text(composer)
            show = True

        self._composer_label.set_visible(show)
        self._composer_info.set_visible(show)

    @log
    def _on_selection_changed(self, widget):
        items = self._disc_listbox.get_selected_items()
        self.selection_toolbar._add_to_playlist_button.set_sensitive(len(items) > 0)
        if len(items) > 0:
            self._header_bar._selection_menu_label.set_text(
                ngettext("Selected %d item", "Selected %d items", len(items)) % len(items)
            )
        else:
            self._header_bar._selection_menu_label.set_text(_("Click on items to select them"))

    @log
    def _on_header_cancel_button_clicked(self, button):
        """Cancel selection mode callback."""
        self._disc_listbox.set_selection_mode(False)
        self._header_bar.set_selection_mode(False)
        self._header_bar.header_bar.title = self._album

    @log
    def _on_header_select_button_toggled(self, button):
        """Selection mode button clicked callback."""
        if button.get_active():
            self._selection_mode = True
            self._disc_listbox.set_selection_mode(True)
            self._header_bar.set_selection_mode(True)
            self._player.actionbar.set_visible(False)
            self._header_bar.header_bar.set_custom_title(self._header_bar._selection_menu_button)
        else:
            self._selection_mode = False
            self._disc_listbox.set_selection_mode(False)
            self._header_bar.set_selection_mode(False)
            if self._player.get_playback_status() != 2:
                self._player.actionbar.set_visible(True)

    @log
    def _create_disc_box(self, disc_nr, disc_tracks):
        disc_box = DiscBox(self._model)
        disc_box.set_tracks(disc_tracks)
        disc_box.set_disc_number(disc_nr)
        disc_box.set_columns(1)
        disc_box.show_song_numbers(False)
        disc_box.connect("track-activated", self._track_activated)
        disc_box.connect("selection-toggle", self._selection_mode_toggled)

        return disc_box

    @log
    def _selection_mode_toggled(self, widget):
        if not self._selection_mode_allowed:
            return

        self._selection_mode = not self._selection_mode
        self._on_selection_mode_request()

    @log
    def _track_activated(self, widget, song_widget):
        if not song_widget.can_be_played:
            return

        if self._selection_mode:
            song_widget.check_button.toggled()
            return

        self._player.stop()
        self._player.set_playlist("Artist", "test", song_widget.model, song_widget.itr, 5, 11)
        self._player.set_playing(True)
        return True

    @log
    def add_item(self, source, prefs, track, remaining, data=None):
        """Add a song to the item to album list.

        :param source: The grilo source
        :param prefs:
        :param track: The grilo media object
        :param remaining: Remaining number of items to add
        :param data: User data
        """
        if track:
            self._tracks.append(track)

            self._duration = self._duration + track.get_duration()
            return

        discs = {}
        for track in self._tracks:
            disc_nr = track.get_album_disc_number()
            if disc_nr not in discs.keys():
                discs[disc_nr] = [track]
            else:
                discs[disc_nr].append(track)
        for disc_nr in discs:
            disc = self._create_disc_box(disc_nr, discs[disc_nr])
            self._disc_listbox.add(disc)
            if len(discs) == 1:
                disc.show_disc_label(False)

        if remaining == 0:
            self._builder.get_object("running_length_label_info").set_text(_("%d min") % (int(self._duration / 60) + 1))

            self.show_all()

    @log
    def _on_lookup(self, surface, data=None):
        """Albumart retrieved callback.

        :param surface: The Cairo surface retrieved
        :param path: The filesystem location the pixbuf
        :param data: User data
        """
        self._builder.get_object("cover").set_from_surface(surface)

    @log
    def _update_model(self, player, playlist, current_iter):
        """Player changed callback.

        :param player: The player object
        :param playlist: The current playlist
        :param current_iter: The current iter of the playlist model
        """
        if playlist != self._model:
            return True

        current_song = playlist[current_iter][5]

        self._duration = 0

        song_passed = False
        _iter = playlist.get_iter_first()

        while _iter:
            song = playlist[_iter][5]
            song_widget = song.song_widget
            self._duration += song.get_duration()
            escaped_title = GLib.markup_escape_text(utils.get_media_title(song))

            if song == current_song:
                song_widget.now_playing_sign.show()
                song_widget.title.set_markup("<b>{}</b>".format(escaped_title))
                song_passed = True
            elif song_passed:
                song_widget.now_playing_sign.hide()
                song_widget.title.set_markup("<span>{}</span>".format(escaped_title))
            else:
                song_widget.now_playing_sign.hide()
                song_widget.title.set_markup("<span color='grey'>{}</span>".format(escaped_title))

            _iter = playlist.iter_next(_iter)

        self._builder.get_object("running_length_label_info").set_text(_("%d min") % (int(self._duration / 60) + 1))

        return True

    @log
    def select_all(self):
        self._disc_listbox.select_all()

    @log
    def select_none(self):
        self._disc_listbox.select_none()
Example #19
0
class Player(GObject.GObject):
    nextTrack = None
    timeout = None
    _seconds_timeout = None
    shuffleHistory = deque(maxlen=10)

    __gsignals__ = {
        'playing-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playlist-item-changed': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreeModel, Gtk.TreeIter)),
        'current-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playback-status-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'repeat-mode-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'volume-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'prev-next-invalidated': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seeked': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        'thumbnail-updated': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, parent_window):
        GObject.GObject.__init__(self)
        self._parent_window = parent_window
        self.playlist = None
        self.playlistType = None
        self.playlistId = None
        self.playlistField = None
        self.currentTrack = None
        self.currentTrackUri = None
        self._lastState = Gst.State.PAUSED
        scale = parent_window.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.xsmall)
        self._missingPluginMessages = []

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self.discoverer = GstPbutils.Discoverer()
        self.discoverer.connect('discovered', self._on_discovered)
        self.discoverer.start()
        self._discovering_urls = {}

        self.player = Gst.ElementFactory.make('playbin', 'player')
        self.bus = self.player.get_bus()
        self.bus.add_signal_watch()
        self.setup_replaygain()

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect('changed::repeat', self._on_repeat_setting_changed)
        self._settings.connect('changed::replaygain', self._on_replaygain_setting_changed)
        self.repeat = self._settings.get_enum('repeat')
        self.replaygain = self._settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

        self.bus.connect('message::state-changed', self._on_bus_state_changed)
        self.bus.connect('message::error', self._onBusError)
        self.bus.connect('message::element', self._on_bus_element)
        self.bus.connect('message::eos', self._on_bus_eos)
        self._setup_view()

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._check_last_fm()

    @log
    def _check_last_fm(self):
        try:
            self.last_fm = None
            gi.require_version('Goa', '1.0')
            from gi.repository import Goa
            client = Goa.Client.new_sync(None)
            accounts = client.get_accounts()

            for obj in accounts:
                account = obj.get_account()
                if account.props.provider_name == "Last.fm":
                    self.last_fm = obj.get_oauth2_based()
                    return
        except Exception as e:
            logger.info("Error reading Last.fm credentials: %s" % str(e))
            self.last_fm = None

    @log
    def _on_replaygain_setting_changed(self, settings, value):
        self.replaygain = settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

    @log
    def setup_replaygain(self):
        """
        Set up replaygain
        See https://github.com/gnumdk/lollypop/commit/429383c3742e631b34937d8987d780edc52303c0
        """
        self._rgfilter = Gst.ElementFactory.make("bin", "bin")
        self._rg_audioconvert1 = Gst.ElementFactory.make("audioconvert", "audioconvert")
        self._rg_audioconvert2 = Gst.ElementFactory.make("audioconvert", "audioconvert2")
        self._rgvolume = Gst.ElementFactory.make("rgvolume", "rgvolume")
        self._rglimiter = Gst.ElementFactory.make("rglimiter", "rglimiter")
        self._rg_audiosink = Gst.ElementFactory.make("autoaudiosink", "autoaudiosink")
        if not self._rgfilter or not self._rg_audioconvert1 or not self._rg_audioconvert2 \
           or not self._rgvolume or not self._rglimiter or not self._rg_audiosink:
            logger.debug("Replay Gain is not available")
            return
        self._rgvolume.props.pre_amp = 0.0
        self._rgfilter.add(self._rgvolume)
        self._rgfilter.add(self._rg_audioconvert1)
        self._rgfilter.add(self._rg_audioconvert2)
        self._rgfilter.add(self._rglimiter)
        self._rgfilter.add(self._rg_audiosink)
        self._rg_audioconvert1.link(self._rgvolume)
        self._rgvolume.link(self._rg_audioconvert2)
        self._rgvolume.link(self._rglimiter)
        self._rg_audioconvert2.link(self._rg_audiosink)
        self._rgfilter.add_pad(Gst.GhostPad.new("sink", self._rg_audioconvert1.get_static_pad("sink")))

    @log
    def toggle_replaygain(self, state=False):
        if state and self._rgfilter:
            self.player.set_property("audio-sink", self._rgfilter)
        else:
            self.player.set_property("audio-sink", None)

    def discover_item(self, item, callback, data=None):
        url = item.get_url()
        if not url:
            logger.warn("The item %s doesn't have a URL set", item)
            return

        if not url.startswith("file://"):
            logger.debug("Skipping discovery of %s as not a local file", url)
            return

        obj = (callback, data)

        if url in self._discovering_urls:
            self._discovering_urls[url] += [obj]
        else:
            self._discovering_urls[url] = [obj]
            self.discoverer.discover_uri_async(url)

    def _on_discovered(self, discoverer, info, error):
        try:
            cbs = self._discovering_urls[info.get_uri()]
            del(self._discovering_urls[info.get_uri()])

            for callback, data in cbs:
                if data is not None:
                    callback(info, error, data)
                else:
                    callback(info, error)
        except KeyError:
            # Not something we're interested in
            return

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.repeat = settings.get_enum('repeat')
        self._sync_prev_next()
        self._sync_repeat_image()
        self._validate_next_track()

    @log
    def _on_bus_state_changed(self, bus, message):
        # Note: not all state changes are signaled through here, in particular
        # transitions between Gst.State.READY and Gst.State.NULL are never async
        # and thus don't cause a message
        # In practice, self means only Gst.State.PLAYING and Gst.State.PAUSED are
        self._sync_playing()

    @log
    def _gst_plugins_base_check_version(self, major, minor, micro):
        gst_major, gst_minor, gst_micro, gst_nano = GstPbutils.plugins_base_version()
        return ((gst_major > major) or
                (gst_major == major and gst_minor > minor) or
                (gst_major == major and gst_minor == minor and gst_micro >= micro) or
                (gst_major == major and gst_minor == minor and gst_micro + 1 == micro and gst_nano > 0))

    @log
    def _start_plugin_installation(self, missing_plugin_messages, confirm_search):
        install_ctx = GstPbutils.InstallPluginsContext.new()

        if self._gst_plugins_base_check_version(1, 5, 0):
            install_ctx.set_desktop_id('gnome-music.desktop')
            install_ctx.set_confirm_search(confirm_search)

            startup_id = '_TIME%u' % Gtk.get_current_event_time()
            install_ctx.set_startup_notification_id(startup_id)

        installer_details = []
        for message in missing_plugin_messages:
            installer_detail = GstPbutils.missing_plugin_message_get_installer_detail(message)
            installer_details.append(installer_detail)

        def on_install_done(res):
            # We get the callback too soon, before the installation has
            # actually finished. Do nothing for now.
            pass

        GstPbutils.install_plugins_async(installer_details, install_ctx, on_install_done)

    @log
    def _show_codec_confirmation_dialog(self, install_helper_name, missing_plugin_messages):
        dialog = MissingCodecsDialog(self._parent_window, install_helper_name)

        def on_dialog_response(dialog, response_type):
            if response_type == Gtk.ResponseType.ACCEPT:
                self._start_plugin_installation(missing_plugin_messages, False)

            dialog.destroy()

        descriptions = []
        for message in missing_plugin_messages:
            description = GstPbutils.missing_plugin_message_get_description(message)
            descriptions.append(description)

        dialog.set_codec_names(descriptions)
        dialog.connect('response', on_dialog_response)
        dialog.present()

    @log
    def _handle_missing_plugins(self):
        if not self._missingPluginMessages:
            return

        missing_plugin_messages = self._missingPluginMessages
        self._missingPluginMessages = []

        if self._gst_plugins_base_check_version(1, 5, 0):
            proxy = Gio.DBusProxy.new_sync(Gio.bus_get_sync(Gio.BusType.SESSION, None),
                                           Gio.DBusProxyFlags.NONE,
                                           None,
                                           'org.freedesktop.PackageKit',
                                           '/org/freedesktop/PackageKit',
                                           'org.freedesktop.PackageKit.Modify2',
                                           None)
            prop = Gio.DBusProxy.get_cached_property(proxy, 'DisplayName')
            if prop:
                display_name = prop.get_string()
                if display_name:
                    self._show_codec_confirmation_dialog(display_name, missing_plugin_messages)
                    return

        # If the above failed, fall back to immediately starting the codec installation
        self._start_plugin_installation(missing_plugin_messages, True)

    @log
    def _is_missing_plugin_message(self, message):
        error, debug = message.parse_error()

        if error.matches(Gst.CoreError.quark(), Gst.CoreError.MISSING_PLUGIN):
            return True

        return False

    @log
    def _on_bus_element(self, bus, message):
        if GstPbutils.is_missing_plugin_message(message):
            self._missingPluginMessages.append(message)

    def _onBusError(self, bus, message):
        if self._is_missing_plugin_message(message):
            self.pause()
            self._handle_missing_plugins()
            return True

        media = self.get_current_media()
        if media is not None:
            if self.currentTrack and self.currentTrack.valid():
                currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
                self.playlist.set_value(currentTrack, self.discovery_status_field, DiscoveryStatus.FAILED)
            uri = media.get_url()
        else:
            uri = 'none'
        logger.warn('URI: %s', uri)
        error, debug = message.parse_error()
        debug = debug.split('\n')
        debug = [('     ') + line.lstrip() for line in debug]
        debug = '\n'.join(debug)
        logger.warn('Error from element %s: %s', message.src.get_name(), error.message)
        logger.warn('Debugging info:\n%s', debug)
        self.play_next()
        return True

    @log
    def _on_bus_eos(self, bus, message):
        if self.nextTrack:
            GLib.idle_add(self._on_glib_idle)
        elif (self.repeat == RepeatType.NONE):
            self.stop()
            self.playBtn.set_image(self._playImage)
            self._progress_scale_zero()
            self.progressScale.set_sensitive(False)
            if self.playlist is not None:
                currentTrack = self.playlist.get_path(self.playlist.get_iter_first())
                if currentTrack:
                    self.currentTrack = Gtk.TreeRowReference.new(self.playlist, currentTrack)
                    self.currentTrackUri = self.playlist.get_value(
                        self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
                else:
                    self.currentTrack = None
                self.load(self.get_current_media())
            self.emit('playback-status-changed')
        else:
            # Stop playback
            self.stop()
            self.playBtn.set_image(self._playImage)
            self._progress_scale_zero()
            self.progressScale.set_sensitive(False)
            self.emit('playback-status-changed')

    @log
    def _on_glib_idle(self):
        self.currentTrack = self.nextTrack
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
        self.play()

    @log
    def _on_playlist_size_changed(self, path, _iter=None, data=None):
        self._sync_prev_next()

    @log
    def _get_random_iter(self, currentTrack):
        first_iter = self.playlist.get_iter_first()
        if not currentTrack:
            currentTrack = first_iter
        if not currentTrack:
            return None
        if hasattr(self.playlist, "iter_is_valid") and\
           not self.playlist.iter_is_valid(currentTrack):
            return None
        currentPath = int(self.playlist.get_path(currentTrack).to_string())
        rows = self.playlist.iter_n_children(None)
        if rows == 1:
            return currentTrack
        rand = currentPath
        while rand == currentPath:
            rand = randint(0, rows - 1)
        return self.playlist.get_iter_from_string(str(rand))

    @log
    def _get_next_track(self):
        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        else:
            currentTrack = None

        nextTrack = None

        if self.repeat == RepeatType.SONG:
            if currentTrack:
                nextTrack = currentTrack
            else:
                nextTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.ALL:
            if currentTrack:
                nextTrack = self.playlist.iter_next(currentTrack)
            if not nextTrack:
                nextTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.NONE:
            if currentTrack:
                nextTrack = self.playlist.iter_next(currentTrack)
        elif self.repeat == RepeatType.SHUFFLE:
            nextTrack = self._get_random_iter(currentTrack)
            if currentTrack:
                self.shuffleHistory.append(currentTrack)

        if nextTrack:
            return Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(nextTrack))
        else:
            return None

    @log
    def _get_iter_last(self):
        iter = self.playlist.get_iter_first()
        last = None

        while iter is not None:
            last = iter
            iter = self.playlist.iter_next(iter)

        return last

    @log
    def _get_previous_track(self):
        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        else:
            currentTrack = None

        previousTrack = None

        if self.repeat == RepeatType.SONG:
            if currentTrack:
                previousTrack = currentTrack
            else:
                previousTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.ALL:
            if currentTrack:
                previousTrack = self.playlist.iter_previous(currentTrack)
            if not previousTrack:
                previousTrack = self._get_iter_last()
        elif self.repeat == RepeatType.NONE:
            if currentTrack:
                previousTrack = self.playlist.iter_previous(currentTrack)
        elif self.repeat == RepeatType.SHUFFLE:
            if currentTrack:
                if self.played_seconds < 10 and len(self.shuffleHistory) > 0:
                    previousTrack = self.shuffleHistory.pop()

                    # Discard the current song, which is already queued
                    if self.playlist.get_path(previousTrack) == self.playlist.get_path(currentTrack):
                        previousTrack = None

                if previousTrack is None and len(self.shuffleHistory) > 0:
                    previousTrack = self.shuffleHistory.pop()
                else:
                    previousTrack = self._get_random_iter(currentTrack)

        if previousTrack:
            return Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(previousTrack))
        else:
            return None

    @log
    def has_next(self):
        if not self.playlist or self.playlist.iter_n_children(None) < 1:
            return False
        elif not self.currentTrack:
            return False
        elif self.repeat in [RepeatType.ALL, RepeatType.SONG, RepeatType.SHUFFLE]:
            return True
        elif self.currentTrack.valid():
            tmp = self.playlist.get_iter(self.currentTrack.get_path())
            return self.playlist.iter_next(tmp) is not None
        else:
            return True

    @log
    def has_previous(self):
        if not self.playlist or self.playlist.iter_n_children(None) < 1:
            return False
        elif not self.currentTrack:
            return False
        elif self.repeat in [RepeatType.ALL, RepeatType.SONG, RepeatType.SHUFFLE]:
            return True
        elif self.currentTrack.valid():
            tmp = self.playlist.get_iter(self.currentTrack.get_path())
            return self.playlist.iter_previous(tmp) is not None
        else:
            return True

    @log
    def _get_playing(self):
        ok, state, pending = self.player.get_state(0)
        # log('get playing(), [ok, state, pending] = [%s, %s, %s]'.format(ok, state, pending))
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    @property
    def playing(self):
        return self._get_playing()

    @log
    def _sync_playing(self):
        if self._get_playing():
            image = self._pauseImage
            tooltip = _("Pause")
        else:
            image = self._playImage
            tooltip = _("Play")

        if self.playBtn.get_image() != image:
            self.playBtn.set_image(image)

        self.playBtn.set_tooltip_text(tooltip)

    @log
    def _sync_prev_next(self):
        hasNext = self.has_next()
        hasPrevious = self.has_previous()

        self.nextBtn.set_sensitive(hasNext)
        self.prevBtn.set_sensitive(hasPrevious)

        self.emit('prev-next-invalidated')

    @log
    def set_playing(self, value):
        self.actionbar.show()

        if value:
            self.play()
        else:
            self.pause()

        media = self.get_current_media()
        self.playBtn.set_image(self._pauseImage)
        return media

    @log
    def load(self, media):
        self._progress_scale_zero()
        self._set_duration(media.get_duration())
        self.songTotalTimeLabel.set_label(
            utils.seconds_to_string(media.get_duration()))
        self.progressScale.set_sensitive(True)

        self.playBtn.set_sensitive(True)
        self._sync_prev_next()

        artist = utils.get_artist_name(media)
        self.artistLabel.set_label(artist)
        self._currentArtist = artist

        self.coverImg.set_from_surface(self._loading_icon_surface)
        self.cache.lookup(media, ArtSize.xsmall, self._on_cache_lookup, None)

        self._currentTitle = utils.get_media_title(media)
        self.titleLabel.set_label(self._currentTitle)

        self._currentTimestamp = int(time.time())

        url = media.get_url()
        if url != self.player.get_value('current-uri', 0):
            self.player.set_property('uri', url)

        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
            self.emit('playlist-item-changed', self.playlist, currentTrack)
            self.emit('current-changed')

        self._validate_next_track()

    def _on_next_item_validated(self, info, error, _iter):
        if error:
            print("Info %s: error: %s" % (info, error))
            self.playlist.set_value(_iter, self.discovery_status_field, DiscoveryStatus.FAILED)
            nextTrack = self.playlist.iter_next(_iter)

            if nextTrack:
                self._validate_next_track(Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(nextTrack)))

    @log
    def _validate_next_track(self, track=None):
        if track is None:
            track = self._get_next_track()

        self.nextTrack = track

        if track is None:
            return

        _iter = self.playlist.get_iter(self.nextTrack.get_path())
        status = self.playlist.get_value(_iter, self.discovery_status_field)
        nextSong = self.playlist.get_value(_iter, self.playlistField)
        url = self.playlist.get_value(_iter, 5).get_url()

        # Skip remote tracks discovery
        if url.startswith('http://') or url.startswith('https://'):
            return False
        elif status == DiscoveryStatus.PENDING:
            self.discover_item(nextSong, self._on_next_item_validated, _iter)
        elif status == DiscoveryStatus.FAILED:
            GLib.idle_add(self._validate_next_track)

        return False

    @log
    def _on_cache_lookup(self, surface, data=None):
        self.coverImg.set_from_surface(surface)
        self.emit('thumbnail-updated')

    @log
    def play(self):
        if self.playlist is None:
            return

        media = None

        if self.player.get_state(1)[1] != Gst.State.PAUSED:
            self.stop()

            media = self.get_current_media()
            if not media:
                return

            self.load(media)

        self.player.set_state(Gst.State.PLAYING)
        self._update_position_callback()
        if media:
            t = Thread(target=self.update_now_playing_in_lastfm, args=(media.get_url(),))
            t.setDaemon(True)
            t.start()
        if not self.timeout and self.progressScale.get_realized():
            self._update_timeout()

        self.emit('playback-status-changed')
        self.emit('playing-changed')

    @log
    def pause(self):
        self._remove_timeout()

        self.player.set_state(Gst.State.PAUSED)
        self.emit('playback-status-changed')
        self.emit('playing-changed')

    @log
    def stop(self):
        self._remove_timeout()

        self.player.set_state(Gst.State.NULL)
        self.emit('playing-changed')

    @log
    def play_next(self):
        if self.playlist is None:
            return True

        if not self.nextBtn.get_sensitive():
            return True

        self.stop()
        self.currentTrack = self.nextTrack

        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
            self.play()

    @log
    def play_previous(self):
        if self.playlist is None:
            return

        if self.prevBtn.get_sensitive() is False:
            return

        position = self.get_position() / 1000000
        if position >= 5:
            self._progress_scale_zero()
            self.on_progress_scale_change_value(self.progressScale)
            return

        self.stop()

        self.currentTrack = self._get_previous_track()
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
            self.play()

    @log
    def play_pause(self):
        if self.player.get_state(1)[1] == Gst.State.PLAYING:
            self.set_playing(False)
        else:
            self.set_playing(True)

    @log
    def set_playlist(self, type, id, model, iter, field, discovery_status_field):
        self.stop()

        old_playlist = self.playlist
        if old_playlist != model:
            self.playlist = model
            if self.playlist_insert_handler:
                old_playlist.disconnect(self.playlist_insert_handler)
            if self.playlist_delete_handler:
                old_playlist.disconnect(self.playlist_delete_handler)

        self.playlistType = type
        self.playlistId = id
        self.currentTrack = Gtk.TreeRowReference.new(model, model.get_path(iter))
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
        self.playlistField = field
        self.discovery_status_field = discovery_status_field

        if old_playlist != model:
            self.playlist_insert_handler = model.connect('row-inserted', self._on_playlist_size_changed)
            self.playlist_delete_handler = model.connect('row-deleted', self._on_playlist_size_changed)
            self.emit('playlist-changed')
        self.emit('current-changed')

    @log
    def running_playlist(self, type, id):
        if type == self.playlistType and id == self.playlistId:
            return self.playlist
        else:
            return None

    @log
    def _setup_view(self):
        self._ui = Gtk.Builder()
        self._ui.add_from_resource('/org/gnome/Music/PlayerToolbar.ui')
        self.actionbar = self._ui.get_object('actionbar')
        self.prevBtn = self._ui.get_object('previous_button')
        self.playBtn = self._ui.get_object('play_button')
        self.nextBtn = self._ui.get_object('next_button')
        self._playImage = self._ui.get_object('play_image')
        self._pauseImage = self._ui.get_object('pause_image')
        self.progressScale = self._ui.get_object('progress_scale')
        self.songPlaybackTimeLabel = self._ui.get_object('playback')
        self.songTotalTimeLabel = self._ui.get_object('duration')
        self.titleLabel = self._ui.get_object('title')
        self.artistLabel = self._ui.get_object('artist')
        self.coverImg = self._ui.get_object('cover')
        self.coverImg.set_property("width-request", ArtSize.xsmall.width)
        self.coverImg.set_property("height-request", ArtSize.xsmall.height)

        self.duration = self._ui.get_object('duration')
        self.repeatBtnImage = self._ui.get_object('playlistRepeat')

        if Gtk.Settings.get_default().get_property('gtk_application_prefer_dark_theme'):
            color = Gdk.RGBA(red=1.0, green=1.0, blue=1.0, alpha=1.0)
        else:
            color = Gdk.RGBA(red=0.0, green=0.0, blue=0.0, alpha=0.0)
        self._playImage.override_color(Gtk.StateFlags.ACTIVE, color)
        self._pauseImage.override_color(Gtk.StateFlags.ACTIVE, color)

        self._sync_repeat_image()

        self.prevBtn.connect('clicked', self._on_prev_btn_clicked)
        self.playBtn.connect('clicked', self._on_play_btn_clicked)
        self.nextBtn.connect('clicked', self._on_next_btn_clicked)
        self.progressScale.connect('button-press-event', self._on_progress_scale_event)
        self.progressScale.connect('value-changed', self._on_progress_value_changed)
        self.progressScale.connect('button-release-event', self._on_progress_scale_button_released)
        self._ps_draw = self.progressScale.connect('draw',
            self._on_progress_scale_draw)

    @log
    def _on_progress_scale_button_released(self, scale, data):
        self.on_progress_scale_change_value(self.progressScale)
        self._update_position_callback()
        self.player.set_state(self._lastState)
        self._update_timeout()
        return False

    def _on_progress_value_changed(self, widget):
        seconds = int(self.progressScale.get_value() / 60)
        self.songPlaybackTimeLabel.set_label(utils.seconds_to_string(seconds))
        return False

    @log
    def _on_progress_scale_event(self, scale, data):
        self._lastState = self.player.get_state(1)[1]
        self.player.set_state(Gst.State.PAUSED)
        self._remove_timeout()
        return False

    def _on_progress_scale_draw(self, cr, data):
        self._update_timeout()
        self.progressScale.disconnect(self._ps_draw)
        return False

    def _update_timeout(self):
        """Update the duration for self.timeout and self._seconds_timeout

        Sets the period of self.timeout to a value small enough to make the
        slider of self.progressScale move smoothly based on the current song
        duration and progressScale length.  self._seconds_timeout is always set
        to a fixed value, short enough to hide irregularities in GLib event
        timing from the user, for updating the songPlaybackTimeLabel.
        """
        # Don't run until progressScale has been realized
        if self.progressScale.get_realized() == False:
            return

        # Update self.timeout
        width = self.progressScale.get_allocated_width()
        padding = self.progressScale.get_style_context().get_padding(
            Gtk.StateFlags.NORMAL)
        width -= padding.left + padding.right
        duration = self.player.query_duration(Gst.Format.TIME)[1] / 10**9
        timeout_period = min(1000 * duration // width, 1000)

        if self.timeout:
            GLib.source_remove(self.timeout)
        self.timeout = GLib.timeout_add(
            timeout_period, self._update_position_callback)

        # Update self._seconds_timeout
        if not self._seconds_timeout:
            self.seconds_period = 1000
            self._seconds_timeout = GLib.timeout_add(
                self.seconds_period, self._update_seconds_callback)

    def _remove_timeout(self):
        if self.timeout:
            GLib.source_remove(self.timeout)
            self.timeout = None
        if self._seconds_timeout:
            GLib.source_remove(self._seconds_timeout)
            self._seconds_timeout = None

    def _progress_scale_zero(self):
        self.progressScale.set_value(0)
        self._on_progress_value_changed(None)

    @log
    def _on_play_btn_clicked(self, btn):
        if self._get_playing():
            self.pause()
        else:
            self.play()

    @log
    def _on_next_btn_clicked(self, btn):
        self.play_next()

    @log
    def _on_prev_btn_clicked(self, btn):
        self.play_previous()

    @log
    def _set_duration(self, duration):
        self.duration = duration
        self.played_seconds = 0
        self.scrobbled = False
        self.progressScale.set_range(0.0, duration * 60)

    @log
    def scrobble_song(self, url):
        # Update playlists
        playlists.update_all_static_playlists()

        if self.last_fm:
            api_key = self.last_fm.props.client_id
            sk = self.last_fm.call_get_access_token_sync(None)[0]
            secret = self.last_fm.props.client_secret

            sig = "api_key%sartist[0]%smethodtrack.scrobblesk%stimestamp[0]%strack[0]%s%s" %\
                (api_key, self._currentArtist, sk, self._currentTimestamp, self._currentTitle, secret)

            api_sig = md5(sig.encode()).hexdigest()
            requests_dict = {
                "api_key": api_key,
                "method": "track.scrobble",
                "artist[0]": self._currentArtist,
                "track[0]": self._currentTitle,
                "timestamp[0]": self._currentTimestamp,
                "sk": sk,
                "api_sig": api_sig
            }
            try:
                r = requests.post("https://ws.audioscrobbler.com/2.0/", requests_dict)
                if r.status_code != 200:
                    logger.warn("Failed to scrobble track: %s %s" % (r.status_code, r.reason))
                    logger.warn(r.text)
            except Exception as e:
                logger.warn(e)

    @log
    def update_now_playing_in_lastfm(self, url):
        if self.last_fm:
            api_key = self.last_fm.props.client_id
            sk = self.last_fm.call_get_access_token_sync(None)[0]
            secret = self.last_fm.props.client_secret

            sig = "api_key%sartist%smethodtrack.updateNowPlayingsk%strack%s%s" % \
                (api_key, self._currentArtist, sk, self._currentTitle, secret)

            api_sig = md5(sig.encode()).hexdigest()
            request_dict = {
                "api_key": api_key,
                "method": "track.updateNowPlaying",
                "artist": self._currentArtist,
                "track": self._currentTitle,
                "sk": sk,
                "api_sig": api_sig
            }
            try:
                r = requests.post("https://ws.audioscrobbler.com/2.0/", request_dict)
                if r.status_code != 200:
                    logger.warn("Failed to update currently played track: %s %s" % (r.status_code, r.reason))
                    logger.warn(r.text)
            except Exception as e:
                logger.warn(e)

    def _update_position_callback(self):
        position = self.player.query_position(Gst.Format.TIME)[1] / 1000000000
        if position > 0:
            self.progressScale.set_value(position * 60)
        self._update_timeout()
        return False

    def _update_seconds_callback(self):
        self._on_progress_value_changed(None)

        position = self.player.query_position(Gst.Format.TIME)[1] / 10**9
        if position > 0:
            self.played_seconds += self.seconds_period / 1000
            try:
                percentage = self.played_seconds / self.duration
                if not self.scrobbled and percentage > 0.4:
                    current_media = self.get_current_media()
                    self.scrobbled = True
                    if current_media:
                        grilo.bump_play_count(self.get_current_media())
                        grilo.set_last_played(current_media)
                        just_played_url = self.get_current_media().get_url()
                        t = Thread(target=self.scrobble_song, args=(just_played_url,))
                        t.setDaemon(True)
                        t.start()
            except Exception as e:
                logger.warn("Error: %s, %s", e.__class__, e)
        return True

    @log
    def _sync_repeat_image(self):
        icon = None
        if self.repeat == RepeatType.NONE:
            icon = 'media-playlist-consecutive-symbolic'
        elif self.repeat == RepeatType.SHUFFLE:
            icon = 'media-playlist-shuffle-symbolic'
        elif self.repeat == RepeatType.ALL:
            icon = 'media-playlist-repeat-symbolic'
        elif self.repeat == RepeatType.SONG:
            icon = 'media-playlist-repeat-song-symbolic'

        self.repeatBtnImage.set_from_icon_name(icon, Gtk.IconSize.MENU)
        self.emit('repeat-mode-changed')

    @log
    def on_progress_scale_change_value(self, scroll):
        seconds = scroll.get_value() / 60
        if seconds != self.duration:
            self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seconds * 1000000000)
            try:
                self.emit('seeked', seconds * 1000000)
            except TypeError:
                # See https://bugzilla.gnome.org/show_bug.cgi?id=733095
                pass
        else:
            duration = self.player.query_duration(Gst.Format.TIME)
            if duration:
                # Rewind a second back before the track end
                self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, duration[1] - 1000000000)
                try:
                    self.emit('seeked', (duration[1] - 1000000000) / 1000)
                except TypeError:
                    # See https://bugzilla.gnome.org/show_bug.cgi?id=733095
                    pass
        return True

    # MPRIS

    @log
    def Stop(self):
        self._progress_scale_zero()
        self.progressScale.set_sensitive(False)
        self.playBtn.set_image(self._playImage)
        self.stop()
        self.emit('playback-status-changed')

    @log
    def get_playback_status(self):
        ok, state, pending = self.player.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            return PlaybackStatus.STOPPED

        if state == Gst.State.PLAYING:
            return PlaybackStatus.PLAYING
        elif state == Gst.State.PAUSED:
            return PlaybackStatus.PAUSED
        else:
            return PlaybackStatus.STOPPED

    @log
    def get_repeat_mode(self):
        return self.repeat

    @log
    def set_repeat_mode(self, mode):
        self.repeat = mode
        self._sync_repeat_image()

    @log
    def get_position(self):
        return self.player.query_position(Gst.Format.TIME)[1] / 1000

    @log
    def set_position(self, offset, start_if_ne=False, next_on_overflow=False):
        if offset < 0:
            if start_if_ne:
                offset = 0
            else:
                return

        duration = self.player.query_duration(Gst.Format.TIME)
        if duration is None:
            return

        if duration[1] >= offset * 1000:
            self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, offset * 1000)
            self.emit('seeked', offset)
        elif next_on_overflow:
            self.play_next()

    @log
    def get_volume(self):
        return self.player.get_volume(GstAudio.StreamVolumeFormat.LINEAR)

    @log
    def set_volume(self, rate):
        self.player.set_volume(GstAudio.StreamVolumeFormat.LINEAR, rate)
        self.emit('volume-changed')

    @log
    def get_current_media(self):
        if not self.currentTrack or not self.currentTrack.valid():
            return None
        currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        if self.playlist.get_value(currentTrack, self.discovery_status_field) == DiscoveryStatus.FAILED:
            return None
        return self.playlist.get_value(currentTrack, self.playlistField)
Example #20
0
    def __init__(self, name, title, window, view_type, use_sidebar=False, sidebar=None):
        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self._adjustmentValueId = 0
        self._adjustmentChangedId = 0
        self._scrollbarVisibleId = 0
        self.old_vsbl_range = None
        self.model = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,
            GObject.TYPE_OBJECT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,
            GObject.TYPE_STRING,
            GObject.TYPE_INT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT
        )

        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT,
            )
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self.star_handler = StarHandlerWidget(self, 9)
        self._cursor = None
        self.window = window
        self.header_bar = window.toolbar
        self.selection_toolbar = window.selection_toolbar
        self.header_bar._select_button.connect(
            'toggled', self._on_header_bar_toggled)
        self.header_bar._cancel_button.connect(
            'clicked', self._on_cancel_button_clicked)

        self.name = name
        self.title = title
        self.add(self._grid)

        self.show_all()
        self.view.hide()
        self._items = []

        scale = self.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.medium)


        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self.selection_socket = None
        self.header_bar.connect('selection-mode-changed',
                                self._on_selection_mode_changed)

        self._discovering_urls = {}
        grilo.connect('changes-pending', self._on_changes_pending)
Example #21
0
    def __init__(self,
                 name,
                 title,
                 window,
                 view_type,
                 use_sidebar=False,
                 sidebar=None):
        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self._adjustmentValueId = 0
        self._adjustmentChangedId = 0
        self._scrollbarVisibleId = 0
        self.old_vsbl_range = None
        self.model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GdkPixbuf.Pixbuf, GObject.TYPE_OBJECT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT,
                                   GObject.TYPE_STRING, GObject.TYPE_INT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT)

        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT, )
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self.star_handler = StarHandlerWidget(self, 9)
        self._cursor = None
        self.window = window
        self.header_bar = window.toolbar
        self.selection_toolbar = window.selection_toolbar
        self.header_bar._select_button.connect('toggled',
                                               self._on_header_bar_toggled)
        self.header_bar._cancel_button.connect('clicked',
                                               self._on_cancel_button_clicked)

        self.name = name
        self.title = title
        self.add(self._grid)

        self.show_all()
        self.view.hide()
        self._items = []

        scale = self.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.medium)

        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self.selection_socket = None
        self.header_bar.connect('selection-mode-changed',
                                self._on_selection_mode_changed)

        self._discovering_urls = {}
        grilo.connect('changes-pending', self._on_changes_pending)
Example #22
0
class BaseView(Gtk.Stack):
    nowPlayingIconName = 'media-playback-start-symbolic'
    errorIconName = 'dialog-error-symbolic'

    selection_mode = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<BaseView>'

    @log
    def __init__(self, name, title, window, view_type, use_sidebar=False, sidebar=None):
        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self._adjustmentValueId = 0
        self._adjustmentChangedId = 0
        self._scrollbarVisibleId = 0
        self.old_vsbl_range = None
        self.model = Gtk.ListStore(
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,
            GObject.TYPE_OBJECT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,
            GObject.TYPE_STRING,
            GObject.TYPE_INT,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT
        )

        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT,
            )
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self.star_handler = StarHandlerWidget(self, 9)
        self._cursor = None
        self.window = window
        self.header_bar = window.toolbar
        self.selection_toolbar = window.selection_toolbar
        self.header_bar._select_button.connect(
            'toggled', self._on_header_bar_toggled)
        self.header_bar._cancel_button.connect(
            'clicked', self._on_cancel_button_clicked)

        self.name = name
        self.title = title
        self.add(self._grid)

        self.show_all()
        self.view.hide()
        self._items = []

        scale = self.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.medium)


        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self.selection_socket = None
        self.header_bar.connect('selection-mode-changed',
                                self._on_selection_mode_changed)

        self._discovering_urls = {}
        grilo.connect('changes-pending', self._on_changes_pending)

    @log
    def _on_changes_pending(self, data=None):
        pass

    @log
    def _setup_view(self, view_type):
        self.view = Gd.MainView(shadow_type=Gtk.ShadowType.NONE)
        self.view.set_view_type(view_type)

        self.view.click_handler = self.view.connect('item-activated', self._on_item_activated)
        self.view.connect('selection-mode-request', self._on_selection_mode_request)

        self.view.bind_property('selection-mode', self, 'selection_mode',
                                GObject.BindingFlags.BIDIRECTIONAL)

        self.view.connect('view-selection-changed', self._on_view_selection_changed)

        self._box.pack_start(self.view, True, True, 0)

    @log
    def _on_header_bar_toggled(self, button):
        self.selection_mode = button.get_active()

        if self.selection_mode:
            self.header_bar.set_selection_mode(True)
            self.player.actionbar.set_visible(False)
            self.selection_toolbar.actionbar.set_visible(True)
            self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
            self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
        else:
            self.header_bar.set_selection_mode(False)
            self.player.actionbar.set_visible(self.player.currentTrack is not None)
            self.selection_toolbar.actionbar.set_visible(False)
            self.unselect_all()

    @log
    def _on_cancel_button_clicked(self, button):
        self.view.set_selection_mode(False)
        self.header_bar.set_selection_mode(False)

    @log
    def _on_grilo_ready(self, data=None):
        # FIXME: with async changes in Window this seems never to be
        # called anymore. Fix it proper or remove.
        if (self.header_bar.get_stack().get_visible_child() == self and not self._init):
            self._populate()
        self.header_bar.get_stack().connect('notify::visible-child',
                                            self._on_headerbar_visible)

    @log
    def _on_headerbar_visible(self, widget, param):
        if self == widget.get_visible_child() and not self._init:
            self._populate()

    @log
    def _on_view_selection_changed(self, widget):
        if not self.selection_mode:
            return

        items = self.view.get_selection()
        self.update_header_from_selection(len(items))

    @log
    def update_header_from_selection(self, n_items):
        self.selection_toolbar._add_to_playlist_button.\
            set_sensitive(n_items > 0)
        self.selection_toolbar._remove_from_playlist_button.\
            set_sensitive(n_items > 0)
        if n_items > 0:
            self.header_bar._selection_menu_label.set_text(
                ngettext("Selected %d item", "Selected %d items", n_items) % n_items)
        else:
            self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))

    @log
    def _populate(self, data=None):
        self._init = True
        self.populate()

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        pass

    @log
    def populate(self):
        pass

    @log
    def _add_item(self, source, param, item, remaining=0, data=None):
        if not item:
            if remaining == 0:
                self.view.set_model(self.model)
                self.window.pop_loading_notification()
                self.view.show()
            return

        self._offset += 1
        artist = utils.get_artist_name(item)
        title = utils.get_media_title(item)

        _iter = self.model.append(None)

        loading_icon = Gdk.pixbuf_get_from_surface(
            self._loadin_icon_surface, 0, 0,
            self._loading_icon_surface.get_width(),
            self._loading_icon_surface.get_height())

        self.model[_iter][0, 1, 2, 3, 4, 5, 7, 9] = [
            str(item.get_id()),
            '',
            title,
            artist,
            loading_icon,
            item,
            0,
            False
        ]
        self.cache.lookup(item, self._iconWidth, self._iconHeight,
                          self._on_lookup_ready, _iter)

    @log
    def _on_lookup_ready(self, surface, _iter):
        if surface:
            pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
                                                 surface.get_width(),
                                                 surface.get_height())

            self.model[_iter][4] = pixbuf

    @log
    def _add_list_renderers(self):
        pass

    @log
    def _on_item_activated(self, widget, id, path):
        pass

    @log
    def _on_selection_mode_request(self, *args):
        self.header_bar._select_button.clicked()

    @log
    def get_selected_tracks(self, callback):
        callback([])

    @log
    def _set_selection(self, value, parent=None):
        count = 0
        _iter = self.model.iter_children(parent)
        while _iter is not None:
            if self.model.iter_has_child(_iter):
                count += self._set_selection(value, _iter)
            if self.model[_iter][5]:
                self.model[_iter][6] = value
                count += 1
            _iter = self.model.iter_next(_iter)
        return count

    @log
    def select_all(self):
        """Select all the available tracks."""
        count = self._set_selection(True)

        if count > 0:
            self.selection_toolbar._add_to_playlist_button.set_sensitive(True)
            self.selection_toolbar._remove_from_playlist_button.set_sensitive(True)

        self.update_header_from_selection(count)
        self.view.queue_draw()

    @log
    def unselect_all(self):
        """Unselects all the selected tracks."""
        self._set_selection(False)
        self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
        self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
        self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
        self.queue_draw()
Example #23
0
    def _get_metadata(self, media=None):
        if not media:
            media = self.player.get_current_media()
        if not media:
            return {}

        metadata = {
            'mpris:trackid': GLib.Variant('o', self._get_media_id(media)),
            'xesam:url': GLib.Variant('s', media.get_url())
        }

        try:
            length = media.get_duration() * 1000000
            assert length is not None
            metadata['mpris:length'] = GLib.Variant('x', length)
        except:
            pass

        try:
            trackNumber = media.get_track_number()
            assert trackNumber is not None
            metadata['xesam:trackNumber'] = GLib.Variant('i', trackNumber)
        except:
            pass

        try:
            useCount = media.get_play_count()
            assert useCount is not None
            metadata['xesam:useCount'] = GLib.Variant('i', useCount)
        except:
            pass

        try:
            userRating = media.get_rating()
            assert userRating is not None
            metadata['xesam:userRating'] = GLib.Variant('d', userRating)
        except:
            pass

        try:
            title = AlbumArtCache.get_media_title(media)
            assert title is not None
            metadata['xesam:title'] = GLib.Variant('s', title)
        except:
            pass

        try:
            album = media.get_album()
            assert album is not None
        except:
            try:
                album = media.get_album()
                assert album is not None
            except:
                album = _("Unknown Album")
        finally:
            metadata['xesam:album'] = GLib.Variant('s', album)

        artist = utils.get_artist_name(media)
        metadata['xesam:artist'] = GLib.Variant('as', [artist])
        metadata['xesam:albumArtist'] = GLib.Variant('as', [artist])

        try:
            genre = media.get_genre()
            assert genre is not None
            metadata['xesam:genre'] = GLib.Variant('as', genre)
        except:
            pass

        try:
            lastUsed = media.get_last_played()
            assert lastUsed is not None
            metadata['xesam:lastUsed'] = GLib.Variant('s', lastUsed)
        except:
            pass

        try:
            artUrl = media.get_thumbnail()
            assert artUrl is not None
            metadata['mpris:artUrl'] = GLib.Variant('s', artUrl)
        except:
            pass

        return metadata
Example #24
0
class BaseView(Gtk.Stack):
    nowPlayingIconName = 'media-playback-start-symbolic'
    errorIconName = 'dialog-error-symbolic'

    selection_mode = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<BaseView>'

    @log
    def __init__(self,
                 name,
                 title,
                 window,
                 view_type,
                 use_sidebar=False,
                 sidebar=None):
        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self._adjustmentValueId = 0
        self._adjustmentChangedId = 0
        self._scrollbarVisibleId = 0
        self.old_vsbl_range = None
        self.model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GdkPixbuf.Pixbuf, GObject.TYPE_OBJECT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT,
                                   GObject.TYPE_STRING, GObject.TYPE_INT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT)

        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT, )
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self.star_handler = StarHandlerWidget(self, 9)
        self._cursor = None
        self.window = window
        self.header_bar = window.toolbar
        self.selection_toolbar = window.selection_toolbar
        self.header_bar._select_button.connect('toggled',
                                               self._on_header_bar_toggled)
        self.header_bar._cancel_button.connect('clicked',
                                               self._on_cancel_button_clicked)

        self.name = name
        self.title = title
        self.add(self._grid)

        self.show_all()
        self.view.hide()
        self._items = []

        scale = self.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.medium)

        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self.selection_socket = None
        self.header_bar.connect('selection-mode-changed',
                                self._on_selection_mode_changed)

        self._discovering_urls = {}
        grilo.connect('changes-pending', self._on_changes_pending)

    @log
    def _on_changes_pending(self, data=None):
        pass

    @log
    def _setup_view(self, view_type):
        self.view = Gd.MainView(shadow_type=Gtk.ShadowType.NONE)
        self.view.set_view_type(view_type)

        self.view.click_handler = self.view.connect('item-activated',
                                                    self._on_item_activated)
        self.view.connect('selection-mode-request',
                          self._on_selection_mode_request)

        self.view.bind_property('selection-mode', self, 'selection_mode',
                                GObject.BindingFlags.BIDIRECTIONAL)

        self.view.connect('view-selection-changed',
                          self._on_view_selection_changed)

        self._box.pack_start(self.view, True, True, 0)

    @log
    def _on_header_bar_toggled(self, button):
        self.selection_mode = button.get_active()

        if self.selection_mode:
            self.header_bar.set_selection_mode(True)
            self.player.actionbar.set_visible(False)
            self.selection_toolbar.actionbar.set_visible(True)
            self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
            self.selection_toolbar._remove_from_playlist_button.set_sensitive(
                False)
        else:
            self.header_bar.set_selection_mode(False)
            self.player.actionbar.set_visible(
                self.player.currentTrack is not None)
            self.selection_toolbar.actionbar.set_visible(False)
            self.unselect_all()

    @log
    def _on_cancel_button_clicked(self, button):
        self.view.set_selection_mode(False)
        self.header_bar.set_selection_mode(False)

    @log
    def _on_grilo_ready(self, data=None):
        if (self.header_bar.get_stack().get_visible_child() == self
                and not self._init):
            self._populate()
        self.header_bar.get_stack().connect('notify::visible-child',
                                            self._on_headerbar_visible)

    @log
    def _on_headerbar_visible(self, widget, param):
        if self == widget.get_visible_child() and not self._init:
            self._populate()

    @log
    def _on_view_selection_changed(self, widget):
        if not self.selection_mode:
            return

        items = self.view.get_selection()
        self.update_header_from_selection(len(items))

    @log
    def update_header_from_selection(self, n_items):
        self.selection_toolbar._add_to_playlist_button.\
            set_sensitive(n_items > 0)
        self.selection_toolbar._remove_from_playlist_button.\
            set_sensitive(n_items > 0)
        if n_items > 0:
            self.header_bar._selection_menu_label.set_text(
                ngettext("Selected %d item", "Selected %d items", n_items) %
                n_items)
        else:
            self.header_bar._selection_menu_label.set_text(
                _("Click on items to select them"))

    @log
    def _populate(self, data=None):
        self._init = True
        self.populate()

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        pass

    @log
    def populate(self):
        print('populate')

    @log
    def _add_item(self, source, param, item, remaining=0, data=None):
        self.window.notification.set_timeout(0)
        if not item:
            if remaining == 0:
                self.view.set_model(self.model)
                self.window.notification.dismiss()
                self.view.show()
            return

        self._offset += 1
        artist = utils.get_artist_name(item)
        title = utils.get_media_title(item)

        _iter = self.model.append(None)

        loading_icon = Gdk.pixbuf_get_from_surface(
            self._loadin_icon_surface, 0, 0,
            self._loading_icon_surface.get_width(),
            self._loading_icon_surface.get_height())

        self.model[_iter][0, 1, 2, 3, 4, 5, 7, 9] = [
            str(item.get_id()), '', title, artist, loading_icon, item, 0, False
        ]
        self.cache.lookup(item, self._iconWidth, self._iconHeight,
                          self._on_lookup_ready, _iter)

    @log
    def _on_lookup_ready(self, surface, _iter):
        if surface:
            pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
                                                 surface.get_width(),
                                                 surface.get_height())

            self.model[_iter][4] = pixbuf

    @log
    def _add_list_renderers(self):
        pass

    @log
    def _on_item_activated(self, widget, id, path):
        pass

    @log
    def _on_selection_mode_request(self, *args):
        self.header_bar._select_button.clicked()

    @log
    def get_selected_tracks(self, callback):
        callback([])

    def _on_list_widget_star_render(self, col, cell, model, _iter, data):
        pass

    @log
    def _set_selection(self, value, parent=None):
        count = 0
        _iter = self.model.iter_children(parent)
        while _iter is not None:
            if self.model.iter_has_child(_iter):
                count += self._set_selection(value, _iter)
            if self.model[_iter][5]:
                self.model[_iter][6] = value
                count += 1
            _iter = self.model.iter_next(_iter)
        return count

    @log
    def select_all(self):
        """Select all the available tracks."""
        count = self._set_selection(True)

        if count > 0:
            self.selection_toolbar._add_to_playlist_button.set_sensitive(True)
            self.selection_toolbar._remove_from_playlist_button.set_sensitive(
                True)

        self.update_header_from_selection(count)
        self.view.queue_draw()

    @log
    def unselect_all(self):
        """Unselects all the selected tracks."""
        self._set_selection(False)
        self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
        self.selection_toolbar._remove_from_playlist_button.set_sensitive(
            False)
        self.header_bar._selection_menu_label.set_text(
            _("Click on items to select them"))
        self.queue_draw()
class AlbumWidget(Gtk.EventBox):
    """Album widget.

    The album widget consists of an image with the album art
    on the left and a list of songs on the right.
    """

    _duration = 0

    def __repr__(self):
        return '<AlbumWidget>'

    @log
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._ui = Gtk.Builder()
        self._ui.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
        self._create_model()
        self.view = Gd.MainView(shadow_type=Gtk.ShadowType.NONE)
        self.view.set_view_type(Gd.MainViewType.LIST)
        self._album = None
        self._header_bar = None
        self.view.connect('item-activated', self._on_item_activated)

        view_box = self._ui.get_object('view')
        self._ui.get_object('scrolledWindow').set_placement(
            Gtk.CornerType.TOP_LEFT)
        self.view.connect('selection-mode-request',
                          self._on_selection_mode_request)
        child_view = self.view.get_children()[0]
        child_view.set_margin_top(64)
        child_view.set_margin_bottom(64)
        child_view.set_margin_end(32)
        self.view.remove(child_view)
        view_box.add(child_view)

        self.add(self._ui.get_object('AlbumWidget'))
        self._star_handler = StarHandlerWidget(self, 9)
        self._add_list_renderers()
        self.get_style_context().add_class('view')
        self.get_style_context().add_class('content-view')
        self.view.get_generic_view().get_style_context().remove_class('view')
        self.show_all()

    @log
    def _on_selection_mode_request(self, *args):
        """Selection mode toggled."""
        self._header_bar._select_button.clicked()

    @log
    def _on_item_activated(self, widget, id, path):
        """List row activated."""
        if self._star_handler.star_renderer_click:
            self._star_handler.star_renderer_click = False
            return

        _iter = self.model.get_iter(path)

        if self.model[_iter][10] != DiscoveryStatus.FAILED:
            if (self._iter_to_clean
                    and self._player.playlistId == self._album):
                item = self.model[self._iter_to_clean][5]
                title = utils.get_media_title(item)
                self.model[self._iter_to_clean][0] = title
                # Hide now playing icon
                self.model[self._iter_to_clean][6] = False
            self._player.set_playlist('Album', self._album, self.model, _iter,
                                      5, 11)
            self._player.set_playing(True)

    @log
    def _add_list_renderers(self):
        """Create the ListView columns."""
        list_widget = self.view.get_generic_view()

        cols = list_widget.get_columns()
        cols[0].set_min_width(100)
        cols[0].set_max_width(200)
        cells = cols[0].get_cells()
        cells[2].set_visible(False)
        cells[1].set_visible(False)

        now_playing_symbol_renderer = Gtk.CellRendererPixbuf(xpad=0,
                                                             xalign=0.5,
                                                             yalign=0.5)

        column_now_playing = Gtk.TreeViewColumn()
        column_now_playing.set_fixed_width(48)
        column_now_playing.pack_start(now_playing_symbol_renderer, False)
        column_now_playing.set_cell_data_func(now_playing_symbol_renderer,
                                              self._on_list_widget_icon_render,
                                              None)
        list_widget.insert_column(column_now_playing, 0)

        type_renderer = Gd.StyledTextRenderer(
            xpad=16, ellipsize=Pango.EllipsizeMode.END, xalign=0.0)

        list_widget.add_renderer(type_renderer, lambda *args: None, None)
        cols[0].clear_attributes(type_renderer)
        cols[0].add_attribute(type_renderer, 'markup', 0)

        duration_renderer = Gd.StyledTextRenderer(
            xpad=16, ellipsize=Pango.EllipsizeMode.END, xalign=1.0)

        duration_renderer.add_class('dim-label')
        list_widget.add_renderer(duration_renderer, lambda *args: None, None)
        cols[0].clear_attributes(duration_renderer)
        cols[0].add_attribute(duration_renderer, 'markup', 1)

        self._star_handler.add_star_renderers(list_widget, cols)

    def _on_list_widget_icon_render(self, col, cell, model, _iter, data):
        if not self._player.currentTrackUri:
            cell.set_visible(False)
            return

        if model[_iter][10] == DiscoveryStatus.FAILED:
            cell.set_property('icon-name', ERROR_ICON_NAME)
            cell.set_visible(True)
        elif model[_iter][5].get_url() == self._player.currentTrackUri:
            cell.set_property('icon-name', NOW_PLAYING_ICON_NAME)
            cell.set_visible(True)
        else:
            cell.set_visible(False)

    @log
    def _create_model(self):
        """Create the ListStore model for this widget."""
        self.model = Gtk.ListStore(
            GObject.TYPE_STRING,  # title
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,  # icon
            GObject.TYPE_OBJECT,  # song object
            GObject.TYPE_BOOLEAN,  # item selected
            GObject.TYPE_STRING,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,  # icon shown
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT)

    @log
    def update(self, artist, album, item, header_bar, selection_toolbar):
        """Update the album widget.

        :param str artist: The artist name
        :param str album: The album name
        :param item: The grilo media item
        :param header_bar: The header bar object
        :param selection_toolbar: The selection toolbar object
        """
        self.selection_toolbar = selection_toolbar
        self._header_bar = header_bar
        self._album = album
        self._ui.get_object('cover').set_from_surface(
            self._loading_icon_surface)
        self._cache.lookup(item, ArtSize.large, self._on_lookup, None)
        self._duration = 0
        self._create_model()
        GLib.idle_add(grilo.populate_album_songs, item, self.add_item)
        header_bar._select_button.connect(
            'toggled', self._on_header_select_button_toggled)
        header_bar._cancel_button.connect(
            'clicked', self._on_header_cancel_button_clicked)
        self.view.connect('view-selection-changed',
                          self._on_view_selection_changed)
        self.view.set_model(self.model)
        escaped_artist = GLib.markup_escape_text(artist)
        escaped_album = GLib.markup_escape_text(album)
        self._ui.get_object('artist_label').set_markup(escaped_artist)
        self._ui.get_object('title_label').set_markup(escaped_album)
        if (item.get_creation_date()):
            self._ui.get_object('released_label_info').set_text(
                str(item.get_creation_date().get_year()))
        else:
            self._ui.get_object('released_label_info').set_text('----')
        self._player.connect('playlist-item-changed', self._update_model)

    @log
    def _on_view_selection_changed(self, widget):
        items = self.view.get_selection()
        self.selection_toolbar._add_to_playlist_button.set_sensitive(
            len(items) > 0)
        if len(items) > 0:
            self._header_bar._selection_menu_label.set_text(
                ngettext("Selected %d item", "Selected %d items", len(items)) %
                len(items))
        else:
            self._header_bar._selection_menu_label.set_text(
                _("Click on items to select them"))

    @log
    def _on_header_cancel_button_clicked(self, button):
        """Cancel selection mode callback."""
        self.view.set_selection_mode(False)
        self._header_bar.set_selection_mode(False)
        self._header_bar.header_bar.title = self._album

    @log
    def _on_header_select_button_toggled(self, button):
        """Selection mode button clicked callback."""
        if button.get_active():
            self.view.set_selection_mode(True)
            self._header_bar.set_selection_mode(True)
            self._player.actionbar.set_visible(False)
            self.selection_toolbar.actionbar.set_visible(True)
            self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
            self._header_bar.header_bar.set_custom_title(
                self._header_bar._selection_menu_button)
        else:
            self.view.set_selection_mode(False)
            self._header_bar.set_selection_mode(False)
            self._header_bar.title = self._album
            self.selection_toolbar.actionbar.set_visible(False)
            if (self._player.get_playback_status() != 2):
                self._player.actionbar.set_visible(True)

    @log
    def add_item(self, source, prefs, track, remaining, data=None):
        """Add a song to the item to album list.

        :param source: The grilo source
        :param prefs:
        :param track: The grilo media object
        :param remaining: Remaining number of items to add
        :param data: User data
        """
        if track:
            self._duration = self._duration + track.get_duration()
            _iter = self.model.append()
            title = utils.get_media_title(track)
            escaped_title = GLib.markup_escape_text(title)
            self.model[_iter][0, 1, 2, 3, 4, 5, 9] = [
                escaped_title,
                self._player.seconds_to_string(track.get_duration()), '', '',
                None, track,
                bool(track.get_lyrics())
            ]
            self._ui.get_object('running_length_label_info').set_text(
                _("%d min") % (int(self._duration / 60) + 1))

    @log
    def _on_lookup(self, surface, data=None):
        """Albumart retrieved callback.

        :param surface: The Cairo surface retrieved
        :param path: The filesystem location the pixbuf
        :param data: User data
        """
        self._ui.get_object('cover').set_from_surface(surface)

    @log
    def _update_model(self, player, playlist, current_iter):
        """Player changed callback.

        :param player: The player object
        :param playlist: The current playlist
        :param current_iter: The current iter of the playlist model
        """
        # self is not our playlist, return
        if (playlist != self.model):
            return False

        current_song = playlist[current_iter][5]
        song_passed = False
        _iter = playlist.get_iter_first()
        self._duration = 0

        while _iter:
            song = playlist[_iter][5]
            self._duration += song.get_duration()
            escaped_title = GLib.markup_escape_text(
                utils.get_media_title(song))
            if (song == current_song):
                title = '<b>%s</b>' % escaped_title
                song_passed = True
            elif (song_passed):
                title = '<span>%s</span>' % escaped_title
            else:
                title = '<span color=\'grey\'>%s</span>' % escaped_title
            playlist[_iter][0] = title
            _iter = playlist.iter_next(_iter)
            self._ui.get_object('running_length_label_info').set_text(
                _("%d min") % (int(self._duration / 60) + 1))

        return False
Example #26
0
class AlbumWidget(Gtk.EventBox):
    """Album widget.

    The album widget consists of an image with the album art
    on the left and a list of songs on the right.
    """

    _duration = 0

    def __repr__(self):
        return '<AlbumWidget>'

    @log
    def __init__(self, player, parent_view):
        """Initialize the AlbumWidget.

        :param player: The player object
        :param parent_view: The view this widget is part of
        """
        Gtk.EventBox.__init__(self)

        self._songs = []

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.large)

        self._player = player
        self._iter_to_clean = None

        self._selection_mode = False

        self._builder = Gtk.Builder()
        self._builder.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
        self._create_model()
        self._album = None
        self._header_bar = None
        self._selection_mode_allowed = True

        self._composer_label = self._builder.get_object('composer_label')
        self._composer_info = self._builder.get_object('composer_info')

        view_box = self._builder.get_object('view')
        self._disc_listbox = DiscListBox()
        self._disc_listbox.set_selection_mode_allowed(True)
        # TODO: The top of the coverart is the same vertical
        # position as the top of the album songs, however
        # since we set a top margins for the discbox
        # subtract that margin here. A cleaner solution is
        # appreciated.
        self._disc_listbox.set_margin_top(64 - 16)
        self._disc_listbox.set_margin_bottom(64)
        self._disc_listbox.set_margin_end(32)
        self._disc_listbox.connect('selection-changed',
                                   self._on_selection_changed)
        view_box.add(self._disc_listbox)

        # FIXME: Assigned to appease searchview
        # _get_selected_songs
        self.view = self._disc_listbox

        self.add(self._builder.get_object('AlbumWidget'))
        self.get_style_context().add_class('view')
        self.get_style_context().add_class('content-view')

        self.show_all()

    @log
    def _on_selection_mode_request(self, *args):
        """Selection mode toggled."""
        self._header_bar._select_button.clicked()

    @log
    def _create_model(self):
        """Create the ListStore model for this widget."""
        self._model = Gtk.ListStore(
            GObject.TYPE_STRING,  # title
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GObject.TYPE_STRING,
            GdkPixbuf.Pixbuf,  # icon
            GObject.TYPE_OBJECT,  # song object
            GObject.TYPE_BOOLEAN,  # item selected
            GObject.TYPE_STRING,
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT,  # icon shown
            GObject.TYPE_BOOLEAN,
            GObject.TYPE_INT)

    @log
    def update(self, artist, album, item, header_bar, selection_toolbar):
        """Update the album widget.

        :param str artist: The artist name
        :param str album: The album name
        :param item: The grilo media item
        :param header_bar: The header bar object
        :param selection_toolbar: The selection toolbar object
        """
        # reset view
        self._songs = []
        self._create_model()
        for widget in self._disc_listbox.get_children():
            self._disc_listbox.remove(widget)

        self.selection_toolbar = selection_toolbar
        self._header_bar = header_bar
        self._album = album
        self._builder.get_object('cover').set_from_surface(
            self._loading_icon_surface)
        self._cache.lookup(item, ArtSize.large, self._on_lookup, None)
        self._duration = 0

        GLib.idle_add(grilo.populate_album_songs, item, self.add_item)
        header_bar._select_button.connect(
            'toggled', self._on_header_select_button_toggled)
        header_bar._cancel_button.connect(
            'clicked', self._on_header_cancel_button_clicked)

        # FIXME: use utils
        escaped_artist = GLib.markup_escape_text(artist)
        escaped_album = GLib.markup_escape_text(album)
        self._builder.get_object('artist_label').set_markup(escaped_artist)
        self._builder.get_object('title_label').set_markup(escaped_album)

        if (item.get_creation_date()):
            self._builder.get_object('released_label_info').set_text(
                str(item.get_creation_date().get_year()))
        else:
            self._builder.get_object('released_label_info').set_text('----')

        self._set_composer_label(item)

        self._player.connect('playlist-item-changed', self._update_model)

    @log
    def _set_composer_label(self, item):
        composer = item.get_composer()
        show = False

        if composer:
            self._composer_info.set_text(composer)
            show = True

        self._composer_label.set_visible(show)
        self._composer_info.set_visible(show)

    @log
    def _on_selection_changed(self, widget):
        items = self._disc_listbox.get_selected_items()
        self.selection_toolbar._add_to_playlist_button.set_sensitive(
            len(items) > 0)
        if len(items) > 0:
            self._header_bar._selection_menu_label.set_text(
                ngettext("Selected %d item", "Selected %d items", len(items)) %
                len(items))
        else:
            self._header_bar._selection_menu_label.set_text(
                _("Click on items to select them"))

    @log
    def _on_header_cancel_button_clicked(self, button):
        """Cancel selection mode callback."""
        self._disc_listbox.set_selection_mode(False)
        self._header_bar.set_selection_mode(False)
        self._header_bar.header_bar.title = self._album

    @log
    def _on_header_select_button_toggled(self, button):
        """Selection mode button clicked callback."""
        if button.get_active():
            self._selection_mode = True
            self._disc_listbox.set_selection_mode(True)
            self._header_bar.set_selection_mode(True)
            self._player.actionbar.set_visible(False)
            self._header_bar.header_bar.set_custom_title(
                self._header_bar._selection_menu_button)
        else:
            self._selection_mode = False
            self._disc_listbox.set_selection_mode(False)
            self._header_bar.set_selection_mode(False)
            if (self._player.get_playback_status() != 2):
                self._player.actionbar.set_visible(True)

    @log
    def _create_disc_box(self, disc_nr, disc_songs):
        disc_box = DiscBox(self._model)
        disc_box.set_songs(disc_songs)
        disc_box.set_disc_number(disc_nr)
        disc_box.set_columns(1)
        disc_box.show_song_numbers(False)
        disc_box.connect('song-activated', self._song_activated)
        disc_box.connect('selection-toggle', self._selection_mode_toggled)

        return disc_box

    @log
    def _selection_mode_toggled(self, widget):
        if not self._selection_mode_allowed:
            return

        self._selection_mode = not self._selection_mode
        self._on_selection_mode_request()

    @log
    def _song_activated(self, widget, song_widget):
        if not song_widget.can_be_played:
            return

        if self._selection_mode:
            song_widget.check_button.toggled()
            return

        self._player.stop()
        self._player.set_playlist('Album', self._album, song_widget.model,
                                  song_widget.itr, 5, 11)
        self._player.set_playing(True)
        return True

    @log
    def add_item(self, source, prefs, song, remaining, data=None):
        """Add a song to the item to album list.

        :param source: The grilo source
        :param prefs:
        :param song: The grilo media object
        :param remaining: Remaining number of items to add
        :param data: User data
        """
        if song:
            self._songs.append(song)

            self._duration = self._duration + song.get_duration()
            return

        discs = {}
        for song in self._songs:
            disc_nr = song.get_album_disc_number()
            if disc_nr not in discs.keys():
                discs[disc_nr] = [song]
            else:
                discs[disc_nr].append(song)
        for disc_nr in discs:
            disc = self._create_disc_box(disc_nr, discs[disc_nr])
            self._disc_listbox.add(disc)
            if len(discs) == 1:
                disc.show_disc_label(False)

        if remaining == 0:
            self._builder.get_object('running_length_label_info').set_text(
                _("%d min") % (int(self._duration / 60) + 1))

            self.show_all()

    @log
    def _on_lookup(self, surface, data=None):
        """Albumart retrieved callback.

        :param surface: The Cairo surface retrieved
        :param path: The filesystem location the pixbuf
        :param data: User data
        """
        self._builder.get_object('cover').set_from_surface(surface)

    @log
    def _update_model(self, player, playlist, current_iter):
        """Player changed callback.

        :param player: The player object
        :param playlist: The current playlist
        :param current_iter: The current iter of the playlist model
        """
        if (playlist != self._model):
            return True

        current_song = playlist[current_iter][5]

        self._duration = 0

        song_passed = False
        _iter = playlist.get_iter_first()

        while _iter:
            song = playlist[_iter][5]
            song_widget = song.song_widget
            self._duration += song.get_duration()
            escaped_title = GLib.markup_escape_text(
                utils.get_media_title(song))

            if (song == current_song):
                song_widget.now_playing_sign.show()
                song_widget.title.set_markup("<b>{}</b>".format(escaped_title))
                song_passed = True
            elif (song_passed):
                song_widget.now_playing_sign.hide()
                song_widget.title.set_markup(
                    "<span>{}</span>".format(escaped_title))
            else:
                song_widget.now_playing_sign.hide()
                song_widget.title.set_markup(
                    "<span color=\'grey\'>{}</span>".format(escaped_title))

            _iter = playlist.iter_next(_iter)

        self._builder.get_object('running_length_label_info').set_text(
            _("%d min") % (int(self._duration / 60) + 1))

        return True

    @log
    def select_all(self):
        self._disc_listbox.select_all()

    @log
    def select_none(self):
        self._disc_listbox.select_none()
Example #27
0
class ArtistAlbumWidget(Gtk.Box):

    __gsignals__ = {
        'tracks-loaded': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __repr__(self):
        return '<ArtistAlbumWidget>'

    @log
    def __init__(self, artist, album, player, model, header_bar, selectionModeAllowed):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.large)

        self.player = player
        self.album = album
        self.artist = artist
        self.model = model
        self.model.connect('row-changed', self._model_row_changed)
        self.header_bar = header_bar
        self.selectionMode = False
        self.selectionModeAllowed = selectionModeAllowed
        self.songs = []
        self.ui = Gtk.Builder()
        self.ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')

        GLib.idle_add(self._update_album_art)

        self.cover = self.ui.get_object('cover')
        self.cover.set_from_surface(self._loading_icon_surface)
        self.songsGrid = self.ui.get_object('grid1')
        self.ui.get_object('title').set_label(album.get_title())
        if album.get_creation_date():
            self.ui.get_object('year').set_markup(
                '<span color=\'grey\'>(%s)</span>' %
                str(album.get_creation_date().get_year())
            )
        self.tracks = []
        grilo.populate_album_songs(album, self.add_item)
        self.pack_start(self.ui.get_object('ArtistAlbumWidget'), True, True, 0)

    @log
    def add_item(self, source, prefs, track, remaining, data=None):
        if remaining == 0:
            self.songsGrid.show_all()
            self.emit("tracks-loaded")

        if track:
            self.tracks.append(track)
        else:
            for i, track in enumerate(self.tracks):
                ui = Gtk.Builder()
                ui.add_from_resource('/org/gnome/Music/TrackWidget.ui')
                song_widget = ui.get_object('eventbox1')
                self.songs.append(song_widget)
                ui.get_object('num')\
                    .set_markup('<span color=\'grey\'>%d</span>'
                                % len(self.songs))
                title = utils.get_media_title(track)
                ui.get_object('title').set_text(title)
                ui.get_object('title').set_alignment(0.0, 0.5)
                ui.get_object('title').set_max_width_chars(MAX_TITLE_WIDTH)

                self.songsGrid.attach(
                    song_widget,
                    int(i / (len(self.tracks) / 2)),
                    int(i % (len(self.tracks) / 2)), 1, 1
                )
                track.song_widget = song_widget
                itr = self.model.append(None)
                song_widget._iter = itr
                song_widget.model = self.model
                song_widget.title = ui.get_object('title')
                song_widget.checkButton = ui.get_object('select')
                song_widget.checkButton.set_visible(self.selectionMode)
                song_widget.checkButton.connect(
                    'toggled', self._check_button_toggled, song_widget
                )
                self.model.set(itr,
                               [0, 1, 2, 3, 5],
                               [title, '', '', False, track])
                song_widget.now_playing_sign = ui.get_object('image1')
                song_widget.now_playing_sign.set_from_icon_name(
                    NOW_PLAYING_ICON_NAME,
                    Gtk.IconSize.SMALL_TOOLBAR)
                song_widget.now_playing_sign.set_no_show_all('True')
                song_widget.now_playing_sign.set_alignment(1, 0.6)
                song_widget.can_be_played = True
                song_widget.connect('button-release-event',
                                    self.track_selected)

    @log
    def _update_album_art(self):
        self._cache.lookup(self.album, ArtSize.medium, self._get_album_cover,
                           None)

    @log
    def _get_album_cover(self, surface, data=None):
        self.cover.set_from_surface(surface)

    @log
    def track_selected(self, widget, event):
        if not widget.can_be_played:
            return

        if not self.selectionMode and \
            (event.button == Gdk.BUTTON_SECONDARY or
                (event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK)):
            if self.selectionModeAllowed:
                self.header_bar._select_button.set_active(True)
            else:
                return

        if self.selectionMode:
            self.model[widget._iter][6] = not self.model[widget._iter][6]
            return

        self.player.stop()
        self.player.set_playlist('Artist', self.artist,
                                 widget.model, widget._iter, 5, 6)
        self.player.set_playing(True)

    @log
    def set_selection_mode(self, selectionMode):
        if self.selectionMode == selectionMode:
            return
        self.selectionMode = selectionMode
        for songWidget in self.songs:
            songWidget.checkButton.set_visible(selectionMode)
            if not selectionMode:
                songWidget.model[songWidget._iter][6] = False

    @log
    def _check_button_toggled(self, button, songWidget):
        if songWidget.model[songWidget._iter][6] != button.get_active():
            songWidget.model[songWidget._iter][6] = button.get_active()

    @log
    def _model_row_changed(self, model, path, _iter):
        if not self.selectionMode:
            return
        if not model[_iter][5]:
            return
        songWidget = model[_iter][5].song_widget
        selected = model[_iter][6]

        if model[_iter][11] == DiscoveryStatus.FAILED:
            songWidget.now_playing_sign.set_from_icon_name(
                ERROR_ICON_NAME,
                Gtk.IconSize.SMALL_TOOLBAR)
            songWidget.now_playing_sign.show()
            songWidget.can_be_played = False

        if selected != songWidget.checkButton.get_active():
            songWidget.checkButton.set_active(selected)
Example #28
0
import logging

from gi.repository import Gtk, Gdk, Gd, GLib, GObject, Pango, Gio, GdkPixbuf
from gettext import gettext as _, ngettext

from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon
from gnomemusic.grilo import grilo
from gnomemusic import log
from gnomemusic.player import DiscoveryStatus
from gnomemusic.playlists import Playlists, StaticPlaylists
import gnomemusic.utils as utils


logger = logging.getLogger(__name__)

ALBUM_ART_CACHE = AlbumArtCache.get_default()
NOW_PLAYING_ICON_NAME = 'media-playback-start-symbolic'
ERROR_ICON_NAME = 'dialog-error-symbolic'

try:
    settings = Gio.Settings.new('org.gnome.Music')
    MAX_TITLE_WIDTH = settings.get_int('max-width-chars')
except Exception as e:
    MAX_TITLE_WIDTH = 20
    logger.error("Error on setting widget max-width-chars: %s", str(e))

playlists = Playlists.get_default()


class StarHandler():
    """Handles the treeview column for favorites (stars)."""
class ArtistAlbumWidget(Gtk.Box):

    __gsignals__ = {
        'songs-loaded': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __repr__(self):
        return '<ArtistAlbumWidget>'

    @log
    def __init__(self,
                 media,
                 player,
                 model,
                 header_bar,
                 selection_mode_allowed,
                 size_group=None,
                 cover_size_group=None):
        super().__init__(orientation=Gtk.Orientation.HORIZONTAL)

        self._size_group = size_group
        self._cover_size_group = cover_size_group
        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.MEDIUM)

        self._media = media
        self._player = player
        self._artist = utils.get_artist_name(self._media)
        self._album_title = utils.get_album_title(self._media)
        self._model = model
        self._header_bar = header_bar
        self._selection_mode = False
        self._selection_mode_allowed = selection_mode_allowed

        self._songs = []

        self._header_bar._select_button.connect(
            'toggled', self._on_header_select_button_toggled)

        ui = Gtk.Builder()
        ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')

        self.cover = ui.get_object('cover')
        self.cover.set_from_surface(self._loading_icon_surface)

        self._disc_listbox = ui.get_object('disclistbox')
        self._disc_listbox.set_selection_mode_allowed(
            self._selection_mode_allowed)

        ui.get_object('title').set_label(self._album_title)
        creation_date = self._media.get_creation_date()
        if creation_date:
            year = creation_date.get_year()
            ui.get_object('year').set_markup(
                '<span color=\'grey\'>{}</span>'.format(year))

        if self._size_group:
            self._size_group.add_widget(ui.get_object('box1'))

        if self._cover_size_group:
            self._cover_size_group.add_widget(self.cover)

        self.pack_start(ui.get_object('ArtistAlbumWidget'), True, True, 0)

        GLib.idle_add(self._update_album_art)
        grilo.populate_album_songs(self._media, self._add_item)

    def create_disc_box(self, disc_nr, disc_songs):
        disc_box = DiscBox(self._model)
        disc_box.set_songs(disc_songs)
        disc_box.set_disc_number(disc_nr)
        disc_box.set_columns(2)
        disc_box.show_duration(False)
        disc_box.show_favorites(False)
        disc_box.connect('song-activated', self._song_activated)
        disc_box.connect('selection-toggle', self._selection_mode_toggled)

        return disc_box

    def _selection_mode_toggled(self, widget):
        if not self._selection_mode_allowed:
            return

        self._selection_mode = not self._selection_mode
        self._on_selection_mode_request()

        return True

    def _on_selection_mode_request(self):
        self._header_bar._select_button.clicked()

    def _on_header_select_button_toggled(self, button):
        """Selection mode button clicked callback."""
        if button.get_active():
            self._selection_mode = True
            self._disc_listbox.set_selection_mode(True)
            self._header_bar.set_selection_mode(True)
            self._player.actionbar.set_visible(False)
            self._header_bar.header_bar.set_custom_title(
                self._header_bar._selection_menu_button)
        else:
            self._selection_mode = False
            self._disc_listbox.set_selection_mode(False)
            self._header_bar.set_selection_mode(False)
            if (self._player.get_playback_status() != 2):
                self._player.actionbar.set_visible(True)

    @log
    def _add_item(self, source, prefs, song, remaining, data=None):
        if song:
            self._songs.append(song)
            return

        discs = {}
        for song in self._songs:
            disc_nr = song.get_album_disc_number()
            if disc_nr not in discs.keys():
                discs[disc_nr] = [song]
            else:
                discs[disc_nr].append(song)

        for disc_nr in discs:
            disc = self.create_disc_box(disc_nr, discs[disc_nr])
            self._disc_listbox.add(disc)
            if len(discs) == 1:
                disc.show_disc_label(False)

        if remaining == 0:
            self.emit("songs-loaded")

    @log
    def _update_album_art(self):
        self._cache.lookup(self._media, ArtSize.MEDIUM, self._get_album_cover,
                           None)

    @log
    def _get_album_cover(self, surface, data=None):
        self.cover.set_from_surface(surface)

    @log
    def _song_activated(self, widget, song_widget):
        if (not song_widget.can_be_played or self._selection_mode):
            return

        self._player.stop()
        self._player.set_playlist('Artist', self._artist, song_widget.model,
                                  song_widget.itr, 5, 11)
        self._player.set_playing(True)

        return True

    @log
    def set_selection_mode(self, selection_mode):
        if self._selection_mode == selection_mode:
            return
        self._selection_mode = selection_mode

        self._disc_listbox.set_selection_mode(selection_mode)
Example #30
0
class Player(GObject.GObject):
    nextTrack = None
    timeout = None
    _seconds_timeout = None
    shuffleHistory = deque(maxlen=10)

    __gsignals__ = {
        'playing-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playlist-item-changed': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreeModel, Gtk.TreeIter)),
        'current-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'playback-status-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'repeat-mode-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'volume-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'prev-next-invalidated': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seeked': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        'thumbnail-updated': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, parent_window):
        super().__init__()

        self._parent_window = parent_window
        self.playlist = None
        self.playlistType = None
        self.playlistId = None
        self.playlistField = None
        self.currentTrack = None
        self.currentTrackUri = None
        scale = parent_window.get_scale_factor()
        self.cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading,
            ArtSize.XSMALL)
        self._missingPluginMessages = []

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self.discoverer = GstPbutils.Discoverer()
        self.discoverer.connect('discovered', self._on_discovered)
        self.discoverer.start()
        self._discovering_urls = {}

        self.player = Gst.ElementFactory.make('playbin', 'player')
        self.bus = self.player.get_bus()
        self.bus.add_signal_watch()
        self.setup_replaygain()

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect('changed::repeat', self._on_repeat_setting_changed)
        self._settings.connect('changed::replaygain', self._on_replaygain_setting_changed)
        self.repeat = self._settings.get_enum('repeat')
        self.replaygain = self._settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

        self.bus.connect('message::state-changed', self._on_bus_state_changed)
        self.bus.connect('message::error', self._onBusError)
        self.bus.connect('message::element', self._on_bus_element)
        self.bus.connect('message::eos', self._on_bus_eos)
        self._setup_view()

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._lastfm = LastFmScrobbler()

    @log
    def _on_replaygain_setting_changed(self, settings, value):
        self.replaygain = settings.get_value('replaygain') is not None
        self.toggle_replaygain(self.replaygain)

    @log
    def setup_replaygain(self):
        """
        Set up replaygain
        See https://github.com/gnumdk/lollypop/commit/429383c3742e631b34937d8987d780edc52303c0
        """
        self._rgfilter = Gst.ElementFactory.make("bin", "bin")
        self._rg_audioconvert1 = Gst.ElementFactory.make("audioconvert", "audioconvert")
        self._rg_audioconvert2 = Gst.ElementFactory.make("audioconvert", "audioconvert2")
        self._rgvolume = Gst.ElementFactory.make("rgvolume", "rgvolume")
        self._rglimiter = Gst.ElementFactory.make("rglimiter", "rglimiter")
        self._rg_audiosink = Gst.ElementFactory.make("autoaudiosink", "autoaudiosink")
        if not self._rgfilter or not self._rg_audioconvert1 or not self._rg_audioconvert2 \
           or not self._rgvolume or not self._rglimiter or not self._rg_audiosink:
            logger.debug("Replay Gain is not available")
            return
        self._rgvolume.props.pre_amp = 0.0
        self._rgfilter.add(self._rgvolume)
        self._rgfilter.add(self._rg_audioconvert1)
        self._rgfilter.add(self._rg_audioconvert2)
        self._rgfilter.add(self._rglimiter)
        self._rgfilter.add(self._rg_audiosink)
        self._rg_audioconvert1.link(self._rgvolume)
        self._rgvolume.link(self._rg_audioconvert2)
        self._rgvolume.link(self._rglimiter)
        self._rg_audioconvert2.link(self._rg_audiosink)
        self._rgfilter.add_pad(Gst.GhostPad.new("sink", self._rg_audioconvert1.get_static_pad("sink")))

    @log
    def toggle_replaygain(self, state=False):
        if state and self._rgfilter:
            self.player.set_property("audio-sink", self._rgfilter)
        else:
            self.player.set_property("audio-sink", None)

    def discover_item(self, item, callback, data=None):
        url = item.get_url()
        if not url:
            logger.warn("The item %s doesn't have a URL set", item)
            return

        if not url.startswith("file://"):
            logger.debug("Skipping discovery of %s as not a local file", url)
            return

        obj = (callback, data)

        if url in self._discovering_urls:
            self._discovering_urls[url] += [obj]
        else:
            self._discovering_urls[url] = [obj]
            self.discoverer.discover_uri_async(url)

    def _on_discovered(self, discoverer, info, error):
        try:
            cbs = self._discovering_urls[info.get_uri()]
            del(self._discovering_urls[info.get_uri()])

            for callback, data in cbs:
                if data is not None:
                    callback(info, error, data)
                else:
                    callback(info, error)
        except KeyError:
            # Not something we're interested in
            return

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.repeat = settings.get_enum('repeat')
        self._sync_prev_next()
        self._sync_repeat_image()
        self._validate_next_track()

    @log
    def _on_bus_state_changed(self, bus, message):
        # Note: not all state changes are signaled through here, in particular
        # transitions between Gst.State.READY and Gst.State.NULL are never async
        # and thus don't cause a message
        # In practice, self means only Gst.State.PLAYING and Gst.State.PAUSED are
        self._sync_playing()

    @log
    def _gst_plugins_base_check_version(self, major, minor, micro):
        gst_major, gst_minor, gst_micro, gst_nano = GstPbutils.plugins_base_version()
        return ((gst_major > major) or
                (gst_major == major and gst_minor > minor) or
                (gst_major == major and gst_minor == minor and gst_micro >= micro) or
                (gst_major == major and gst_minor == minor and gst_micro + 1 == micro and gst_nano > 0))

    @log
    def _start_plugin_installation(self, missing_plugin_messages, confirm_search):
        install_ctx = GstPbutils.InstallPluginsContext.new()

        if self._gst_plugins_base_check_version(1, 5, 0):
            install_ctx.set_desktop_id('org.gnome.Music.desktop')
            install_ctx.set_confirm_search(confirm_search)

            startup_id = '_TIME%u' % Gtk.get_current_event_time()
            install_ctx.set_startup_notification_id(startup_id)

        installer_details = []
        for message in missing_plugin_messages:
            installer_detail = GstPbutils.missing_plugin_message_get_installer_detail(message)
            installer_details.append(installer_detail)

        def on_install_done(res):
            # We get the callback too soon, before the installation has
            # actually finished. Do nothing for now.
            pass

        GstPbutils.install_plugins_async(installer_details, install_ctx, on_install_done)

    @log
    def _show_codec_confirmation_dialog(self, install_helper_name, missing_plugin_messages):
        dialog = MissingCodecsDialog(self._parent_window, install_helper_name)

        def on_dialog_response(dialog, response_type):
            if response_type == Gtk.ResponseType.ACCEPT:
                self._start_plugin_installation(missing_plugin_messages, False)

            dialog.destroy()

        descriptions = []
        for message in missing_plugin_messages:
            description = GstPbutils.missing_plugin_message_get_description(message)
            descriptions.append(description)

        dialog.set_codec_names(descriptions)
        dialog.connect('response', on_dialog_response)
        dialog.present()

    @log
    def _handle_missing_plugins(self):
        if not self._missingPluginMessages:
            return

        missing_plugin_messages = self._missingPluginMessages
        self._missingPluginMessages = []

        if self._gst_plugins_base_check_version(1, 5, 0):
            proxy = Gio.DBusProxy.new_sync(Gio.bus_get_sync(Gio.BusType.SESSION, None),
                                           Gio.DBusProxyFlags.NONE,
                                           None,
                                           'org.freedesktop.PackageKit',
                                           '/org/freedesktop/PackageKit',
                                           'org.freedesktop.PackageKit.Modify2',
                                           None)
            prop = Gio.DBusProxy.get_cached_property(proxy, 'DisplayName')
            if prop:
                display_name = prop.get_string()
                if display_name:
                    self._show_codec_confirmation_dialog(display_name, missing_plugin_messages)
                    return

        # If the above failed, fall back to immediately starting the codec installation
        self._start_plugin_installation(missing_plugin_messages, True)

    @log
    def _is_missing_plugin_message(self, message):
        error, debug = message.parse_error()

        if error.matches(Gst.CoreError.quark(), Gst.CoreError.MISSING_PLUGIN):
            return True

        return False

    @log
    def _on_bus_element(self, bus, message):
        if GstPbutils.is_missing_plugin_message(message):
            self._missingPluginMessages.append(message)

    def _onBusError(self, bus, message):
        if self._is_missing_plugin_message(message):
            self.pause()
            self._handle_missing_plugins()
            return True

        media = self.get_current_media()
        if media is not None:
            if self.currentTrack and self.currentTrack.valid():
                currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
                self.playlist.set_value(currentTrack, self.discovery_status_field, DiscoveryStatus.FAILED)
            uri = media.get_url()
        else:
            uri = 'none'
        logger.warn('URI: %s', uri)
        error, debug = message.parse_error()
        debug = debug.split('\n')
        debug = [('     ') + line.lstrip() for line in debug]
        debug = '\n'.join(debug)
        logger.warn('Error from element %s: %s', message.src.get_name(), error.message)
        logger.warn('Debugging info:\n%s', debug)
        self.play_next()
        return True

    @log
    def _on_bus_eos(self, bus, message):
        if self.nextTrack:
            GLib.idle_add(self._on_glib_idle)
        elif (self.repeat == RepeatType.NONE):
            self.stop()
            self.playBtn.set_image(self._playImage)
            self._progress_scale_zero()
            self.progressScale.set_sensitive(False)
            if self.playlist is not None:
                currentTrack = self.playlist.get_path(self.playlist.get_iter_first())
                if currentTrack:
                    self.currentTrack = Gtk.TreeRowReference.new(self.playlist, currentTrack)
                    self.currentTrackUri = self.playlist.get_value(
                        self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
                else:
                    self.currentTrack = None
                self.load(self.get_current_media())
            self.emit('playback-status-changed')
        else:
            # Stop playback
            self.stop()
            self.playBtn.set_image(self._playImage)
            self._progress_scale_zero()
            self.progressScale.set_sensitive(False)
            self.emit('playback-status-changed')

    @log
    def _on_glib_idle(self):
        self.currentTrack = self.nextTrack
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
        self.play()

    @log
    def _on_playlist_size_changed(self, path, _iter=None, data=None):
        self._sync_prev_next()

    @log
    def _get_random_iter(self, currentTrack):
        first_iter = self.playlist.get_iter_first()
        if not currentTrack:
            currentTrack = first_iter
        if not currentTrack:
            return None
        if hasattr(self.playlist, "iter_is_valid") and\
           not self.playlist.iter_is_valid(currentTrack):
            return None
        currentPath = int(self.playlist.get_path(currentTrack).to_string())
        rows = self.playlist.iter_n_children(None)
        if rows == 1:
            return currentTrack
        rand = currentPath
        while rand == currentPath:
            rand = randint(0, rows - 1)
        return self.playlist.get_iter_from_string(str(rand))

    @log
    def _get_next_track(self):
        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        else:
            currentTrack = None

        nextTrack = None

        if self.repeat == RepeatType.SONG:
            if currentTrack:
                nextTrack = currentTrack
            else:
                nextTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.ALL:
            if currentTrack:
                nextTrack = self.playlist.iter_next(currentTrack)
            if not nextTrack:
                nextTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.NONE:
            if currentTrack:
                nextTrack = self.playlist.iter_next(currentTrack)
        elif self.repeat == RepeatType.SHUFFLE:
            nextTrack = self._get_random_iter(currentTrack)
            if currentTrack:
                self.shuffleHistory.append(currentTrack)

        if nextTrack:
            return Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(nextTrack))
        else:
            return None

    @log
    def _get_iter_last(self):
        iter = self.playlist.get_iter_first()
        last = None

        while iter is not None:
            last = iter
            iter = self.playlist.iter_next(iter)

        return last

    @log
    def _get_previous_track(self):
        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        else:
            currentTrack = None

        previousTrack = None

        if self.repeat == RepeatType.SONG:
            if currentTrack:
                previousTrack = currentTrack
            else:
                previousTrack = self.playlist.get_iter_first()
        elif self.repeat == RepeatType.ALL:
            if currentTrack:
                previousTrack = self.playlist.iter_previous(currentTrack)
            if not previousTrack:
                previousTrack = self._get_iter_last()
        elif self.repeat == RepeatType.NONE:
            if currentTrack:
                previousTrack = self.playlist.iter_previous(currentTrack)
        elif self.repeat == RepeatType.SHUFFLE:
            if currentTrack:
                if self.played_seconds < 10 and len(self.shuffleHistory) > 0:
                    previousTrack = self.shuffleHistory.pop()

                    # Discard the current song, which is already queued
                    if self.playlist.get_path(previousTrack) == self.playlist.get_path(currentTrack):
                        previousTrack = None

                if previousTrack is None and len(self.shuffleHistory) > 0:
                    previousTrack = self.shuffleHistory.pop()
                else:
                    previousTrack = self._get_random_iter(currentTrack)

        if previousTrack:
            return Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(previousTrack))
        else:
            return None

    @log
    def has_next(self):
        if not self.playlist or self.playlist.iter_n_children(None) < 1:
            return False
        elif not self.currentTrack:
            return False
        elif self.repeat in [RepeatType.ALL, RepeatType.SONG, RepeatType.SHUFFLE]:
            return True
        elif self.currentTrack.valid():
            tmp = self.playlist.get_iter(self.currentTrack.get_path())
            return self.playlist.iter_next(tmp) is not None
        else:
            return True

    @log
    def has_previous(self):
        if not self.playlist or self.playlist.iter_n_children(None) < 1:
            return False
        elif not self.currentTrack:
            return False
        elif self.repeat in [RepeatType.ALL, RepeatType.SONG, RepeatType.SHUFFLE]:
            return True
        elif self.currentTrack.valid():
            tmp = self.playlist.get_iter(self.currentTrack.get_path())
            return self.playlist.iter_previous(tmp) is not None
        else:
            return True

    @log
    def _get_playing(self):
        ok, state, pending = self.player.get_state(0)
        # log('get playing(), [ok, state, pending] = [%s, %s, %s]'.format(ok, state, pending))
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    @property
    def playing(self):
        return self._get_playing()

    @log
    def _sync_playing(self):
        if self._get_playing():
            image = self._pauseImage
            tooltip = _("Pause")
        else:
            image = self._playImage
            tooltip = _("Play")

        if self.playBtn.get_image() != image:
            self.playBtn.set_image(image)

        self.playBtn.set_tooltip_text(tooltip)

    @log
    def _sync_prev_next(self):
        hasNext = self.has_next()
        hasPrevious = self.has_previous()

        self.nextBtn.set_sensitive(hasNext)
        self.prevBtn.set_sensitive(hasPrevious)

        self.emit('prev-next-invalidated')

    @log
    def set_playing(self, value):
        self.actionbar.show()

        if value:
            self.play()
        else:
            self.pause()

        media = self.get_current_media()
        self.playBtn.set_image(self._pauseImage)
        return media

    @log
    def load(self, media):
        self._progress_scale_zero()
        self._set_duration(media.get_duration())
        self.songTotalTimeLabel.set_label(
            utils.seconds_to_string(media.get_duration()))
        self.progressScale.set_sensitive(True)

        self.playBtn.set_sensitive(True)
        self._sync_prev_next()

        artist = utils.get_artist_name(media)
        self.artistLabel.set_label(artist)

        self.coverImg.set_from_surface(self._loading_icon_surface)
        self.cache.lookup(media, ArtSize.XSMALL, self._on_cache_lookup, None)

        title = utils.get_media_title(media)
        self.titleLabel.set_label(title)

        self._time_stamp = int(time.time())

        url = media.get_url()
        if url != self.player.get_value('current-uri', 0):
            self.player.set_property('uri', url)

        if self.currentTrack and self.currentTrack.valid():
            currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
            self.emit('playlist-item-changed', self.playlist, currentTrack)
            self.emit('current-changed')

        self._validate_next_track()

    def _on_next_item_validated(self, info, error, _iter):
        if error:
            print("Info %s: error: %s" % (info, error))
            self.playlist.set_value(_iter, self.discovery_status_field, DiscoveryStatus.FAILED)
            nextTrack = self.playlist.iter_next(_iter)

            if nextTrack:
                self._validate_next_track(Gtk.TreeRowReference.new(self.playlist, self.playlist.get_path(nextTrack)))

    @log
    def _validate_next_track(self, track=None):
        if track is None:
            track = self._get_next_track()

        self.nextTrack = track

        if track is None:
            return

        _iter = self.playlist.get_iter(self.nextTrack.get_path())
        status = self.playlist.get_value(_iter, self.discovery_status_field)
        nextSong = self.playlist.get_value(_iter, self.playlistField)
        url = self.playlist.get_value(_iter, 5).get_url()

        # Skip remote songs discovery
        if url.startswith('http://') or url.startswith('https://'):
            return False
        elif status == DiscoveryStatus.PENDING:
            self.discover_item(nextSong, self._on_next_item_validated, _iter)
        elif status == DiscoveryStatus.FAILED:
            GLib.idle_add(self._validate_next_track)

        return False

    @log
    def _on_cache_lookup(self, surface, data=None):
        self.coverImg.set_from_surface(surface)
        self.emit('thumbnail-updated')

    @log
    def play(self):
        if self.playlist is None:
            return

        media = None

        if self.player.get_state(1)[1] != Gst.State.PAUSED:
            self.stop()

            media = self.get_current_media()
            if not media:
                return

            self.load(media)

        self.player.set_state(Gst.State.PLAYING)
        self._update_position_callback()
        if media:
            self._lastfm.now_playing(media)
        if not self.timeout and self.progressScale.get_realized():
            self._update_timeout()

        self.emit('playback-status-changed')
        self.emit('playing-changed')

    @log
    def pause(self):
        self._remove_timeout()

        self.player.set_state(Gst.State.PAUSED)
        self.emit('playback-status-changed')
        self.emit('playing-changed')

    @log
    def stop(self):
        self._remove_timeout()

        self.player.set_state(Gst.State.NULL)
        self.emit('playing-changed')

    @log
    def play_next(self):
        if self.playlist is None:
            return True

        if not self.nextBtn.get_sensitive():
            return True

        self.stop()
        self.currentTrack = self.nextTrack

        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
            self.play()

    @log
    def play_previous(self):
        if self.playlist is None:
            return

        if self.prevBtn.get_sensitive() is False:
            return

        position = self.get_position() / 1000000
        if position >= 5:
            self._progress_scale_zero()
            self.on_progress_scale_change_value(self.progressScale)
            return

        self.stop()

        self.currentTrack = self._get_previous_track()
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
            self.play()

    @log
    def play_pause(self):
        if self.player.get_state(1)[1] == Gst.State.PLAYING:
            self.set_playing(False)
        else:
            self.set_playing(True)

    # FIXME: set the discovery field to 11 to be safe, but for some
    # models it is 12.
    @log
    def set_playlist(self, type, id, model, iter, field,
                     discovery_status_field=11):
        self.stop()

        old_playlist = self.playlist
        if old_playlist != model:
            self.playlist = model
            if self.playlist_insert_handler:
                old_playlist.disconnect(self.playlist_insert_handler)
            if self.playlist_delete_handler:
                old_playlist.disconnect(self.playlist_delete_handler)

        self.playlistType = type
        self.playlistId = id
        self.currentTrack = Gtk.TreeRowReference.new(model, model.get_path(iter))
        if self.currentTrack and self.currentTrack.valid():
            self.currentTrackUri = self.playlist.get_value(
                self.playlist.get_iter(self.currentTrack.get_path()), 5).get_url()
        self.playlistField = field
        self.discovery_status_field = discovery_status_field

        if old_playlist != model:
            self.playlist_insert_handler = model.connect('row-inserted', self._on_playlist_size_changed)
            self.playlist_delete_handler = model.connect('row-deleted', self._on_playlist_size_changed)
            self.emit('playlist-changed')
        self.emit('current-changed')

    @log
    def running_playlist(self, type, id):
        if type == self.playlistType and id == self.playlistId:
            return self.playlist
        else:
            return None

    @log
    def _setup_view(self):
        self._ui = Gtk.Builder()
        self._ui.add_from_resource('/org/gnome/Music/PlayerToolbar.ui')
        self.actionbar = self._ui.get_object('actionbar')
        self.prevBtn = self._ui.get_object('previous_button')
        self.playBtn = self._ui.get_object('play_button')
        self.nextBtn = self._ui.get_object('next_button')
        self._playImage = self._ui.get_object('play_image')
        self._pauseImage = self._ui.get_object('pause_image')
        self.progressScale = self._ui.get_object('progress_scale')
        self.songPlaybackTimeLabel = self._ui.get_object('playback')
        self.songTotalTimeLabel = self._ui.get_object('duration')
        self.titleLabel = self._ui.get_object('title')
        self.artistLabel = self._ui.get_object('artist')
        self.coverImg = self._ui.get_object('cover')
        self.coverImg.set_property("width-request", ArtSize.XSMALL.width)
        self.coverImg.set_property("height-request", ArtSize.XSMALL.height)

        self.duration = self._ui.get_object('duration')
        self.repeatBtnImage = self._ui.get_object('playlistRepeat')

        if Gtk.Settings.get_default().get_property('gtk_application_prefer_dark_theme'):
            color = Gdk.RGBA(red=1.0, green=1.0, blue=1.0, alpha=1.0)
        else:
            color = Gdk.RGBA(red=0.0, green=0.0, blue=0.0, alpha=0.0)
        self._playImage.override_color(Gtk.StateFlags.ACTIVE, color)
        self._pauseImage.override_color(Gtk.StateFlags.ACTIVE, color)

        self._sync_repeat_image()

        self.prevBtn.connect('clicked', self._on_prev_btn_clicked)
        self.playBtn.connect('clicked', self._on_play_btn_clicked)
        self.nextBtn.connect('clicked', self._on_next_btn_clicked)
        self.progressScale.connect('button-press-event', self._on_progress_scale_event)
        self.progressScale.connect('value-changed', self._on_progress_value_changed)
        self.progressScale.connect('button-release-event', self._on_progress_scale_button_released)
        self.progressScale.connect('change-value', self._on_progress_scale_seek)
        self._ps_draw = self.progressScale.connect('draw',
            self._on_progress_scale_draw)
        self._seek_timeout = None
        self._old_progress_scale_value = 0.0
        self.progressScale.set_increments(300, 600)

    def _on_progress_scale_seek_finish(self, value):
        """Prevent stutters when seeking with infinitesimal amounts"""
        self._seek_timeout = None
        round_digits = self.progressScale.get_property('round-digits')
        if self._old_progress_scale_value != round(value, round_digits):
            self.on_progress_scale_change_value(self.progressScale)
            self._old_progress_scale_value = round(value, round_digits)
        return False

    def _on_progress_scale_seek(self, scale, scroll_type, value):
        """Smooths out the seeking process

        Called every time progress scale is moved. Only after a seek has been
        stable for 100ms, we play the song from its location.
        """
        if self._seek_timeout:
            GLib.source_remove(self._seek_timeout)

        Gtk.Range.do_change_value(scale, scroll_type, value)
        if scroll_type == Gtk.ScrollType.JUMP:
            self._seek_timeout = GLib.timeout_add(
                100, self._on_progress_scale_seek_finish, value)
        else:
            # scroll with keys, hence no smoothing
            self._on_progress_scale_seek_finish(value)
            self._update_position_callback()

        return True

    @log
    def _on_progress_scale_button_released(self, scale, data):
        if self._seek_timeout:
            GLib.source_remove(self._seek_timeout)
            self._on_progress_scale_seek_finish(self.progressScale.get_value())

        self._update_position_callback()
        return False

    def _on_progress_value_changed(self, widget):
        seconds = int(self.progressScale.get_value() / 60)
        self.songPlaybackTimeLabel.set_label(utils.seconds_to_string(seconds))
        return False

    @log
    def _on_progress_scale_event(self, scale, data):
        self._remove_timeout()
        self._old_progress_scale_value = self.progressScale.get_value()
        return False

    def _on_progress_scale_draw(self, cr, data):
        self._update_timeout()
        self.progressScale.disconnect(self._ps_draw)
        return False

    def _update_timeout(self):
        """Update the duration for self.timeout and self._seconds_timeout

        Sets the period of self.timeout to a value small enough to make the
        slider of self.progressScale move smoothly based on the current song
        duration and progressScale length.  self._seconds_timeout is always set
        to a fixed value, short enough to hide irregularities in GLib event
        timing from the user, for updating the songPlaybackTimeLabel.
        """
        # Don't run until progressScale has been realized
        if self.progressScale.get_realized() is False:
            return

        # Update self.timeout
        width = self.progressScale.get_allocated_width()
        padding = self.progressScale.get_style_context().get_padding(
            Gtk.StateFlags.NORMAL)
        width -= padding.left + padding.right
        success, duration = self.player.query_duration(Gst.Format.TIME)
        timeout_period = 1000
        if success:
            timeout_period = min(1000 * (duration / 10**9) // width, 1000)

        if self.timeout:
            GLib.source_remove(self.timeout)
        self.timeout = GLib.timeout_add(
            timeout_period, self._update_position_callback)

        # Update self._seconds_timeout
        if not self._seconds_timeout:
            self.seconds_period = 1000
            self._seconds_timeout = GLib.timeout_add(
                self.seconds_period, self._update_seconds_callback)

    def _remove_timeout(self):
        if self.timeout:
            GLib.source_remove(self.timeout)
            self.timeout = None
        if self._seconds_timeout:
            GLib.source_remove(self._seconds_timeout)
            self._seconds_timeout = None

    def _progress_scale_zero(self):
        self.progressScale.set_value(0)
        self._on_progress_value_changed(None)

    @log
    def _on_play_btn_clicked(self, btn):
        if self._get_playing():
            self.pause()
        else:
            self.play()

    @log
    def _on_next_btn_clicked(self, btn):
        self.play_next()

    @log
    def _on_prev_btn_clicked(self, btn):
        self.play_previous()

    @log
    def _set_duration(self, duration):
        self.duration = duration
        self.played_seconds = 0
        self.progressScale.set_range(0.0, duration * 60)

    @log
    def _update_position_callback(self):
        position = self.player.query_position(Gst.Format.TIME)[1] / 1000000000
        if position > 0:
            self.progressScale.set_value(position * 60)
        self._update_timeout()
        return False

    @log
    def _update_seconds_callback(self):
        self._on_progress_value_changed(None)

        position = self.player.query_position(Gst.Format.TIME)[1] / 10**9
        if position > 0:
            self.played_seconds += self.seconds_period / 1000
            try:
                percentage = self.played_seconds / self.duration
                if (not self._lastfm.scrobbled
                        and percentage > 0.4):
                    current_media = self.get_current_media()
                    if current_media:
                        # FIXME: we should not need to update static
                        # playlists here but removing it may introduce
                        # a bug. So, we keep it for the time being.
                        playlists.update_all_static_playlists()
                        grilo.bump_play_count(current_media)
                        grilo.set_last_played(current_media)
                        self._lastfm.scrobble(current_media, self._time_stamp)

            except Exception as e:
                logger.warn("Error: %s, %s", e.__class__, e)
        return True

    @log
    def _sync_repeat_image(self):
        icon = None
        if self.repeat == RepeatType.NONE:
            icon = 'media-playlist-consecutive-symbolic'
        elif self.repeat == RepeatType.SHUFFLE:
            icon = 'media-playlist-shuffle-symbolic'
        elif self.repeat == RepeatType.ALL:
            icon = 'media-playlist-repeat-symbolic'
        elif self.repeat == RepeatType.SONG:
            icon = 'media-playlist-repeat-song-symbolic'

        self.repeatBtnImage.set_from_icon_name(icon, Gtk.IconSize.MENU)
        self.emit('repeat-mode-changed')

    @log
    def on_progress_scale_change_value(self, scroll):
        seconds = scroll.get_value() / 60
        if seconds != self.duration:
            self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seconds * 1000000000)
            try:
                self.emit('seeked', seconds * 1000000)
            except TypeError:
                # See https://bugzilla.gnome.org/show_bug.cgi?id=733095
                pass
        else:
            duration = self.player.query_duration(Gst.Format.TIME)
            if duration:
                # Rewind a second back before the track end
                self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, duration[1] - 1000000000)
                try:
                    self.emit('seeked', (duration[1] - 1000000000) / 1000)
                except TypeError:
                    # See https://bugzilla.gnome.org/show_bug.cgi?id=733095
                    pass
        return True

    # MPRIS

    @log
    def Stop(self):
        self._progress_scale_zero()
        self.progressScale.set_sensitive(False)
        self.playBtn.set_image(self._playImage)
        self.stop()
        self.emit('playback-status-changed')

    @log
    def get_playback_status(self):
        ok, state, pending = self.player.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            return PlaybackStatus.STOPPED

        if state == Gst.State.PLAYING:
            return PlaybackStatus.PLAYING
        elif state == Gst.State.PAUSED:
            return PlaybackStatus.PAUSED
        else:
            return PlaybackStatus.STOPPED

    @log
    def get_repeat_mode(self):
        return self.repeat

    @log
    def set_repeat_mode(self, mode):
        self.repeat = mode
        self._sync_repeat_image()

    @log
    def get_position(self):
        return self.player.query_position(Gst.Format.TIME)[1] / 1000

    @log
    def set_position(self, offset, start_if_ne=False, next_on_overflow=False):
        if offset < 0:
            if start_if_ne:
                offset = 0
            else:
                return

        duration = self.player.query_duration(Gst.Format.TIME)
        if duration is None:
            return

        if duration[1] >= offset * 1000:
            self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, offset * 1000)
            self.emit('seeked', offset)
        elif next_on_overflow:
            self.play_next()

    @log
    def get_volume(self):
        return self.player.get_volume(GstAudio.StreamVolumeFormat.LINEAR)

    @log
    def set_volume(self, rate):
        self.player.set_volume(GstAudio.StreamVolumeFormat.LINEAR, rate)
        self.emit('volume-changed')

    @log
    def get_current_media(self):
        if not self.currentTrack or not self.currentTrack.valid():
            return None
        currentTrack = self.playlist.get_iter(self.currentTrack.get_path())
        if self.playlist.get_value(currentTrack, self.discovery_status_field) == DiscoveryStatus.FAILED:
            return None
        return self.playlist.get_value(currentTrack, self.playlistField)
Example #31
0
    def __init__(self,
                 name,
                 title,
                 window,
                 view_type,
                 use_sidebar=False,
                 sidebar=None):
        """Initialize
        :param name: The view name
        :param title: The view title
        :param GtkWidget window: The main window
        :param view_type: The Gtk view type
        :param use_sidebar: Whether to use sidebar
        :param sidebar: The sidebar object (Default: Gtk.Box)
        """

        Gtk.Stack.__init__(self,
                           transition_type=Gtk.StackTransitionType.CROSSFADE)
        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
        self._offset = 0
        self.model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GObject.TYPE_STRING, GObject.TYPE_STRING,
                                   GdkPixbuf.Pixbuf, GObject.TYPE_OBJECT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT,
                                   GObject.TYPE_STRING, GObject.TYPE_INT,
                                   GObject.TYPE_BOOLEAN, GObject.TYPE_INT)
        self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Setup the main view
        self._setup_view(view_type)

        if use_sidebar:
            self.stack = Gtk.Stack(
                transition_type=Gtk.StackTransitionType.SLIDE_RIGHT, )
            dummy = Gtk.Frame(visible=False)
            self.stack.add_named(dummy, 'dummy')
            if sidebar:
                self.stack.add_named(sidebar, 'sidebar')
            else:
                self.stack.add_named(self._box, 'sidebar')
            self.stack.set_visible_child_name('dummy')
            self._grid.add(self.stack)
        if not use_sidebar or sidebar:
            self._grid.add(self._box)

        self._star_handler = StarHandlerWidget(self, 9)
        self._window = window
        self._header_bar = window.toolbar
        self._selection_toolbar = window.selection_toolbar
        self._header_bar._select_button.connect('toggled',
                                                self._on_header_bar_toggled)
        self._header_bar._cancel_button.connect('clicked',
                                                self._on_cancel_button_clicked)

        self.name = name
        self.title = title

        self.add(self._grid)
        self.show_all()
        self._view.hide()

        scale = self.get_scale_factor()
        self._cache = AlbumArtCache(scale)
        self._loading_icon_surface = DefaultIcon(scale).get(
            DefaultIcon.Type.loading, ArtSize.medium)

        self._init = False
        grilo.connect('ready', self._on_grilo_ready)
        self._header_bar.connect('selection-mode-changed',
                                 self._on_selection_mode_changed)
        grilo.connect('changes-pending', self._on_changes_pending)