def bluetooth_send_file(filename, device=None, callback_finished=None): """ Sends a file via bluetooth using gnome-obex send. Optional parameter device is the bluetooth address of the device; optional parameter callback_finished is a callback function that will be called when the sending process has finished - it gets one parameter that is either True (when sending succeeded) or False when there was some error. This function tries to use "bluetooth-sendto", and if it is not available, it also tries "gnome-obex-send". """ command_line=None if find_command('bluetooth-sendto'): command_line=['bluetooth-sendto'] if device is not None: command_line.append('--device=%s' % device) elif find_command('gnome-obex-send'): command_line=['gnome-obex-send'] if device is not None: command_line += ['--dest', device] if command_line is not None: command_line.append(filename) result=(subprocess.Popen(command_line).wait() == 0) if callback_finished is not None: callback_finished(result) return result else: log('Cannot send file. Please install "bluetooth-sendto" or "gnome-obex-send".') if callback_finished is not None: callback_finished(False) return False
def calculate_size( path): """ Tries to calculate the size of a directory, including any subdirectories found. The returned value might not be correct if the user doesn't have appropriate permissions to list all subdirectories of the given path. """ if path is None: return 0L if os.path.dirname( path) == '/': return 0L if os.path.isfile( path): return os.path.getsize( path) if os.path.isdir( path) and not os.path.islink( path): sum = os.path.getsize( path) try: for item in os.listdir(path): try: sum += calculate_size(os.path.join(path, item)) except: log('Cannot get size for %s', path) except: log('Cannot access: %s', path) return sum return 0L
def object_string_formatter( s, **kwargs): """ Makes attributes of object passed in as keyword arguments available as {OBJECTNAME.ATTRNAME} in the passed-in string and returns a string with the above arguments replaced with the attribute values of the corresponding object. Example: e = Episode() e.title = 'Hello' s = '{episode.title} World' print object_string_formatter( s, episode = e) => 'Hello World' """ result = s for ( key, o ) in kwargs.items(): matches = re.findall( r'\{%s\.([^\}]+)\}' % key, s) for attr in matches: if hasattr( o, attr): try: from_s = '{%s.%s}' % ( key, attr ) to_s = getattr( o, attr) result = result.replace( from_s, to_s) except: log( 'Could not replace attribute "%s" in string "%s".', attr, s) return result
def _save_object(self, o, table, schema): self.lock.acquire() try: cur = self.cursor() columns = [ name for name, typ, required, default in schema if name != 'id' ] values = [getattr(o, name) for name in columns] if o.id is None: qmarks = ', '.join('?' * len(columns)) sql = 'INSERT INTO %s (%s) VALUES (%s)' % ( table, ', '.join(columns), qmarks) cur.execute(sql, values) o.id = cur.lastrowid else: qmarks = ', '.join('%s = ?' % name for name in columns) values.append(o.id) sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks) cur.execute(sql, values) except Exception, e: log('Cannot save %s to %s: %s', o, table, e, sender=self, traceback=True)
def get_episode_info_from_url(url): """ Try to get information about a podcast episode by sending a HEAD request to the HTTP server and parsing the result. The return value is a dict containing all fields that could be parsed from the URL. This currently contains: "length": The size of the file in bytes "pubdate": The unix timestamp for the pubdate If there is an error, this function returns {}. This will only function with http:// and https:// URLs. """ if not (url.startswith('http://') or url.startswith('https://')): return {} r = http_request(url) result = {} log('Trying to get metainfo for %s', url) if 'content-length' in r.msg: try: length = int(r.msg['content-length']) result['length'] = length except ValueError, e: log('Error converting content-length header.')
def read_device(self): """ read all files from the device """ log('Reading files from %s', self._config.mp3_player_folder, sender=self) tracks = [] for root, dirs, files in os.walk(self._config.mp3_player_folder): for file in files: filename = os.path.join(root, file) if filename == self.playlist_file or fnmatch.fnmatch( filename, '*.dat') or fnmatch.fnmatch( filename, '*.DAT'): # We don't want to have our playlist file as # an entry in our file list, so skip it! # We also don't want to include dat files continue if self._config.mp3_player_playlist_absolute_path: filename = filename[len(self.mountpoint):] else: filename = util.relpath(os.path.dirname(self.playlist_file), os.path.dirname(filename)) + \ os.sep + os.path.basename(filename) if self._config.mp3_player_playlist_win_path: filename = filename.replace(os.sep, '\\') tracks.append(filename) return tracks
def commit(self): self.lock.acquire() try: self.log("COMMIT") self.db.commit() except Exception, e: log('Error commiting changes: %s', e, sender=self, traceback=True)
def get_free_disk_space(path): """ Calculates the free disk space available to the current user on the file system that contains the given path. If the path (or its parent folder) does not yet exist, this function returns zero. """ if not os.path.exists(path): return 0 if gpodder.win32: return get_free_disk_space_win32(path) s = os.statvfs(path) free_space = s.f_bavail * s.f_bsize if free_space == 0: # Try to fallback to using GIO to determine free space # This fixes issues with GVFS-mounted iPods (bug 1361) try: import gio file = gio.File(path) info = file.query_filesystem_info( gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) return info.get_attribute_uint64( gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) except Exception, e: log('Free space is zero. Fallback using "gio" failed.', traceback=True) return free_space
def write_m3u(self): """ write the list into the playlist on the device """ log('Writing playlist file: %s', self.playlist_file, sender=self) playlist_folder = os.path.split(self.playlist_file)[0] if not util.make_directory(playlist_folder): self.show_message( _('Folder %s could not be created.') % playlist_folder, _('Error writing playlist')) else: try: fp = open(self.playlist_file, 'w') fp.write('#EXTM3U%s' % self.linebreak) for icon, checked, filename in self.playlist: fp.write(self.build_extinf(filename)) if not checked: fp.write('#') fp.write(filename) fp.write(self.linebreak) fp.close() self.show_message( _('The playlist on your MP3 player has been updated.'), _('Update successful')) except IOError, ioe: self.show_message(str(ioe), _('Error writing playlist file'))
def sanitize_filename(filename, max_length=0, use_ascii=False): """ Generate a sanitized version of a filename that can be written on disk (i.e. remove/replace invalid characters and encode in the native language) and trim filename if greater than max_length (0 = no limit). If use_ascii is True, don't encode in the native language, but use only characters from the ASCII character set. """ global encoding if use_ascii: e = 'ascii' else: e = encoding if not isinstance(filename, unicode): filename = filename.decode(encoding, 'ignore') if max_length > 0 and len(filename) > max_length: log('Limiting file/folder name "%s" to %d characters.', filename, max_length) filename = filename[:max_length] return re.sub('[/|?*<>:+\[\]\"\\\]', '_', filename.strip().encode(e, 'ignore'))
def calculate_total_size(self): if self.size_attribute is not None: (total_size, count) = (0, 0) for episode in self.get_selected_episodes(): try: total_size += int(getattr(episode, self.size_attribute)) count += 1 except: log('Cannot get size for %s', episode.title, sender=self) text = [] if count == 0: text.append(_('Nothing selected')) text.append( N_('%(count)d episode', '%(count)d episodes', count) % {'count': count}) if total_size > 0: text.append(_('size: %s') % util.format_filesize(total_size)) self.labelTotalSize.set_text(', '.join(text)) self.btnOK.set_sensitive(count > 0) self.btnRemoveAction.set_sensitive(count > 0) if count > 0: self.btnCancel.set_label(gtk.STOCK_CANCEL) else: self.btnCancel.set_label(gtk.STOCK_CLOSE) else: self.btnOK.set_sensitive(False) self.btnRemoveAction.set_sensitive(False) for index, row in enumerate(self.model): if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True: self.btnOK.set_sensitive(True) self.btnRemoveAction.set_sensitive(True) break self.labelTotalSize.set_text('')
def __setattr__(self, name, value): if name in self.Settings: fieldtype, default = self.Settings[name][:2] try: if self[name] != fieldtype(value): old_value = self[name] log('Update %s: %s => %s', name, old_value, value, sender=self) self[name] = fieldtype(value) for observer in self.__observers: try: # Notify observer about config change observer(name, old_value, self[name]) except: log('Error while calling observer: %s', repr(observer), sender=self, traceback=True) self.schedule_save() except: raise ValueError('%s has to be of type %s' % (name, fieldtype.__name__)) else: object.__setattr__(self, name, value)
def create_cmml(html, ogg_file): soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES) startzeit = soup.findAll(text='Startzeit') if len(startzeit) == 1: m = re.match('(.*)\\.[^\\.]+$', ogg_file) if m is not None: to_file = m.group(1) + ".cmml" cmml = ET.Element('cmml', attrib={'lang': 'en'}) remove_ws = re.compile('\s+') for s in startzeit: tr = s.parent.parent.parent for row in tr.findNextSiblings(name='tr'): txt = '' tds = row.findAll(name='td') t = remove_ws.sub('', tds[1].string) for c in tds[0].findAll(text=True): txt += c txt = remove_ws.sub(' ', txt) txt = txt.strip() log("found chapter %s at %s" % (txt, t)) # totem want's escaped html in the title attribute (not & but &amp;) txt = txt.replace('&', '&') clip = ET.Element('clip') clip.set('id', t) clip.set('start', ('npt:' + t)) clip.set('title', txt) cmml.append(clip) ET.ElementTree(cmml).write(to_file, encoding='utf-8')
def _get_icon_from_image(self,image_path, icon_size): """ Load an local image file and transform it into an icon. Return a pixbuf scaled to the desired size and may return None if the icon creation is impossible (file not found etc). """ if not os.path.exists(image_path): return None # load image from disc (code adapted from CoverDownloader # except that no download is needed here) loader = gtk.gdk.PixbufLoader() pixbuf = None try: loader.write(open(image_path, 'rb').read()) loader.close() pixbuf = loader.get_pixbuf() except: log('Data error while loading image %s', image_path, sender=self) return None # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio) # Resize if too wide if pixbuf.get_width() > icon_size: f = float(icon_size)/pixbuf.get_width() (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) # Resize if too high if pixbuf.get_height() > icon_size: f = float(icon_size)/pixbuf.get_height() (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) return pixbuf
def get_all_tracks(self): tracks = [] for track in gpod.sw_get_playlist_tracks(self.podcasts_playlist): filename = gpod.itdb_filename_on_ipod(track) if filename is None: # This can happen if the episode is deleted on the device log('Episode has no file: %s', track.title, sender=self) self.remove_track_gpod(track) continue length = util.calculate_size(filename) timestamp = util.file_modification_timestamp(filename) modified = util.format_date(timestamp) try: released = gpod.itdb_time_mac_to_host(track.time_released) released = util.format_date(released) except ValueError, ve: # timestamp out of range for platform time_t (bug 418) log('Cannot convert track time: %s', ve, sender=self) released = 0 t = SyncTrack(track.title, length, modified, modified_sort=timestamp, libgpodtrack=track, playcount=track.playcount, released=released, podcast=track.artist) tracks.append(t)
def addDownloadedItem( self, item): # no multithreaded access global_lock.acquire() downloaded_episodes=self.load_downloaded_episodes() already_in_list=item.url in [ episode.url for episode in downloaded_episodes ] # only append if not already in list if not already_in_list: downloaded_episodes.append( item) self.save_downloaded_episodes( downloaded_episodes) # Update metadata on file (if possible and wanted) if gl.config.update_tags and libtagupdate.tagging_supported(): filename=item.local_filename() try: libtagupdate.update_metadata_on_file(filename, title=item.title, artist=self.title) except: log('Error while calling update_metadata_on_file() :(') gl.history_mark_downloaded(item.url) self.update_m3u_playlist(downloaded_episodes) if item.file_type() == 'torrent': torrent_filename=item.local_filename() destination_filename=util.torrent_filename( torrent_filename) gl.invoke_torrent(item.url, torrent_filename, destination_filename) global_lock.release() return not already_in_list
def write( self, channel): doc=xml.dom.minidom.Document() rss=doc.createElement( 'rss') rss.setAttribute( 'version', '1.0') doc.appendChild( rss) channele=doc.createElement( 'channel') channele.appendChild( self.create_node( doc, 'title', channel.title)) channele.appendChild( self.create_node( doc, 'description', channel.description)) channele.appendChild( self.create_node( doc, 'link', channel.link)) rss.appendChild( channele) for episode in channel: if episode.is_downloaded(): rss.appendChild( self.create_item( doc, episode)) try: fp=open( self.filename, 'w') fp.write( doc.toxml( encoding='utf-8')) fp.close() except: log( 'Could not open file for writing: %s', self.filename, sender=self) return False return True
def read_device(self): """ read all files from the device """ log('Reading files from %s', self._config.mp3_player_folder, sender=self) tracks = [] for root, dirs, files in os.walk(self._config.mp3_player_folder): for file in files: filename = os.path.join(root, file) if filename == self.playlist_file or fnmatch.fnmatch(filename, '*.dat') or fnmatch.fnmatch(filename, '*.DAT'): # We don't want to have our playlist file as # an entry in our file list, so skip it! # We also don't want to include dat files continue if self._config.mp3_player_playlist_absolute_path: filename = filename[len(self.mountpoint):] else: filename = util.relpath(os.path.dirname(self.playlist_file), os.path.dirname(filename)) + \ os.sep + os.path.basename(filename) if self._config.mp3_player_playlist_win_path: filename = filename.replace(os.sep, '\\') tracks.append(filename) return tracks
def calculate_total_size( self): if self.size_attribute is not None: (total_size, count) = (0, 0) for episode in self.get_selected_episodes(): try: total_size += int(getattr( episode, self.size_attribute)) count += 1 except: log( 'Cannot get size for %s', episode.title, sender = self) text = [] if count == 0: text.append(_('Nothing selected')) else: text.append(N_('%(count)d episode', '%(count)d episodes', count) % {'count':count}) if total_size > 0: text.append(_('size: %s') % util.format_filesize(total_size)) self.labelTotalSize.set_text(', '.join(text)) self.btnOK.set_sensitive(count>0) self.btnRemoveAction.set_sensitive(count>0) if count > 0: self.btnCancel.set_label(gtk.STOCK_CANCEL) else: self.btnCancel.set_label(gtk.STOCK_CLOSE) else: self.btnOK.set_sensitive(False) self.btnRemoveAction.set_sensitive(False) for index, row in enumerate(self.model): if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True: self.btnOK.set_sensitive(True) self.btnRemoveAction.set_sensitive(True) break self.labelTotalSize.set_text('')
def get_header_param(headers, param, header_name): """Extract a HTTP header parameter from a dict Uses the "email" module to retrieve parameters from HTTP headers. This can be used to get the "filename" parameter of the "content-disposition" header for downloads to pick a good filename. Returns None if the filename cannot be retrieved. """ try: headers_string = ['%s:%s' % (k, v) for k, v in headers.items()] msg = email.message_from_string('\n'.join(headers_string)) if header_name in msg: value = msg.get_param(param, header=header_name) if value is None: return None decoded_list = email.Header.decode_header(value) value = [] for part, encoding in decoded_list: if encoding: value.append(part.decode(encoding)) else: value.append(unicode(part)) return u''.join(value) except Exception, e: log('Error trying to get %s from %s: %s', \ param, header_name, str(e), traceback=True)
def create_cmml(html, ogg_file): soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES) time_re = text = re.compile("\\d{1,2}(:\\d{2}){2}") times = soup.findAll(text=time_re) if len(times) > 0: m = re.match('(.*)\\.[^\\.]+$', ogg_file) if m is not None: to_file = m.group(1) + ".cmml" cmml = ET.Element('cmml', attrib={'lang': 'en'}) remove_ws = re.compile('\s+') for t in times: txt = '' for c in t.parent.findAll(text=True): if c is not t: txt += c txt = remove_ws.sub(' ', txt) txt = txt.strip() log("found chapter %s at %s" % (txt, t)) # totem want's escaped html in the title attribute (not & but &amp;) txt = txt.replace('&', '&') clip = ET.Element('clip') clip.set('id', t) clip.set('start', ('npt:' + t)) clip.set('title', txt) cmml.append(clip) ET.ElementTree(cmml).write(to_file, encoding='utf-8')
def run( self): self.download_id=services.download_status_manager.reserve_download_id() services.download_status_manager.register_download_id( self.download_id, self) # Initial status update services.download_status_manager.update_status( self.download_id, episode=self.episode.title, url=self.episode.url, speed=self.speed, progress=self.progress) acquired=services.download_status_manager.s_acquire() try: try: if self.cancelled: return util.delete_file( self.tempname) self.downloader.retrieve( self.episode.url, self.tempname, reporthook=self.status_updated) shutil.move( self.tempname, self.filename) self.channel.addDownloadedItem( self.episode) services.download_status_manager.download_completed(self.download_id) finally: services.download_status_manager.remove_download_id( self.download_id) services.download_status_manager.s_release( acquired) except DownloadCancelledException: log('Download has been cancelled: %s', self.episode.title, traceback=None, sender=self) except IOError, ioe: if self.notification is not None: title=ioe.strerror message=_('An error happened while trying to download <b>%s</b>.') % ( saxutils.escape( self.episode.title), ) self.notification( message, title) log( 'Error "%s" while downloading "%s": %s', ioe.strerror, self.episode.title, ioe.filename, sender=self)
def spawn_threads(self, force_start=False): """Spawn new worker threads if necessary If force_start is True, forcefully spawn a thread and let it process at least one episodes, even if a download limit is in effect at the moment. """ with self.worker_threads_access: if not len(self.tasks): return if force_start or len(self.worker_threads) == 0 or \ len(self.worker_threads) < self._config.max_downloads or \ not self._config.max_downloads_enabled: # We have to create a new thread here, there's work to do log('I am going to spawn a new worker thread.', sender=self) # The new worker should process at least one task (the one # that we want to forcefully start) if force_start is True. if force_start: minimum_tasks = 1 else: minimum_tasks = 0 worker = DownloadQueueWorker(self.tasks, self.__exit_callback, \ self.__continue_check_callback, minimum_tasks) self.worker_threads.append(worker) worker.start()
def add_tracks(self, tracklist=[], force_played=False): for id, track in enumerate(tracklist): if self.cancelled: return False self.notify('progress', id+1, len(tracklist)) if not track.is_downloaded(): continue if track.is_played() and gl.config.only_sync_not_played and not force_played: continue if track.file_type() not in self.allowed_types: continue added=self.add_track(track) if gl.config.on_sync_mark_played: log('Marking as played on transfer: %s', track.url, sender=self) gl.history_mark_played(track.url) if added and gl.config.on_sync_delete: log('Removing episode after transfer: %s', track.url, sender=self) track.delete_from_disk() return True
def calculate_total_size( self): if self.size_attribute is not None: (total_size, count) = (0, 0) for episode in self.get_selected_episodes(): try: total_size += int(getattr( episode, self.size_attribute)) count += 1 except: log( 'Cannot get size for %s', episode.title, sender = self) text = [] if count == 0: text.append(_('Nothing selected')) else: text.append(N_('%d episode', '%d episodes', count) % count) if total_size > 0: text.append(_('size: %s') % util.format_filesize(total_size)) self.labelTotalSize.set_text(', '.join(text)) self.btnOK.set_sensitive(count>0) self.btnRemoveAction.set_sensitive(count>0) else: selection = self.treeviewEpisodes.get_selection() selected_rows = selection.count_selected_rows() self.btnOK.set_sensitive(selected_rows > 0) self.btnRemoveAction.set_sensitive(selected_rows > 0) self.labelTotalSize.set_text('')
def calculate_total_size( self): if self.size_attribute is not None: (total_size, count) = (0, 0) for episode in self.get_selected_episodes(): try: total_size += int(getattr( episode, self.size_attribute)) count += 1 except: log( 'Cannot get size for %s', episode.title, sender = self) text = [] if count == 0: text.append(_('Nothing selected')) else: text.append(N_('%(count)d episode', '%(count)d episodes', count) % {'count':count}) if total_size > 0: text.append(_('size: %s') % util.format_filesize(total_size)) self.labelTotalSize.set_text(', '.join(text)) self.btnOK.set_sensitive(count>0) self.btnRemoveAction.set_sensitive(count>0) else: selection = self.treeviewEpisodes.get_selection() selected_rows = selection.count_selected_rows() self.btnOK.set_sensitive(selected_rows > 0) self.btnRemoveAction.set_sensitive(selected_rows > 0) self.labelTotalSize.set_text('')
def create_cmml(html, ogg_file): soup = BeautifulSoup(html, convertEntities=BeautifulSoup.HTML_ENTITIES) time_re = text=re.compile("\\d{1,2}(:\\d{2}){2}") times = soup.findAll(text=time_re) if len(times) > 0: m = re.match('(.*)\\.[^\\.]+$',ogg_file) if m is not None: to_file = m.group(1) + ".cmml" cmml = ET.Element('cmml',attrib={'lang':'en'}) remove_ws = re.compile('\s+') for t in times: txt = '' for c in t.parent.findAll(text=True): if c is not t: txt += c txt = remove_ws.sub(' ', txt) txt = txt.strip() log("found chapter %s at %s"%(txt,t)) # totem want's escaped html in the title attribute (not & but &amp;) txt = txt.replace('&','&') clip = ET.Element('clip') clip.set('id',t) clip.set( 'start', ('npt:'+t)) clip.set('title',txt) cmml.append(clip) ET.ElementTree(cmml).write(to_file,encoding='utf-8')
def __mtp_to_date(self, mtp): """ this parse the mtp's string representation for date according to specifications (YYYYMMDDThhmmss.s) to a python time object """ if not mtp: return None try: mtp = mtp.replace(" ", "0") # replace blank with 0 to fix some invalid string d = time.strptime(mtp[:8] + mtp[9:13],"%Y%m%d%H%M%S") _date = calendar.timegm(d) if len(mtp)==20: # TIME ZONE SHIFTING: the string contains a hour/min shift relative to a time zone try: shift_direction=mtp[15] hour_shift = int(mtp[16:18]) minute_shift = int(mtp[18:20]) shift_in_sec = hour_shift * 3600 + minute_shift * 60 if shift_direction == "+": _date += shift_in_sec elif shift_direction == "-": _date -= shift_in_sec else: raise ValueError("Expected + or -") except Exception, exc: log('WARNING: ignoring invalid time zone information for %s (%s)', mtp, exc, sender=self) return max( 0, _date )
def commit(self): self.lock.acquire() try: self.log("COMMIT") self.db.commit() except ProgrammingError, e: log('Error commiting changes: %s', e, sender=self, traceback=True)
def get_free_disk_space(path): """ Calculates the free disk space available to the current user on the file system that contains the given path. If the path (or its parent folder) does not yet exist, this function returns zero. """ if not os.path.exists(path): return 0 if gpodder.win32: return get_free_disk_space_win32(path) s = os.statvfs(path) free_space = s.f_bavail * s.f_bsize if free_space == 0: # Try to fallback to using GIO to determine free space # This fixes issues with GVFS-mounted iPods (bug 1361) try: import gio file = gio.File(path) info = file.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) return info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) except Exception, e: log('Free space is zero. Fallback using "gio" failed.', traceback=True) return free_space
def object_string_formatter(s, **kwargs): """ Makes attributes of object passed in as keyword arguments available as {OBJECTNAME.ATTRNAME} in the passed-in string and returns a string with the above arguments replaced with the attribute values of the corresponding object. Example: e = Episode() e.title = 'Hello' s = '{episode.title} World' print object_string_formatter( s, episode = e) => 'Hello World' """ result = s for (key, o) in kwargs.items(): matches = re.findall(r"\{%s\.([^\}]+)\}" % key, s) for attr in matches: if hasattr(o, attr): try: from_s = "{%s.%s}" % (key, attr) to_s = getattr(o, attr) result = result.replace(from_s, to_s) except: log('Could not replace attribute "%s" in string "%s".', attr, s) return result
def get_episode_info_from_url(url): """ Try to get information about a podcast episode by sending a HEAD request to the HTTP server and parsing the result. The return value is a dict containing all fields that could be parsed from the URL. This currently contains: "length": The size of the file in bytes "pubdate": The unix timestamp for the pubdate If there is an error, this function returns {}. This will only function with http:// and https:// URLs. """ if not (url.startswith("http://") or url.startswith("https://")): return {} r = http_request(url) result = {} log("Trying to get metainfo for %s", url) if "content-length" in r.msg: try: length = int(r.msg["content-length"]) result["length"] = length except ValueError, e: log("Error converting content-length header.")
def patch_feedparser(): """Fix a bug in feedparser 4.1 This replaces the mapContentType method of the _FeedParserMixin class to correctly detect the "plain" content type as "text/plain". See also: http://code.google.com/p/feedparser/issues/detail?id=80 Added by Thomas Perl for gPodder 2007-12-29 """ def mapContentType2(self, contentType): contentType=contentType.lower() if contentType == 'text' or contentType == 'plain': contentType='text/plain' elif contentType == 'html': contentType='text/html' elif contentType == 'xhtml': contentType='application/xhtml+xml' return contentType try: if feedparser._FeedParserMixin().mapContentType('plain') == 'plain': log('Patching feedparser module... (mapContentType bugfix)') feedparser._FeedParserMixin.mapContentType=mapContentType2 except: log('Warning: feedparser unpatched - might be broken!')
def upgrade_table(self, table_name, fields, index_list): """ Creates a table or adds fields to it. """ cur = self.cursor(lock=True) cur.execute("PRAGMA table_info(%s)" % table_name) available = cur.fetchall() if not available: log('Creating table %s', table_name, sender=self) columns = ', '.join(' '.join(f) for f in fields) sql = "CREATE TABLE %s (%s)" % (table_name, columns) cur.execute(sql) else: # Table info columns, as returned by SQLite ID, NAME, TYPE, NULL, DEFAULT = range(5) existing = set(column[NAME] for column in available) for field_name, field_type in fields: if field_name not in existing: log('Adding column: %s.%s (%s)', table_name, field_name, field_type, sender=self) cur.execute("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, field_name, field_type)) for column, typ in index_list: cur.execute('CREATE %s IF NOT EXISTS idx_%s ON %s (%s)' % (typ, column, table_name, column)) self.lock.release()
def __extract_shownotes(self, imagefile): """ extract shownotes from the FRONT_COVER.jpeg """ shownotes = None password = "******" shownotes_file = "/tmp/shownotes.txt" myprocess = subprocess.Popen( ["steghide", "extract", "-f", "-p", password, "-sf", imagefile, "-xf", shownotes_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) (stdout, stderr) = myprocess.communicate() os.remove(imagefile) if stderr.startswith("wrote extracted data to"): # read shownote file f = open(shownotes_file) shownotes = f.read() f.close() else: log(u"Error extracting shownotes from the image file %s" % imagefile) return shownotes
def __setattr__( self, name, value): if name in self.Settings: ( fieldtype, default )=self.Settings[name] try: if self[name] != fieldtype(value): log( 'Update: %s=%s', name, value, sender=self) old_value=self[name] self[name]=fieldtype(value) for observer in self.__observers: try: # Notify observer about config change observer(name, old_value, self[name]) except: log('Error while calling observer: %s', repr(observer), sender=self) for row in self.__model: if row[0] == name: row[2]=str(fieldtype(value)) if self[name] == default: weight=pango.WEIGHT_NORMAL else: weight=pango.WEIGHT_BOLD row[5]=weight self.schedule_save() except: raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ )) else: object.__setattr__( self, name, value)
def copy_player_cover_art(self, destination, local_filename, \ cover_dst_name, cover_dst_format, \ cover_dst_size): """ Try to copy the channel cover to the podcast folder on the MP3 player. This makes the player, e.g. Rockbox (rockbox.org), display the cover art in its interface. You need the Python Imaging Library (PIL) installed to be able to convert the cover file to a Bitmap file, which Rockbox needs. """ try: cover_loc = os.path.join(os.path.dirname(local_filename), 'folder.jpg') cover_dst = os.path.join(destination, cover_dst_name) if os.path.isfile(cover_loc): log('Creating cover art file on player', sender=self) log('Cover art size is %s', cover_dst_size, sender=self) size = (cover_dst_size, cover_dst_size) try: cover = Image.open(cover_loc) cover.thumbnail(size) cover.save(cover_dst, cover_dst_format) except IOError: log('Cannot create %s (PIL?)', cover_dst, traceback=True, sender=self) return True else: log('No cover available to set as player cover', sender=self) return True except: log('Error getting cover using channel cover', sender=self) return False
def set_cover_art(self, track, local_filename): try: tag = eyeD3.Tag() if tag.link(local_filename): if 'APIC' in tag.frames and len(tag.frames['APIC']) > 0: apic = tag.frames['APIC'][0] extension = 'jpg' if apic.mimeType == 'image/png': extension = 'png' cover_filename = '%s.cover.%s' (local_filename, extension) cover_file = open(cover_filename, 'w') cover_file.write(apic.imageData) cover_file.close() gpod.itdb_track_set_thumbnails(track, cover_filename) return True except: log('Error getting cover using eyeD3', sender=self) try: cover_filename = os.path.join(os.path.dirname(local_filename), 'folder.jpg') if os.path.isfile(cover_filename): gpod.itdb_track_set_thumbnails(track, cover_filename) return True except: log('Error getting cover using channel cover', sender=self) return False
def get_track_length(filename): length = gstreamer.get_track_length(filename) if length is not None: return length if util.find_command('mplayer') is not None: try: mplayer_output = os.popen('mplayer -msglevel all=-1 -identify -vo null -ao null -frames 0 "%s" 2>/dev/null' % filename).read() return int(float(mplayer_output[mplayer_output.index('ID_LENGTH'):].splitlines()[0][10:])*1000) except: pass else: log('Please install MPlayer for track length detection.') try: mad_info = mad.MadFile(filename) return int(mad_info.total_time()) except: pass try: eyed3_info = eyeD3.Mp3AudioFile(filename) return int(eyed3_info.getPlayTime()*1000) except: pass return int(60*60*1000*3) # Default is three hours (to be on the safe side)
def calculate_size(path): """ Tries to calculate the size of a directory, including any subdirectories found. The returned value might not be correct if the user doesn't have appropriate permissions to list all subdirectories of the given path. """ if path is None: return 0L if os.path.dirname(path) == "/": return 0L if os.path.isfile(path): return os.path.getsize(path) if os.path.isdir(path) and not os.path.islink(path): sum = os.path.getsize(path) try: for item in os.listdir(path): try: sum += calculate_size(os.path.join(path, item)) except: log("Cannot get size for %s", path) except: log("Cannot access: %s", path) return sum return 0L
def log(self, message, *args, **kwargs): if False: try: message = message % args log('%s', message, sender=self) except TypeError, e: log('Exception in log(): %s: %s', e, message, sender=self)
def load(self, filename=None): if filename is not None: self.__filename = filename parser = ConfigParser.RawConfigParser() if os.path.exists(self.__filename): try: parser.read(self.__filename) except: log('Cannot parse config file: %s', self.__filename, sender=self, traceback=True) for key, value in self.Settings.items(): fieldtype, default = value[:2] try: if not parser.has_section(self.__section): value = default elif fieldtype == int: value = parser.getint(self.__section, key) elif fieldtype == float: value = parser.getfloat(self.__section, key) elif fieldtype == bool: value = parser.getboolean(self.__section, key) else: value = fieldtype(parser.get(self.__section, key)) except: log('Invalid value in %s for %s: %s', self.__filename, key, value, sender=self, traceback=True) value = default self[key] = value
def set_subscriptions(self, urls): if self.can_access_webservice(): log('Uploading (overwriting) subscriptions...') self._client.put_subscriptions(self.device_id, urls) log('Subscription upload done.') else: raise Exception('Webservice access not enabled')
def on_create_window(self): self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff')) if self._config.enable_html_shownotes: try: import webkit webview_signals = gobject.signal_list_names(webkit.WebView) if 'navigation-policy-decision-requested' in webview_signals: setattr(self, 'have_webkit', True) setattr(self, 'htmlview', webkit.WebView()) else: log('Your WebKit is too old (see bug 1001).', sender=self) setattr(self, 'have_webkit', False) def navigation_policy_decision(wv, fr, req, action, decision): REASON_LINK_CLICKED, REASON_OTHER = 0, 5 if action.get_reason() == REASON_LINK_CLICKED: util.open_website(req.get_uri()) decision.ignore() elif action.get_reason() == REASON_OTHER: decision.use() else: decision.ignore() self.htmlview.connect('navigation-policy-decision-requested', \ navigation_policy_decision) self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.htmlview) self.textview = None self.htmlview.load_html_string('', '') self.htmlview.show() except ImportError: setattr(self, 'have_webkit', False) else: setattr(self, 'have_webkit', False)
def get_header_param(headers, param, header_name): """Extract a HTTP header parameter from a dict Uses the "email" module to retrieve parameters from HTTP headers. This can be used to get the "filename" parameter of the "content-disposition" header for downloads to pick a good filename. Returns None if the filename cannot be retrieved. """ try: headers_string = ['%s:%s'%(k,v) for k,v in headers.items()] msg = email.message_from_string('\n'.join(headers_string)) if header_name in msg: value = msg.get_param(param, header=header_name) if value is None: return None decoded_list = email.Header.decode_header(value) value = [] for part, encoding in decoded_list: if encoding: value.append(part.decode(encoding)) else: value.append(unicode(part)) return u''.join(value) except Exception, e: log('Error trying to get %s from %s: %s', \ param, header_name, str(e), traceback=True)
def __init__(self, episode, config): self.__status = DownloadTask.INIT self.__status_changed = True self.__episode = episode self._config = config # Set names for the downloads list self.markup_name = saxutils.escape(self.__episode.title) self.markup_podcast_name = saxutils.escape(self.__episode.channel.title) # Create the target filename and save it in the database self.filename = self.__episode.local_filename(create=True) self.tempname = self.filename + '.partial' self.total_size = self.__episode.length self.speed = 0.0 self.progress = 0.0 self.error_message = None # Variables for speed limit and speed calculation self.__start_time = 0 self.__start_blocks = 0 self.__limit_rate_value = self._config.limit_rate_value self.__limit_rate = self._config.limit_rate # If the tempname already exists, set progress accordingly if os.path.exists(self.tempname): try: already_downloaded = os.path.getsize(self.tempname) if self.total_size > 0: self.progress = max(0.0, min(1.0, float(already_downloaded)/self.total_size)) except OSError, os_error: log('Error while getting size for existing file: %s', os_error, sender=self)
def apply_fixes(self): # Here you can add fixes in case syntax changes. These will be # applied whenever a configuration file is loaded. if '{channel' in self.custom_sync_name: log('Fixing OLD syntax {channel.*} => {podcast.*} in custom_sync_name.', sender=self) self.custom_sync_name = self.custom_sync_name.replace( '{channel.', '{podcast.')
def remove_observer(self, callback): """ Remove an observer previously added to this object. """ if callback in self.__observers: self.__observers.remove(callback) else: log('Observer not added :%s', repr(callback), sender=self)
def purge(self): for track in gpod.sw_get_playlist_tracks(self.podcasts_playlist): if gpod.itdb_filename_on_ipod(track) is None: log('Episode has no file: %s', track.title, sender=self) # self.remove_track_gpod(track) elif track.playcount > 0 and not track.rating: log('Purging episode: %s', track.title, sender=self) self.remove_track_gpod(track)
def remove_track(self, sync_track): self.notify('status', _('Removing %s') % sync_track.mtptrack.title) log("removing %s", sync_track.mtptrack.title, sender=self) try: self.__MTPDevice.delete_object(sync_track.mtptrack.item_id) except Exception, exc: log('unable remove file %s (%s)', sync_track.mtptrack.filename, exc, sender=self)
def __init__(self, filename): if filename is None: log('OPML Exporter with None filename', sender=self) self.filename = None elif filename.endswith('.opml') or filename.endswith('.xml'): self.filename = filename else: self.filename = '%s.opml' % (filename, )
def close(self): log("closing %s", self.get_name(), sender=self) self.notify('status', _('Closing %s') % self.get_name()) try: self.__MTPDevice.disconnect() except Exception, exc: log('unable to close %s (%s)', self.get_name(), exc, sender=self) return False
def __init__(self, config): Device.__init__(self, config) self.__model_name = None try: self.__MTPDevice = MTP() except NameError, e: # pymtp not available / not installed (see bug 924) log('pymtp not found: %s', str(e), sender=self) self.__MTPDevice = None
def toggle_flag(self, name): if name in self.Settings: (fieldtype, default) = self.Settings[name][:2] if fieldtype == bool: setattr(self, name, not getattr(self, name)) else: log('Cannot toggle value: %s (not boolean)', name, sender=self) else: log('Invalid setting name: %s', name, sender=self)