CoverArt Browser
v2.0
Browse your cover-art albums in Rhythmbox
|
00001 # -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 00002 # 00003 # Copyright (C) 2012 - fossfreedom 00004 # Copyright (C) 2012 - Agustin Carrasco 00005 # 00006 # This program is free software; you can redistribute it and/or modify 00007 # it under the terms of thie GNU General Public License as published by 00008 # the Free Software Foundation; either version 2, or (at your option) 00009 # any later version. 00010 # 00011 # This program is distributed in the hope that it will be useful, 00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 # GNU General Public License for more details. 00015 # 00016 # You should have received a copy of the GNU General Public License 00017 # along with this program; if not, write to the Free Software 00018 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 00019 00020 import random 00021 from collections import OrderedDict 00022 import unicodedata 00023 import re 00024 00025 from gi.repository import GObject 00026 from gi.repository import GLib 00027 from gi.repository import Gio 00028 from gi.repository import Gdk 00029 from gi.repository import Gtk 00030 from gi.repository import RB 00031 00032 import rb 00033 from coverart_album import AlbumManager 00034 from coverart_browser_prefs import GSetting 00035 from coverart_browser_prefs import CoverLocale 00036 from coverart_browser_prefs import Preferences 00037 from coverart_widgets import PanedCollapsible 00038 from coverart_controllers import AlbumQuickSearchController 00039 from coverart_controllers import ViewController 00040 from coverart_export import CoverArtExport 00041 from coverart_rb3compat import Menu 00042 from coverart_rb3compat import ActionGroup 00043 from coverart_covericonview import CoverIconView 00044 from coverart_coverflowview import CoverFlowView 00045 from coverart_artistview import ArtistView 00046 from coverart_listview import ListView 00047 from coverart_queueview import QueueView 00048 from coverart_playsourceview import PlaySourceView 00049 from coverart_toolbar import ToolbarManager 00050 from coverart_artistinfo import ArtistInfoPane 00051 from coverart_external_plugins import CreateExternalPluginMenu 00052 from coverart_playlists import EchoNestPlaylist 00053 from coverart_entryview import EntryViewPane 00054 from coverart_play_source import CoverArtPlaySource 00055 import coverart_rb3compat as rb3compat 00056 00057 00058 class CoverArtBrowserSource(RB.Source): 00059 ''' 00060 Source utilized by the plugin to show all it's ui. 00061 ''' 00062 rating_threshold = GObject.property(type=float, default=0) 00063 artist_paned_pos = GObject.property(type=str) 00064 min_paned_pos = 80 00065 00066 # unique instance of the source 00067 instance = None 00068 00069 def __init__(self, **kargs): 00070 ''' 00071 Initializes the source. 00072 ''' 00073 super(CoverArtBrowserSource, self).__init__(**kargs) 00074 00075 # create source_source_settings and connect the source's properties 00076 self.gs = GSetting() 00077 00078 self._connect_properties() 00079 00080 self.hasActivated = False 00081 self.last_width = 0 00082 self.last_selected_album = None 00083 self.click_count = 0 00084 self.favourites = False 00085 self.follow_song = False 00086 self.task_progress = None 00087 self._from_paned_handle = False 00088 00089 def _connect_properties(self): 00090 ''' 00091 Connects the source properties to the saved preferences. 00092 ''' 00093 print("CoverArtBrowser DEBUG - _connect_properties") 00094 setting = self.gs.get_setting(self.gs.Path.PLUGIN) 00095 00096 setting.bind( 00097 self.gs.PluginKey.RATING, 00098 self, 00099 'rating_threshold', 00100 Gio.SettingsBindFlags.GET) 00101 00102 print("CoverArtBrowser DEBUG - end _connect_properties") 00103 00104 def do_get_status(self, *args): 00105 ''' 00106 Method called by Rhythmbox to figure out what to show on this source 00107 statusbar. 00108 If the custom statusbar is disabled, the source will 00109 show the selected album info. 00110 Also, it makes sure to show the progress on the album loading 00111 ''' 00112 00113 if not self.task_progress: 00114 00115 self.task_progress = RB.TaskProgressSimple.new() 00116 00117 try: 00118 progress = self.album_manager.progress 00119 progress_text = _('Loading...') if progress < 1 else '' 00120 00121 if progress < 1: 00122 if self.props.shell.props.task_list.get_model().n_items() == 0: 00123 self.props.shell.props.task_list.add_task(self.task_progress) 00124 00125 self.task_progress.props.task_progress = progress 00126 self.task_progress.props.task_label = progress_text 00127 else: 00128 self.task_progress.props.task_outcome = RB.TaskOutcome.COMPLETE 00129 00130 except: 00131 progress = 1 00132 progress_text = '' 00133 00134 self.task_progress.props.task_outcome = RB.TaskOutcome.COMPLETE 00135 00136 return (self.status, progress_text, progress) 00137 00138 def do_selected(self): 00139 ''' 00140 Called by Rhythmbox when the source is selected. It makes sure to 00141 create the ui the first time the source is showed. 00142 ''' 00143 print("CoverArtBrowser DEBUG - do_selected") 00144 00145 # first time of activation -> add graphical stuff 00146 if not self.hasActivated: 00147 self.do_impl_activate() 00148 00149 # indicate that the source was activated before 00150 self.hasActivated = True 00151 00152 print("CoverArtBrowser DEBUG - end do_selected") 00153 00154 def do_impl_activate(self): 00155 ''' 00156 Called by do_selected the first time the source is activated. 00157 It creates all the source ui and connects the necessary signals for it 00158 correct behavior. 00159 ''' 00160 print("CoverArtBrowser DEBUG - do_impl_activate") 00161 00162 # initialise some variables 00163 self.plugin = self.props.plugin 00164 self.shell = self.props.shell 00165 self.status = '' 00166 self.search_text = '' 00167 self.actiongroup = ActionGroup(self.shell, 'coverplaylist_submenu') 00168 self._browser_preferences = None 00169 self._search_preferences = None 00170 00171 # indicate that the source was activated before 00172 self.hasActivated = True 00173 00174 # define a query model that we'll use for playing 00175 self.source_query_model = RB.RhythmDBQueryModel.new_empty(self.shell.props.db) 00176 00177 # define the associated playsource so we can interact with this query model 00178 self.playlist_source = GObject.new( 00179 CoverArtPlaySource, 00180 name=_("CoverArt Playlist"), 00181 shell=self.shell, 00182 plugin=self.plugin, 00183 entry_type=self.plugin.entry_type) 00184 self.playlist_source.initialise(self.plugin, self.shell, self) 00185 self.shell.append_display_page(self.playlist_source, self.plugin.source) 00186 00187 self._create_ui() 00188 self._setup_source() 00189 self._apply_settings() 00190 00191 print("CoverArtBrowser DEBUG - end do_impl_activate") 00192 00193 def _create_ui(self): 00194 ''' 00195 Creates the ui for the source and saves the important widgets onto 00196 properties. 00197 ''' 00198 print("CoverArtBrowser DEBUG - _create_ui") 00199 00200 # dialog has not been created so lets do so. 00201 cl = CoverLocale() 00202 ui = Gtk.Builder() 00203 ui.set_translation_domain(cl.Locale.LOCALE_DOMAIN) 00204 ui.add_from_file(rb.find_plugin_file(self.plugin, 00205 'ui/coverart_browser.ui')) 00206 ui.connect_signals(self) 00207 00208 # load the page and put it in the source 00209 # first setup the notification stuff 00210 00211 def hide_infobar(widget, *args): 00212 widget.hide() 00213 00214 overlay = Gtk.Overlay() 00215 overlay.show_all() 00216 self.notification_infobar = Gtk.InfoBar.new() 00217 00218 self.notification_infobar.set_message_type(Gtk.MessageType.INFO) 00219 self.notification_infobar.set_valign(Gtk.Align.START) 00220 self.notification_infobar.connect('response', hide_infobar) 00221 00222 self.notification_text = Gtk.Label.new("") 00223 area = self.notification_infobar.get_content_area() 00224 area.props.halign = Gtk.Align.CENTER 00225 area.add(self.notification_text) 00226 00227 self.notification_infobar.show_all() 00228 self.notification_infobar.set_visible(False) 00229 00230 self.page = ui.get_object('main_box') 00231 overlay.add(self.page) 00232 overlay.add_overlay(self.notification_infobar) 00233 00234 self.pack_start(overlay, True, True, 0) 00235 self.page.reorder_child(overlay, 0) 00236 00237 # get widgets for main icon-view 00238 self.status_label = ui.get_object('status_label') 00239 window = ui.get_object('scrolled_window') 00240 00241 self.viewmgr = ViewManager(self, window) 00242 00243 # get widgets for the artist paned 00244 self.artist_paned = ui.get_object('vertical_paned') 00245 self.artist_paned.set_name('vertical_paned') 00246 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50, self._change_artist_paned_pos, self.viewmgr.view_name) 00247 self.viewmgr.connect('new-view', self.on_view_changed) 00248 self.artist_treeview = ui.get_object('artist_treeview') 00249 self.artist_scrolledwindow = ui.get_object('artist_scrolledwindow') 00250 00251 # define menu's 00252 self.popup_menu = Menu(self.plugin, self.shell) 00253 self.popup_menu.load_from_file('ui/coverart_browser_pop_rb2.ui', 00254 'ui/coverart_browser_pop_rb3.ui') 00255 self._external_plugins = None 00256 00257 signals = \ 00258 {'play_album_menu_item': self.play_album_menu_item_callback, 00259 'queue_album_menu_item': self.queue_album_menu_item_callback, 00260 'add_to_playing_menu_item': self.add_to_playing_menu_item_callback, 00261 'new_playlist': self.add_playlist_menu_item_callback, 00262 'cover_search_menu_item': self.cover_search_menu_item_callback, 00263 'export_embed_menu_item': self.export_embed_menu_item_callback, 00264 'show_properties_menu_item': self.show_properties_menu_item_callback, 00265 'play_similar_artist_menu_item': self.play_similar_artist_menu_item_callback} 00266 00267 self.popup_menu.connect_signals(signals) 00268 self.popup_menu.connect('pre-popup', self.pre_popup_menu_callback) 00269 00270 self.status_label = ui.get_object('status_label') 00271 self.request_status_box = ui.get_object('request_status_box') 00272 self.request_spinner = ui.get_object('request_spinner') 00273 self.request_statusbar = ui.get_object('request_statusbar') 00274 self.request_cancel_button = ui.get_object('request_cancel_button') 00275 self.paned = ui.get_object('paned') 00276 self.entry_view_grid = ui.get_object('bottom_grid') 00277 00278 #setup Track Pane 00279 setting = self.gs.get_setting(self.gs.Path.PLUGIN) 00280 setting.bind(self.gs.PluginKey.PANED_POSITION, 00281 self.paned, 'collapsible-y', Gio.SettingsBindFlags.DEFAULT) 00282 self.entryviewpane = EntryViewPane(self.shell, 00283 self.plugin, 00284 self, 00285 self.entry_view_grid, 00286 self.viewmgr) 00287 00288 #---- set up info pane -----# 00289 info_stack = ui.get_object('info_stack') 00290 info_button_box = ui.get_object('info_button_box') 00291 artist_info_paned = ui.get_object('vertical_info_paned') 00292 artist_info_paned.set_name('vertical_paned') 00293 00294 self.artist_info = ArtistInfoPane(info_button_box, 00295 info_stack, 00296 artist_info_paned, 00297 self) 00298 00299 # quick search 00300 self.quick_search = ui.get_object('quick_search_entry') 00301 00302 # theme override option 00303 cssProvider = Gtk.CssProvider() 00304 css = rb.find_plugin_file(self.plugin, 'ui/gtkthemeoverride.css') 00305 cssProvider.load_from_path(css) 00306 screen = Gdk.Screen.get_default() 00307 styleContext = Gtk.StyleContext() 00308 styleContext.add_provider_for_screen(screen, cssProvider, 00309 Gtk.STYLE_PROVIDER_PRIORITY_USER) 00310 00311 print("CoverArtBrowser DEBUG - end _create_ui") 00312 00313 def _setup_source(self): 00314 ''' 00315 Setup the different parts of the source so they are ready to be used 00316 by the user. It also creates and configure some custom widgets. 00317 ''' 00318 print("CoverArtBrowser DEBUG - _setup_source") 00319 00320 cl = CoverLocale() 00321 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00322 00323 # setup iconview popup 00324 self.viewmgr.current_view.set_popup_menu(self.popup_menu) 00325 00326 # create an album manager 00327 self.album_manager = AlbumManager(self.plugin, self.viewmgr.current_view) 00328 00329 self.viewmgr.current_view.initialise(self) 00330 # setup cover search pane 00331 self.entryviewpane.setup_source() 00332 self.entry_view = self.entryviewpane.get_entry_view() 00333 00334 # connect a signal to when the info of albums is ready 00335 self.load_fin_id = self.album_manager.loader.connect( 00336 'model-load-finished', self.load_finished_callback) 00337 00338 # prompt the loader to load the albums 00339 self.album_manager.loader.load_albums(self.props.base_query_model) 00340 00341 # initialise the variables of the quick search 00342 self.quick_search_controller = AlbumQuickSearchController( 00343 self.album_manager) 00344 self.quick_search_controller.connect_quick_search(self.quick_search) 00345 00346 # set sensitivity of export menu item for iconview 00347 self.popup_menu.set_sensitive('export_embed_menu_item', 00348 CoverArtExport(self.plugin, 00349 self.shell, self.album_manager).is_search_plugin_enabled()) 00350 00351 # setup the statusbar component 00352 self.statusbar = Statusbar(self) 00353 00354 # initialise the toolbar manager 00355 self.toolbar_manager = ToolbarManager(self.plugin, self.page, 00356 self.viewmgr) 00357 self.viewmgr.current_view.emit('update-toolbar') 00358 00359 cl.switch_locale(cl.Locale.RB) 00360 # setup the artist paned 00361 artist_pview = None 00362 for view in self.shell.props.library_source.get_property_views(): 00363 print(view.props.title) 00364 print(_("Artist")) 00365 if view.props.title == _("Artist"): 00366 artist_pview = view 00367 break 00368 00369 assert artist_pview, "cannot find artist property view" 00370 00371 self.artist_treeview.set_model(artist_pview.get_model()) 00372 setting = self.gs.get_setting(self.gs.Path.PLUGIN) 00373 setting.bind(self.gs.PluginKey.ARTIST_PANED_POSITION, 00374 self, 'artist-paned-pos', Gio.SettingsBindFlags.DEFAULT) 00375 00376 self.artist_paned.connect('button-press-event', 00377 self.artist_paned_button_press_callback) 00378 self.artist_paned.connect('button-release-event', 00379 self.artist_paned_button_release_callback) 00380 00381 # intercept JumpToPlaying Song action so that we can scroll to the playing album 00382 appshell = rb3compat.ApplicationShell(self.shell) 00383 action = appshell.lookup_action("", "jump-to-playing", "win") 00384 action.action.connect("activate", self.jump_to_playing, None) 00385 00386 # connect to the playing song event so that if we are following a song, the album 00387 # should be automatically selected 00388 00389 self.shell.props.shell_player.connect('playing-song-changed', self.playing_song_callback) 00390 00391 self.echonest_similar_playlist = None 00392 00393 print("CoverArtBrowser DEBUG - end _setup_source") 00394 00395 def playing_song_callback(self, *args): 00396 if not self.follow_song: 00397 return 00398 00399 self.jump_to_playing() 00400 00401 def pre_popup_menu_callback(self, *args): 00402 ''' 00403 Callback when the popup menu is about to be displayed 00404 ''' 00405 00406 state, sensitive = self.shell.props.shell_player.get_playing() 00407 if not state: 00408 sensitive = False 00409 00410 #self.popup_menu.get_menu_object('add_to_playing_menu_item') 00411 self.popup_menu.set_sensitive('add_to_playing_menu_item', sensitive) 00412 00413 if not self._external_plugins: 00414 # initialise external plugin menu support 00415 self._external_plugins = \ 00416 CreateExternalPluginMenu("ca_covers_view", 00417 8, self.popup_menu) 00418 self._external_plugins.create_menu('popup_menu', True) 00419 00420 self.playlist_menu_item_callback() 00421 00422 def jump_to_playing(self, *args): 00423 ''' 00424 Callback when the JumpToPlaying action is invoked 00425 This will scroll the view to the playing song 00426 ''' 00427 00428 if not self.shell.props.selected_page.props.name == self.props.name: 00429 # if the source page that was played from is not the plugin then 00430 # nothing to do 00431 return 00432 00433 album = None 00434 00435 entry = self.shell.props.shell_player.get_playing_entry() 00436 00437 if entry: 00438 album = self.album_manager.model.get_from_dbentry(entry) 00439 00440 self.viewmgr.current_view.scroll_to_album(album) 00441 00442 def artist_paned_button_press_callback(self, widget, *args): 00443 self._from_paned_handle = True 00444 00445 def artist_paned_button_release_callback(self, widget, *args): 00446 ''' 00447 Callback when the artist paned handle is released from its mouse click. 00448 ''' 00449 if not self._from_paned_handle: 00450 return False 00451 else: 00452 self._from_paned_handle = False 00453 00454 print ("artist_paned_button_release_callback") 00455 child_width = self._get_child_width() 00456 print (child_width) 00457 print (self.artist_paned.get_position()) 00458 child_width = self.artist_paned.get_position() 00459 paned_positions = eval(self.artist_paned_pos) 00460 00461 found = None 00462 for viewpos in paned_positions: 00463 if self.viewmgr.view_name in viewpos: 00464 found = viewpos 00465 break 00466 00467 if not found: 00468 print ("not found %s" % self.viewmgr.view_name) 00469 return True 00470 00471 print ("current paned_positions %s" % paned_positions) 00472 paned_positions.remove(found) 00473 print ("Child Width %d" % child_width) 00474 if child_width <= self.min_paned_pos: 00475 print (found) 00476 print (found.split(':')[1]) 00477 if int(found.split(':')[1]) == 0: 00478 child_width = self.min_paned_pos + 1 00479 print ("opening") 00480 else: 00481 child_width = 0 00482 print ("smaller") 00483 self.artist_paned.set_position(child_width) 00484 00485 00486 print ("Child Width2 %d" % child_width) 00487 00488 paned_positions.append(self.viewmgr.view_name + ":" + str(child_width)) 00489 00490 print ("after paned positions %s" % paned_positions) 00491 self.artist_paned_pos = repr(paned_positions) 00492 print ("artist_paned_pos %s" % self.artist_paned_pos) 00493 00494 def on_view_changed(self, widget, view_name): 00495 self._change_artist_paned_pos(view_name) 00496 00497 def _change_artist_paned_pos(self, view_name): 00498 print ("change artist paned") 00499 paned_positions = eval(self.artist_paned_pos) 00500 print(paned_positions) 00501 found = None 00502 for viewpos in paned_positions: 00503 if view_name in viewpos: 00504 found = viewpos 00505 break 00506 print(found) 00507 if not found: 00508 print ("not found %s" % view_name) 00509 return 00510 00511 child_width = int(found.split(":")[1]) 00512 print(child_width) 00513 00514 # odd case - if the pane is not visible but the position is zero 00515 # then the paned position on visible=true is some large arbitary value 00516 # hence - set it to be 1 px larger than the real value, then set it back 00517 # to its expected value 00518 self.artist_paned.set_position(child_width + 1) 00519 self.artist_paned.set_visible(True) 00520 self.artist_paned.set_position(child_width) 00521 00522 def _get_child_width(self): 00523 child = self.artist_paned.get_child1() 00524 return child.get_allocated_width() 00525 00526 def on_artist_treeview_selection_changed(self, view): 00527 model, artist_iter = view.get_selected() 00528 if artist_iter: 00529 artist = model[artist_iter][0] 00530 00531 cl = CoverLocale() 00532 cl.switch_locale(cl.Locale.RB) 00533 #. TRANSLATORS - "All" is used in the context of "All artist names" 00534 if artist == _('All'): 00535 self.album_manager.model.remove_filter('quick_artist') 00536 else: 00537 self.album_manager.model.replace_filter('quick_artist', artist) 00538 00539 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00540 00541 def _apply_settings(self): 00542 ''' 00543 Applies all the settings related to the source and connects those that 00544 must be updated when the preferences dialog changes it's values. Also 00545 enables differents parts of the ui if the settings says so. 00546 ''' 00547 print("CoverArtBrowser DEBUG - _apply_settings") 00548 00549 # connect some signals to the loader to keep the source informed 00550 self.album_mod_id = self.album_manager.model.connect('album-updated', 00551 self.on_album_updated) 00552 00553 self.notify_prog_id = self.album_manager.connect( 00554 'notify::progress', lambda *args: self.notify_status_changed()) 00555 00556 print("CoverArtBrowser DEBUG - end _apply_settings") 00557 00558 def load_finished_callback(self, _): 00559 ''' 00560 Callback called when the loader finishes loading albums into the 00561 covers view model. 00562 ''' 00563 print("CoverArtBrowser DEBUG - load_finished_callback") 00564 00565 #if not self.request_status_box.get_visible(): 00566 # it should only be enabled if no cover request is going on 00567 #self.source_menu_search_all_item.set_sensitive(True) 00568 00569 # enable sorting on the entryview 00570 self.entry_view.set_columns_clickable(True) 00571 self.shell.props.library_source.get_entry_view().set_columns_clickable( 00572 True) 00573 00574 print("CoverArtBrowser DEBUG - end load_finished_callback") 00575 00576 def get_entry_view(self): 00577 return self.entry_view 00578 00579 def on_album_updated(self, model, path, tree_iter): 00580 ''' 00581 Callback called by the album loader when one of the albums managed 00582 by him gets modified in some way. 00583 ''' 00584 album = model.get_from_path(path) 00585 selected = self.viewmgr.current_view.get_selected_objects() 00586 00587 if album in selected: 00588 # update the selection since it may have changed 00589 self.viewmgr.current_view.selectionchanged_callback() 00590 00591 if album is selected[0]: 00592 self.entryviewpane.update_cover(album, 00593 self.album_manager) 00594 00595 def play_similar_artist_menu_item_callback(self, *args): 00596 ''' 00597 Callback called when the play similar artist option is selected from 00598 the cover view popup. It plays similar artists music. 00599 ''' 00600 00601 def play_similar_artist_menu_item_callback(self, *args): 00602 if not self.echonest_similar_playlist: 00603 self.echonest_similar_playlist = \ 00604 EchoNestPlaylist(self.shell, 00605 self.shell.props.queue_source) 00606 00607 selected_albums = self.viewmgr.current_view.get_selected_objects() 00608 album = selected_albums[0] 00609 tracks = album.get_tracks() 00610 00611 entry = tracks[0].entry 00612 self.echonest_similar_playlist.start(entry, reinitialise=True) 00613 00614 def show_properties_menu_item_callback(self, *args): 00615 ''' 00616 Callback called when the show album properties option is selected from 00617 the cover view popup. It shows a SongInfo dialog showing the selected 00618 albums' entries info, which can be modified. 00619 ''' 00620 print("CoverArtBrowser DEBUG - show_properties_menu_item_callback") 00621 00622 self.entry_view.select_all() 00623 00624 info_dialog = RB.SongInfo(source=self, entry_view=self.entry_view) 00625 00626 info_dialog.show_all() 00627 00628 print("CoverArtBrowser DEBUG - end show_properties_menu_item_callback") 00629 00630 def play_selected_album(self, favourites=False): 00631 ''' 00632 Utilitary method that plays all entries from an album into the play 00633 queue. 00634 ''' 00635 # callback when play an album 00636 print("CoverArtBrowser DEBUG - play_selected_album") 00637 00638 for row in self.source_query_model: 00639 self.source_query_model.remove_entry(row[0]) 00640 00641 self.queue_selected_album(self.source_query_model, favourites) 00642 00643 if len(self.source_query_model) > 0: 00644 self.props.query_model = self.source_query_model 00645 00646 # Start the music 00647 player = self.shell.props.shell_player 00648 00649 player.play_entry(self.source_query_model[0][0], self) 00650 00651 print("CoverArtBrowser DEBUG - end play_selected_album") 00652 00653 def queue_selected_album(self, source, favourites=False): 00654 ''' 00655 Utilitary method that queues all entries from an album into the play 00656 queue. 00657 ''' 00658 print("CoverArtBrowser DEBUG - queue_selected_album") 00659 00660 if source == None: 00661 source = self.source_query_model 00662 00663 selected_albums = self.viewmgr.current_view.get_selected_objects() 00664 threshold = self.rating_threshold if favourites else 0 00665 00666 total = 0 00667 for album in selected_albums: 00668 # Retrieve and sort the entries of the album 00669 tracks = album.get_tracks(threshold) 00670 total = total + len(tracks) 00671 # Add the songs to the play queue 00672 for track in tracks: 00673 source.add_entry(track.entry, -1) 00674 00675 if total == 0 and threshold: 00676 dialog = Gtk.MessageDialog(None, 00677 Gtk.DialogFlags.MODAL, 00678 Gtk.MessageType.INFO, 00679 Gtk.ButtonsType.OK, 00680 _( 00681 "No tracks have been added because no tracks meet the favourite rating threshold")) 00682 00683 dialog.run() 00684 dialog.destroy() 00685 print("CoverArtBrowser DEBUG - end queue_select_album") 00686 00687 def play_album_menu_item_callback(self, *args): 00688 ''' 00689 Callback called when the play album item from the cover view popup is 00690 selected. It cleans the play queue and queues the selected album. 00691 ''' 00692 print("CoverArtBrowser DEBUG - play_album_menu_item_callback") 00693 00694 self.play_selected_album(self.favourites) 00695 00696 print("CoverArtBrowser DEBUG - end play_album_menu_item_callback") 00697 00698 def add_to_playing_menu_item_callback(self, *args): 00699 ''' 00700 Callback called when the add-to-playing item from the cover view popup is 00701 selected. It adds the selected album at the end of the currently playing source. 00702 ''' 00703 00704 self.queue_selected_album(None, self.favourites) 00705 00706 def queue_album_menu_item_callback(self, *args): 00707 ''' 00708 Callback called when the queue album item from the cover view popup is 00709 selected. It queues the selected album at the end of the play queue. 00710 ''' 00711 print("CoverArtBrowser DEBUG - queue_album_menu_item_callback()") 00712 self.queue_selected_album(self.shell.props.queue_source, self.favourites) 00713 00714 print("CoverArtBrowser DEBUG - end queue_album_menu_item_callback()") 00715 00716 def playlist_menu_item_callback(self, *args): 00717 print("CoverArtBrowser DEBUG - playlist_menu_item_callback") 00718 00719 self.playlist_fillmenu(self.popup_menu, 'playlist_submenu', 'playlist_section', 00720 self.actiongroup, 00721 self.add_to_static_playlist_menu_item_callback, 00722 self.favourites) 00723 00724 def playlist_fillmenu(self, popup_menu, menubar, section_name, 00725 actiongroup, func, favourite=False): 00726 print("CoverArtBrowser DEBUG - playlist_fillmenu") 00727 00728 playlist_manager = self.shell.props.playlist_manager 00729 playlists_entries = playlist_manager.get_playlists() 00730 00731 # tidy up old playlists menu items before recreating the list 00732 actiongroup.remove_actions() 00733 popup_menu.remove_menu_items(menubar, section_name) 00734 00735 if playlists_entries: 00736 for playlist in playlists_entries: 00737 if playlist.props.is_local and \ 00738 isinstance(playlist, RB.StaticPlaylistSource): 00739 args = (playlist, favourite) 00740 00741 # take the name of the playlist, strip out non-english characters and reduce the string 00742 # to just a-to-z characters i.e. this will make the action_name valid in RB3 00743 00744 ascii_name = unicodedata.normalize('NFKD', \ 00745 rb3compat.unicodestr(playlist.props.name, 'utf-8')).encode( 00746 'ascii', 'ignore') 00747 ascii_name = ascii_name.decode(encoding='UTF-8') 00748 ascii_name = re.sub(r'[^a-zA-Z]', '', ascii_name) 00749 action = actiongroup.add_action(func=func, 00750 action_name=ascii_name, 00751 playlist=playlist, favourite=favourite, 00752 label=playlist.props.name) 00753 00754 popup_menu.add_menu_item(menubar, section_name, 00755 action) 00756 00757 def add_to_static_playlist_menu_item_callback(self, action, param, args): 00758 print('''CoverArtBrowser DEBUG - 00759 add_to_static_playlist_menu_item_callback''') 00760 00761 playlist = args['playlist'] 00762 favourite = args['favourite'] 00763 00764 self.queue_selected_album(playlist, favourite) 00765 00766 def add_playlist_menu_item_callback(self, *args): 00767 print('''CoverArtBrowser DEBUG - add_playlist_menu_item_callback''') 00768 playlist_manager = self.shell.props.playlist_manager 00769 playlist = playlist_manager.new_playlist(_('New Playlist'), False) 00770 00771 self.queue_selected_album(playlist, self.favourites) 00772 00773 def play_random_album_menu_item_callback(self, favourites=False): 00774 print('''CoverArtBrowser DEBUG - play_random_album_menu_item_callback''') 00775 query_model = RB.RhythmDBQueryModel.new_empty(self.shell.props.db) 00776 00777 num_albums = len(self.album_manager.model.store) 00778 00779 #random_list = [] 00780 selected_albums = [] 00781 00782 gs = GSetting() 00783 settings = gs.get_setting(gs.Path.PLUGIN) 00784 to_queue = settings[gs.PluginKey.RANDOM] 00785 00786 if num_albums <= to_queue: 00787 dialog = Gtk.MessageDialog(None, 00788 Gtk.DialogFlags.MODAL, 00789 Gtk.MessageType.INFO, 00790 Gtk.ButtonsType.OK, 00791 _("The number of albums to randomly play is less than that displayed.")) 00792 00793 dialog.run() 00794 dialog.destroy() 00795 return 00796 00797 album_col = self.album_manager.model.columns['album'] 00798 00799 chosen = {} 00800 00801 # now loop through finding unique random albums 00802 # i.e. ensure we dont queue the same album twice 00803 00804 for loop in range(0, to_queue): 00805 while True: 00806 pos = random.randint(0, num_albums - 1) 00807 if pos not in chosen: 00808 chosen[pos] = None 00809 selected_albums.append(self.album_manager.model.store[pos][album_col]) 00810 break 00811 00812 threshold = self.rating_threshold if favourites else 0 00813 00814 total = 0 00815 for album in selected_albums: 00816 # Retrieve and sort the entries of the album 00817 tracks = album.get_tracks(threshold) 00818 total = total + len(tracks) 00819 # Add the songs to the play queue 00820 for track in tracks: 00821 query_model.add_entry(track.entry, -1) 00822 00823 if total == 0 and threshold: 00824 dialog = Gtk.MessageDialog(None, 00825 Gtk.DialogFlags.MODAL, 00826 Gtk.MessageType.INFO, 00827 Gtk.ButtonsType.OK, 00828 _( 00829 "No tracks have been added because no tracks meet the favourite rating threshold")) 00830 00831 dialog.run() 00832 dialog.destroy() 00833 00834 self.props.query_model = query_model 00835 00836 # Start the music 00837 player = self.shell.props.shell_player 00838 00839 player.play_entry(query_model[0][0], self) 00840 00841 print("CoverArtBrowser DEBUG - end play_selected_album") 00842 00843 def cover_search_menu_item_callback(self, *args): 00844 ''' 00845 Callback called when the search cover option is selected from the 00846 cover view popup. It prompts the album loader to retrieve the selected 00847 album cover 00848 ''' 00849 print("CoverArtBrowser DEBUG - cover_search_menu_item_callback()") 00850 selected_albums = self.viewmgr.current_view.get_selected_objects() 00851 00852 self.request_status_box.show_all() 00853 00854 self.album_manager.cover_man.search_covers(selected_albums, 00855 self.update_request_status_bar) 00856 00857 print("CoverArtBrowser DEBUG - end cover_search_menu_item_callback()") 00858 00859 def export_embed_menu_item_callback(self, *args): 00860 ''' 00861 Callback called when the export and embed coverart option 00862 is selected from the cover view popup. 00863 It prompts the exporter to copy and embed art for the albums chosen 00864 ''' 00865 print("CoverArtBrowser DEBUG - export_embed_menu_item_callback()") 00866 selected_albums = self.viewmgr.current_view.get_selected_objects() 00867 00868 CoverArtExport(self.plugin, 00869 self.shell, self.album_manager).embed_albums(selected_albums) 00870 00871 print("CoverArtBrowser DEBUG - export_embed_menu_item_callback()") 00872 00873 def update_request_status_bar(self, coverobject): 00874 ''' 00875 Callback called by the album loader starts performing a new cover 00876 request. It prompts the source to change the content of the request 00877 statusbar. 00878 ''' 00879 print("CoverArtBrowser DEBUG - update_request_status_bar") 00880 00881 if coverobject: 00882 # for example "Requesting the picture cover for the music artist Michael Jackson" 00883 tranlation_string = _('Requesting cover for %s...') 00884 self.request_statusbar.set_text( 00885 rb3compat.unicodedecode(_('Requesting cover for %s...') % (coverobject.name), 'UTF-8')) 00886 else: 00887 self.request_status_box.hide() 00888 self.popup_menu.set_sensitive('cover_search_menu_item', True) 00889 self.request_cancel_button.set_sensitive(True) 00890 print("CoverArtBrowser DEBUG - end update_request_status_bar") 00891 00892 def cancel_request_callback(self, _): 00893 ''' 00894 Callback connected to the cancel button on the request statusbar. 00895 When called, it prompts the album loader to cancel the full cover 00896 search after the current cover. 00897 ''' 00898 print("CoverArtBrowser DEBUG - cancel_request_callback") 00899 00900 self.request_cancel_button.set_sensitive(False) 00901 self._cover_search_manager.cover_man.cancel_cover_request() 00902 00903 print("CoverArtBrowser DEBUG - end cancel_request_callback") 00904 00905 def show_hide_pane(self, params): 00906 ''' 00907 helper function - if the entry is manually expanded 00908 then if necessary scroll the view to the last selected album 00909 params is "album" or a tuple of "album" and "force_expand" boolean 00910 ''' 00911 print ('show_hide_pane') 00912 if isinstance(params, tuple): 00913 album, force = params 00914 else: 00915 album = params 00916 force = PanedCollapsible.Paned.DEFAULT 00917 00918 if (album and self.click_count == 1 \ 00919 and self.last_selected_album is album) or force != PanedCollapsible.Paned.DEFAULT: 00920 # check if it's a second or third click on the album and expand 00921 # or collapse the entry view accordingly 00922 self.paned.expand(force) 00923 00924 # update the selected album 00925 selected = self.viewmgr.current_view.get_selected_objects() 00926 self.last_selected_album = selected[0] if len(selected) == 1 else None 00927 00928 # clear the click count 00929 self.click_count = 0 00930 00931 print ('show_hide_pane end') 00932 00933 def update_with_selection(self): 00934 self.last_selected_album, self.click_count = \ 00935 self.entryviewpane.update_selection(self.last_selected_album, 00936 self.click_count) 00937 00938 self.statusbar.emit('display-status', self.viewmgr.current_view) 00939 00940 def propertiesbutton_callback(self, choice): 00941 print ("properties chosen: %s" % choice) 00942 00943 if choice == 'download': 00944 self.request_status_box.show_all() 00945 self._cover_search_manager = self.viewmgr.current_view.get_default_manager() 00946 self._cover_search_manager.cover_man.search_covers( 00947 callback=self.update_request_status_bar) 00948 elif choice == 'random': 00949 self.play_random_album_menu_item_callback() 00950 elif choice == 'random favourite': 00951 self.play_random_album_menu_item_callback(True) 00952 elif choice == 'favourite': 00953 self.favourites = not self.favourites 00954 self.viewmgr.current_view.set_popup_menu(self.popup_menu) 00955 elif choice == 'follow': 00956 self.follow_song = not self.follow_song 00957 elif choice == 'browser prefs': 00958 if not self._browser_preferences: 00959 self._browser_preferences = Preferences() 00960 00961 self._browser_preferences.display_preferences_dialog(self.plugin) 00962 elif choice == 'search prefs': 00963 try: 00964 if not self._search_preferences: 00965 from gi.repository import Peas 00966 00967 peas = Peas.Engine.get_default() 00968 plugin_info = peas.get_plugin_info('coverart_search_providers') 00969 module_name = plugin_info.get_module_name() 00970 mod = __import__(module_name) 00971 sp = getattr(mod, "SearchPreferences") 00972 self._search_preferences = sp() 00973 self._search_preferences.plugin_info = plugin_info 00974 00975 self._search_preferences.display_preferences_dialog(self._search_preferences) 00976 except: 00977 dialog = Gtk.MessageDialog(None, 00978 Gtk.DialogFlags.MODAL, 00979 Gtk.MessageType.INFO, 00980 Gtk.ButtonsType.OK, 00981 _( 00982 "Please install and activate the latest version of the Coverart Search Providers plugin")) 00983 00984 dialog.run() 00985 dialog.destroy() 00986 else: 00987 assert 1 == 2, ("unknown choice %s", choice) 00988 00989 @classmethod 00990 def get_instance(cls, **kwargs): 00991 ''' 00992 Returns the unique instance of the manager. 00993 ''' 00994 if not cls.instance: 00995 cls.instance = CoverArtBrowserSource(**kwargs) 00996 00997 return cls.instance 00998 00999 01000 class Statusbar(GObject.Object): 01001 # signals 01002 __gsignals__ = { 01003 'display-status': (GObject.SIGNAL_RUN_LAST, None, (object,)) 01004 } 01005 01006 custom_statusbar_enabled = GObject.property(type=bool, default=False) 01007 01008 def __init__(self, source): 01009 super(Statusbar, self).__init__() 01010 01011 self.status = '' 01012 01013 self._source_statusbar = SourceStatusBar(source) 01014 self._custom_statusbar = CustomStatusBar(source.status_label) 01015 self.current_statusbar = self._source_statusbar 01016 01017 self._connect_signals(source) 01018 self._connect_properties() 01019 01020 def _connect_properties(self): 01021 gs = GSetting() 01022 settings = gs.get_setting(gs.Path.PLUGIN) 01023 01024 settings.bind(gs.PluginKey.CUSTOM_STATUSBAR, self, 01025 'custom_statusbar_enabled', Gio.SettingsBindFlags.GET) 01026 01027 def _connect_signals(self, source): 01028 self.connect('notify::custom-statusbar-enabled', 01029 self._custom_statusbar_enabled_changed) 01030 self.connect('display-status', self._update) 01031 01032 def _custom_statusbar_enabled_changed(self, *args): 01033 self.current_statusbar.hide() 01034 01035 if self.custom_statusbar_enabled: 01036 self.current_statusbar = self._custom_statusbar 01037 else: 01038 self.current_statusbar = self._source_statusbar 01039 01040 self.current_statusbar.show() 01041 self.current_statusbar.update(self.status) 01042 01043 def _generate_status(self, albums=None): 01044 self.status = '' 01045 01046 if albums: 01047 track_count = 0 01048 duration = 0 01049 01050 for album in albums: 01051 # Calculate duration and number of tracks from that album 01052 track_count += album.track_count 01053 duration += album.duration / 60 01054 01055 # now lets build up a status label containing some 01056 # 'interesting stuff' about the album 01057 if len(albums) == 1: 01058 #. TRANSLATORS - for example "abba's greatest hits by ABBA" 01059 self.status = rb3compat.unicodedecode(_('%s by %s') % 01060 (album.name, album.artist), 'UTF-8') 01061 else: 01062 #. TRANSLATORS - the number of albums that have been selected/highlighted 01063 self.status = rb3compat.unicodedecode(_('%d selected albums') % 01064 (len(albums)), 'UTF-8') 01065 01066 if track_count == 1: 01067 self.status += rb3compat.unicodedecode(_(' with 1 track'), 'UTF-8') 01068 else: 01069 self.status += rb3compat.unicodedecode(_(' with %d tracks') % 01070 track_count, 'UTF-8') 01071 01072 if duration == 1: 01073 self.status += rb3compat.unicodedecode(_(' and a duration of 1 minute'), 'UTF-8') 01074 else: 01075 self.status += rb3compat.unicodedecode(_(' and a duration of %d minutes') % 01076 duration, 'UTF-8') 01077 01078 def _update(self, widget, current_view): 01079 albums = current_view.get_selected_objects() 01080 self._generate_status(albums) 01081 self.current_statusbar.update(self.status) 01082 01083 01084 class SourceStatusBar(object): 01085 def __init__(self, source): 01086 self._source = source 01087 01088 def show(self): 01089 pass 01090 01091 def hide(self): 01092 self.update('') 01093 01094 def update(self, status): 01095 self._source.status = status 01096 self._source.notify_status_changed() 01097 01098 01099 class CustomStatusBar(object): 01100 def __init__(self, status_label): 01101 self._label = status_label 01102 01103 def show(self): 01104 self._label.show() 01105 01106 def hide(self): 01107 self._label.hide() 01108 01109 def update(self, status): 01110 self._label.set_text(status) 01111 01112 01113 class Views: 01114 ''' 01115 This class describes the different views available 01116 ''' 01117 # storage for the instance reference 01118 __instance = None 01119 01120 class _impl(GObject.Object): 01121 """ Implementation of the singleton interface """ 01122 01123 # below public variables and methods that can be called for Views 01124 def __init__(self, shell): 01125 ''' 01126 Initializes the singleton interface, assigning all the constants 01127 used to access the plugin's settings. 01128 ''' 01129 super(Views._impl, self).__init__() 01130 01131 from coverart_covericonview import CoverIconView 01132 from coverart_coverflowview import CoverFlowView 01133 from coverart_artistview import ArtistView 01134 from coverart_listview import ListView 01135 from coverart_queueview import QueueView 01136 from coverart_playsourceview import PlaySourceView 01137 from coverart_browser_prefs import webkit_support 01138 01139 library_name = shell.props.library_source.props.name 01140 queue_name = shell.props.queue_source.props.name 01141 01142 self._values = OrderedDict() 01143 01144 cl = CoverLocale() 01145 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 01146 01147 self._values[CoverIconView.name] = [_('Tiles'), 01148 GLib.Variant.new_string('coverart-browser-tile')] 01149 if webkit_support(): 01150 self._values[CoverFlowView.name] = [_('Flow'), 01151 GLib.Variant.new_string('coverart-browser-coverflow')] 01152 self._values[ArtistView.name] = [_('Artist'), 01153 GLib.Variant.new_string('coverart-browser-artist')] 01154 self._values[ListView.name] = [library_name, 01155 GLib.Variant.new_string('coverart-browser-list')] 01156 self._values[QueueView.name] = [queue_name, 01157 GLib.Variant.new_string('coverart-browser-queue')] 01158 #self._values[PlaySourceView.name] = [_('CoverArt Playlist'), 01159 # GLib.Variant.new_string('coverart-browser-playsource')] 01160 cl.switch_locale(cl.Locale.RB) 01161 print(self._values) 01162 01163 def get_view_names(self): 01164 return list(self._values.keys()) 01165 01166 def get_view_name_for_action(self, action_name): 01167 for view_name in self.get_view_names(): 01168 if self.get_action_name(view_name) == action_name: 01169 return view_name 01170 01171 return None 01172 01173 def get_menu_name(self, view_name): 01174 return self._values[view_name][0] 01175 01176 def get_action_name(self, view_name): 01177 return self._values[view_name][1] 01178 01179 def __init__(self, plugin): 01180 """ Create singleton instance """ 01181 # Check whether we already have an instance 01182 if Views.__instance is None: 01183 # Create and remember instance 01184 Views.__instance = Views._impl(plugin) 01185 01186 # Store instance reference as the only member in the handle 01187 self.__dict__['_Views__instance'] = Views.__instance 01188 01189 def __getattr__(self, attr): 01190 """ Delegate access to implementation """ 01191 return getattr(self.__instance, attr) 01192 01193 def __setattr__(self, attr, value): 01194 """ Delegate access to implementation """ 01195 return setattr(self.__instance, attr, value) 01196 01197 01198 class ViewManager(GObject.Object): 01199 # signals 01200 __gsignals__ = { 01201 'new-view': (GObject.SIGNAL_RUN_LAST, None, (str,)) 01202 } 01203 01204 # properties 01205 view_name = GObject.property(type=str, default=CoverIconView.name) 01206 01207 def __init__(self, source, window): 01208 super(ViewManager, self).__init__() 01209 01210 self.source = source 01211 self.window = window 01212 01213 # initialize views 01214 self._views = {} 01215 ui = Gtk.Builder() 01216 ui.add_from_file(rb.find_plugin_file(source.plugin, 01217 'ui/coverart_iconview.ui')) 01218 self._views[CoverIconView.name] = ui.get_object('covers_view') 01219 self._views[CoverFlowView.name] = CoverFlowView() 01220 self._views[ListView.name] = ListView() 01221 self._views[QueueView.name] = QueueView() 01222 #self._views[PlaySourceView.name] = PlaySourceView() 01223 ui.add_from_file(rb.find_plugin_file(source.plugin, 01224 'ui/coverart_artistview.ui')) 01225 self._views[ArtistView.name] = ui.get_object('artist_view') 01226 self._lastview = None 01227 01228 self.controller = ViewController(source.shell, self) 01229 01230 # connect signal and properties 01231 self._connect_signals() 01232 self._connect_properties() 01233 self._lastview = self.view_name 01234 if self.current_view.use_plugin_window: 01235 window.add(self.current_view.view) 01236 window.show_all() 01237 01238 @property 01239 def current_view(self): 01240 return self._views[self.view_name] 01241 01242 def get_view(self, view_name): 01243 return self._views[view_name] 01244 01245 def _connect_signals(self): 01246 self.connect('notify::view-name', self.on_notify_view_name) 01247 01248 def _connect_properties(self): 01249 gs = GSetting() 01250 setting = gs.get_setting(gs.Path.PLUGIN) 01251 setting.bind(gs.PluginKey.VIEW_NAME, self, 'view_name', 01252 Gio.SettingsBindFlags.DEFAULT) 01253 01254 def on_notify_view_name(self, *args): 01255 if self._lastview and self.view_name != self._lastview: 01256 selected = self._views[self._lastview].get_selected_objects() 01257 current_album = None 01258 if len(selected) > 0: 01259 current_album = self._views[self._lastview].get_selected_objects()[0] 01260 01261 if self._views[self.view_name].use_plugin_window: 01262 child = self.window.get_child() 01263 01264 if child: 01265 self.window.remove(child) 01266 self.window.add(self._views[self.view_name].view) 01267 self.window.show_all() 01268 self.click_count = 0 01269 01270 self._views[self._lastview].panedposition = self.source.paned.get_expansion_status() 01271 01272 self._views[self.view_name].switch_to_view(self.source, current_album) 01273 self._views[self.view_name].emit('update-toolbar') 01274 self._views[self.view_name].get_default_manager().emit('sort', None) 01275 01276 if self._views[self.view_name].use_plugin_window: 01277 self.source.paned.expand(self._views[self.view_name].panedposition) 01278 01279 self.current_view.set_popup_menu(self.source.popup_menu) 01280 self.source.album_manager.current_view = self.current_view 01281 01282 if self._views[self.view_name].use_plugin_window: 01283 # we only ever save plugin views not external views 01284 saved_view = self.view_name 01285 else: 01286 saved_view = self._lastview 01287 01288 self._lastview = self.view_name 01289 01290 gs = GSetting() 01291 setting = gs.get_setting(gs.Path.PLUGIN) 01292 setting[gs.PluginKey.VIEW_NAME] = saved_view 01293 01294 self.emit('new-view', self.view_name) 01295 01296 def get_view_icon_name(self, view_name): 01297 return self._views[view_name].get_view_icon_name() 01298 01299 def get_selection_colour(self): 01300 try: 01301 colour = self._views[CoverIconView.name].view.get_style_context().get_background_color( 01302 Gtk.StateFlags.SELECTED) 01303 colour = '#%s%s%s' % ( 01304 str(hex(int(colour.red * 255))).replace('0x', ''), 01305 str(hex(int(colour.green * 255))).replace('0x', ''), 01306 str(hex(int(colour.blue * 255))).replace('0x', '')) 01307 except: 01308 colour = '#0000FF' 01309 01310 return colour 01311 01312 01313 GObject.type_register(CoverArtBrowserSource)