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 the 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 json 00021 import os 00022 from xml.sax.saxutils import escape 00023 00024 from gi.repository import Gdk 00025 from gi.repository import Gtk 00026 from gi.repository import GLib 00027 from gi.repository import GObject 00028 from gi.repository import Gio 00029 00030 from coverart_browser_prefs import GSetting 00031 from coverart_browser_prefs import webkit_support 00032 from coverart_widgets import AbstractView 00033 from coverart_widgets import PanedCollapsible 00034 import rb 00035 00036 00037 class FlowShowingPolicy(GObject.Object): 00038 ''' 00039 Policy that mostly takes care of how and when things should be showed on 00040 the view that makes use of the `AlbumsModel`. 00041 ''' 00042 00043 def __init__(self, flow_view): 00044 super(FlowShowingPolicy, self).__init__() 00045 00046 self._flow_view = flow_view 00047 self.counter = 0 00048 self._has_initialised = False 00049 00050 def initialise(self, album_manager): 00051 if self._has_initialised: 00052 return 00053 00054 self._has_initialised = True 00055 self._album_manager = album_manager 00056 self._model = album_manager.model 00057 00058 00059 class CoverFlowView(AbstractView): 00060 __gtype_name__ = "CoverFlowView" 00061 00062 name = 'coverflowview' 00063 00064 #properties 00065 flow_background = GObject.property(type=str, default='W') 00066 flow_automatic = GObject.property(type=bool, default=False) 00067 flow_scale = GObject.property(type=int, default=100) 00068 flow_hide = GObject.property(type=bool, default=False) 00069 flow_width = GObject.property(type=int, default=600) 00070 flow_appearance = GObject.property(type=str, default='coverflow') 00071 flow_max = GObject.property(type=int, default=100) 00072 panedposition = PanedCollapsible.Paned.EXPAND 00073 00074 def __init__(self): 00075 super(CoverFlowView, self).__init__() 00076 00077 self.show_policy = FlowShowingPolicy(self) 00078 if webkit_support(): 00079 from gi.repository import WebKit 00080 00081 self.view = WebKit.WebView() 00082 else: 00083 self.view = None 00084 00085 self._last_album = None 00086 self._has_initialised = False 00087 self._filter_changed_inprogress = False 00088 self._on_first_use = True 00089 00090 def _connect_properties(self): 00091 gs = GSetting() 00092 settings = gs.get_setting(gs.Path.PLUGIN) 00093 settings.bind(gs.PluginKey.FLOW_APPEARANCE, self, 00094 'flow_appearance', Gio.SettingsBindFlags.GET) 00095 settings.bind(gs.PluginKey.FLOW_HIDE_CAPTION, self, 00096 'flow_hide', Gio.SettingsBindFlags.GET) 00097 settings.bind(gs.PluginKey.FLOW_SCALE, self, 00098 'flow_scale', Gio.SettingsBindFlags.GET) 00099 settings.bind(gs.PluginKey.FLOW_AUTOMATIC, self, 00100 'flow_automatic', Gio.SettingsBindFlags.GET) 00101 settings.bind(gs.PluginKey.FLOW_BACKGROUND_COLOUR, self, 00102 'flow_background', Gio.SettingsBindFlags.GET) 00103 settings.bind(gs.PluginKey.FLOW_WIDTH, self, 00104 'flow_width', Gio.SettingsBindFlags.GET) 00105 settings.bind(gs.PluginKey.FLOW_MAX, self, 00106 'flow_max', Gio.SettingsBindFlags.GET) 00107 00108 def _connect_signals(self, source): 00109 self.connect('notify::flow-background', 00110 self.filter_changed) 00111 self.connect('notify::flow-scale', 00112 self.filter_changed) 00113 self.connect('notify::flow-hide', 00114 self.filter_changed) 00115 self.connect('notify::flow-width', 00116 self.filter_changed) 00117 self.connect('notify::flow-appearance', 00118 self.filter_changed) 00119 self.connect('notify::flow-max', 00120 self.filter_changed) 00121 00122 def filter_changed(self, *args): 00123 # we can get several filter_changed calls per second 00124 # lets simplify the processing & potential flickering when the 00125 # call to this method has slowed stopped 00126 00127 self._filter_changed_event = True 00128 00129 if self._filter_changed_inprogress: 00130 return 00131 00132 self._filter_changed_inprogress = True 00133 00134 def filter_events(*args): 00135 if not self._filter_changed_event: 00136 self._filter_changed() 00137 self._filter_changed_inprogress = False 00138 else: 00139 self._filter_changed_event = False 00140 return True 00141 00142 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250, filter_events, None) 00143 00144 00145 def _filter_changed(self, *args): 00146 path = rb.find_plugin_file(self.plugin, 'coverflow/index.html') 00147 f = open(path) 00148 string = f.read() 00149 f.close() 00150 00151 if self.flow_background == 'W': 00152 background_colour = 'white' 00153 if len(self.album_manager.model.store) <= self.flow_max: 00154 foreground_colour = 'white' 00155 else: 00156 foreground_colour = 'black' 00157 else: 00158 background_colour = 'black' 00159 if len(self.album_manager.model.store) <= self.flow_max: 00160 foreground_colour = 'black' 00161 else: 00162 foreground_colour = 'white' 00163 00164 string = string.replace('#BACKGROUND_COLOUR', background_colour) 00165 string = string.replace('#FOREGROUND_COLOUR', foreground_colour) 00166 string = string.replace('#FACTOR', str(float(self.flow_scale) / 100)) 00167 00168 if self.flow_hide: 00169 caption = "" 00170 else: 00171 caption = '<div class="globalCaption"></div>' 00172 00173 string = string.replace('#GLOBAL_CAPTION', caption) 00174 00175 addon = background_colour 00176 if self.flow_appearance == 'flow-vert': 00177 addon += " vertical" 00178 elif self.flow_appearance == 'carousel': 00179 addon += " carousel" 00180 elif self.flow_appearance == 'roundabout': 00181 addon += " roundabout" 00182 00183 string = string.replace('#ADDON', addon) 00184 00185 string = string.replace('#WIDTH', str(self.flow_width)) 00186 00187 identifier = self.flow.get_identifier(self.last_album) 00188 if not identifier: 00189 identifier = "'start'" 00190 else: 00191 identifier = str(identifier) 00192 00193 string = string.replace('#START', identifier) 00194 00195 #TRANSLATORS: for example 'Number of covers limited to 150' 00196 display_message = _("Number of covers limited to %d") % self.flow_max 00197 string = string.replace('#MAXCOVERS', 00198 '<p>' + display_message + '</p>') 00199 00200 items = self.flow.initialise(self.album_manager.model, self.flow_max) 00201 00202 string = string.replace('#ITEMS', items) 00203 00204 base = os.path.dirname(path) + "/" 00205 Gdk.threads_enter() 00206 print(string) 00207 self.view.load_string(string, "text/html", "UTF-8", "file://" + base) 00208 Gdk.threads_leave() 00209 00210 if self._on_first_use: 00211 self._on_first_use = False 00212 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250, 00213 self.source.show_hide_pane, (self.last_album, PanedCollapsible.Paned.EXPAND)) 00214 00215 def get_view_icon_name(self): 00216 return "flowview.png" 00217 00218 def initialise(self, source): 00219 if self._has_initialised: 00220 return 00221 00222 self._has_initialised = True 00223 00224 super(CoverFlowView, self).initialise(source) 00225 00226 self.album_manager = source.album_manager 00227 self.ext_menu_pos = 6 00228 00229 self._connect_properties() 00230 self._connect_signals(source) 00231 00232 # lets check that all covers have finished loading before 00233 # initialising the flowcontrol and other signals 00234 if not self.album_manager.cover_man.has_finished_loading: 00235 self.album_manager.cover_man.connect('load-finished', self._covers_loaded) 00236 else: 00237 self._covers_loaded() 00238 00239 def _covers_loaded(self, *args): 00240 self.flow = FlowControl(self) 00241 self.view.connect("notify::title", self.flow.receive_message_signal) 00242 00243 #self.album_manager.model.connect('album-updated', self.flow.update_album, self.view) 00244 #self.album_manager.model.connect('visual-updated', self.flow.update_album, self.view) 00245 self.album_manager.model.connect('album-updated', self.filter_changed) 00246 self.album_manager.model.connect('visual-updated', self.filter_changed) 00247 self.album_manager.model.connect('filter-changed', self.filter_changed) 00248 00249 self.filter_changed() 00250 00251 @property 00252 def last_album(self): 00253 return self._last_album 00254 00255 @last_album.setter 00256 def last_album(self, new_album): 00257 if self._last_album != new_album: 00258 self._last_album = new_album 00259 self.source.click_count = 0 00260 self.selectionchanged_callback() 00261 00262 def item_rightclicked_callback(self, album): 00263 self.last_album = album 00264 self.popup.popup(self.source, 'popup_menu', 3, Gtk.get_current_event_time()) 00265 00266 def item_clicked_callback(self, album): 00267 ''' 00268 Callback called when the user clicks somewhere on the flow_view. 00269 Along with source "show_hide_pane", takes care of showing/hiding the bottom 00270 pane after a second click on a selected album. 00271 ''' 00272 # to expand the entry view 00273 if self.flow_automatic: 00274 self.source.click_count += 1 00275 00276 self.last_album = album 00277 00278 if self.source.click_count == 1: 00279 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250, 00280 self.source.show_hide_pane, album) 00281 00282 def item_activated_callback(self, album): 00283 ''' 00284 Callback called when the flow view is double clicked. It plays the selected album 00285 ''' 00286 self.last_album = album 00287 self.source.play_selected_album() 00288 00289 return True 00290 00291 def item_drop_callback(self, album, webpath): 00292 ''' 00293 Callback called when something is dropped onto the flow view - hopefully a webpath 00294 to a picture 00295 ''' 00296 print("item_drop_callback %s" % webpath) 00297 print("dropped on album %s" % album) 00298 self.album_manager.cover_man.update_cover(album, uri=webpath) 00299 00300 def get_selected_objects(self): 00301 if self.last_album: 00302 return [self.last_album] 00303 else: 00304 return [] 00305 00306 def select_and_scroll_to_path(self, path): 00307 album = self.source.album_manager.model.get_from_path(path) 00308 self.flow.scroll_to_album(album, self.view) 00309 self.item_clicked_callback(album) 00310 00311 def switch_to_view(self, source, album): 00312 self.initialise(source) 00313 self.show_policy.initialise(source.album_manager) 00314 00315 self.last_album = album 00316 self.scroll_to_album(self.last_album) 00317 00318 def grab_focus(self): 00319 self.view.grab_focus() 00320 00321 def scroll_to_album(self, album): 00322 self.flow.scroll_to_album(album, self.view) 00323 00324 00325 class FlowControl(object): 00326 def __init__(self, callback_view): 00327 self.callback_view = callback_view 00328 self.album_identifier = {} 00329 00330 def get_identifier(self, album): 00331 index = -1 00332 for row in self.album_identifier: 00333 if self.album_identifier[row] == album: 00334 index = row 00335 break 00336 00337 if index == -1: 00338 return None 00339 else: 00340 return row 00341 00342 def update_album(self, model, album_path, album_iter, webview): 00343 album = model.get_from_path(album_path) 00344 index = -1 00345 for row in self.album_identifier: 00346 if self.album_identifier[row] == album: 00347 index = row 00348 break 00349 00350 if index == -1: 00351 return 00352 00353 obj = {} 00354 obj['filename'] = album.cover.original 00355 obj['title'] = album.artist 00356 obj['caption'] = album.name 00357 obj['identifier'] = str(index) 00358 00359 webview.execute_script("update_album('%s')" % json.dumps(obj)) 00360 00361 def receive_message_signal(self, webview, param): 00362 # this will be key to passing stuff back and forth - need 00363 # to develop some-sort of message protocol to distinguish "events" 00364 00365 title = webview.get_title() 00366 if (not title) or (title == '"clear"'): 00367 return 00368 00369 args = json.loads(title) 00370 try: 00371 signal = args["signal"] 00372 except: 00373 print("unhandled: %s " % title) 00374 return 00375 00376 if signal == 'clickactive': 00377 self.callback_view.item_clicked_callback(self.album_identifier[int(args['param'][0])]) 00378 elif signal == 'rightclickactive': 00379 self.callback_view.item_rightclicked_callback( 00380 self.album_identifier[int(args['param'][0])]) 00381 elif signal == 'doubleclickactive': 00382 self.callback_view.item_activated_callback(self.album_identifier[int(args['param'][0])]) 00383 elif signal == 'dropactive': 00384 self.callback_view.item_drop_callback(self.album_identifier[int(args['param'][0])], 00385 args['param'][1]) 00386 else: 00387 print("unhandled signal: %s" % signal) 00388 00389 def scroll_to_album(self, album, webview): 00390 for row in self.album_identifier: 00391 if self.album_identifier[row] == album: 00392 webview.execute_script("scroll_to_identifier('%s')" % str(row)) 00393 break 00394 00395 def initialise(self, model, max_covers): 00396 00397 album_col = model.columns['album'] 00398 index = 0 00399 items = "" 00400 self.album_identifier = {} 00401 00402 def html_elements(fullfilename, title, caption, identifier): 00403 00404 return '<div class="item"><img class="content" src="' + \ 00405 escape(fullfilename) + '" title="' + \ 00406 escape(title) + '" identifier="' + \ 00407 identifier + '"/> <div class="caption">' + \ 00408 escape(caption) + '</div> </div>' 00409 00410 00411 for row in model.store: 00412 00413 cover = row[album_col].cover.original 00414 cover = cover.replace( 00415 'rhythmbox-missing-artwork.svg', 00416 'rhythmbox-missing-artwork.png') ## need a white vs black when we change the background colour 00417 00418 self.album_identifier[index] = row[album_col] 00419 items += html_elements( 00420 fullfilename=cover, 00421 caption=row[album_col].name, 00422 title=row[album_col].artist, 00423 identifier=str(index)) 00424 00425 index += 1 00426 00427 if index == max_covers: 00428 break 00429 00430 if index != 0: 00431 #self.callback_view.last_album = self.album_identifier[0] 00432 pass 00433 else: 00434 self.callback_view.last_album = None 00435 00436 return items