1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 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/>.
21 # util.py -- Misc utility functions
22 # Thomas Perl <thp@perli.net> 2007-08-04
25 """Miscellaneous helper functions for gPodder
27 This module provides helper and utility functions for gPodder that
28 are not tied to any specific part of gPodder.
33 from gpodder
.liblogger
import log
45 from htmlentitydefs
import entitydefs
62 import xml
.dom
.minidom
65 # Try to detect OS encoding (by Leonid Ponomarev)
66 encoding
= 'iso-8859-15'
67 if 'LANG' in os
.environ
and '.' in os
.environ
['LANG']:
68 lang
= os
.environ
['LANG']
69 (language
, encoding
) = lang
.rsplit('.', 1)
70 log('Detected encoding: %s', encoding
)
73 # Using iso-8859-15 here as (hopefully) sane default
74 # see http://en.wikipedia.org/wiki/ISO/IEC_8859-1
75 log('Using ISO-8859-15 as encoding. If this')
76 log('is incorrect, please set your $LANG variable.')
79 if gpodder
.interface
== gpodder
.GUI
:
80 ICON_UNPLAYED
= gtk
.STOCK_YES
81 ICON_LOCKED
= 'emblem-nowrite'
82 ICON_MISSING
= gtk
.STOCK_STOP
83 elif gpodder
.interface
== gpodder
.MAEMO
:
84 ICON_UNPLAYED
= 'qgn_list_gene_favor'
85 ICON_LOCKED
= 'qgn_indi_KeypadLk_lock'
86 ICON_MISSING
= gtk
.STOCK_STOP
# FIXME!
88 def make_directory( path
):
90 Tries to create a directory if it does not exist already.
91 Returns True if the directory exists after the function
92 call, False otherwise.
94 if os
.path
.isdir( path
):
100 log( 'Could not create directory: %s', path
)
106 def normalize_feed_url( url
):
108 Converts any URL to http:// or ftp:// so that it can be
109 used with "wget". If the URL cannot be converted (invalid
110 or unknown scheme), "None" is returned.
112 This will also normalize feed:// and itpc:// to http://
113 Also supported are phobos.apple.com links (iTunes podcast)
114 and itms:// links (iTunes podcast direct link).
116 If no URL scheme is defined (e.g. "curry.com"), we will
117 simply assume the user intends to add a http:// feed.
120 if not url
or len( url
) < 8:
124 url
= 'http://' + url
126 if url
.startswith('itms://'):
127 url
= parse_itunes_xml(url
)
129 # Links to "phobos.apple.com"
130 url
= itunes_discover_rss(url
)
134 if url
.startswith( 'http://') or url
.startswith( 'https://') or url
.startswith( 'ftp://'):
137 if url
.startswith('feed://') or url
.startswith('itpc://'):
138 return 'http://' + url
[7:]
143 def username_password_from_url( url
):
145 Returns a tuple (username,password) containing authentication
146 data from the specified URL or (None,None) if no authentication
147 data can be found in the URL.
149 (username
, password
) = (None, None)
151 (scheme
, netloc
, path
, params
, query
, fragment
) = urlparse
.urlparse( url
)
154 (authentication
, netloc
) = netloc
.rsplit('@', 1)
155 if ':' in authentication
:
156 (username
, password
) = authentication
.split(':', 1)
157 username
= urllib
.unquote(username
)
158 password
= urllib
.unquote(password
)
160 username
= urllib
.unquote(authentication
)
162 return (username
, password
)
165 def directory_is_writable( path
):
167 Returns True if the specified directory exists and is writable
170 return os
.path
.isdir( path
) and os
.access( path
, os
.W_OK
)
173 def calculate_size( path
):
175 Tries to calculate the size of a directory, including any
176 subdirectories found. The returned value might not be
177 correct if the user doesn't have appropriate permissions
178 to list all subdirectories of the given path.
183 if os
.path
.dirname( path
) == '/':
186 if os
.path
.isfile( path
):
187 return os
.path
.getsize( path
)
189 if os
.path
.isdir( path
) and not os
.path
.islink( path
):
190 sum = os
.path
.getsize( path
)
193 for item
in os
.listdir(path
):
195 sum += calculate_size(os
.path
.join(path
, item
))
197 log('Cannot get size for %s', path
)
199 log('Cannot access: %s', path
)
206 def file_modification_datetime(filename
):
208 Returns the modification date of the specified file
209 as a datetime.datetime object or None if the modification
210 date cannot be determined.
215 if not os
.access(filename
, os
.R_OK
):
219 s
= os
.stat(filename
)
220 timestamp
= s
[stat
.ST_MTIME
]
221 return datetime
.datetime
.fromtimestamp(timestamp
)
223 log('Cannot get modification timestamp for %s', filename
)
227 def file_age_in_days(filename
):
229 Returns the age of the specified filename in days or
230 zero if the modification date cannot be determined.
232 dt
= file_modification_datetime(filename
)
236 return (datetime
.datetime
.now()-dt
).days
239 def file_age_to_string(days
):
241 Converts a "number of days" value to a string that
242 can be used in the UI to display the file age.
244 >>> file_age_to_string(0)
246 >>> file_age_to_string(1)
248 >>> file_age_to_String(2)
252 return _('one day ago')
254 return _('%d days ago') % days
259 def get_free_disk_space(path
):
261 Calculates the free disk space available to the current user
262 on the file system that contains the given path.
264 If the path (or its parent folder) does not yet exist, this
265 function returns zero.
268 if not os
.path
.exists(path
):
273 return s
.f_bavail
* s
.f_bsize
276 def format_date(timestamp
):
278 Converts a UNIX timestamp to a date representation. This
279 function returns "Today", "Yesterday", a weekday name or
280 the date in %x format, which (according to the Python docs)
281 is the "Locale's appropriate date representation".
283 Returns None if there has been an error converting the
284 timestamp to a string representation.
286 if timestamp
is None:
289 seconds_in_a_day
= 60*60*24
291 today
= time
.localtime()[:3]
292 yesterday
= time
.localtime(time
.time() - seconds_in_a_day
)[:3]
293 timestamp_date
= time
.localtime(timestamp
)[:3]
295 if timestamp_date
== today
:
297 elif timestamp_date
== yesterday
:
298 return _('Yesterday')
301 diff
= int( (time
.time() - timestamp
)/seconds_in_a_day
)
303 log('Warning: Cannot convert "%s" to date.', timestamp
, traceback
=True)
308 return str(datetime
.datetime
.fromtimestamp(timestamp
).strftime('%A'))
310 # Locale's appropriate date representation
311 return str(datetime
.datetime
.fromtimestamp(timestamp
).strftime('%x'))
314 def format_filesize(bytesize
, use_si_units
=False, digits
=2):
316 Formats the given size in bytes to be human-readable,
318 Returns a localized "(unknown)" string when the bytesize
319 has a negative value.
334 bytesize
= float( bytesize
)
336 return _('(unknown)')
339 return _('(unknown)')
346 ( used_unit
, used_value
) = ( 'B', bytesize
)
348 for ( unit
, value
) in units
:
349 if bytesize
>= value
:
350 used_value
= bytesize
/ float(value
)
353 return ('%.'+str(digits
)+'f %s') % (used_value
, used_unit
)
356 def delete_file( path
):
358 Tries to delete the given filename and silently
359 ignores deletion errors (if the file doesn't exist).
360 Also deletes extracted cover files if they exist.
362 log( 'Trying to delete: %s', path
)
365 # Remove any extracted cover art that might exist
366 for cover_file
in glob
.glob( '%s.cover.*' % ( path
, )):
367 os
.unlink( cover_file
)
374 def remove_html_tags(html
):
376 Remove HTML tags from a string and replace numeric and
377 named entities with the corresponding character, so the
378 HTML text can be displayed in a simple text view.
380 # If we would want more speed, we could make these global
381 re_strip_tags
= re
.compile('<[^>]*>')
382 re_unicode_entities
= re
.compile('&#(\d{2,4});')
383 re_html_entities
= re
.compile('&(.{2,8});')
384 re_newline_tags
= re
.compile('(<br[^>]*>|<[/]?ul[^>]*>|</li>)', re
.I
)
385 re_listing_tags
= re
.compile('<li[^>]*>', re
.I
)
389 # Convert common HTML elements to their text equivalent
390 result
= re_newline_tags
.sub('\n', result
)
391 result
= re_listing_tags
.sub('\n * ', result
)
392 result
= re
.sub('<[Pp]>', '\n\n', result
)
394 # Remove all HTML/XML tags from the string
395 result
= re_strip_tags
.sub('', result
)
397 # Convert numeric XML entities to their unicode character
398 result
= re_unicode_entities
.sub(lambda x
: unichr(int(x
.group(1))), result
)
400 # Convert named HTML entities to their unicode character
401 result
= re_html_entities
.sub(lambda x
: unicode(entitydefs
.get(x
.group(1),''), 'iso-8859-1'), result
)
403 # Convert more than two newlines to two newlines
404 result
= re
.sub('([\r\n]{2})([\r\n])+', '\\1', result
)
406 return result
.strip()
409 def torrent_filename( filename
):
411 Checks if a file is a ".torrent" file by examining its
412 contents and searching for the file name of the file
415 Returns the name of the file the ".torrent" will download
416 or None if no filename is found (the file is no ".torrent")
418 if not os
.path
.exists( filename
):
421 header
= open( filename
).readline()
423 header
.index( '6:pieces')
424 name_length_pos
= header
.index('4:name') + 6
426 colon_pos
= header
.find( ':', name_length_pos
)
427 name_length
= int(header
[name_length_pos
:colon_pos
]) + 1
428 name
= header
[(colon_pos
+ 1):(colon_pos
+ name_length
)]
433 def extension_from_mimetype(mimetype
):
435 Simply guesses what the file extension should be from the mimetype
437 return mimetypes
.guess_extension(mimetype
) or ''
439 def filename_from_url(url
):
441 Extracts the filename and (lowercase) extension (with dot)
442 from a URL, e.g. http://server.com/file.MP3?download=yes
443 will result in the string ("file", ".mp3") being returned.
445 This function will also try to best-guess the "real"
446 extension for a media file (audio, video, torrent) by
447 trying to match an extension to these types and recurse
448 into the query string to find better matches, if the
449 original extension does not resolve to a known type.
451 http://my.net/redirect.php?my.net/file.ogg => ("file", ".ogg")
452 http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
453 http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
455 (scheme
, netloc
, path
, para
, query
, fragid
) = urlparse
.urlparse(url
)
456 (filename
, extension
) = os
.path
.splitext(os
.path
.basename( urllib
.unquote(path
)))
458 if file_type_by_extension(extension
) is not None and not \
459 query
.startswith(scheme
+'://'):
460 # We have found a valid extension (audio, video, torrent)
461 # and the query string doesn't look like a URL
462 return ( filename
, extension
.lower() )
464 # If the query string looks like a possible URL, try that first
465 if len(query
.strip()) > 0 and query
.find('/') != -1:
466 query_url
= '://'.join((scheme
, urllib
.unquote(query
)))
467 (query_filename
, query_extension
) = filename_from_url(query_url
)
469 if file_type_by_extension(query_extension
) is not None:
470 return os
.path
.splitext(os
.path
.basename(query_url
))
472 # No exact match found, simply return the original filename & extension
473 return ( filename
, extension
.lower() )
476 def file_type_by_extension( extension
):
478 Tries to guess the file type by looking up the filename
479 extension from a table of known file types. Will return
480 the type as string ("audio", "video" or "torrent") or
481 None if the file type cannot be determined.
484 'audio': [ 'mp3', 'ogg', 'wav', 'wma', 'aac', 'm4a' ],
485 'video': [ 'mp4', 'avi', 'mpg', 'mpeg', 'm4v', 'mov', 'divx', 'flv', 'wmv', '3gp' ],
486 'torrent': [ 'torrent' ],
492 if extension
[0] == '.':
493 extension
= extension
[1:]
495 extension
= extension
.lower()
498 if extension
in types
[type]:
504 def get_tree_icon(icon_name
, add_bullet
=False, add_padlock
=False, add_missing
=False, icon_cache
=None, icon_size
=32):
506 Loads an icon from the current icon theme at the specified
507 size, suitable for display in a gtk.TreeView.
509 Optionally adds a green bullet (the GTK Stock "Yes" icon)
510 to the Pixbuf returned. Also, a padlock icon can be added.
512 If an icon_cache parameter is supplied, it has to be a
513 dictionary and will be used to store generated icons.
515 On subsequent calls, icons will be loaded from cache if
516 the cache is supplied again and the icon is found in
519 global ICON_UNPLAYED
, ICON_LOCKED
, ICON_MISSING
521 if icon_cache
is not None and (icon_name
,add_bullet
,add_padlock
,icon_size
) in icon_cache
:
522 return icon_cache
[(icon_name
,add_bullet
,add_padlock
,icon_size
)]
524 icon_theme
= gtk
.icon_theme_get_default()
527 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
529 log( '(get_tree_icon) Warning: Cannot load icon with name "%s", will use default icon.', icon_name
)
530 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, icon_size
, 0)
532 if icon
and (add_bullet
or add_padlock
or add_missing
):
533 # We'll modify the icon, so use .copy()
538 emblem
= icon_theme
.load_icon(ICON_MISSING
, int(float(icon_size
)*1.2/3.0), 0)
539 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
540 xpos
= icon
.get_width() - width
541 ypos
= icon
.get_height() - height
542 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
544 log('(get_tree_icon) Error adding emblem to icon "%s".', icon_name
)
548 emblem
= icon_theme
.load_icon(ICON_UNPLAYED
, int(float(icon_size
)*1.2/3.0), 0)
549 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
550 xpos
= icon
.get_width() - width
551 ypos
= icon
.get_height() - height
552 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
554 log('(get_tree_icon) Error adding emblem to icon "%s".', icon_name
)
558 emblem
= icon_theme
.load_icon(ICON_LOCKED
, int(float(icon_size
)/2.0), 0)
559 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
560 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
562 log('(get_tree_icon) Error adding emblem to icon "%s".', icon_name
)
564 if icon_cache
is not None:
565 icon_cache
[(icon_name
,add_bullet
,add_padlock
,icon_size
)] = icon
570 def get_first_line( s
):
572 Returns only the first line of a string, stripped so
573 that it doesn't have whitespace before or after.
575 return s
.strip().split('\n')[0].strip()
578 def object_string_formatter( s
, **kwargs
):
580 Makes attributes of object passed in as keyword
581 arguments available as {OBJECTNAME.ATTRNAME} in
582 the passed-in string and returns a string with
583 the above arguments replaced with the attribute
584 values of the corresponding object.
590 s = '{episode.title} World'
592 print object_string_formatter( s, episode = e)
596 for ( key
, o
) in kwargs
.items():
597 matches
= re
.findall( r
'\{%s\.([^\}]+)\}' % key
, s
)
599 if hasattr( o
, attr
):
601 from_s
= '{%s.%s}' % ( key
, attr
)
602 to_s
= getattr( o
, attr
)
603 result
= result
.replace( from_s
, to_s
)
605 log( 'Could not replace attribute "%s" in string "%s".', attr
, s
)
610 def format_desktop_command( command
, filename
):
612 Formats a command template from the "Exec=" line of a .desktop
613 file to a string that can be invoked in a shell.
615 Handled format strings: %U, %u, %F, %f and a fallback that
616 appends the filename as first parameter of the command.
618 See http://standards.freedesktop.org/desktop-entry-spec/1.0/ar01s06.html
621 '%U': 'file://%s' % filename
,
622 '%u': 'file://%s' % filename
,
627 for key
, value
in items
.items():
628 if command
.find( key
) >= 0:
629 return command
.replace( key
, value
)
631 return '%s "%s"' % ( command
, filename
)
634 def find_command( command
):
636 Searches the system's PATH for a specific command that is
637 executable by the user. Returns the first occurence of an
638 executable binary in the PATH, or None if the command is
642 if 'PATH' not in os
.environ
:
645 for path
in os
.environ
['PATH'].split( os
.pathsep
):
646 command_file
= os
.path
.join( path
, command
)
647 if os
.path
.isfile( command_file
) and os
.access( command_file
, os
.X_OK
):
653 def parse_itunes_xml(url
):
655 Parses an XML document in the "url" parameter (this has to be
656 a itms:// or http:// URL to a XML doc) and searches all "<dict>"
657 elements for the first occurence of a "<key>feedURL</key>"
658 element and then continues the search for the string value of
661 This returns the RSS feed URL for Apple iTunes Podcast XML
662 documents that are retrieved by itunes_discover_rss().
664 url
= url
.replace('itms://', 'http://')
665 doc
= http_get_and_gunzip(url
)
667 d
= xml
.dom
.minidom
.parseString(doc
)
669 log('Error parsing document from itms:// URL: %s', e
)
672 for pairs
in d
.getElementsByTagName('dict'):
673 for node
in pairs
.childNodes
:
674 if node
.nodeType
!= node
.ELEMENT_NODE
:
677 if node
.tagName
== 'key' and node
.childNodes
.length
> 0:
678 if node
.firstChild
.nodeType
== node
.TEXT_NODE
:
679 last_key
= node
.firstChild
.data
681 if last_key
!= 'feedURL':
684 if node
.tagName
== 'string' and node
.childNodes
.length
> 0:
685 if node
.firstChild
.nodeType
== node
.TEXT_NODE
:
686 return node
.firstChild
.data
691 def http_get_and_gunzip(uri
):
693 Does a HTTP GET request and tells the server that we accept
694 gzip-encoded data. This is necessary, because the Apple iTunes
695 server will always return gzip-encoded data, regardless of what
698 Returns the uncompressed document at the given URI.
700 request
= urllib2
.Request(uri
)
701 request
.add_header("Accept-encoding", "gzip")
702 usock
= urllib2
.urlopen(request
)
704 if usock
.headers
.get('content-encoding', None) == 'gzip':
705 data
= gzip
.GzipFile(fileobj
=StringIO
.StringIO(data
)).read()
709 def itunes_discover_rss(url
):
711 Takes an iTunes-specific podcast URL and turns it
712 into a "normal" RSS feed URL. If the given URL is
713 not a phobos.apple.com URL, we will simply return
714 the URL and assume it's already an RSS feed URL.
716 Idea from Andrew Clarke's itunes-url-decoder.py
722 if not 'phobos.apple.com' in url
.lower():
723 # This doesn't look like an iTunes URL
727 data
= http_get_and_gunzip(url
)
728 (url
,) = re
.findall("itmsOpen\('([^']*)", data
)
729 return parse_itunes_xml(url
)
734 def idle_add(func
, *args
):
736 This is a wrapper function that does the Right
737 Thing depending on if we are running a GTK+ GUI or
738 not. If not, we're simply calling the function.
740 If we are a GUI app, we use gobject.idle_add() to
741 call the function later - this is needed for
742 threads to be able to modify GTK+ widget data.
744 if gpodder
.interface
in (gpodder
.GUI
, gpodder
.MAEMO
):
749 gobject
.idle_add(func
, *args
)
754 def discover_bluetooth_devices():
756 This is a generator function that returns
757 (address, name) tuples of all nearby bluetooth
760 If the user has python-bluez installed, it will
761 be used. If not, we're trying to use "hcitool".
763 If neither python-bluez or hcitool are available,
764 this function is the empty generator.
767 # If the user has python-bluez installed
769 log('Using python-bluez to find nearby bluetooth devices')
770 for name
, addr
in bluetooth
.discover_devices(lookup_names
=True):
773 if find_command('hcitool') is not None:
774 log('Using hcitool to find nearby bluetooth devices')
775 # If the user has "hcitool" installed
776 p
= subprocess
.Popen(['hcitool', 'scan'], stdout
=subprocess
.PIPE
)
777 for line
in p
.stdout
:
778 match
= re
.match('^\t([^\t]+)\t([^\t]+)\n$', line
)
779 if match
is not None:
780 (addr
, name
) = match
.groups()
783 log('Cannot find either python-bluez or hcitool - no bluetooth?')
784 return # <= empty generator
787 def bluetooth_send_file(filename
, device
=None, callback_finished
=None):
789 Sends a file via bluetooth using gnome-obex send.
790 Optional parameter device is the bluetooth address
791 of the device; optional parameter callback_finished
792 is a callback function that will be called when the
793 sending process has finished - it gets one parameter
794 that is either True (when sending succeeded) or False
795 when there was some error.
797 This function tries to use "bluetooth-sendto", and if
798 it is not available, it also tries "gnome-obex-send".
802 if find_command('bluetooth-sendto'):
803 command_line
= ['bluetooth-sendto']
804 if device
is not None:
805 command_line
.append('--device=%s' % device
)
806 elif find_command('gnome-obex-send'):
807 command_line
= ['gnome-obex-send']
808 if device
is not None:
809 command_line
+= ['--dest', device
]
811 if command_line
is not None:
812 command_line
.append(filename
)
813 result
= (subprocess
.Popen(command_line
).wait() == 0)
814 if callback_finished
is not None:
815 callback_finished(result
)
818 log('Cannot send file. Please install "bluetooth-sendto" or "gnome-obex-send".')
819 if callback_finished
is not None:
820 callback_finished(False)
824 def format_seconds_to_hour_min_sec(seconds
):
826 Take the number of seconds and format it into a
827 human-readable string (duration).
829 >>> format_seconds_to_hour_min_sec(3834)
830 '1 hour, 3 minutes and 54 seconds'
831 >>> format_seconds_to_hour_min_sec(2600)
833 >>> format_seconds_to_hour_min_sec(62)
834 '1 minute and 2 seconds'
838 return _('0 seconds')
843 seconds
= seconds
%3600
849 result
.append(_('1 hour'))
851 result
.append(_('%i hours') % hours
)
854 result
.append(_('1 minute'))
856 result
.append(_('%i minutes') % minutes
)
859 result
.append(_('1 second'))
861 result
.append(_('%i seconds') % seconds
)
864 return (' '+_('and')+' ').join((', '.join(result
[:-1]), result
[-1]))
868 def proxy_request(url
, proxy
=None):
869 if proxy
is None or proxy
.strip() == '':
870 (scheme
, netloc
, path
, parms
, qry
, fragid
) = urlparse
.urlparse(url
)
871 conn
= httplib
.HTTPConnection(netloc
)
872 start
= len(scheme
) + len('://') + len(netloc
)
873 conn
.request('HEAD', url
[start
:])
875 (scheme
, netloc
, path
, parms
, qry
, fragid
) = urlparse
.urlparse(proxy
)
876 conn
= httplib
.HTTPConnection(netloc
)
877 conn
.request('HEAD', url
)
879 return conn
.getresponse()
881 def get_episode_info_from_url(url
, proxy
=None):
883 Try to get information about a podcast episode by sending
884 a HEAD request to the HTTP server and parsing the result.
886 The return value is a dict containing all fields that
887 could be parsed from the URL. This currently contains:
889 "length": The size of the file in bytes
890 "pubdate": The unix timestamp for the pubdate
892 If the "proxy" parameter is used, it has to be the URL
893 of the HTTP proxy server to use, e.g. http://proxy:8080/
895 If there is an error, this function returns {}. This will
896 only function with http:// and https:// URLs.
898 if not (url
.startswith('http://') or url
.startswith('https://')):
901 r
= proxy_request(url
, proxy
)
904 log('Trying to get metainfo for %s', url
)
906 if 'content-length' in r
.msg
:
908 length
= int(r
.msg
['content-length'])
909 result
['length'] = length
910 except ValueError, e
:
911 log('Error converting content-length header.')
913 if 'last-modified' in r
.msg
:
915 parsed_date
= feedparser
._parse
_date
(r
.msg
['last-modified'])
916 pubdate
= time
.mktime(parsed_date
)
917 result
['pubdate'] = pubdate
919 log('Error converting last-modified header.')
924 def gui_open(filename
):
926 Open a file or folder with the default application set
927 by the Desktop environment. This uses "xdg-open".
930 subprocess
.Popen(['xdg-open', filename
])
931 # FIXME: Win32-specific "open" code needed here
932 # as fallback when xdg-open not available
934 log('Cannot open file/folder: "%s"', filename
, sender
=self
, traceback
=True)
937 def open_website(url
):
939 Opens the specified URL using the default system web
940 browser. This uses Python's "webbrowser" module, so
941 make sure your system is set up correctly.
943 threading
.Thread(target
=webbrowser
.open, args
=(url
,)).start()
945 def sanitize_encoding(filename
):
947 Generate a sanitized version of a string (i.e.
948 remove invalid characters and encode in the
949 detected native language encoding)
952 return filename
.encode(encoding
, 'ignore')
955 def sanitize_filename(filename
, max_length
=0):
957 Generate a sanitized version of a filename that can
958 be written on disk (i.e. remove/replace invalid
959 characters and encode in the native language) and
960 trim filename if greater than max_length (0 = no limit).
962 if max_length
> 0 and len(filename
) > max_length
:
963 log('Limiting file/folder name "%s" to %d characters.', filename
, max_length
)
964 filename
= filename
[:max_length
]
967 return re
.sub('[/|?*<>:+\[\]\"\\\]', '_', filename
.strip().encode(encoding
, 'ignore'))
970 def find_mount_point(directory
):
972 Try to find the mount point for a given directory.
973 If the directory is itself a mount point, return
974 it. If not, remove the last part of the path and
975 re-check if it's a mount point. If the directory
976 resides on your root filesystem, "/" is returned.
978 while os
.path
.split(directory
)[0] != '/':
979 if os
.path
.ismount(directory
):
982 (directory
, tail_data
) = os
.path
.split(directory
)
987 def resize_pixbuf_keep_ratio(pixbuf
, max_width
, max_height
, key
=None, cache
=None):
989 Resizes a GTK Pixbuf but keeps its aspect ratio.
991 Returns None if the pixbuf does not need to be
992 resized or the newly resized pixbuf if it does.
994 The optional parameter "key" is used to identify
995 the image in the "cache", which is a dict-object
996 that holds already-resized pixbufs to access.
1000 if cache
is not None:
1001 if (key
, max_width
, max_height
) in cache
:
1002 return cache
[(key
, max_width
, max_height
)]
1004 # Resize if too wide
1005 if pixbuf
.get_width() > max_width
:
1006 f
= float(max_width
)/pixbuf
.get_width()
1007 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
1008 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
1011 # Resize if too high
1012 if pixbuf
.get_height() > max_height
:
1013 f
= float(max_height
)/pixbuf
.get_height()
1014 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
1015 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
1020 if cache
is not None:
1021 cache
[(key
, max_width
, max_height
)] = result