Refactor CoverDownloader to gtkui.services
This makes the gpodder.services module GTK+-free, and also removes the global state object cover_downloader.
This commit is contained in:
parent
26dda59ad9
commit
94513d4365
|
@ -28,7 +28,6 @@ import gpodder
|
|||
_ = gpodder.gettext
|
||||
|
||||
from gpodder import util
|
||||
from gpodder import services
|
||||
|
||||
from gpodder.gtkui import draw
|
||||
|
||||
|
@ -203,12 +202,13 @@ class PodcastListModel(gtk.ListStore):
|
|||
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
|
||||
C_COVER, C_ERROR, C_PILL_VISIBLE = range(8)
|
||||
|
||||
def __init__(self, max_image_side):
|
||||
def __init__(self, max_image_side, cover_downloader):
|
||||
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
|
||||
object, gtk.gdk.Pixbuf, str, bool)
|
||||
|
||||
self._cover_cache = {}
|
||||
self._max_image_side = max_image_side
|
||||
self._cover_downloader = cover_downloader
|
||||
|
||||
def _resize_pixbuf_keep_ratio(self, url, pixbuf):
|
||||
"""
|
||||
|
@ -249,7 +249,10 @@ class PodcastListModel(gtk.ListStore):
|
|||
return self._resize_pixbuf_keep_ratio(url, pixbuf) or pixbuf
|
||||
|
||||
def _get_cover_image(self, channel):
|
||||
pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True)
|
||||
if self._cover_downloader is None:
|
||||
return None
|
||||
|
||||
pixbuf = self._cover_downloader.get_cover(channel, avoid_downloading=True)
|
||||
return self._resize_pixbuf(channel.url, pixbuf)
|
||||
|
||||
def _get_pill_image(self, channel):
|
||||
|
|
|
@ -22,11 +22,20 @@
|
|||
# gpodder.gtkui.services - UI parts for the services module (2009-08-24)
|
||||
#
|
||||
|
||||
import gpodder
|
||||
|
||||
import gpodder
|
||||
_ = gpodder.gettext
|
||||
|
||||
from gpodder.services import ObservableService
|
||||
from gpodder.liblogger import log
|
||||
|
||||
from gpodder import util
|
||||
from gpodder import resolver
|
||||
|
||||
import gtk
|
||||
import os
|
||||
import threading
|
||||
import urllib2
|
||||
|
||||
class DependencyModel(gtk.ListStore):
|
||||
C_NAME, C_DESCRIPTION, C_AVAILABLE_TEXT, C_AVAILABLE, C_MISSING = range(5)
|
||||
|
@ -55,3 +64,125 @@ class DependencyModel(gtk.ListStore):
|
|||
|
||||
self.append((feature_name, description, available_str, available, missing_str))
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
log('cover download request for %s', channel.url, sender=self)
|
||||
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
|
||||
|
||||
new_url = resolver.get_real_cover(channel.url)
|
||||
if new_url is not None:
|
||||
url = new_url
|
||||
|
||||
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)
|
||||
try:
|
||||
fp = open(channel.cover_file, 'wb')
|
||||
fp.write(image_data)
|
||||
fp.close()
|
||||
except IOError, ioe:
|
||||
log('Cannot save image due to I/O error', sender=self, traceback=True)
|
||||
|
||||
if os.path.exists(channel.cover_file):
|
||||
try:
|
||||
loader.write(open(channel.cover_file, 'rb').read())
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
except:
|
||||
log('Data error while loading %s', channel.cover_file, sender=self)
|
||||
else:
|
||||
try:
|
||||
loader.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
if async:
|
||||
self.notify('cover-available', channel.url, pixbuf)
|
||||
else:
|
||||
return (channel.url, pixbuf)
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ from gpodder.gtkui.opml import OpmlListModel
|
|||
from gpodder.gtkui.config import ConfigModel
|
||||
from gpodder.gtkui.download import DownloadStatusModel
|
||||
from gpodder.gtkui.services import DependencyModel
|
||||
from gpodder.gtkui.services import CoverDownloader
|
||||
|
||||
from gpodder.libgpodder import db
|
||||
from gpodder.libgpodder import gl
|
||||
|
@ -565,8 +566,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
|
||||
self.treeChannels.append_column(namecolumn)
|
||||
|
||||
self.cover_downloader = CoverDownloader()
|
||||
|
||||
# Generate list models for podcasts and their episodes
|
||||
self.podcast_list_model = PodcastListModel(gl.config.podcast_list_icon_size)
|
||||
self.podcast_list_model = PodcastListModel(gl.config.podcast_list_icon_size, self.cover_downloader)
|
||||
self.treeChannels.set_model(self.podcast_list_model)
|
||||
|
||||
self.episode_list_model = EpisodeListModel()
|
||||
|
@ -715,8 +718,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if self.tray_icon and gl.config.minimize_to_tray:
|
||||
self.tray_icon.set_visible(False)
|
||||
|
||||
services.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
services.cover_downloader.register('cover-removed', self.cover_file_removed)
|
||||
self.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
self.cover_downloader.register('cover-removed', self.cover_file_removed)
|
||||
|
||||
self.treeDownloads.set_model(self.download_status_model)
|
||||
self.download_tasks_seen = set()
|
||||
|
@ -1864,7 +1867,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
result = sel.data
|
||||
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)
|
||||
self.cover_downloader.replace_cover(dnd_channel, result)
|
||||
else:
|
||||
self.add_new_channel(result)
|
||||
|
||||
|
@ -2076,6 +2079,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
self.pbFeedUpdate.set_text(message)
|
||||
|
||||
def _update_cover(self, channel):
|
||||
if not os.path.exists(channel.cover_file) and channel.image:
|
||||
self.cover_downloader.request_cover(channel)
|
||||
|
||||
def update_feed_cache_proc(self, channels, select_url_afterwards):
|
||||
total = len(channels)
|
||||
|
||||
|
@ -2083,6 +2090,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if not self.feed_cache_update_cancelled:
|
||||
try:
|
||||
channel.update(max_episodes=gl.config.max_episodes_per_feed)
|
||||
self._update_cover(channel)
|
||||
# except feedcore.Offline:
|
||||
# self.feed_cache_update_cancelled = True
|
||||
# if not self.minimized:
|
||||
|
@ -2796,7 +2804,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.show_message( message, title)
|
||||
return
|
||||
|
||||
gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True))
|
||||
gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), cover_downloader=self.cover_downloader)
|
||||
|
||||
def on_itemRemoveChannel_activate(self, widget, *args):
|
||||
try:
|
||||
|
@ -3301,8 +3309,8 @@ class gPodderChannel(BuilderWidget):
|
|||
if self.channel.password:
|
||||
self.FeedPassword.set_text( self.channel.password)
|
||||
|
||||
services.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
services.cover_downloader.request_cover(self.channel)
|
||||
self.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
self.cover_downloader.request_cover(self.channel)
|
||||
|
||||
# Hide the website button if we don't have a valid URL
|
||||
if not self.channel.link:
|
||||
|
@ -3332,12 +3340,12 @@ class gPodderChannel(BuilderWidget):
|
|||
|
||||
if dlg.run() == gtk.RESPONSE_OK:
|
||||
url = dlg.get_uri()
|
||||
services.cover_downloader.replace_cover(self.channel, url)
|
||||
self.cover_downloader.replace_cover(self.channel, url)
|
||||
|
||||
dlg.destroy()
|
||||
|
||||
def on_btnClearCover_clicked(self, widget):
|
||||
services.cover_downloader.replace_cover(self.channel)
|
||||
self.cover_downloader.replace_cover(self.channel)
|
||||
|
||||
def cover_download_finished(self, channel_url, pixbuf):
|
||||
if pixbuf is not None:
|
||||
|
@ -3353,13 +3361,13 @@ class gPodderChannel(BuilderWidget):
|
|||
file = files[0]
|
||||
|
||||
if file.startswith('file://') or file.startswith('http://'):
|
||||
services.cover_downloader.replace_cover(self.channel, file)
|
||||
self.cover_downloader.replace_cover(self.channel, file)
|
||||
return
|
||||
|
||||
self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
|
||||
|
||||
def on_gPodderChannel_destroy(self, widget, *args):
|
||||
services.cover_downloader.unregister('cover-available', self.cover_download_finished)
|
||||
self.cover_downloader.unregister('cover-available', self.cover_download_finished)
|
||||
|
||||
def on_btnOK_clicked(self, widget, *args):
|
||||
self.channel.sync_to_devices = not self.cbNoSync.get_active()
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
import gpodder
|
||||
from gpodder import util
|
||||
from gpodder import feedcore
|
||||
from gpodder import services
|
||||
from gpodder import resolver
|
||||
from gpodder import corestats
|
||||
|
||||
|
@ -149,9 +148,6 @@ class PodcastChannel(PodcastModelObject):
|
|||
return PodcastEpisode.create_from_dict(d, self)
|
||||
|
||||
def _consume_updated_feed(self, feed, max_episodes=0):
|
||||
# update the cover if it's not there
|
||||
self.update_cover()
|
||||
|
||||
self.parse_error = feed.get('bozo_exception', None)
|
||||
|
||||
self.title = feed.feed.get('title', self.url)
|
||||
|
@ -172,8 +168,6 @@ class PodcastChannel(PodcastModelObject):
|
|||
if hasattr(feed.feed.image, 'href') and feed.feed.image.href:
|
||||
old = self.image
|
||||
self.image = feed.feed.image.href
|
||||
if old != self.image:
|
||||
self.update_cover(force=True)
|
||||
|
||||
self.save()
|
||||
|
||||
|
@ -269,11 +263,6 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
self.db.commit()
|
||||
|
||||
def update_cover(self, force=False):
|
||||
if self.cover_file is None or not os.path.exists(self.cover_file) or force:
|
||||
if self.image is not None:
|
||||
services.cover_downloader.request_cover(self)
|
||||
|
||||
def delete(self):
|
||||
self.db.delete_channel(self)
|
||||
|
||||
|
@ -537,11 +526,9 @@ class PodcastChannel(PodcastModelObject):
|
|||
def remove_downloaded( self):
|
||||
shutil.rmtree( self.save_dir, True)
|
||||
|
||||
def get_cover_file( self):
|
||||
# gets cover filename for cover download cache
|
||||
return os.path.join( self.save_dir, 'cover')
|
||||
|
||||
cover_file = property(fget=get_cover_file)
|
||||
@property
|
||||
def cover_file(self):
|
||||
return os.path.join(self.save_dir, 'cover')
|
||||
|
||||
def delete_episode_by_url(self, url):
|
||||
episode = self.db.load_episode(url, factory=self.episode_factory)
|
||||
|
|
|
@ -24,8 +24,6 @@
|
|||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import gpodder
|
||||
from gpodder.liblogger import log
|
||||
|
||||
|
@ -33,8 +31,6 @@ from gpodder import util
|
|||
from gpodder import resolver
|
||||
from gpodder import download
|
||||
|
||||
import gtk
|
||||
|
||||
import threading
|
||||
import time
|
||||
import urllib2
|
||||
|
@ -126,136 +122,3 @@ dependency_manager = DependencyManager()
|
|||
dependency_manager.depend_on(_('Bluetooth file transfer'), _('Send podcast episodes to Bluetooth devices. Needs Python Bluez bindings.'), ['bluetooth'], ['bluetooth-sendto'])
|
||||
dependency_manager.depend_on(_('HTML episode shownotes'), _('Display episode shownotes in HTML format using GTKHTML2.'), ['gtkhtml2'], [])
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
log('cover download request for %s', channel.url, sender=self)
|
||||
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
|
||||
|
||||
new_url = resolver.get_real_cover(channel.url)
|
||||
if new_url is not None:
|
||||
url = new_url
|
||||
|
||||
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)
|
||||
try:
|
||||
fp = open(channel.cover_file, 'wb')
|
||||
fp.write(image_data)
|
||||
fp.close()
|
||||
except IOError, ioe:
|
||||
log('Cannot save image due to I/O error', sender=self, traceback=True)
|
||||
|
||||
if os.path.exists(channel.cover_file):
|
||||
try:
|
||||
loader.write(open(channel.cover_file, 'rb').read())
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
except:
|
||||
log('Data error while loading %s', channel.cover_file, sender=self)
|
||||
else:
|
||||
try:
|
||||
loader.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue