try downloading the favicon if no image is defined for a feed
[gpodder.git] / src / gpodder / gtkui / services.py
blob060ec1fb95993d8dbe5d377413278a86d3b03ff2
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # gpodder.gtkui.services - UI parts for the services module (2009-08-24)
26 import gpodder
27 _ = gpodder.gettext
29 from gpodder.services import ObservableService
30 from gpodder.liblogger import log
32 from gpodder import util
33 from gpodder import youtube
35 import gtk
36 import os
37 import threading
39 class DependencyModel(gtk.ListStore):
40 C_NAME, C_DESCRIPTION, C_AVAILABLE_TEXT, C_AVAILABLE, C_MISSING = range(5)
42 def __init__(self, depman):
43 gtk.ListStore.__init__(self, str, str, str, bool, str)
45 for feature_name, description, modules, tools in depman.dependencies:
46 modules_available, module_info = depman.modules_available(modules)
47 tools_available, tool_info = depman.tools_available(tools)
49 available = modules_available and tools_available
50 if available:
51 available_str = _('Available')
52 else:
53 available_str = _('Missing dependencies')
55 missing_str = []
56 for module in modules:
57 if not module_info[module]:
58 missing_str.append(_('Python module "%s" not installed') % module)
59 for tool in tools:
60 if not tool_info[tool]:
61 missing_str.append(_('Command "%s" not installed') % tool)
62 missing_str = '\n'.join(missing_str)
64 self.append((feature_name, description, available_str, available, missing_str))
67 class CoverDownloader(ObservableService):
68 """
69 This class manages downloading cover art and notification
70 of other parts of the system. Downloading cover art can
71 happen either synchronously via get_cover() or in
72 asynchronous mode via request_cover(). When in async mode,
73 the cover downloader will send the cover via the
74 'cover-available' message (via the ObservableService).
75 """
77 # Maximum width/height of the cover in pixels
78 MAX_SIZE = 400
80 def __init__(self):
81 signal_names = ['cover-available', 'cover-removed']
82 ObservableService.__init__(self, signal_names)
84 def request_cover(self, channel, custom_url=None):
85 """
86 Sends an asynchronous request to download a
87 cover for the specific channel.
89 After the cover has been downloaded, the
90 "cover-available" signal will be sent with
91 the channel url and new cover as pixbuf.
93 If you specify a custom_url, the cover will
94 be downloaded from the specified URL and not
95 taken from the channel metadata.
96 """
97 log('cover download request for %s', channel.url, sender=self)
98 args = [channel, custom_url, True]
99 threading.Thread(target=self.__get_cover, args=args).start()
101 def get_cover(self, channel, custom_url=None, avoid_downloading=False):
103 Sends a synchronous request to download a
104 cover for the specified channel.
106 The cover will be returned to the caller.
108 The custom_url has the same semantics as
109 in request_cover().
111 The optional parameter "avoid_downloading",
112 when true, will make sure we return only
113 already-downloaded covers and return None
114 when we have no cover on the local disk.
116 (url, pixbuf) = self.__get_cover(channel, custom_url, False, avoid_downloading)
117 return pixbuf
119 def remove_cover(self, channel):
121 Removes the current cover for the channel
122 so that a new one is downloaded the next
123 time we request the channel cover.
125 util.delete_file(channel.cover_file)
126 self.notify('cover-removed', channel.url)
128 def replace_cover(self, channel, custom_url=None):
130 This is a convenience function that deletes
131 the current cover file and requests a new
132 cover from the URL specified.
134 self.remove_cover(channel)
135 self.request_cover(channel, custom_url)
137 def __get_cover(self, channel, url, async=False, avoid_downloading=False):
138 if not async and avoid_downloading and not os.path.exists(channel.cover_file):
139 return (channel.url, None)
141 loader = gtk.gdk.PixbufLoader()
142 pixbuf = None
144 if not os.path.exists(channel.cover_file):
145 if url is None:
146 url = channel.image
148 new_url = youtube.get_real_cover(channel.url)
149 if new_url is not None:
150 url = new_url
152 if url is None and channel.url.startswith("http://"):
153 # try to download the favicon directly at the root
154 url = "http://" + channel.link[7:].split("/")[0] + "/favicon.ico"
156 if url is not None:
157 image_data = None
158 try:
159 log('Trying to download: %s', url, sender=self)
161 image_data = util.urlopen(url).read()
162 except:
163 log('Cannot get image from %s', url, sender=self)
165 if image_data is not None:
166 log('Saving image data to %s', channel.cover_file, sender=self)
167 try:
168 fp = open(channel.cover_file, 'wb')
169 fp.write(image_data)
170 fp.close()
171 except IOError, ioe:
172 log('Cannot save image due to I/O error', sender=self, traceback=True)
174 if os.path.exists(channel.cover_file):
175 try:
176 loader.write(open(channel.cover_file, 'rb').read())
177 loader.close()
178 pixbuf = loader.get_pixbuf()
179 except:
180 log('Data error while loading %s', channel.cover_file, sender=self)
181 else:
182 try:
183 loader.close()
184 except:
185 pass
187 if async:
188 self.notify('cover-available', channel.url, pixbuf)
189 else:
190 return (channel.url, pixbuf)