Fix yet another encoding problem (bug 1124)
[gpodder.git] / src / gpodder / gtkui / services.py
blobf43076793adce4b52757002b8709f70fe64bde09
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 urlparse
38 import threading
40 class DependencyModel(gtk.ListStore):
41 C_NAME, C_DESCRIPTION, C_AVAILABLE_TEXT, C_AVAILABLE, C_MISSING = range(5)
43 def __init__(self, depman):
44 gtk.ListStore.__init__(self, str, str, str, bool, str)
46 for feature_name, description, modules, tools in depman.dependencies:
47 modules_available, module_info = depman.modules_available(modules)
48 tools_available, tool_info = depman.tools_available(tools)
50 available = modules_available and tools_available
51 if available:
52 available_str = _('Available')
53 else:
54 available_str = _('Missing dependencies')
56 missing_str = []
57 for module in modules:
58 if not module_info[module]:
59 missing_str.append(_('Python module "%s" not installed') % module)
60 for tool in tools:
61 if not tool_info[tool]:
62 missing_str.append(_('Command "%s" not installed') % tool)
63 missing_str = '\n'.join(missing_str)
65 self.append((feature_name, description, available_str, available, missing_str))
68 class CoverDownloader(ObservableService):
69 """
70 This class manages downloading cover art and notification
71 of other parts of the system. Downloading cover art can
72 happen either synchronously via get_cover() or in
73 asynchronous mode via request_cover(). When in async mode,
74 the cover downloader will send the cover via the
75 'cover-available' message (via the ObservableService).
76 """
78 # Maximum width/height of the cover in pixels
79 MAX_SIZE = 360
81 def __init__(self):
82 signal_names = ['cover-available', 'cover-removed']
83 ObservableService.__init__(self, signal_names)
85 def request_cover(self, channel, custom_url=None):
86 """
87 Sends an asynchronous request to download a
88 cover for the specific channel.
90 After the cover has been downloaded, the
91 "cover-available" signal will be sent with
92 the channel url and new cover as pixbuf.
94 If you specify a custom_url, the cover will
95 be downloaded from the specified URL and not
96 taken from the channel metadata.
97 """
98 log('cover download request for %s', channel.url, sender=self)
99 args = [channel, custom_url, True]
100 threading.Thread(target=self.__get_cover, args=args).start()
102 def get_cover(self, channel, custom_url=None, avoid_downloading=False):
104 Sends a synchronous request to download a
105 cover for the specified channel.
107 The cover will be returned to the caller.
109 The custom_url has the same semantics as
110 in request_cover().
112 The optional parameter "avoid_downloading",
113 when true, will make sure we return only
114 already-downloaded covers and return None
115 when we have no cover on the local disk.
117 (url, pixbuf) = self.__get_cover(channel, custom_url, False, avoid_downloading)
118 return pixbuf
120 def remove_cover(self, channel):
122 Removes the current cover for the channel
123 so that a new one is downloaded the next
124 time we request the channel cover.
126 util.delete_file(channel.cover_file)
127 self.notify('cover-removed', channel.url)
129 def replace_cover(self, channel, custom_url=None):
131 This is a convenience function that deletes
132 the current cover file and requests a new
133 cover from the URL specified.
135 self.remove_cover(channel)
136 self.request_cover(channel, custom_url)
138 def get_default_cover(self, channel):
139 # "randomly" choose a cover based on the podcast title
140 basename = 'podcast-%d.png' % (hash(channel.title)%5)
141 filename = os.path.join(gpodder.images_folder, basename)
142 return gtk.gdk.pixbuf_new_from_file(filename)
144 def __get_cover(self, channel, url, async=False, avoid_downloading=False):
145 if not async and avoid_downloading and not os.path.exists(channel.cover_file):
146 return (channel.url, self.get_default_cover(channel))
148 if not os.path.exists(channel.cover_file):
149 if url is None:
150 url = channel.image
152 new_url = youtube.get_real_cover(channel.url)
153 if new_url is not None:
154 url = new_url
156 if url is None and channel.link is not None and \
157 channel.link.startswith('http://'):
158 # Try to use the favicon of the linked website's host
159 split_result = urlparse.urlsplit(channel.link)
160 scheme, netloc, path, query, fragment = split_result
161 path = '/favicon.ico'
162 query = ''
163 split_result = (scheme, netloc, path, query, fragment)
164 url = urlparse.urlunsplit(split_result)
165 log('Trying favicon: %s', url, sender=self)
167 if url is not None:
168 image_data = None
169 try:
170 log('Trying to download: %s', url, sender=self)
172 image_data = util.urlopen(url).read()
173 except:
174 log('Cannot get image from %s', url, sender=self)
176 if image_data is not None:
177 log('Saving image data to %s', channel.cover_file, sender=self)
178 try:
179 fp = open(channel.cover_file, 'wb')
180 fp.write(image_data)
181 fp.close()
182 except IOError, ioe:
183 log('Cannot save image due to I/O error', sender=self, traceback=True)
185 pixbuf = None
186 if os.path.exists(channel.cover_file):
187 try:
188 pixbuf = gtk.gdk.pixbuf_new_from_file(channel.cover_file.decode(util.encoding, 'ignore'))
189 except:
190 log('Data error while loading %s', channel.cover_file, sender=self)
192 if pixbuf is None:
193 pixbuf = self.get_default_cover(channel)
195 # Resize if width is too large
196 if pixbuf.get_width() > self.MAX_SIZE:
197 f = float(self.MAX_SIZE)/pixbuf.get_width()
198 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
199 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
201 # Resize if height is too large
202 if pixbuf.get_height() > self.MAX_SIZE:
203 f = float(self.MAX_SIZE)/pixbuf.get_height()
204 (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
205 pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
207 if async:
208 self.notify('cover-available', channel.url, pixbuf)
209 else:
210 return (channel.url, pixbuf)