1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2013 Thomas Perl and the gPodder Team
5 # Copyright (c) 2011 Neal H. Walfield
7 # gPodder is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # gPodder is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # util.py -- Misc utility functions
23 # Thomas Perl <thp@perli.net> 2007-08-04
26 """Miscellaneous helper functions for gPodder
28 This module provides helper and utility functions for gPodder that
29 are not tied to any specific part of gPodder.
36 logger
= logging
.getLogger(__name__
)
51 from htmlentitydefs
import entitydefs
68 import xml
.dom
.minidom
74 logger
.warn('Running on Win32 but win32api/win32file not installed.')
83 locale
.setlocale(locale
.LC_ALL
, '')
85 logger
.warn('Cannot set locale (%s)', e
, exc_info
=True)
87 # Native filesystem encoding detection
88 encoding
= sys
.getfilesystemencoding()
91 if 'LANG' in os
.environ
and '.' in os
.environ
['LANG']:
92 lang
= os
.environ
['LANG']
93 (language
, encoding
) = lang
.rsplit('.', 1)
94 logger
.info('Detected encoding: %s', encoding
)
95 elif gpodder
.ui
.harmattan
or gpodder
.ui
.sailfish
:
97 elif gpodder
.ui
.win32
:
98 # To quote http://docs.python.org/howto/unicode.html:
99 # ,,on Windows, Python uses the name "mbcs" to refer
100 # to whatever the currently configured encoding is``
103 encoding
= 'iso-8859-15'
104 logger
.info('Assuming encoding: ISO-8859-15 ($LANG not set).')
107 # Filename / folder name sanitization
108 def _sanitize_char(c
):
109 if c
in string
.whitespace
:
113 elif c
in string
.punctuation
or ord(c
) <= 31:
118 SANITIZATION_TABLE
= ''.join(map(_sanitize_char
, map(chr, range(256))))
122 ('.aac', 'audio/aac'),
123 ('.axa', 'audio/annodex'),
124 ('.flac', 'audio/flac'),
125 ('.m4b', 'audio/m4b'),
126 ('.m4a', 'audio/mp4'),
127 ('.mp3', 'audio/mpeg'),
128 ('.spx', 'audio/ogg'),
129 ('.oga', 'audio/ogg'),
130 ('.ogg', 'audio/ogg'),
131 ('.wma', 'audio/x-ms-wma'),
132 ('.3gp', 'video/3gpp'),
133 ('.axv', 'video/annodex'),
134 ('.divx', 'video/divx'),
135 ('.m4v', 'video/m4v'),
136 ('.mp4', 'video/mp4'),
137 ('.ogv', 'video/ogg'),
138 ('.mov', 'video/quicktime'),
139 ('.flv', 'video/x-flv'),
140 ('.mkv', 'video/x-matroska'),
141 ('.wmv', 'video/x-ms-wmv'),
142 ('.opus', 'audio/opus'),
145 _MIME_TYPES
= dict((k
, v
) for v
, k
in _MIME_TYPE_LIST
)
146 _MIME_TYPES_EXT
= dict(_MIME_TYPE_LIST
)
149 def make_directory( path
):
151 Tries to create a directory if it does not exist already.
152 Returns True if the directory exists after the function
153 call, False otherwise.
155 if os
.path
.isdir( path
):
161 logger
.warn('Could not create directory: %s', path
)
167 def normalize_feed_url(url
):
169 Converts any URL to http:// or ftp:// so that it can be
170 used with "wget". If the URL cannot be converted (invalid
171 or unknown scheme), "None" is returned.
173 This will also normalize feed:// and itpc:// to http://.
175 >>> normalize_feed_url('itpc://example.org/podcast.rss')
176 'http://example.org/podcast.rss'
178 If no URL scheme is defined (e.g. "curry.com"), we will
179 simply assume the user intends to add a http:// feed.
181 >>> normalize_feed_url('curry.com')
184 There are even some more shortcuts for advanced users
185 and lazy typists (see the source for details).
187 >>> normalize_feed_url('fb:43FPodcast')
188 'http://feeds.feedburner.com/43FPodcast'
190 It will also take care of converting the domain name to
191 all-lowercase (because domains are not case sensitive):
193 >>> normalize_feed_url('http://Example.COM/')
194 'http://example.com/'
196 Some other minimalistic changes are also taken care of,
197 e.g. a ? with an empty query is removed:
199 >>> normalize_feed_url('http://example.org/test?')
200 'http://example.org/test'
202 if not url
or len(url
) < 8:
205 # This is a list of prefixes that you can use to minimize the amount of
206 # keystrokes that you have to use.
207 # Feel free to suggest other useful prefixes, and I'll add them here.
209 'fb:': 'http://feeds.feedburner.com/%s',
210 'yt:': 'http://www.youtube.com/rss/user/%s/videos.rss',
211 'sc:': 'http://soundcloud.com/%s',
212 'fm4od:': 'http://onapp1.orf.at/webcam/fm4/fod/%s.xspf',
213 # YouTube playlists. To get a list of playlists per-user, use:
214 # https://gdata.youtube.com/feeds/api/users/<username>/playlists
215 'ytpl:': 'http://gdata.youtube.com/feeds/api/playlists/%s',
218 for prefix
, expansion
in PREFIXES
.iteritems():
219 if url
.startswith(prefix
):
220 url
= expansion
% (url
[len(prefix
):],)
223 # Assume HTTP for URLs without scheme
225 url
= 'http://' + url
227 scheme
, netloc
, path
, query
, fragment
= urlparse
.urlsplit(url
)
229 # Schemes and domain names are case insensitive
230 scheme
, netloc
= scheme
.lower(), netloc
.lower()
232 # Normalize empty paths to "/"
236 # feed://, itpc:// and itms:// are really http://
237 if scheme
in ('feed', 'itpc', 'itms'):
240 if scheme
not in ('http', 'https', 'ftp', 'file'):
243 # urlunsplit might return "a slighty different, but equivalent URL"
244 return urlparse
.urlunsplit((scheme
, netloc
, path
, query
, fragment
))
247 def username_password_from_url(url
):
249 Returns a tuple (username,password) containing authentication
250 data from the specified URL or (None,None) if no authentication
251 data can be found in the URL.
253 See Section 3.1 of RFC 1738 (http://www.ietf.org/rfc/rfc1738.txt)
255 >>> username_password_from_url('https://@host.com/')
257 >>> username_password_from_url('telnet://host.com/')
259 >>> username_password_from_url('ftp://foo:@host.com/')
261 >>> username_password_from_url('http://a:b@host.com/')
263 >>> username_password_from_url(1)
264 Traceback (most recent call last):
266 ValueError: URL has to be a string or unicode object.
267 >>> username_password_from_url(None)
268 Traceback (most recent call last):
270 ValueError: URL has to be a string or unicode object.
271 >>> username_password_from_url('http://a@b:c@host.com/')
273 >>> username_password_from_url('ftp://a:b:c@host.com/')
275 >>> username_password_from_url('http://i%2Fo:P%40ss%3A@host.com/')
277 >>> username_password_from_url('ftp://%C3%B6sterreich@host.com/')
278 ('\xc3\xb6sterreich', None)
279 >>> username_password_from_url('http://w%20x:y%20z@example.org/')
281 >>> username_password_from_url('http://example.com/x@y:z@test.com/')
284 if type(url
) not in (str, unicode):
285 raise ValueError('URL has to be a string or unicode object.')
287 (username
, password
) = (None, None)
289 (scheme
, netloc
, path
, params
, query
, fragment
) = urlparse
.urlparse(url
)
292 (authentication
, netloc
) = netloc
.rsplit('@', 1)
293 if ':' in authentication
:
294 (username
, password
) = authentication
.split(':', 1)
296 # RFC1738 dictates that we should not allow ['/', '@', ':']
297 # characters in the username and password field (Section 3.1):
299 # 1. The "/" can't be in there at this point because of the way
300 # urlparse (which we use above) works.
301 # 2. Due to gPodder bug 1521, we allow "@" in the username and
302 # password field. We use netloc.rsplit('@', 1), which will
303 # make sure that we split it at the last '@' in netloc.
304 # 3. The colon must be excluded (RFC2617, Section 2) in the
305 # username, but is apparently allowed in the password. This
306 # is handled by the authentication.split(':', 1) above, and
307 # will cause any extraneous ':'s to be part of the password.
309 username
= urllib
.unquote(username
)
310 password
= urllib
.unquote(password
)
312 username
= urllib
.unquote(authentication
)
314 return (username
, password
)
316 def directory_is_writable(path
):
318 Returns True if the specified directory exists and is writable
321 return os
.path
.isdir(path
) and os
.access(path
, os
.W_OK
)
324 def calculate_size( path
):
326 Tries to calculate the size of a directory, including any
327 subdirectories found. The returned value might not be
328 correct if the user doesn't have appropriate permissions
329 to list all subdirectories of the given path.
334 if os
.path
.dirname( path
) == '/':
337 if os
.path
.isfile( path
):
338 return os
.path
.getsize( path
)
340 if os
.path
.isdir( path
) and not os
.path
.islink( path
):
341 sum = os
.path
.getsize( path
)
344 for item
in os
.listdir(path
):
346 sum += calculate_size(os
.path
.join(path
, item
))
348 logger
.warn('Cannot get size for %s', path
, exc_info
=True)
350 logger
.warn('Cannot access %s', path
, exc_info
=True)
357 def file_modification_datetime(filename
):
359 Returns the modification date of the specified file
360 as a datetime.datetime object or None if the modification
361 date cannot be determined.
366 if not os
.access(filename
, os
.R_OK
):
370 s
= os
.stat(filename
)
371 timestamp
= s
[stat
.ST_MTIME
]
372 return datetime
.datetime
.fromtimestamp(timestamp
)
374 logger
.warn('Cannot get mtime for %s', filename
, exc_info
=True)
378 def file_age_in_days(filename
):
380 Returns the age of the specified filename in days or
381 zero if the modification date cannot be determined.
383 dt
= file_modification_datetime(filename
)
387 return (datetime
.datetime
.now()-dt
).days
389 def file_modification_timestamp(filename
):
391 Returns the modification date of the specified file as a number
392 or -1 if the modification date cannot be determined.
397 s
= os
.stat(filename
)
398 return s
[stat
.ST_MTIME
]
400 logger
.warn('Cannot get modification timestamp for %s', filename
)
404 def file_age_to_string(days
):
406 Converts a "number of days" value to a string that
407 can be used in the UI to display the file age.
409 >>> file_age_to_string(0)
411 >>> file_age_to_string(1)
413 >>> file_age_to_string(2)
419 return N_('%(count)d day ago', '%(count)d days ago', days
) % {'count':days
}
422 def is_system_file(filename
):
424 Checks to see if the given file is a system file.
426 if gpodder
.ui
.win32
and win32file
is not None:
427 result
= win32file
.GetFileAttributes(filename
)
428 #-1 is returned by GetFileAttributes when an error occurs
429 #0x4 is the FILE_ATTRIBUTE_SYSTEM constant
430 return result
!= -1 and result
& 0x4 != 0
435 def get_free_disk_space_win32(path
):
437 Win32-specific code to determine the free disk space remaining
438 for a given path. Uses code from:
440 http://mail.python.org/pipermail/python-list/2003-May/203223.html
442 if win32file
is None:
443 # Cannot determine free disk space
446 drive
, tail
= os
.path
.splitdrive(path
)
447 userFree
, userTotal
, freeOnDisk
= win32file
.GetDiskFreeSpaceEx(drive
)
451 def get_free_disk_space(path
):
453 Calculates the free disk space available to the current user
454 on the file system that contains the given path.
456 If the path (or its parent folder) does not yet exist, this
457 function returns zero.
460 if not os
.path
.exists(path
):
464 return get_free_disk_space_win32(path
)
468 return s
.f_bavail
* s
.f_bsize
471 def format_date(timestamp
):
473 Converts a UNIX timestamp to a date representation. This
474 function returns "Today", "Yesterday", a weekday name or
475 the date in %x format, which (according to the Python docs)
476 is the "Locale's appropriate date representation".
478 Returns None if there has been an error converting the
479 timestamp to a string representation.
481 if timestamp
is None:
484 seconds_in_a_day
= 60*60*24
486 today
= time
.localtime()[:3]
487 yesterday
= time
.localtime(time
.time() - seconds_in_a_day
)[:3]
489 timestamp_date
= time
.localtime(timestamp
)[:3]
490 except ValueError, ve
:
491 logger
.warn('Cannot convert timestamp', exc_info
=True)
494 if timestamp_date
== today
:
496 elif timestamp_date
== yesterday
:
497 return _('Yesterday')
500 diff
= int( (time
.time() - timestamp
)/seconds_in_a_day
)
502 logger
.warn('Cannot convert "%s" to date.', timestamp
, exc_info
=True)
506 timestamp
= datetime
.datetime
.fromtimestamp(timestamp
)
512 return str(timestamp
.strftime('%A').decode(encoding
))
514 # Locale's appropriate date representation
515 return str(timestamp
.strftime('%x'))
518 def format_filesize(bytesize
, use_si_units
=False, digits
=2):
520 Formats the given size in bytes to be human-readable,
522 Returns a localized "(unknown)" string when the bytesize
523 has a negative value.
538 bytesize
= float( bytesize
)
540 return _('(unknown)')
543 return _('(unknown)')
550 ( used_unit
, used_value
) = ( 'B', bytesize
)
552 for ( unit
, value
) in units
:
553 if bytesize
>= value
:
554 used_value
= bytesize
/ float(value
)
557 return ('%.'+str(digits
)+'f %s') % (used_value
, used_unit
)
560 def delete_file(filename
):
561 """Delete a file from the filesystem
563 Errors (permissions errors or file not found)
564 are silently ignored.
572 def remove_html_tags(html
):
574 Remove HTML tags from a string and replace numeric and
575 named entities with the corresponding character, so the
576 HTML text can be displayed in a simple text view.
581 # If we would want more speed, we could make these global
582 re_strip_tags
= re
.compile('<[^>]*>')
583 re_unicode_entities
= re
.compile('&#(\d{2,4});')
584 re_html_entities
= re
.compile('&(.{2,8});')
585 re_newline_tags
= re
.compile('(<br[^>]*>|<[/]?ul[^>]*>|</li>)', re
.I
)
586 re_listing_tags
= re
.compile('<li[^>]*>', re
.I
)
590 # Convert common HTML elements to their text equivalent
591 result
= re_newline_tags
.sub('\n', result
)
592 result
= re_listing_tags
.sub('\n * ', result
)
593 result
= re
.sub('<[Pp]>', '\n\n', result
)
595 # Remove all HTML/XML tags from the string
596 result
= re_strip_tags
.sub('', result
)
598 # Convert numeric XML entities to their unicode character
599 result
= re_unicode_entities
.sub(lambda x
: unichr(int(x
.group(1))), result
)
601 # Convert named HTML entities to their unicode character
602 result
= re_html_entities
.sub(lambda x
: unicode(entitydefs
.get(x
.group(1),''), 'iso-8859-1'), result
)
604 # Convert more than two newlines to two newlines
605 result
= re
.sub('([\r\n]{2})([\r\n])+', '\\1', result
)
607 return result
.strip()
610 def wrong_extension(extension
):
612 Determine if a given extension looks like it's
613 wrong (e.g. empty, extremely long or spaces)
615 Returns True if the extension most likely is a
616 wrong one and should be replaced.
618 >>> wrong_extension('.mp3')
620 >>> wrong_extension('.divx')
622 >>> wrong_extension('mp3')
624 >>> wrong_extension('')
626 >>> wrong_extension('.12 - Everybody')
628 >>> wrong_extension('.mp3 ')
630 >>> wrong_extension('.')
632 >>> wrong_extension('.42')
637 elif len(extension
) > 5:
639 elif ' ' in extension
:
641 elif extension
== '.':
643 elif not extension
.startswith('.'):
647 # ".<number>" is an invalid extension
656 def extension_from_mimetype(mimetype
):
658 Simply guesses what the file extension should be from the mimetype
660 >>> extension_from_mimetype('audio/mp4')
662 >>> extension_from_mimetype('audio/ogg')
664 >>> extension_from_mimetype('audio/mpeg')
666 >>> extension_from_mimetype('video/x-matroska')
668 >>> extension_from_mimetype('wrong-mimetype')
671 if mimetype
in _MIME_TYPES
:
672 return _MIME_TYPES
[mimetype
]
673 return mimetypes
.guess_extension(mimetype
) or ''
676 def mimetype_from_extension(extension
):
678 Simply guesses what the mimetype should be from the file extension
680 >>> mimetype_from_extension('.m4a')
682 >>> mimetype_from_extension('.ogg')
684 >>> mimetype_from_extension('.mp3')
686 >>> mimetype_from_extension('.mkv')
688 >>> mimetype_from_extension('._invalid_file_extension_')
691 if extension
in _MIME_TYPES_EXT
:
692 return _MIME_TYPES_EXT
[extension
]
694 # Need to prepend something to the extension, so guess_type works
695 type, encoding
= mimetypes
.guess_type('file'+extension
)
700 def extension_correct_for_mimetype(extension
, mimetype
):
702 Check if the given filename extension (e.g. ".ogg") is a possible
703 extension for a given mimetype (e.g. "application/ogg") and return
704 a boolean value (True if it's possible, False if not). Also do
706 >>> extension_correct_for_mimetype('.ogg', 'application/ogg')
708 >>> extension_correct_for_mimetype('.ogv', 'video/ogg')
710 >>> extension_correct_for_mimetype('.ogg', 'audio/mpeg')
712 >>> extension_correct_for_mimetype('.m4a', 'audio/mp4')
714 >>> extension_correct_for_mimetype('mp3', 'audio/mpeg')
715 Traceback (most recent call last):
717 ValueError: "mp3" is not an extension (missing .)
718 >>> extension_correct_for_mimetype('.mp3', 'audio mpeg')
719 Traceback (most recent call last):
721 ValueError: "audio mpeg" is not a mimetype (missing /)
723 if not '/' in mimetype
:
724 raise ValueError('"%s" is not a mimetype (missing /)' % mimetype
)
725 if not extension
.startswith('.'):
726 raise ValueError('"%s" is not an extension (missing .)' % extension
)
728 if (extension
, mimetype
) in _MIME_TYPE_LIST
:
731 # Create a "default" extension from the mimetype, e.g. "application/ogg"
732 # becomes ".ogg", "audio/mpeg" becomes ".mpeg", etc...
733 default
= ['.'+mimetype
.split('/')[-1]]
735 return extension
in default
+mimetypes
.guess_all_extensions(mimetype
)
738 def filename_from_url(url
):
740 Extracts the filename and (lowercase) extension (with dot)
741 from a URL, e.g. http://server.com/file.MP3?download=yes
742 will result in the string ("file", ".mp3") being returned.
744 This function will also try to best-guess the "real"
745 extension for a media file (audio, video) by
746 trying to match an extension to these types and recurse
747 into the query string to find better matches, if the
748 original extension does not resolve to a known type.
750 http://my.net/redirect.php?my.net/file.ogg => ("file", ".ogg")
751 http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
752 http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
754 (scheme
, netloc
, path
, para
, query
, fragid
) = urlparse
.urlparse(url
)
755 (filename
, extension
) = os
.path
.splitext(os
.path
.basename( urllib
.unquote(path
)))
757 if file_type_by_extension(extension
) is not None and not \
758 query
.startswith(scheme
+'://'):
759 # We have found a valid extension (audio, video)
760 # and the query string doesn't look like a URL
761 return ( filename
, extension
.lower() )
763 # If the query string looks like a possible URL, try that first
764 if len(query
.strip()) > 0 and query
.find('/') != -1:
765 query_url
= '://'.join((scheme
, urllib
.unquote(query
)))
766 (query_filename
, query_extension
) = filename_from_url(query_url
)
768 if file_type_by_extension(query_extension
) is not None:
769 return os
.path
.splitext(os
.path
.basename(query_url
))
771 # No exact match found, simply return the original filename & extension
772 return ( filename
, extension
.lower() )
775 def file_type_by_extension(extension
):
777 Tries to guess the file type by looking up the filename
778 extension from a table of known file types. Will return
779 "audio", "video" or None.
781 >>> file_type_by_extension('.aif')
783 >>> file_type_by_extension('.3GP')
785 >>> file_type_by_extension('.m4a')
787 >>> file_type_by_extension('.txt') is None
789 >>> file_type_by_extension(None) is None
791 >>> file_type_by_extension('ogg')
792 Traceback (most recent call last):
794 ValueError: Extension does not start with a dot: ogg
799 if not extension
.startswith('.'):
800 raise ValueError('Extension does not start with a dot: %s' % extension
)
802 extension
= extension
.lower()
804 if extension
in _MIME_TYPES_EXT
:
805 return _MIME_TYPES_EXT
[extension
].split('/')[0]
807 # Need to prepend something to the extension, so guess_type works
808 type, encoding
= mimetypes
.guess_type('file'+extension
)
810 if type is not None and '/' in type:
811 filetype
, rest
= type.split('/', 1)
812 if filetype
in ('audio', 'video', 'image'):
818 def get_first_line( s
):
820 Returns only the first line of a string, stripped so
821 that it doesn't have whitespace before or after.
823 return s
.strip().split('\n')[0].strip()
826 def object_string_formatter(s
, **kwargs
):
828 Makes attributes of object passed in as keyword
829 arguments available as {OBJECTNAME.ATTRNAME} in
830 the passed-in string and returns a string with
831 the above arguments replaced with the attribute
832 values of the corresponding object.
836 >>> a.title = 'Hello world'
837 >>> object_string_formatter('{episode.title}', episode=a)
842 >>> a.published = 123
843 >>> object_string_formatter('Hi {episode.published} 456', episode=a)
847 for key
, o
in kwargs
.iteritems():
848 matches
= re
.findall(r
'\{%s\.([^\}]+)\}' % key
, s
)
852 from_s
= '{%s.%s}' % (key
, attr
)
853 to_s
= str(getattr(o
, attr
))
854 result
= result
.replace(from_s
, to_s
)
856 logger
.warn('Replace of "%s" failed for "%s".', attr
, s
)
861 def format_desktop_command(command
, filenames
, start_position
=None):
863 Formats a command template from the "Exec=" line of a .desktop
864 file to a string that can be invoked in a shell.
866 Handled format strings: %U, %u, %F, %f and a fallback that
867 appends the filename as first parameter of the command.
869 Also handles non-standard %p which is replaced with the start_position
870 (probably only makes sense if starting a single file). (see bug 1140)
872 See http://standards.freedesktop.org/desktop-entry-spec/1.0/ar01s06.html
874 Returns a list of commands to execute, either one for
875 each filename if the application does not support multiple
876 file names or one for all filenames (%U, %F or unknown).
878 # Replace backslashes with slashes to fix win32 issues
879 # (even on win32, "/" works, but "\" does not)
880 command
= command
.replace('\\', '/')
882 if start_position
is not None:
883 command
= command
.replace('%p', str(start_position
))
885 command
= shlex
.split(command
)
887 command_before
= command
889 multiple_arguments
= True
890 for fieldcode
in ('%U', '%F', '%u', '%f'):
891 if fieldcode
in command
:
892 command_before
= command
[:command
.index(fieldcode
)]
893 command_after
= command
[command
.index(fieldcode
)+1:]
894 multiple_arguments
= fieldcode
in ('%U', '%F')
897 if multiple_arguments
:
898 return [command_before
+ filenames
+ command_after
]
901 for filename
in filenames
:
902 commands
.append(command_before
+[filename
]+command_after
)
906 def url_strip_authentication(url
):
908 Strips authentication data from an URL. Returns the URL with
909 the authentication data removed from it.
911 >>> url_strip_authentication('https://host.com/')
913 >>> url_strip_authentication('telnet://foo:bar@host.com/')
915 >>> url_strip_authentication('ftp://billy@example.org')
917 >>> url_strip_authentication('ftp://billy:@example.org')
919 >>> url_strip_authentication('http://aa:bc@localhost/x')
921 >>> url_strip_authentication('http://i%2Fo:P%40ss%3A@blubb.lan/u.html')
922 'http://blubb.lan/u.html'
923 >>> url_strip_authentication('http://c:d@x.org/')
925 >>> url_strip_authentication('http://P%40%3A:i%2F@cx.lan')
927 >>> url_strip_authentication('http://x@x.com:s3cret@example.com/')
928 'http://example.com/'
930 url_parts
= list(urlparse
.urlsplit(url
))
931 # url_parts[1] is the HOST part of the URL
933 # Remove existing authentication data
934 if '@' in url_parts
[1]:
935 url_parts
[1] = url_parts
[1].rsplit('@', 1)[1]
937 return urlparse
.urlunsplit(url_parts
)
940 def url_add_authentication(url
, username
, password
):
942 Adds authentication data (username, password) to a given
943 URL in order to construct an authenticated URL.
945 >>> url_add_authentication('https://host.com/', '', None)
947 >>> url_add_authentication('http://example.org/', None, None)
948 'http://example.org/'
949 >>> url_add_authentication('telnet://host.com/', 'foo', 'bar')
950 'telnet://foo:bar@host.com/'
951 >>> url_add_authentication('ftp://example.org', 'billy', None)
952 'ftp://billy@example.org'
953 >>> url_add_authentication('ftp://example.org', 'billy', '')
954 'ftp://billy:@example.org'
955 >>> url_add_authentication('http://localhost/x', 'aa', 'bc')
956 'http://aa:bc@localhost/x'
957 >>> url_add_authentication('http://blubb.lan/u.html', 'i/o', 'P@ss:')
958 'http://i%2Fo:P@ss:@blubb.lan/u.html'
959 >>> url_add_authentication('http://a:b@x.org/', 'c', 'd')
961 >>> url_add_authentication('http://i%2F:P%40%3A@cx.lan', 'P@x', 'i/')
962 'http://P@x:i%2F@cx.lan'
963 >>> url_add_authentication('http://x.org/', 'a b', 'c d')
964 'http://a%20b:c%20d@x.org/'
966 if username
is None or username
== '':
969 # Relaxations of the strict quoting rules (bug 1521):
970 # 1. Accept '@' in username and password
971 # 2. Acecpt ':' in password only
972 username
= urllib
.quote(username
, safe
='@')
974 if password
is not None:
975 password
= urllib
.quote(password
, safe
='@:')
976 auth_string
= ':'.join((username
, password
))
978 auth_string
= username
980 url
= url_strip_authentication(url
)
982 url_parts
= list(urlparse
.urlsplit(url
))
983 # url_parts[1] is the HOST part of the URL
984 url_parts
[1] = '@'.join((auth_string
, url_parts
[1]))
986 return urlparse
.urlunsplit(url_parts
)
989 def urlopen(url
, headers
=None, data
=None, timeout
=None):
991 An URL opener with the User-agent set to gPodder (with version)
993 username
, password
= username_password_from_url(url
)
994 if username
is not None or password
is not None:
995 url
= url_strip_authentication(url
)
996 password_mgr
= urllib2
.HTTPPasswordMgrWithDefaultRealm()
997 password_mgr
.add_password(None, url
, username
, password
)
998 handler
= urllib2
.HTTPBasicAuthHandler(password_mgr
)
999 opener
= urllib2
.build_opener(handler
)
1001 opener
= urllib2
.build_opener()
1006 headers
= dict(headers
)
1008 headers
.update({'User-agent': gpodder
.user_agent
})
1009 request
= urllib2
.Request(url
, data
=data
, headers
=headers
)
1011 return opener
.open(request
)
1013 return opener
.open(request
, timeout
=timeout
)
1015 def get_real_url(url
):
1017 Gets the real URL of a file and resolves all redirects.
1020 return urlopen(url
).geturl()
1022 logger
.error('Getting real url for %s', url
, exc_info
=True)
1026 def find_command(command
):
1028 Searches the system's PATH for a specific command that is
1029 executable by the user. Returns the first occurence of an
1030 executable binary in the PATH, or None if the command is
1033 On Windows, this also looks for "<command>.bat" and
1034 "<command>.exe" files if "<command>" itself doesn't exist.
1037 if 'PATH' not in os
.environ
:
1040 for path
in os
.environ
['PATH'].split(os
.pathsep
):
1041 command_file
= os
.path
.join(path
, command
)
1042 if gpodder
.ui
.win32
and not os
.path
.exists(command_file
):
1043 for extension
in ('.bat', '.exe'):
1044 cmd
= command_file
+ extension
1045 if os
.path
.isfile(cmd
):
1048 if os
.path
.isfile(command_file
) and os
.access(command_file
, os
.X_OK
):
1053 idle_add_handler
= None
1055 def idle_add(func
, *args
):
1056 """Run a function in the main GUI thread
1058 This is a wrapper function that does the Right Thing depending on if we are
1059 running on Gtk+, Qt or CLI.
1061 You should use this function if you are calling from a Python thread and
1062 modify UI data, so that you make sure that the function is called as soon
1063 as possible from the main UI thread.
1067 gobject
.idle_add(func
, *args
)
1068 elif gpodder
.ui
.qml
:
1069 from PySide
.QtCore
import Signal
, QTimer
, QThread
, Qt
, QObject
1071 class IdleAddHandler(QObject
):
1072 signal
= Signal(object)
1074 QObject
.__init
__(self
)
1076 self
.main_thread_id
= QThread
.currentThreadId()
1078 self
.signal
.connect(self
.run_func
)
1080 def run_func(self
, func
):
1081 assert QThread
.currentThreadId() == self
.main_thread_id
, \
1082 ("Running in %s, not %s"
1083 % (str(QThread
.currentThreadId()),
1084 str(self
.main_thread_id
)))
1087 def idle_add(self
, func
, *args
):
1091 except Exception, e
:
1092 logger
.exception("Running %s%s: %s",
1093 func
, str(tuple(args
)), str(e
))
1095 if QThread
.currentThreadId() == self
.main_thread_id
:
1096 # If we emit the signal in the main thread,
1097 # then the function will be run immediately.
1098 # Instead, use a single shot timer with a 0
1099 # timeout: this will run the function when the
1100 # event loop next iterates.
1101 QTimer
.singleShot(0, doit
)
1103 self
.signal
.emit(doit
)
1105 global idle_add_handler
1106 if idle_add_handler
is None:
1107 idle_add_handler
= IdleAddHandler()
1109 idle_add_handler
.idle_add(func
, *args
)
1114 def bluetooth_available():
1116 Returns True or False depending on the availability
1117 of bluetooth functionality on the system.
1119 if find_command('bluetooth-sendto') or \
1120 find_command('gnome-obex-send'):
1126 def bluetooth_send_file(filename
):
1128 Sends a file via bluetooth.
1130 This function tries to use "bluetooth-sendto", and if
1131 it is not available, it also tries "gnome-obex-send".
1135 if find_command('bluetooth-sendto'):
1136 command_line
= ['bluetooth-sendto']
1137 elif find_command('gnome-obex-send'):
1138 command_line
= ['gnome-obex-send']
1140 if command_line
is not None:
1141 command_line
.append(filename
)
1142 return (subprocess
.Popen(command_line
).wait() == 0)
1144 logger
.error('Cannot send file. Please install "bluetooth-sendto" or "gnome-obex-send".')
1148 def format_time(value
):
1149 """Format a seconds value to a string
1155 >>> format_time(3600)
1157 >>> format_time(10921)
1160 dt
= datetime
.datetime
.utcfromtimestamp(value
)
1162 return dt
.strftime('%M:%S')
1164 return dt
.strftime('%H:%M:%S')
1166 def parse_time(value
):
1167 """Parse a time string into seconds
1169 >>> parse_time('00:00')
1171 >>> parse_time('00:00:00')
1173 >>> parse_time('00:20')
1175 >>> parse_time('00:00:20')
1177 >>> parse_time('01:00:00')
1179 >>> parse_time('03:02:01')
1181 >>> parse_time('61:08')
1183 >>> parse_time('25:03:30')
1185 >>> parse_time('25:3:30')
1187 >>> parse_time('61.08')
1194 raise ValueError('Invalid value: %s' % (str(value
),))
1196 m
= re
.match(r
'(\d+)[:.](\d\d?)[:.](\d\d?)', value
)
1198 hours
, minutes
, seconds
= m
.groups()
1199 return (int(hours
) * 60 + int(minutes
)) * 60 + int(seconds
)
1201 m
= re
.match(r
'(\d+)[:.](\d\d?)', value
)
1203 minutes
, seconds
= m
.groups()
1204 return int(minutes
) * 60 + int(seconds
)
1209 def format_seconds_to_hour_min_sec(seconds
):
1211 Take the number of seconds and format it into a
1212 human-readable string (duration).
1214 >>> format_seconds_to_hour_min_sec(3834)
1215 u'1 hour, 3 minutes and 54 seconds'
1216 >>> format_seconds_to_hour_min_sec(3600)
1218 >>> format_seconds_to_hour_min_sec(62)
1219 u'1 minute and 2 seconds'
1223 return N_('%(count)d second', '%(count)d seconds', seconds
) % {'count':seconds
}
1227 seconds
= int(seconds
)
1229 hours
= seconds
/3600
1230 seconds
= seconds
%3600
1232 minutes
= seconds
/60
1233 seconds
= seconds
%60
1236 result
.append(N_('%(count)d hour', '%(count)d hours', hours
) % {'count':hours
})
1239 result
.append(N_('%(count)d minute', '%(count)d minutes', minutes
) % {'count':minutes
})
1242 result
.append(N_('%(count)d second', '%(count)d seconds', seconds
) % {'count':seconds
})
1245 return (' '+_('and')+' ').join((', '.join(result
[:-1]), result
[-1]))
1249 def http_request(url
, method
='HEAD'):
1250 (scheme
, netloc
, path
, parms
, qry
, fragid
) = urlparse
.urlparse(url
)
1251 conn
= httplib
.HTTPConnection(netloc
)
1252 start
= len(scheme
) + len('://') + len(netloc
)
1253 conn
.request(method
, url
[start
:])
1254 return conn
.getresponse()
1257 def gui_open(filename
):
1259 Open a file or folder with the default application set
1260 by the Desktop environment. This uses "xdg-open" on all
1261 systems with a few exceptions:
1263 on Win32, os.startfile() is used
1266 if gpodder
.ui
.win32
:
1267 os
.startfile(filename
)
1268 elif gpodder
.ui
.osx
:
1269 subprocess
.Popen(['open', filename
])
1271 subprocess
.Popen(['xdg-open', filename
])
1274 logger
.error('Cannot open file/folder: "%s"', filename
, exc_info
=True)
1278 def open_website(url
):
1280 Opens the specified URL using the default system web
1281 browser. This uses Python's "webbrowser" module, so
1282 make sure your system is set up correctly.
1284 run_in_background(lambda: webbrowser
.open(url
))
1286 def convert_bytes(d
):
1288 Convert byte strings to unicode strings
1290 This function will decode byte strings into unicode
1291 strings. Any other data types will be left alone.
1293 >>> convert_bytes(None)
1294 >>> convert_bytes(1)
1296 >>> convert_bytes(4711L)
1298 >>> convert_bytes(True)
1300 >>> convert_bytes(3.1415)
1302 >>> convert_bytes('Hello')
1304 >>> convert_bytes(u'Hey')
1309 if any(isinstance(d
, t
) for t
in (int, long, bool, float)):
1311 elif not isinstance(d
, unicode):
1312 return d
.decode('utf-8', 'ignore')
1315 def sanitize_encoding(filename
):
1317 Generate a sanitized version of a string (i.e.
1318 remove invalid characters and encode in the
1319 detected native language encoding).
1321 >>> sanitize_encoding('\x80')
1323 >>> sanitize_encoding(u'unicode')
1326 # The encoding problem goes away in Python 3.. hopefully!
1327 if sys
.version_info
>= (3, 0):
1331 if not isinstance(filename
, unicode):
1332 filename
= filename
.decode(encoding
, 'ignore')
1333 return filename
.encode(encoding
, 'ignore')
1336 def sanitize_filename(filename
, max_length
=0, use_ascii
=False):
1338 Generate a sanitized version of a filename that can
1339 be written on disk (i.e. remove/replace invalid
1340 characters and encode in the native language) and
1341 trim filename if greater than max_length (0 = no limit).
1343 If use_ascii is True, don't encode in the native language,
1344 but use only characters from the ASCII character set.
1346 if not isinstance(filename
, unicode):
1347 filename
= filename
.decode(encoding
, 'ignore')
1349 if max_length
> 0 and len(filename
) > max_length
:
1350 logger
.info('Limiting file/folder name "%s" to %d characters.',
1351 filename
, max_length
)
1352 filename
= filename
[:max_length
]
1354 filename
= filename
.encode('ascii' if use_ascii
else encoding
, 'ignore')
1355 filename
= filename
.translate(SANITIZATION_TABLE
)
1356 filename
= filename
.strip('.' + string
.whitespace
)
1361 def find_mount_point(directory
):
1363 Try to find the mount point for a given directory.
1364 If the directory is itself a mount point, return
1365 it. If not, remove the last part of the path and
1366 re-check if it's a mount point. If the directory
1367 resides on your root filesystem, "/" is returned.
1369 >>> find_mount_point('/')
1372 >>> find_mount_point(u'/something')
1373 Traceback (most recent call last):
1375 ValueError: Convert unicode objects to str first.
1377 >>> find_mount_point(None)
1378 Traceback (most recent call last):
1380 ValueError: Directory names should be of type str.
1382 >>> find_mount_point(42)
1383 Traceback (most recent call last):
1385 ValueError: Directory names should be of type str.
1387 >>> from minimock import mock, restore
1388 >>> mocked_mntpoints = ('/', '/home', '/media/usbdisk', '/media/cdrom')
1389 >>> mock('os.path.ismount', returns_func=lambda x: x in mocked_mntpoints)
1391 >>> # For mocking os.getcwd(), we simply use a lambda to avoid the
1392 >>> # massive output of "Called os.getcwd()" lines in this doctest
1393 >>> os.getcwd = lambda: '/home/thp'
1395 >>> find_mount_point('.')
1396 Called os.path.ismount('/home/thp')
1397 Called os.path.ismount('/home')
1399 >>> find_mount_point('relativity')
1400 Called os.path.ismount('/home/thp/relativity')
1401 Called os.path.ismount('/home/thp')
1402 Called os.path.ismount('/home')
1404 >>> find_mount_point('/media/usbdisk/')
1405 Called os.path.ismount('/media/usbdisk')
1407 >>> find_mount_point('/home/thp/Desktop')
1408 Called os.path.ismount('/home/thp/Desktop')
1409 Called os.path.ismount('/home/thp')
1410 Called os.path.ismount('/home')
1412 >>> find_mount_point('/media/usbdisk/Podcasts/With Spaces')
1413 Called os.path.ismount('/media/usbdisk/Podcasts/With Spaces')
1414 Called os.path.ismount('/media/usbdisk/Podcasts')
1415 Called os.path.ismount('/media/usbdisk')
1417 >>> find_mount_point('/home/')
1418 Called os.path.ismount('/home')
1420 >>> find_mount_point('/media/cdrom/../usbdisk/blubb//')
1421 Called os.path.ismount('/media/usbdisk/blubb')
1422 Called os.path.ismount('/media/usbdisk')
1426 if isinstance(directory
, unicode):
1427 # XXX: This is only valid for Python 2 - misleading error in Python 3?
1428 # We do not accept unicode strings, because they could fail when
1429 # trying to be converted to some native encoding, so fail loudly
1430 # and leave it up to the callee to encode into the proper encoding.
1431 raise ValueError('Convert unicode objects to str first.')
1433 if not isinstance(directory
, str):
1434 # In Python 2, we assume it's a byte str; in Python 3, we assume
1435 # that it's a unicode str. The abspath/ismount/split functions of
1436 # os.path work with unicode str in Python 3, but not in Python 2.
1437 raise ValueError('Directory names should be of type str.')
1439 directory
= os
.path
.abspath(directory
)
1441 while directory
!= '/':
1442 if os
.path
.ismount(directory
):
1445 (directory
, tail_data
) = os
.path
.split(directory
)
1450 # matches http:// and ftp:// and mailto://
1451 protocolPattern
= re
.compile(r
'^\w+://')
1455 @return true if string is an absolute path or protocoladdress
1456 for addresses beginning in http:// or ftp:// or ldap:// -
1457 they are considered "absolute" paths.
1458 Source: http://code.activestate.com/recipes/208993/
1460 if protocolPattern
.match(string
): return 1
1461 return os
.path
.isabs(string
)
1464 def commonpath(l1
, l2
, common
=[]):
1466 helper functions for relpath
1467 Source: http://code.activestate.com/recipes/208993/
1469 if len(l1
) < 1: return (common
, l1
, l2
)
1470 if len(l2
) < 1: return (common
, l1
, l2
)
1471 if l1
[0] != l2
[0]: return (common
, l1
, l2
)
1472 return commonpath(l1
[1:], l2
[1:], common
+[l1
[0]])
1474 def relpath(p1
, p2
):
1476 Finds relative path from p1 to p2
1477 Source: http://code.activestate.com/recipes/208993/
1479 pathsplit
= lambda s
: s
.split(os
.path
.sep
)
1481 (common
,l1
,l2
) = commonpath(pathsplit(p1
), pathsplit(p2
))
1484 p
= [ ('..'+os
.sep
) * len(l1
) ]
1489 return os
.path
.join(*p
)
1493 """Return the hostname of this computer
1495 This can be implemented in a different way on each
1496 platform and should yield a unique-per-user device ID.
1498 nodename
= platform
.node()
1503 # Fallback - but can this give us "localhost"?
1504 return socket
.gethostname()
1506 def detect_device_type():
1507 """Device type detection for gpodder.net
1509 This function tries to detect on which
1510 kind of device gPodder is running on.
1512 Possible return values:
1513 desktop, laptop, mobile, server, other
1515 if gpodder
.ui
.harmattan
or gpodder
.ui
.sailfish
:
1517 elif glob
.glob('/proc/acpi/battery/*'):
1518 # Linux: If we have a battery, assume Laptop
1524 def write_m3u_playlist(m3u_filename
, episodes
, extm3u
=True):
1525 """Create an M3U playlist from a episode list
1527 If the parameter "extm3u" is False, the list of
1528 episodes should be a list of filenames, and no
1529 extended information will be written into the
1530 M3U files (#EXTM3U / #EXTINF).
1532 If the parameter "extm3u" is True (default), then the
1533 list of episodes should be PodcastEpisode objects,
1534 as the extended metadata will be taken from them.
1536 f
= open(m3u_filename
, 'w')
1539 # Mandatory header for extended playlists
1540 f
.write('#EXTM3U\n')
1542 for episode
in episodes
:
1544 # Episode objects are strings that contain file names
1545 f
.write(episode
+'\n')
1548 if episode
.was_downloaded(and_exists
=True):
1549 filename
= episode
.local_filename(create
=False)
1550 assert filename
is not None
1552 if os
.path
.dirname(filename
).startswith(os
.path
.dirname(m3u_filename
)):
1553 filename
= filename
[len(os
.path
.dirname(m3u_filename
)+os
.sep
):]
1554 f
.write('#EXTINF:0,'+episode
.playlist_title()+'\n')
1555 f
.write(filename
+'\n')
1560 def generate_names(filename
):
1561 basename
, ext
= os
.path
.splitext(filename
)
1562 for i
in itertools
.count():
1564 yield '%s (%d)%s' % (basename
, i
+1, ext
)
1569 def is_known_redirecter(url
):
1570 """Check if a URL redirect is expected, and no filenames should be updated
1572 We usually honor URL redirects, and update filenames accordingly.
1573 In some cases (e.g. Soundcloud) this results in a worse filename,
1574 so we hardcode and detect these cases here to avoid renaming files
1575 for which we know that a "known good default" exists.
1577 The problem here is that by comparing the currently-assigned filename
1578 with the new filename determined by the URL, we cannot really determine
1579 which one is the "better" URL (e.g. "n5rMSpXrqmR9.128.mp3" for Soundcloud).
1582 # Soundcloud-hosted media downloads (we take the track name as filename)
1583 if url
.startswith('http://ak-media.soundcloud.com/'):
1589 def atomic_rename(old_name
, new_name
):
1590 """Atomically rename/move a (temporary) file
1592 This is usually used when updating a file safely by writing
1593 the new contents into a temporary file and then moving the
1594 temporary file over the original file to replace it.
1596 if gpodder
.ui
.win32
:
1597 # Win32 does not support atomic rename with os.rename
1598 shutil
.move(old_name
, new_name
)
1600 os
.rename(old_name
, new_name
)
1603 def check_command(self
, cmd
):
1604 """Check if a command line command/program exists"""
1605 # Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
1606 cmd
= sanitize_encoding(cmd
)
1607 program
= shlex
.split(cmd
)[0]
1608 return (find_command(program
) is not None)
1611 def rename_episode_file(episode
, filename
):
1612 """Helper method to update a PodcastEpisode object
1614 Useful after renaming/converting its download file.
1616 if not os
.path
.exists(filename
):
1617 raise ValueError('Target filename does not exist.')
1619 basename
, extension
= os
.path
.splitext(filename
)
1621 episode
.download_filename
= os
.path
.basename(filename
)
1622 episode
.file_size
= os
.path
.getsize(filename
)
1623 episode
.mime_type
= mimetype_from_extension(extension
)
1628 def get_update_info(url
='http://gpodder.org/downloads'):
1630 Get up to date release information from gpodder.org.
1632 Returns a tuple: (up_to_date, latest_version, release_date, days_since)
1634 Example result (up to date version, 20 days after release):
1635 (True, '3.0.4', '2012-01-24', 20)
1637 Example result (outdated version, 10 days after release):
1638 (False, '3.0.5', '2012-02-29', 10)
1640 data
= urlopen(url
).read()
1641 id_field_re
= re
.compile(r
'<([a-z]*)[^>]*id="([^"]*)"[^>]*>([^<]*)</\1>')
1642 info
= dict((m
.group(2), m
.group(3)) for m
in id_field_re
.finditer(data
))
1644 latest_version
= info
['latest-version']
1645 release_date
= info
['release-date']
1647 release_parsed
= datetime
.datetime
.strptime(release_date
, '%Y-%m-%d')
1648 days_since_release
= (datetime
.datetime
.today() - release_parsed
).days
1650 convert
= lambda s
: tuple(int(x
) for x
in s
.split('.'))
1651 up_to_date
= (convert(gpodder
.__version
__) >= convert(latest_version
))
1653 return up_to_date
, latest_version
, release_date
, days_since_release
1656 def run_in_background(function
, daemon
=False):
1657 logger
.debug('run_in_background: %s (%s)', function
, str(daemon
))
1658 thread
= threading
.Thread(target
=function
)
1659 thread
.setDaemon(daemon
)
1664 def linux_get_active_interfaces():
1665 """Get active network interfaces using 'ip link'
1667 Returns a list of active network interfaces or an
1668 empty list if the device is offline. The loopback
1669 interface is not included.
1671 process
= subprocess
.Popen(['ip', 'link'], stdout
=subprocess
.PIPE
)
1672 data
, _
= process
.communicate()
1673 for interface
, _
in re
.findall(r
'\d+: ([^:]+):.*state (UP|UNKNOWN)', data
):
1674 if interface
!= 'lo':
1678 def osx_get_active_interfaces():
1679 """Get active network interfaces using 'ifconfig'
1681 Returns a list of active network interfaces or an
1682 empty list if the device is offline. The loopback
1683 interface is not included.
1685 process
= subprocess
.Popen(['ifconfig'], stdout
=subprocess
.PIPE
)
1686 stdout
, _
= process
.communicate()
1687 for i
in re
.split('\n(?!\t)', stdout
, re
.MULTILINE
):
1688 b
= re
.match('(\\w+):.*status: active$', i
, re
.MULTILINE | re
.DOTALL
)
1692 def unix_get_active_interfaces():
1693 """Get active network interfaces using 'ifconfig'
1695 Returns a list of active network interfaces or an
1696 empty list if the device is offline. The loopback
1697 interface is not included.
1699 process
= subprocess
.Popen(['ifconfig'], stdout
=subprocess
.PIPE
)
1700 stdout
, _
= process
.communicate()
1701 for i
in re
.split('\n(?!\t)', stdout
, re
.MULTILINE
):
1702 b
= re
.match('(\\w+):.*status: active$', i
, re
.MULTILINE | re
.DOTALL
)
1707 def connection_available():
1708 """Check if an Internet connection is available
1710 Returns True if a connection is available (or if there
1711 is no way to determine the connection). Returns False
1712 if no network interfaces are up (i.e. no connectivity).
1715 if gpodder
.ui
.win32
:
1716 # FIXME: Implement for Windows
1718 elif gpodder
.ui
.osx
:
1719 return len(list(osx_get_active_interfaces())) > 0
1721 # By default, we assume we're not offline (bug 1730)
1724 if find_command('ifconfig') is not None:
1725 # If ifconfig is available, and it says we don't have
1726 # any active interfaces, assume we're offline
1727 if len(list(unix_get_active_interfaces())) == 0:
1730 # If we assume we're offline, try the "ip" command as fallback
1731 if offline
and find_command('ip') is not None:
1732 if len(list(linux_get_active_interfaces())) == 0:
1740 except Exception, e
:
1741 logger
.warn('Cannot get connection status: %s', e
, exc_info
=True)
1742 # When we can't determine the connection status, act as if we're online (bug 1730)
1746 def website_reachable(url
):
1748 Check if a specific website is available.
1750 if not connection_available():
1751 # No network interfaces up - assume website not reachable
1752 return (False, None)
1755 response
= urllib2
.urlopen(url
, timeout
=1)
1756 return (True, response
)
1757 except urllib2
.URLError
as err
:
1760 return (False, None)