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
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
51 available_str
= _('Available')
53 available_str
= _('Missing dependencies')
56 for module
in modules
:
57 if not module_info
[module
]:
58 missing_str
.append(_('Python module "%s" not installed') % module
)
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
):
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).
77 # Maximum width/height of the cover in pixels
81 signal_names
= ['cover-available', 'cover-removed']
82 ObservableService
.__init
__(self
, signal_names
)
84 def request_cover(self
, channel
, custom_url
=None):
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.
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
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
)
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()
144 if not os
.path
.exists(channel
.cover_file
):
148 new_url
= youtube
.get_real_cover(channel
.url
)
149 if new_url
is not None:
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"
159 log('Trying to download: %s', url
, sender
=self
)
161 image_data
= util
.urlopen(url
).read()
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
)
168 fp
= open(channel
.cover_file
, 'wb')
172 log('Cannot save image due to I/O error', sender
=self
, traceback
=True)
174 if os
.path
.exists(channel
.cover_file
):
176 loader
.write(open(channel
.cover_file
, 'rb').read())
178 pixbuf
= loader
.get_pixbuf()
180 log('Data error while loading %s', channel
.cover_file
, sender
=self
)
188 self
.notify('cover-available', channel
.url
, pixbuf
)
190 return (channel
.url
, pixbuf
)