Fri, 13 Jun 2008 21:32:57 +0200 <thp@perli.net>
Automatically download channel cover file; improve channel cover handling * data/gpodder.glade: Simplify and clean-up the podcast editor dialog, especially with respect to the cover art stuff * src/gpodder/config.py: Add configuration option "podcast_list_icon_size" that determines the pixel size of the cover art displayed in the podcast list * src/gpodder/gui.py: Add cover cache, register with the cover downloader service in the main window, handle messages from the cover downloader (removed and download finished); request covers for channels when refreshing the channel list; make sure drag'n'drop of image files to the channel list works directly and sets the corresponding channel cover; Rework cover download handling and add an open dialog as suggested by the May 2008 Usability Evaluation * src/gpodder/libgpodder.py: Remove old, attic image downloading code from gPodderLib, because it now has its own service class * src/gpodder/libpodcasts.py: Remove unneeded get_cover_pixbuf helper function for podcastChannel; improve channels_to_model to take advantage of the new cover downloader service * src/gpodder/services.py: Add CoverDownloader service that acts as a central hub for all downloading and modifying of channel cover art, including notification of observers (through ObservableService) * src/gpodder/util.py: Add resize_pixbuf_keep_ratio helper function to resize a gtk pixbuf while keeping the aspect radio (with optional caching support through a dictionary parameter) (Closes: http://bugs.gpodder.org/show_bug.cgi?id=88) Fri, 13 Jun 2008 20:10:13 +0200 <thp@perli.net> Fix a bug in the experimental file naming support * src/gpodder/util.py: Fix bug that stopped the experimental file naming patch from working; thanks to Shane Donohoe for reporting git-svn-id: svn://svn.berlios.de/gpodder/trunk@737 b0d088ad-0a06-0410-aad2-9ed5178a7e87
This commit is contained in:
parent
0aec290ec2
commit
d3533acc1f
34
ChangeLog
34
ChangeLog
|
@ -1,3 +1,37 @@
|
|||
Fri, 13 Jun 2008 21:32:57 +0200 <thp@perli.net>
|
||||
Automatically download channel cover file; improve channel cover handling
|
||||
|
||||
* data/gpodder.glade: Simplify and clean-up the podcast editor dialog,
|
||||
especially with respect to the cover art stuff
|
||||
* src/gpodder/config.py: Add configuration option
|
||||
"podcast_list_icon_size" that determines the pixel size of the cover
|
||||
art displayed in the podcast list
|
||||
* src/gpodder/gui.py: Add cover cache, register with the cover
|
||||
downloader service in the main window, handle messages from the cover
|
||||
downloader (removed and download finished); request covers for
|
||||
channels when refreshing the channel list; make sure drag'n'drop of
|
||||
image files to the channel list works directly and sets the
|
||||
corresponding channel cover; Rework cover download handling and add an
|
||||
open dialog as suggested by the May 2008 Usability Evaluation
|
||||
* src/gpodder/libgpodder.py: Remove old, attic image downloading code
|
||||
from gPodderLib, because it now has its own service class
|
||||
* src/gpodder/libpodcasts.py: Remove unneeded get_cover_pixbuf helper
|
||||
function for podcastChannel; improve channels_to_model to take
|
||||
advantage of the new cover downloader service
|
||||
* src/gpodder/services.py: Add CoverDownloader service that acts as a
|
||||
central hub for all downloading and modifying of channel cover art,
|
||||
including notification of observers (through ObservableService)
|
||||
* src/gpodder/util.py: Add resize_pixbuf_keep_ratio helper function to
|
||||
resize a gtk pixbuf while keeping the aspect radio (with optional
|
||||
caching support through a dictionary parameter)
|
||||
(Closes: http://bugs.gpodder.org/show_bug.cgi?id=88)
|
||||
|
||||
Fri, 13 Jun 2008 20:10:13 +0200 <thp@perli.net>
|
||||
Fix a bug in the experimental file naming support
|
||||
|
||||
* src/gpodder/util.py: Fix bug that stopped the experimental file
|
||||
naming patch from working; thanks to Shane Donohoe for reporting
|
||||
|
||||
Fri, 13 Jun 2008 16:08:16 +0200 <thp@perli.net>
|
||||
Merge patch to add experimental support for "normal" file naming
|
||||
|
||||
|
|
|
@ -1428,7 +1428,7 @@
|
|||
|
||||
<widget class="GtkWindow" id="gPodderChannel">
|
||||
<property name="border_width">10</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">gPodder Podcast Editor</property>
|
||||
<property name="type">GTK_WINDOW_TOPLEVEL</property>
|
||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||
|
@ -2108,31 +2108,6 @@
|
|||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="labelCoverStatus">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes"></property>
|
||||
<property name="use_underline">False</property>
|
||||
<property name="use_markup">False</property>
|
||||
<property name="justify">GTK_JUSTIFY_LEFT</property>
|
||||
<property name="wrap">False</property>
|
||||
<property name="selectable">False</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="yalign">0.5</property>
|
||||
<property name="xpad">0</property>
|
||||
<property name="ypad">4</property>
|
||||
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
|
||||
<property name="width_chars">-1</property>
|
||||
<property name="single_line_mode">False</property>
|
||||
<property name="angle">0</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkHButtonBox" id="hbuttonbox1">
|
||||
<property name="visible">True</property>
|
||||
|
@ -2142,86 +2117,23 @@
|
|||
<child>
|
||||
<widget class="GtkButton" id="btnDownloadCover">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="sensitive">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="label">gtk-open</property>
|
||||
<property name="relief">GTK_RELIEF_NORMAL</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal name="clicked" handler="on_btnDownloadCover_clicked" last_modification_time="Wed, 19 Sep 2007 14:09:48 GMT"/>
|
||||
|
||||
<child>
|
||||
<widget class="GtkAlignment" id="alignment17">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="yalign">0.5</property>
|
||||
<property name="xscale">0</property>
|
||||
<property name="yscale">0</property>
|
||||
<property name="top_padding">0</property>
|
||||
<property name="bottom_padding">0</property>
|
||||
<property name="left_padding">0</property>
|
||||
<property name="right_padding">0</property>
|
||||
|
||||
<child>
|
||||
<widget class="GtkHBox" id="hbox28">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">2</property>
|
||||
|
||||
<child>
|
||||
<widget class="GtkImage" id="image2631">
|
||||
<property name="visible">True</property>
|
||||
<property name="stock">gtk-go-down</property>
|
||||
<property name="icon_size">4</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="yalign">0.5</property>
|
||||
<property name="xpad">0</property>
|
||||
<property name="ypad">0</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label99">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">Download</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_markup">False</property>
|
||||
<property name="justify">GTK_JUSTIFY_LEFT</property>
|
||||
<property name="wrap">False</property>
|
||||
<property name="selectable">False</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="yalign">0.5</property>
|
||||
<property name="xpad">0</property>
|
||||
<property name="ypad">0</property>
|
||||
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
|
||||
<property name="width_chars">-1</property>
|
||||
<property name="single_line_mode">False</property>
|
||||
<property name="angle">0</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkButton" id="btnClearCover">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="sensitive">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label">gtk-clear</property>
|
||||
<property name="label">gtk-refresh</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="relief">GTK_RELIEF_NORMAL</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
|
|
|
@ -113,6 +113,7 @@ gPodderSettings = {
|
|||
'rockbox_copy_coverart' : (bool, False),
|
||||
'rockbox_coverart_size' : (int, 100),
|
||||
'experimental_file_naming': (bool, False),
|
||||
'podcast_list_icon_size': (int, 32),
|
||||
|
||||
# Hide the cover/pill from the podcast sidebar when it gets too small
|
||||
'podcast_sidebar_save_space': (bool, True),
|
||||
|
|
|
@ -456,6 +456,9 @@ class gPodder(GladeWidget):
|
|||
|
||||
services.download_status_manager.register( 'list-changed', self.download_status_updated)
|
||||
services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
|
||||
services.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
services.cover_downloader.register('cover-removed', self.cover_file_removed)
|
||||
self.cover_cache = {}
|
||||
|
||||
self.treeDownloads.set_model( services.download_status_manager.tree_model)
|
||||
|
||||
|
@ -680,6 +683,33 @@ class gPodder(GladeWidget):
|
|||
else:
|
||||
self.on_gPodder_delete_event(widget)
|
||||
|
||||
def cover_file_removed(self, channel_url):
|
||||
"""
|
||||
The Cover Downloader calls this when a previously-
|
||||
available cover has been removed from the disk. We
|
||||
have to update our cache to reflect this change.
|
||||
"""
|
||||
(COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
|
||||
for row in self.treeChannels.get_model():
|
||||
if row[COLUMN_URL] == channel_url:
|
||||
row[COLUMN_PIXBUF] = None
|
||||
del self.cover_cache[(channel_url, gl.config.podcast_list_icon_size, \
|
||||
gl.config.podcast_list_icon_size)]
|
||||
|
||||
|
||||
def cover_download_finished(self, channel_url, pixbuf):
|
||||
"""
|
||||
The Cover Downloader calls this when it has finished
|
||||
downloading (or registering, if already downloaded)
|
||||
a new channel cover, which is ready for displaying.
|
||||
"""
|
||||
if pixbuf is not None:
|
||||
(COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
|
||||
for row in self.treeChannels.get_model():
|
||||
if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
|
||||
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size, channel_url, self.cover_cache)
|
||||
row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
|
||||
|
||||
def save_episode_as_file( self, url, *args):
|
||||
episode = self.active_channel.find_episode( url)
|
||||
|
||||
|
@ -1028,8 +1058,10 @@ class gPodder(GladeWidget):
|
|||
selected_url = model.get_value(iter, 0)
|
||||
|
||||
rect = self.treeChannels.get_visible_rect()
|
||||
self.treeChannels.set_model(channels_to_model(self.channels))
|
||||
self.treeChannels.set_model(channels_to_model(self.channels, self.cover_cache, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size))
|
||||
util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
|
||||
for channel in self.channels:
|
||||
services.cover_downloader.request_cover(channel)
|
||||
|
||||
try:
|
||||
selected_path = (0,)
|
||||
|
@ -1060,8 +1092,24 @@ class gPodder(GladeWidget):
|
|||
self.treeAvailable.get_model().clear()
|
||||
|
||||
def drag_data_received(self, widget, context, x, y, sel, ttype, time):
|
||||
(path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
|
||||
|
||||
dnd_channel = None
|
||||
if path is not None:
|
||||
model = self.treeChannels.get_model()
|
||||
iter = model.get_iter(path)
|
||||
url = model.get_value(iter, 0)
|
||||
for channel in self.channels:
|
||||
if channel.url == url:
|
||||
dnd_channel = channel
|
||||
break
|
||||
|
||||
result = sel.data
|
||||
self.add_new_channel( result)
|
||||
rl = result.strip().lower()
|
||||
if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
|
||||
services.cover_downloader.replace_cover(dnd_channel, result)
|
||||
else:
|
||||
self.add_new_channel(result)
|
||||
|
||||
def add_new_channel(self, result=None, ask_download_new=True):
|
||||
result = util.normalize_feed_url( result)
|
||||
|
@ -2072,6 +2120,7 @@ class gPodderChannel(GladeWidget):
|
|||
|
||||
def new(self):
|
||||
global WEB_BROWSER_ICON
|
||||
self.changed = False
|
||||
self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
|
||||
self.gPodderChannel.set_title( self.channel.title)
|
||||
self.entryTitle.set_text( self.channel.title)
|
||||
|
@ -2088,8 +2137,8 @@ class gPodderChannel(GladeWidget):
|
|||
if self.channel.password:
|
||||
self.FeedPassword.set_text( self.channel.password)
|
||||
|
||||
self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
|
||||
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
|
||||
services.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
services.cover_downloader.request_cover(self.channel)
|
||||
|
||||
# Hide the website button if we don't have a valid URL
|
||||
if not self.channel.link:
|
||||
|
@ -2109,29 +2158,27 @@ class gPodderChannel(GladeWidget):
|
|||
def on_btn_website_clicked(self, widget):
|
||||
util.open_website(self.channel.link)
|
||||
|
||||
def on_btnClearCover_clicked( self, widget, delete_file = True):
|
||||
self.imgCover.clear()
|
||||
if delete_file:
|
||||
util.delete_file( self.channel.cover_file)
|
||||
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
|
||||
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
|
||||
self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
|
||||
self.labelCoverStatus.show()
|
||||
def on_btnDownloadCover_clicked(self, widget):
|
||||
if gpodder.interface == gpodder.GUI:
|
||||
dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
|
||||
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
||||
elif gpodder.interface == gpodder.MAEMO:
|
||||
dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
|
||||
|
||||
def on_btnDownloadCover_clicked( self, widget, url = None):
|
||||
if url is None:
|
||||
url = self.channel.image
|
||||
if dlg.run() == gtk.RESPONSE_OK:
|
||||
url = dlg.get_uri()
|
||||
services.cover_downloader.replace_cover(self.channel, url)
|
||||
|
||||
if url != False:
|
||||
self.btnDownloadCover.set_sensitive( False)
|
||||
dlg.destroy()
|
||||
|
||||
self.labelCoverStatus.show()
|
||||
gl.get_image_from_url(url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
|
||||
def on_btnClearCover_clicked(self, widget):
|
||||
services.cover_downloader.replace_cover(self.channel)
|
||||
|
||||
def cover_download_finished( self):
|
||||
self.labelCoverStatus.hide()
|
||||
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
|
||||
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
|
||||
def cover_download_finished(self, channel_url, pixbuf):
|
||||
if pixbuf is not None:
|
||||
self.imgCover.set_from_pixbuf(pixbuf)
|
||||
self.gPodderChannel.show()
|
||||
|
||||
def drag_data_received( self, widget, content, x, y, sel, ttype, time):
|
||||
files = sel.data.strip().split('\n')
|
||||
|
@ -2141,12 +2188,8 @@ class gPodderChannel(GladeWidget):
|
|||
|
||||
file = files[0]
|
||||
|
||||
if file.startswith( 'file://') or file.startswith( 'http://'):
|
||||
self.on_btnClearCover_clicked( self.btnClearCover)
|
||||
if file.startswith( 'file://'):
|
||||
filename = file[len('file://'):]
|
||||
shutil.copyfile( filename, self.channel.cover_file)
|
||||
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
|
||||
if file.startswith('file://') or file.startswith('http://'):
|
||||
services.cover_downloader.replace_cover(self.channel, file)
|
||||
return
|
||||
|
||||
self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
|
||||
|
@ -2171,6 +2214,7 @@ class gPodderChannel(GladeWidget):
|
|||
self.channel.password = self.FeedPassword.get_text()
|
||||
self.channel.save_settings()
|
||||
|
||||
services.cover_downloader.unregister('cover-available', self.cover_download_finished)
|
||||
self.gPodderChannel.destroy()
|
||||
|
||||
class gPodderAddPodcastDialog(GladeWidget):
|
||||
|
|
|
@ -241,60 +241,6 @@ class gPodderLib(object):
|
|||
return ( False, command_line[0] )
|
||||
return ( True, command_line[0] )
|
||||
|
||||
def image_download_thread( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
|
||||
if callback_status is not None:
|
||||
util.idle_add(callback_status, _('Downloading podcast cover...'))
|
||||
pixbuf = gtk.gdk.PixbufLoader()
|
||||
|
||||
if cover_file is None:
|
||||
log( 'Downloading %s', url)
|
||||
pixbuf.write( urllib.urlopen(url).read())
|
||||
|
||||
if cover_file is not None and not os.path.exists(cover_file):
|
||||
log( 'Downloading cover to %s', cover_file)
|
||||
cachefile = open( cover_file, "w")
|
||||
cachefile.write( urllib.urlopen(url).read())
|
||||
cachefile.close()
|
||||
|
||||
if cover_file is not None:
|
||||
log( 'Reading cover from %s', cover_file)
|
||||
try:
|
||||
pixbuf.write( open( cover_file, "r").read())
|
||||
except:
|
||||
# Probably a data error, delete temp file
|
||||
log('Data error while reading pixbuf. Deleting %s', cover_file, sender=self)
|
||||
util.delete_file(cover_file)
|
||||
|
||||
try:
|
||||
pixbuf.close()
|
||||
except:
|
||||
# data error, delete temp file
|
||||
util.delete_file( cover_file)
|
||||
|
||||
MAX_SIZE = 400
|
||||
if callback_pixbuf is not None:
|
||||
pb = pixbuf.get_pixbuf()
|
||||
if pb:
|
||||
if pb.get_width() > MAX_SIZE:
|
||||
factor = MAX_SIZE*1.0/pb.get_width()
|
||||
pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
|
||||
if pb.get_height() > MAX_SIZE:
|
||||
factor = MAX_SIZE*1.0/pb.get_height()
|
||||
pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
|
||||
util.idle_add(callback_pixbuf, pb)
|
||||
if callback_status is not None:
|
||||
util.idle_add(callback_status, '')
|
||||
if callback_finished is not None:
|
||||
util.idle_add(callback_finished)
|
||||
|
||||
def get_image_from_url( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
|
||||
if not url and not os.path.exists( cover_file):
|
||||
return
|
||||
|
||||
args = ( url, callback_pixbuf, callback_status, callback_finished, cover_file )
|
||||
thread = threading.Thread( target = self.image_download_thread, args = args)
|
||||
thread.start()
|
||||
|
||||
def invoke_torrent( self, url, torrent_filename, target_filename):
|
||||
self.history_mark_played( url)
|
||||
|
||||
|
|
|
@ -581,16 +581,6 @@ class podcastChannel(list):
|
|||
|
||||
cover_file = property(fget=get_cover_file)
|
||||
|
||||
def get_cover_pixbuf(self, size=128):
|
||||
fn = self.cover_file
|
||||
if os.path.exists(fn) and os.path.getsize(fn) > 0:
|
||||
try:
|
||||
return gtk.gdk.pixbuf_new_from_file_at_size(fn, size, size)
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def delete_episode_by_url(self, url):
|
||||
global_lock.acquire()
|
||||
downloaded_episodes = self.load_downloaded_episodes()
|
||||
|
@ -843,7 +833,7 @@ class podcastItem(object):
|
|||
|
||||
|
||||
|
||||
def channels_to_model(channels):
|
||||
def channels_to_model(channels, cover_cache=None, max_width=0, max_height=0):
|
||||
new_model = gtk.ListStore(str, str, str, gtk.gdk.Pixbuf, int, gtk.gdk.Pixbuf, str)
|
||||
|
||||
for channel in channels:
|
||||
|
@ -872,27 +862,13 @@ def channels_to_model(channels):
|
|||
else:
|
||||
new_model.set( new_iter, 4, pango.WEIGHT_NORMAL)
|
||||
|
||||
channel_cover_found = False
|
||||
if os.path.exists( channel.cover_file) and os.path.getsize(channel.cover_file) > 0:
|
||||
try:
|
||||
new_model.set( new_iter, 5, gtk.gdk.pixbuf_new_from_file_at_size( channel.cover_file, 32, 32))
|
||||
channel_cover_found = True
|
||||
except:
|
||||
exctype, value = sys.exc_info()[:2]
|
||||
log( 'Could not convert icon file "%s", error was "%s"', channel.cover_file, value )
|
||||
util.delete_file(channel.cover_file)
|
||||
|
||||
if not channel_cover_found:
|
||||
iconsize = gtk.icon_size_from_name('channel-icon')
|
||||
if not iconsize:
|
||||
iconsize = gtk.icon_size_register('channel-icon',32,32)
|
||||
icon_theme = gtk.icon_theme_get_default()
|
||||
globe_icon_name = 'applications-internet'
|
||||
try:
|
||||
new_model.set( new_iter, 5, icon_theme.load_icon(globe_icon_name, iconsize, 0))
|
||||
except:
|
||||
log( 'Cannot load "%s" icon (using an old or incomplete icon theme?)', globe_icon_name)
|
||||
new_model.set( new_iter, 5, None)
|
||||
# Load the cover if we have it, but don't download
|
||||
# it if it's not available (to avoid blocking here)
|
||||
pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True)
|
||||
new_pixbuf = None
|
||||
if pixbuf is not None:
|
||||
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, channel.url, cover_cache)
|
||||
new_model.set(new_iter, 5, new_pixbuf or pixbuf)
|
||||
|
||||
return new_model
|
||||
|
||||
|
|
|
@ -33,6 +33,9 @@ import gtk
|
|||
import gobject
|
||||
|
||||
import threading
|
||||
import urllib2
|
||||
import os
|
||||
import os.path
|
||||
|
||||
|
||||
class ObservableService(object):
|
||||
|
@ -67,6 +70,130 @@ class ObservableService(object):
|
|||
log('Signal "%s" is not available for notification.', signal_name, sender=self)
|
||||
|
||||
|
||||
class CoverDownloader(ObservableService):
|
||||
"""
|
||||
This class manages downloading cover art and notification
|
||||
of other parts of the system. Downloading cover art can
|
||||
happen either synchronously via get_cover() or in
|
||||
asynchronous mode via request_cover(). When in async mode,
|
||||
the cover downloader will send the cover via the
|
||||
'cover-available' message (via the ObservableService).
|
||||
"""
|
||||
|
||||
# Maximum width/height of the cover in pixels
|
||||
MAX_SIZE = 400
|
||||
|
||||
def __init__(self):
|
||||
signal_names = ['cover-available', 'cover-removed']
|
||||
ObservableService.__init__(self, signal_names)
|
||||
|
||||
def request_cover(self, channel, custom_url=None):
|
||||
"""
|
||||
Sends an asynchronous request to download a
|
||||
cover for the specific channel.
|
||||
|
||||
After the cover has been downloaded, the
|
||||
"cover-available" signal will be sent with
|
||||
the channel url and new cover as pixbuf.
|
||||
|
||||
If you specify a custom_url, the cover will
|
||||
be downloaded from the specified URL and not
|
||||
taken from the channel metadata.
|
||||
"""
|
||||
args = [channel, custom_url, True]
|
||||
threading.Thread(target=self.__get_cover, args=args).start()
|
||||
|
||||
def get_cover(self, channel, custom_url=None, avoid_downloading=False):
|
||||
"""
|
||||
Sends a synchronous request to download a
|
||||
cover for the specified channel.
|
||||
|
||||
The cover will be returned to the caller.
|
||||
|
||||
The custom_url has the same semantics as
|
||||
in request_cover().
|
||||
|
||||
The optional parameter "avoid_downloading",
|
||||
when true, will make sure we return only
|
||||
already-downloaded covers and return None
|
||||
when we have no cover on the local disk.
|
||||
"""
|
||||
(url, pixbuf) = self.__get_cover(channel, custom_url, False, avoid_downloading)
|
||||
return pixbuf
|
||||
|
||||
def remove_cover(self, channel):
|
||||
"""
|
||||
Removes the current cover for the channel
|
||||
so that a new one is downloaded the next
|
||||
time we request the channel cover.
|
||||
"""
|
||||
util.delete_file(channel.cover_file)
|
||||
self.notify('cover-removed', channel.url)
|
||||
|
||||
def replace_cover(self, channel, custom_url=None):
|
||||
"""
|
||||
This is a convenience function that deletes
|
||||
the current cover file and requests a new
|
||||
cover from the URL specified.
|
||||
"""
|
||||
self.remove_cover(channel)
|
||||
self.request_cover(channel, custom_url)
|
||||
|
||||
def __get_cover(self, channel, url, async=False, avoid_downloading=False):
|
||||
if not async and avoid_downloading and not os.path.exists(channel.cover_file):
|
||||
return (channel.url, None)
|
||||
|
||||
loader = gtk.gdk.PixbufLoader()
|
||||
pixbuf = None
|
||||
|
||||
if not os.path.exists(channel.cover_file):
|
||||
if url is None:
|
||||
url = channel.image
|
||||
|
||||
if url is not None:
|
||||
image_data = None
|
||||
try:
|
||||
log('Trying to download: %s', url, sender=self)
|
||||
|
||||
image_data = urllib2.urlopen(url).read()
|
||||
except:
|
||||
log('Cannot get image from %s', url, sender=self)
|
||||
|
||||
if image_data is not None:
|
||||
log('Saving image data to %s', channel.cover_file, sender=self)
|
||||
fp = open(channel.cover_file, 'wb')
|
||||
fp.write(image_data)
|
||||
fp.close()
|
||||
|
||||
if os.path.exists(channel.cover_file):
|
||||
loader.write(open(channel.cover_file, 'rb').read())
|
||||
try:
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
except:
|
||||
log('Data error while loading %s', channel.cover_file, sender=self)
|
||||
else:
|
||||
try:
|
||||
loader.close()
|
||||
except:
|
||||
log('Cannot load channel from %s', url, sender=self)
|
||||
|
||||
if pixbuf is not None:
|
||||
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, self.MAX_SIZE, self.MAX_SIZE)
|
||||
if new_pixbuf is not None:
|
||||
# Save the resized cover so we do not have to
|
||||
# resize it next time we load it
|
||||
new_pixbuf.save(channel.cover_file, 'png')
|
||||
pixbuf = new_pixbuf
|
||||
|
||||
if async:
|
||||
self.notify('cover-available', channel.url, pixbuf)
|
||||
else:
|
||||
return (channel.url, pixbuf)
|
||||
|
||||
cover_downloader = CoverDownloader()
|
||||
|
||||
|
||||
class DownloadStatusManager(ObservableService):
|
||||
COLUMN_NAMES = { 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' }
|
||||
COLUMN_TYPES = ( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING )
|
||||
|
|
|
@ -940,8 +940,8 @@ def sanitize_filename(filename, max_length=0):
|
|||
characters and encode in the native language) and
|
||||
trim filename if greater than max_length (0 = no limit).
|
||||
"""
|
||||
if not max_length and len(filename) > max_length:
|
||||
log('Limiting file/folder name "%s" to %d characters.', filename, max_length, sender=self)
|
||||
if max_length > 0 and len(filename) > max_length:
|
||||
log('Limiting file/folder name "%s" to %d characters.', filename, max_length)
|
||||
filename = filename[:max_length]
|
||||
|
||||
global encoding
|
||||
|
@ -964,3 +964,46 @@ def find_mount_point(directory):
|
|||
|
||||
return '/'
|
||||
|
||||
|
||||
def resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, key=None, cache=None):
|
||||
"""
|
||||
Resizes a GTK Pixbuf but keeps its aspect ratio.
|
||||
|
||||
Returns None if the pixbuf does not need to be
|
||||
resized or the newly resized pixbuf if it does.
|
||||
|
||||
The optional parameter "key" is used to identify
|
||||
the image in the "cache", which is a dict-object
|
||||
that holds already-resized pixbufs to access.
|
||||
"""
|
||||
changed = False
|
||||
|
||||
if cache is not None:
|
||||
if (key, max_width, max_height) in cache:
|
||||
log('Loading from cache: %s', key)
|
||||
return cache[(key, max_width, max_height)]
|
||||
|
||||
# Resize if too wide
|
||||
if pixbuf.get_width() > max_width:
|
||||
f = float(max_width)/pixbuf.get_width()
|
||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
||||
changed = True
|
||||
|
||||
# Resize if too high
|
||||
if pixbuf.get_height() > max_height:
|
||||
f = float(max_height)/pixbuf.get_height()
|
||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
result = pixbuf
|
||||
if cache is not None:
|
||||
log('Storing in cache: %s', key)
|
||||
cache[(key, max_width, max_height)] = result
|
||||
else:
|
||||
result = None
|
||||
|
||||
return result
|
||||
|
||||
|
|
Loading…
Reference in New Issue