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)
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()
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()
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, ''))
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
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
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()
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
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()
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
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)
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)
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()
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))
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()
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)
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)
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)
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()
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
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
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()
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)
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)
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)
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)