Refactor libpodcasts; fix episode dialog

Refactor libpodcasts:
 * Rename podcastChannel to PodcastChannel
 * Rename podcastItem to PodcastEpisode
 * Add a new base class with common functions for
   PodcastChannel/PodcastEpisode
 * Remove unneeded / orphaned functions and properties

Fix the buttons in the episode shownotes dialog when a
new episode finishes downloading by reloading the filename
from the database (so we can display the play button).
This commit is contained in:
Thomas Perl 2009-03-10 14:59:01 +01:00
parent 73661afead
commit 0cafc12941
6 changed files with 81 additions and 57 deletions

View File

@ -27,7 +27,7 @@ from gpodder.dbsqlite import db
from libpodcasts import load_channels
from libpodcasts import update_channels
from libpodcasts import save_channels
from libpodcasts import podcastChannel
from libpodcasts import PodcastChannel
import time
@ -52,7 +52,7 @@ def add_channel( url):
return
try:
channel = podcastChannel.load(url, create=True)
channel = PodcastChannel.load(url, create=True)
except:
msg( 'error', _('Could not load feed from URL: %s'), urllib.unquote( url))
return

View File

@ -434,9 +434,11 @@ class Storage(object):
def load_episode(self, url, factory=None):
self.log("load_episode(%s)", url)
list = self.__read_episodes(factory = factory, where = " WHERE url = ?", params = (url, ))
if len(list):
list = self.__read_episodes(factory=factory, where=' WHERE url=? LIMIT ?', params=(url, 1))
if list:
return list[0]
else:
return None
def save_episode(self, e, bulk=False):
if not e.guid:

View File

@ -63,9 +63,8 @@ except Exception, exc:
log('Warning: This probably means your PyGTK installation is too old!')
have_trayicon = False
from libpodcasts import podcastChannel
from libpodcasts import PodcastChannel
from libpodcasts import LocalDBReader
from libpodcasts import podcastItem
from libpodcasts import channels_to_model
from libpodcasts import update_channel_model_by_iter
from libpodcasts import load_channels
@ -1405,7 +1404,7 @@ class gPodder(GladeWidget, dbus.service.Object):
for path in paths:
url = model.get_value( model.get_iter( path), 0)
episode = podcastItem.load(url, self.active_channel)
episode = self.active_channel.find_episode(url)
if episode.file_type() not in ('audio', 'video'):
open_instead_of_play = True
@ -1677,11 +1676,11 @@ class gPodder(GladeWidget, dbus.service.Object):
log( 'Adding new channel: %s', url)
channel = error = None
try:
channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
channel = PodcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
except HTTPAuthError, e:
error = e
except Exception, e:
log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
log('Error in PodcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
util.idle_add( callback, channel, url, error, *callback_args )
@ -3658,6 +3657,9 @@ class gPodderEpisode(GladeWidget):
def on_download_status_changed(self, episode_urls, channel_urls):
if self.gPodderEpisode.get_property('visible'):
# Reload the episode from the database, so a newly-set local_filename
# as a result of a download gets updated in the episode object
self.episode.reload_from_db()
self.hide_show_widgets()
else:
log('download status changed, but not visible', sender=self)

View File

@ -78,9 +78,35 @@ else:
class HTTPAuthError(Exception): pass
class podcastChannel(object):
class PodcastModelObject(object):
"""
A generic base class for our podcast model providing common helper
and utility functions.
"""
@classmethod
def create_from_dict(cls, d, *args):
"""
Create a new object, passing "args" to the constructor
and then updating the object with the values from "d".
"""
o = cls(*args)
o.update_from_dict(d)
return o
def update_from_dict(self, d):
"""
Updates the attributes of this object with values from the
dictionary "d" by using the keys found in "d".
"""
for k in d:
if hasattr(self, k):
setattr(self, k, d[k])
class PodcastChannel(PodcastModelObject):
"""holds data for a complete channel"""
SETTINGS = ('sync_to_devices', 'device_playlist_name','override_title','username','password')
MAX_FOLDERNAME_LENGTH = 150
icon_cache = {}
@ -91,11 +117,11 @@ class podcastChannel(object):
if isinstance(url, unicode):
url = url.encode('utf-8')
tmp = db.load_channels(factory=lambda d: cls.create_from_dict(d), url=url)
tmp = db.load_channels(factory=cls.create_from_dict, url=url)
if len(tmp):
return tmp[0]
elif create:
tmp = podcastChannel(url)
tmp = PodcastChannel(url)
if authentication_tokens is not None:
tmp.username = authentication_tokens[0]
tmp.password = authentication_tokens[1]
@ -109,13 +135,15 @@ class podcastChannel(object):
db.force_last_new(tmp)
return tmp
@staticmethod
def create_from_dict(d):
c = podcastChannel()
for key in d:
if hasattr(c, key):
setattr(c, key, d[key])
return c
def episode_factory(self, d):
"""
This function takes a dictionary containing key-value pairs for
episodes and returns a new PodcastEpisode object that is connected
to this PodcastChannel object.
Returns: A new PodcastEpisode object
"""
return PodcastEpisode.create_from_dict(d, self)
def update(self):
(updated, c) = self.fc.fetch(self.url, self)
@ -201,7 +229,7 @@ class podcastChannel(object):
episode = None
try:
episode = podcastItem.from_feedparser_entry(entry, self)
episode = PodcastEpisode.from_feedparser_entry(entry, self)
except Exception, e:
log('Cannot instantiate episode "%s": %s. Skipping.', entry.get('id', '(no id available)'), e, sender=self, traceback=True)
@ -325,13 +353,10 @@ class podcastChannel(object):
self.override_title = ''
def get_downloaded_episodes(self):
return db.load_episodes(self, factory=lambda c: podcastItem.create_from_dict(c, self), state=db.STATE_DOWNLOADED)
def save_settings(self):
db.save_channel(self)
return db.load_episodes(self, factory=self.episode_factory, state=db.STATE_DOWNLOADED)
def get_new_episodes( self):
return [episode for episode in db.load_episodes(self, factory=lambda x: podcastItem.create_from_dict(x, self)) if episode.state == db.STATE_NORMAL and not episode.is_played and not services.download_status_manager.is_download_in_progress(episode.url)]
return [episode for episode in db.load_episodes(self, factory=self.episode_factory) if episode.state == db.STATE_NORMAL and not episode.is_played and not services.download_status_manager.is_download_in_progress(episode.url)]
def update_m3u_playlist(self):
if gl.config.create_m3u_playlists:
@ -374,7 +399,7 @@ class podcastChannel(object):
self.update_m3u_playlist()
def get_all_episodes(self):
return db.load_episodes(self, factory = lambda d: podcastItem.create_from_dict(d, self))
return db.load_episodes(self, factory=self.episode_factory)
def iter_set_downloading_columns( self, model, iter, episode=None):
global ICON_AUDIO_FILE, ICON_VIDEO_FILE
@ -382,7 +407,7 @@ class podcastChannel(object):
if episode is None:
url = model.get_value( iter, 0)
episode = db.load_episode(url, factory=lambda x: podcastItem.create_from_dict(x, self))
episode = db.load_episode(url, factory=self.episode_factory)
else:
url = episode.url
@ -448,7 +473,7 @@ class podcastChannel(object):
return (new_model, urls)
def find_episode( self, url):
return db.load_episode(url, factory=lambda x: podcastItem.create_from_dict(x, self))
return db.load_episode(url, factory=self.episode_factory)
@classmethod
def find_unique_folder_name(cls, foldername):
@ -533,7 +558,7 @@ class podcastChannel(object):
cover_file = property(fget=get_cover_file)
def delete_episode_by_url(self, url):
episode = db.load_episode(url, lambda c: podcastItem.create_from_dict(c, self))
episode = db.load_episode(url, factory=self.episode_factory)
if episode is not None:
filename = episode.local_filename(create=False)
@ -546,23 +571,27 @@ class podcastChannel(object):
self.update_m3u_playlist()
class podcastItem(object):
class PodcastEpisode(PodcastModelObject):
"""holds data for one object in a channel"""
MAX_FILENAME_LENGTH = 200
@staticmethod
def load(url, channel):
e = podcastItem(channel)
d = db.load_episode(url)
def reload_from_db(self):
"""
Re-reads all episode details for this object from the
database and updates this object accordingly. Can be
used to refresh existing objects when the database has
been updated (e.g. the filename has been set after a
download where it was not set before the download)
"""
d = db.load_episode(self.url)
if d is not None:
for k, v in d.iteritems():
if hasattr(e, k):
setattr(e, k, v)
return e
self.update_from_dict(d)
return self
@staticmethod
def from_feedparser_entry( entry, channel):
episode = podcastItem( channel)
episode = PodcastEpisode( channel)
episode.title = entry.get( 'title', util.get_first_line( util.remove_html_tags( entry.get( 'summary', ''))))
episode.link = entry.get( 'link', '')
@ -673,14 +702,6 @@ class podcastItem(object):
self.save()
db.commit()
@staticmethod
def create_from_dict(d, channel):
e = podcastItem(channel)
for key in d:
if hasattr(e, key):
setattr(e, key, d[key])
return e
@property
def title_and_description(self):
"""
@ -1010,7 +1031,7 @@ def channels_to_model(channels, color_dict, cover_cache=None, max_width=0, max_h
def load_channels():
return db.load_channels(lambda d: podcastChannel.create_from_dict(d))
return db.load_channels(factory=PodcastChannel.create_from_dict)
def update_channels(callback_proc=None, callback_error=None, is_cancelled_cb=None):
log('Updating channels....')
@ -1054,7 +1075,7 @@ class LocalDBReader( object):
return self.get_text( element.getElementsByTagName( name)[0].childNodes)
def get_episode_from_element( self, channel, element):
episode = podcastItem( channel)
episode = PodcastEpisode(channel)
episode.title = self.get_text_by_first_node( element, 'title')
episode.description = self.get_text_by_first_node( element, 'description')
episode.url = self.get_text_by_first_node( element, 'url')
@ -1109,7 +1130,7 @@ class LocalDBReader( object):
channel_element = rss.getElementsByTagName('channel')[0]
channel = podcastChannel( url = self.url)
channel = PodcastChannel(url=self.url)
channel.title = self.get_text_by_first_node( channel_element, 'title')
channel.description = self.get_text_by_first_node( channel_element, 'description')
channel.link = self.get_text_by_first_node( channel_element, 'link')

View File

@ -44,7 +44,7 @@ import os.path
class VCChannel(object):
"""
Fake podcastChannel-like object to allow
Fake podcast channel-like object to allow
opml's Exporter class to write an OPML file.
"""
def __init__(self, title, description, url):

View File

@ -28,7 +28,6 @@ import datetime
import gpodder
from gpodder.liblogger import log
from gpodder.libgpodder import gl
from gpodder.libpodcasts import podcastItem
try:
import pynotify
@ -386,7 +385,7 @@ class GPodderStatusIcon(gtk.StatusIcon):
If the list is too long, it is cut and the string "x others episodes" is append
episode_list
can be either a list containing podcastItem objects
can be either a list containing episode objects
or a list of strings of episode's title.
return
@ -400,10 +399,10 @@ class GPodderStatusIcon(gtk.StatusIcon):
if caption is not None:
result.append('\n%s' % caption)
for episode in episode_list[:min(len(episode_list),MAX_EPISODES)]:
if isinstance(episode, podcastItem):
episode_title = episode.title
else:
if type(episode) in (str, unicode):
episode_title = episode
else:
episode_title = episode.title
if len(episode_title) < MAX_TITLE_LENGTH:
title = episode_title
else: