Support for different audio/video player selection
[gpodder.git] / src / gpodder / libgpodder.py
blobd594cea9bb2f082e85865e87868c4696c55e7c56
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
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/>.
21 # libgpodder.py -- gpodder configuration
22 # thomas perl <thp@perli.net> 20051030
26 import gtk
27 import gtk.gdk
28 import thread
29 import threading
30 import urllib
31 import shutil
32 import xml.dom.minidom
34 from gpodder import util
35 from gpodder import opml
36 from gpodder import config
38 import os
39 import os.path
40 import glob
41 import types
42 import subprocess
44 from liblogger import log
46 import shlex
48 # my gpodderlib variable
49 g_podder_lib = None
51 # some awkward kind of "singleton" ;)
52 def gPodderLib():
53 global g_podder_lib
54 if g_podder_lib == None:
55 g_podder_lib = gPodderLibClass()
56 return g_podder_lib
58 class gPodderLibClass( object):
59 def __init__( self):
60 gpodder_dir = os.path.expanduser( '~/.config/gpodder/')
61 util.make_directory( gpodder_dir)
63 self.feed_cache_file = os.path.join( gpodder_dir, 'feedcache.db')
64 self.channel_settings_file = os.path.join( gpodder_dir, 'channelsettings.db')
66 self.channel_opml_file = os.path.join(gpodder_dir, 'channels.opml')
67 self.channel_xml_file = os.path.join(gpodder_dir, 'channels.xml')
69 if os.path.exists(self.channel_xml_file) and not os.path.exists(self.channel_opml_file):
70 log('Trying to migrate channel list (channels.xml => channels.opml)', sender=self)
71 self.migrate_channels_xml()
73 self.config = config.Config( os.path.join( gpodder_dir, 'gpodder.conf'))
75 # We need to make a seamless upgrade, so by default the video player is not specified
76 # so the first time this application is run it will detect this and set it to the same
77 # as the audio player. This keeps gPodder functionality identical to that prior to the
78 # upgrade. The user can then set a specific video player if they so wish.
79 if self.config.videoplayer == 'unspecified':
80 self.config.videoplayer = self.config.player
82 self.__download_history = HistoryStore( os.path.join( gpodder_dir, 'download-history.txt'))
83 self.__playback_history = HistoryStore( os.path.join( gpodder_dir, 'playback-history.txt'))
84 self.__locked_history = HistoryStore( os.path.join( gpodder_dir, 'lock-history.txt'))
86 def migrate_channels_xml(self):
87 """Migrate old (gPodder < 0.9.5) channels.xml to channels.opml
89 This function does a one-time conversion of the old
90 channels.xml file format to the new (supported by
91 0.9.5, the default on 0.10.0) channels.opml format.
92 """
93 def channels_xml_iter(filename='channels.xml'):
94 for e in xml.dom.minidom.parse(filename).getElementsByTagName('url'):
95 yield ''.join(n.data for n in e.childNodes if n.nodeType==n.TEXT_NODE)
97 def create_outline(doc, url):
98 outline = doc.createElement('outline')
99 for w in (('title', ''), ('text', ''), ('xmlUrl', url), ('type', 'rss')):
100 outline.setAttribute(*w)
101 return outline
103 def export_opml(urls, filename='channels.opml'):
104 doc = xml.dom.minidom.Document()
105 opml = doc.createElement('opml')
106 opml.setAttribute('version', '1.1')
107 doc.appendChild(opml)
108 body = doc.createElement('body')
109 for url in urls:
110 body.appendChild(create_outline(doc, url))
111 opml.appendChild(body)
112 open(filename,'w').write(doc.toxml(encoding='utf-8'))
114 try:
115 export_opml(channels_xml_iter(self.channel_xml_file), self.channel_opml_file)
116 shutil.move(self.channel_xml_file, self.channel_xml_file+'.converted')
117 log('Successfully converted channels.xml to channels.opml', sender=self)
118 except:
119 log('Cannot convert old channels.xml to channels.opml', traceback=True, sender=self)
121 def get_device_name( self):
122 if self.config.device_type == 'ipod':
123 return _('iPod')
124 elif self.config.device_type == 'filesystem':
125 return _('MP3 player')
126 else:
127 log( 'Warning: Called get_device_name() when no device was selected.', sender = self)
128 return '(unknown device)'
130 def format_filesize( self, bytesize):
131 return util.format_filesize( bytesize, self.config.use_si_units)
133 def clean_up_downloads( self, delete_partial = False):
134 # Clean up temporary files left behind by old gPodder versions
135 if delete_partial:
136 temporary_files = glob.glob( '%s/*/.tmp-*' % ( self.downloaddir, ))
137 for tempfile in temporary_files:
138 util.delete_file( tempfile)
140 # Clean up empty download folders
141 download_dirs = glob.glob( '%s/*' % ( self.downloaddir, ))
142 for ddir in download_dirs:
143 if os.path.isdir( ddir):
144 globr = glob.glob( '%s/*' % ( ddir, ))
145 if not globr and ddir != self.config.bittorrent_dir:
146 log( 'Stale download directory found: %s', os.path.basename( ddir))
147 try:
148 os.rmdir( ddir)
149 log( 'Successfully removed %s.', ddir)
150 except:
151 log( 'Could not remove %s.', ddir)
153 def get_download_dir( self):
154 util.make_directory( self.config.download_dir)
155 return self.config.download_dir
157 def set_download_dir( self, new_downloaddir):
158 if self.config.download_dir != new_downloaddir:
159 log( 'Moving downloads from %s to %s', self.config.download_dir, new_downloaddir)
160 try:
161 # Fix error when moving over disk boundaries
162 if os.path.isdir( new_downloaddir) and not os.listdir( new_downloaddir):
163 os.rmdir( new_downloaddir)
165 shutil.move( self.config.download_dir, new_downloaddir)
166 except:
167 log( 'Error while moving %s to %s.', self.config.download_dir, new_downloaddir)
168 return
170 self.config.download_dir = new_downloaddir
172 downloaddir = property(fget=get_download_dir,fset=set_download_dir)
174 def history_mark_downloaded( self, url, add_item = True):
175 if add_item:
176 self.__download_history.add_item( url)
177 else:
178 self.__download_history.del_item( url)
180 def history_mark_played( self, url, add_item = True):
181 if add_item:
182 self.__playback_history.add_item( url)
183 else:
184 self.__playback_history.del_item( url)
186 def history_mark_locked( self, url, add_item = True):
187 if add_item:
188 self.__locked_history.add_item( url)
189 else:
190 self.__locked_history.del_item( url)
192 def history_is_downloaded( self, url):
193 return (url in self.__download_history)
195 def history_is_played( self, url):
196 return (url in self.__playback_history)
198 def history_is_locked( self, url):
199 return (url in self.__locked_history)
201 def playback_episode( self, channel, episode):
202 self.history_mark_played( episode.url)
203 filename = episode.local_filename()
205 # Determine the file type and set the player accordingly.
206 file_type = util.file_type_by_extension(util.file_extension_from_url(episode.url))
208 if file_type == 'video':
209 player = self.config.videoplayer
210 else:
211 player = self.config.player
213 command_line = shlex.split(util.format_desktop_command(player, filename).encode('utf-8'))
214 log( 'Command line: [ %s ]', ', '.join( [ '"%s"' % p for p in command_line ]), sender = self)
215 try:
216 subprocess.Popen( command_line)
217 except:
218 return ( False, command_line[0] )
219 return ( True, command_line[0] )
221 def open_folder( self, folder):
222 try:
223 subprocess.Popen( [ 'xdg-open', folder ])
224 # FIXME: Win32-specific "open" code needed here
225 # as fallback when xdg-open not available
226 except:
227 log( 'Cannot open folder: "%s"', folder, sender = self)
229 def image_download_thread( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
230 if callback_status != None:
231 util.idle_add(callback_status, _('Downloading channel cover...'))
232 pixbuf = gtk.gdk.PixbufLoader()
234 if cover_file == None:
235 log( 'Downloading %s', url)
236 pixbuf.write( urllib.urlopen(url).read())
238 if cover_file != None and not os.path.exists( cover_file):
239 log( 'Downloading cover to %s', cover_file)
240 cachefile = open( cover_file, "w")
241 cachefile.write( urllib.urlopen(url).read())
242 cachefile.close()
244 if cover_file != None:
245 log( 'Reading cover from %s', cover_file)
246 pixbuf.write( open( cover_file, "r").read())
248 try:
249 pixbuf.close()
250 except:
251 # data error, delete temp file
252 util.delete_file( cover_file)
254 MAX_SIZE = 400
255 if callback_pixbuf != None:
256 pb = pixbuf.get_pixbuf()
257 if pb:
258 if pb.get_width() > MAX_SIZE:
259 factor = MAX_SIZE*1.0/pb.get_width()
260 pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
261 if pb.get_height() > MAX_SIZE:
262 factor = MAX_SIZE*1.0/pb.get_height()
263 pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
264 util.idle_add(callback_pixbuf, pb)
265 if callback_status != None:
266 util.idle_add(callback_status, '')
267 if callback_finished != None:
268 util.idle_add(callback_finished)
270 def get_image_from_url( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
271 if not url and not os.path.exists( cover_file):
272 return
274 args = ( url, callback_pixbuf, callback_status, callback_finished, cover_file )
275 thread = threading.Thread( target = self.image_download_thread, args = args)
276 thread.start()
278 def invoke_torrent( self, url, torrent_filename, target_filename):
279 self.history_mark_played( url)
281 if self.config.use_gnome_bittorrent:
282 if util.find_command( 'gnome-btdownload') == None:
283 log( 'Cannot find "gnome-btdownload". Please install gnome-bittorrent.', sender = self)
284 return False
286 command = 'gnome-btdownload "%s" --saveas "%s"' % ( torrent_filename, os.path.join( self.config.bittorrent_dir, target_filename))
287 log( command, sender = self)
288 os.system( '%s &' % command)
289 return True
290 else:
291 # Simply copy the .torrent with a suitable name
292 try:
293 target_filename = os.path.join( self.config.bittorrent_dir, os.path.splitext( target_filename)[0] + '.torrent')
294 shutil.copyfile( torrent_filename, target_filename)
295 return True
296 except:
297 log( 'Torrent copy failed: %s => %s.', torrent_filename, target_filename)
299 return False
302 class HistoryStore( types.ListType):
303 def __init__( self, filename):
304 self.filename = filename
305 try:
306 self.read_from_file()
307 except:
308 log( 'Creating new history list.', sender = self)
310 def read_from_file( self):
311 for line in open( self.filename, 'r'):
312 self.append( line.strip())
314 def save_to_file( self):
315 if len( self):
316 fp = open( self.filename, 'w')
317 for url in self:
318 fp.write( url + "\n")
319 fp.close()
320 log( 'Wrote %d history entries.', len( self), sender = self)
322 def add_item( self, data, autosave = True):
323 affected = 0
324 if data and type( data) is types.ListType:
325 # Support passing a list of urls to this function
326 for url in data:
327 affected = affected + self.add_item( url, autosave = False)
328 else:
329 if data not in self:
330 log( 'Adding: %s', data, sender = self)
331 self.append( data)
332 affected = affected + 1
334 if affected and autosave:
335 self.save_to_file()
337 return affected
339 def del_item( self, data, autosave = True):
340 affected = 0
341 if data and type( data) is types.ListType:
342 # Support passing a list of urls to this function
343 for url in data:
344 affected = affected + self.del_item( url, autosave = False)
345 else:
346 if data in self:
347 log( 'Removing: %s', data, sender = self)
348 self.remove( data)
349 affected = affected + 1
351 if affected and autosave:
352 self.save_to_file()
354 return affected