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
32 import xml
.dom
.minidom
34 from gpodder
import util
35 from gpodder
import opml
36 from gpodder
import config
44 from liblogger
import log
48 # my gpodderlib variable
51 # some awkward kind of "singleton" ;)
54 if g_podder_lib
== None:
55 g_podder_lib
= gPodderLibClass()
58 class gPodderLibClass( object):
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.
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
)
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')
110 body
.appendChild(create_outline(doc
, url
))
111 opml
.appendChild(body
)
112 open(filename
,'w').write(doc
.toxml(encoding
='utf-8'))
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
)
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':
124 elif self
.config
.device_type
== 'filesystem':
125 return _('MP3 player')
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
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
))
149 log( 'Successfully removed %s.', ddir
)
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
)
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
)
167 log( 'Error while moving %s to %s.', self
.config
.download_dir
, new_downloaddir
)
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):
176 self
.__download
_history
.add_item( url
)
178 self
.__download
_history
.del_item( url
)
180 def history_mark_played( self
, url
, add_item
= True):
182 self
.__playback
_history
.add_item( url
)
184 self
.__playback
_history
.del_item( url
)
186 def history_mark_locked( self
, url
, add_item
= True):
188 self
.__locked
_history
.add_item( url
)
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
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
)
216 subprocess
.Popen( command_line
)
218 return ( False, command_line
[0] )
219 return ( True, command_line
[0] )
221 def open_folder( self
, folder
):
223 subprocess
.Popen( [ 'xdg-open', folder
])
224 # FIXME: Win32-specific "open" code needed here
225 # as fallback when xdg-open not available
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())
244 if cover_file
!= None:
245 log( 'Reading cover from %s', cover_file
)
246 pixbuf
.write( open( cover_file
, "r").read())
251 # data error, delete temp file
252 util
.delete_file( cover_file
)
255 if callback_pixbuf
!= None:
256 pb
= pixbuf
.get_pixbuf()
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
):
274 args
= ( url
, callback_pixbuf
, callback_status
, callback_finished
, cover_file
)
275 thread
= threading
.Thread( target
= self
.image_download_thread
, args
= args
)
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
)
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
)
291 # Simply copy the .torrent with a suitable name
293 target_filename
= os
.path
.join( self
.config
.bittorrent_dir
, os
.path
.splitext( target_filename
)[0] + '.torrent')
294 shutil
.copyfile( torrent_filename
, target_filename
)
297 log( 'Torrent copy failed: %s => %s.', torrent_filename
, target_filename
)
302 class HistoryStore( types
.ListType
):
303 def __init__( self
, filename
):
304 self
.filename
= filename
306 self
.read_from_file()
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
):
316 fp
= open( self
.filename
, 'w')
318 fp
.write( url
+ "\n")
320 log( 'Wrote %d history entries.', len( self
), sender
= self
)
322 def add_item( self
, data
, autosave
= True):
324 if data
and type( data
) is types
.ListType
:
325 # Support passing a list of urls to this function
327 affected
= affected
+ self
.add_item( url
, autosave
= False)
330 log( 'Adding: %s', data
, sender
= self
)
332 affected
= affected
+ 1
334 if affected
and autosave
:
339 def del_item( self
, data
, autosave
= True):
341 if data
and type( data
) is types
.ListType
:
342 # Support passing a list of urls to this function
344 affected
= affected
+ self
.del_item( url
, autosave
= False)
347 log( 'Removing: %s', data
, sender
= self
)
349 affected
= affected
+ 1
351 if affected
and autosave
: