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)
29 from gpodder
.services
import ObservableService
30 from gpodder
.liblogger
import log
32 from gpodder
import util
33 from gpodder
import youtube
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
52 available_str
= _('Available')
54 available_str
= _('Missing dependencies')
57 for module
in modules
:
58 if not module_info
[module
]:
59 missing_str
.append(_('Python module "%s" not installed') % module
)
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
):
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).
78 # Maximum width/height of the cover in pixels
82 signal_names
= ['cover-available', 'cover-removed']
83 ObservableService
.__init
__(self
, signal_names
)
85 def request_cover(self
, channel
, custom_url
=None):
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.
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
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
)
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
):
152 new_url
= youtube
.get_real_cover(channel
.url
)
153 if new_url
is not None:
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'
163 split_result
= (scheme
, netloc
, path
, query
, fragment
)
164 url
= urlparse
.urlunsplit(split_result
)
165 log('Trying favicon: %s', url
, sender
=self
)
170 log('Trying to download: %s', url
, sender
=self
)
172 image_data
= util
.urlopen(url
).read()
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
)
179 fp
= open(channel
.cover_file
, 'wb')
183 log('Cannot save image due to I/O error', sender
=self
, traceback
=True)
186 if os
.path
.exists(channel
.cover_file
):
188 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(channel
.cover_file
.decode(util
.encoding
, 'ignore'))
190 log('Data error while loading %s', channel
.cover_file
, sender
=self
)
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
)
208 self
.notify('cover-available', channel
.url
, pixbuf
)
210 return (channel
.url
, pixbuf
)