playback status tracking + ipod playcount sync
git-svn-id: svn://svn.berlios.de/gpodder/trunk@302 b0d088ad-0a06-0410-aad2-9ed5178a7e87
This commit is contained in:
parent
d71d72761b
commit
7bed9a8aae
23
ChangeLog
23
ChangeLog
|
@ -1,3 +1,26 @@
|
|||
Tue, 3 Apr 2007 13:14:02 +0200 <thp@perli.net>
|
||||
* src/gpodder/liblogger.py: Enhance the log() function to accept a
|
||||
"sender" keyword argument that contains the object tha sends the
|
||||
message; the message will be transformed into "(objectclass) message"
|
||||
* src/gpodder/libpodcasts.py: Add "Played" icon to the model for
|
||||
played status display; use gtk.STOCK_NEW instead of "document-new"
|
||||
for new items; mark the latest three items as new if no episodes
|
||||
have been downloaded so far from this channel; generalize the
|
||||
DownloadHistory class and derive a PlaybackHistory class from it
|
||||
* src/gpodder/libgpodder.py: Make use of PlaybackHistory; add config
|
||||
option "show_played" (disable/enable "Played" column in treeview);
|
||||
replace openFilename() function with playback_episode() function
|
||||
that also updates the playback history accordingly
|
||||
* src/gpodder/libipodsync.py: Synchronize playback state ("blue dot")
|
||||
to iPod when syncing (also update playback state when episodes is
|
||||
already on iPod); fixed a small bug that didn't set podcast flags
|
||||
* src/gpodder/gpodder.py: Make use of new playback_episode function;
|
||||
add support for the "Played" column (+config disable/enable)
|
||||
* data/gpodder.glade: Add GtkCheckButton for displaying/hiding the
|
||||
"Played" column in the available episodes list
|
||||
* TODO: Updated TODO list (display played episodes task is done now;
|
||||
mark tag update and search box as partially implemented)
|
||||
|
||||
Tue, 3 Apr 2007 08:18:28 +0200 <thp@perli.net>
|
||||
* src/gpodder/libpodcasts.py: Remove background color code in model;
|
||||
add icons; add new get_file_type() function to check if an episode
|
||||
|
|
6
TODO
6
TODO
|
@ -9,11 +9,9 @@
|
|||
* Utilize DesktopEntry in python-xdg for libplayers
|
||||
|
||||
* Now for a genuine feature request: a search box?
|
||||
-> partially implemented (search-as-you-type in available episodes)
|
||||
(Michel Salim; michel.salim gmail.com)
|
||||
|
||||
* Finally, could it mark via a different color code when a podcast
|
||||
has been played. (John Connor; johnc.connor gmail.com)
|
||||
|
||||
* My second suggestion is to include a systray icon similar to the ones
|
||||
many music players have. It would be great if I could hide/show the
|
||||
gpodder window by clicking on it. Getting current download status on
|
||||
|
@ -52,6 +50,8 @@
|
|||
-> Only populate when MP3 files don't yet have tags
|
||||
-> Always populate; overwrite old tags if existing
|
||||
|
||||
(partially implemented already)
|
||||
|
||||
(Thanks to ubunt2@gmail.com for suggesting the metadata populate feat.)
|
||||
|
||||
* in libipodsync.py: use mime magic instead of extension-based
|
||||
|
|
|
@ -1996,6 +1996,36 @@
|
|||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkHSeparator" id="hseparator3">
|
||||
<property name="visible">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkCheckButton" id="showplayed">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label" translatable="yes">Display "Played" status in episode list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="relief">GTK_RELIEF_NORMAL</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<property name="active">False</property>
|
||||
<property name="inconsistent">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
|
|
|
@ -120,6 +120,11 @@ class Gpodder(SimpleGladeApp):
|
|||
iconcolumn = gtk.TreeViewColumn( _("Status"), iconcell)
|
||||
iconcolumn.add_attribute( iconcell, "icon-name", 4)
|
||||
|
||||
playedcell = gtk.CellRendererPixbuf()
|
||||
playedcolumn = gtk.TreeViewColumn( _("Played"), playedcell)
|
||||
playedcolumn.add_attribute( playedcell, "icon-name", 8)
|
||||
self.played_column = playedcolumn
|
||||
|
||||
namecell = gtk.CellRendererText()
|
||||
#namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
namecolumn = gtk.TreeViewColumn( _("Episode"), namecell, text=1)
|
||||
|
@ -135,7 +140,7 @@ class Gpodder(SimpleGladeApp):
|
|||
desccell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
desccolumn = gtk.TreeViewColumn( _("Description"), desccell, text=6)
|
||||
|
||||
for itemcolumn in ( iconcolumn, namecolumn, sizecolumn, releasecolumn, desccolumn ):
|
||||
for itemcolumn in ( iconcolumn, playedcolumn, namecolumn, sizecolumn, releasecolumn, desccolumn ):
|
||||
itemcolumn.set_resizable( True)
|
||||
itemcolumn.set_reorderable( True)
|
||||
self.treeAvailable.append_column( itemcolumn)
|
||||
|
@ -192,6 +197,10 @@ class Gpodder(SimpleGladeApp):
|
|||
#-- Gpodder.new }
|
||||
|
||||
#-- Gpodder custom methods {
|
||||
def playback_episode( self, current_channel, current_podcast):
|
||||
gPodderLib().playback_episode( current_channel, current_podcast)
|
||||
self.updateTreeView()
|
||||
|
||||
def treeAvailable_search_equal( self, model, column, key, iter, data = None):
|
||||
if model == None:
|
||||
return True
|
||||
|
@ -264,6 +273,9 @@ class Gpodder(SimpleGladeApp):
|
|||
pass
|
||||
|
||||
def updateTreeView( self):
|
||||
gl = gPodderLib()
|
||||
self.played_column.set_visible( gl.show_played)
|
||||
|
||||
rect = self.treeAvailable.get_visible_rect()
|
||||
if self.channels:
|
||||
self.treeAvailable.set_model( self.active_channel.items_liststore( downloading_callback = self.download_status_manager.is_download_in_progress))
|
||||
|
@ -377,6 +389,7 @@ class Gpodder(SimpleGladeApp):
|
|||
sync.sync_channel( self.active_channel, episodes)
|
||||
|
||||
sync.close( success = not sync.cancelled)
|
||||
gobject.idle_add( self.updateTreeView)
|
||||
|
||||
def ipod_cleanup_proc( self, sync):
|
||||
if not sync.open():
|
||||
|
@ -386,6 +399,7 @@ class Gpodder(SimpleGladeApp):
|
|||
|
||||
sync.clean_playlist()
|
||||
sync.close( success = not sync.cancelled, cleaned = True)
|
||||
gobject.idle_add( self.updateTreeView)
|
||||
|
||||
def update_feed_cache_callback( self, label, progressbar, position, count):
|
||||
title = _('Please wait...')
|
||||
|
@ -474,7 +488,7 @@ class Gpodder(SimpleGladeApp):
|
|||
if current_channel.addDownloadedItem( current_podcast):
|
||||
self.ldb.clear_cache()
|
||||
# open the file now
|
||||
gPodderLib().openFilename( filename)
|
||||
self.playback_episode( current_channel, current_podcast)
|
||||
return
|
||||
|
||||
if widget.get_name() == 'treeAvailable':
|
||||
|
@ -482,7 +496,7 @@ class Gpodder(SimpleGladeApp):
|
|||
gpe.set_episode( current_podcast, current_channel)
|
||||
|
||||
if os.path.exists( filename):
|
||||
gpe.set_play_callback( lambda: gPodderLib().openFilename( filename))
|
||||
gpe.set_play_callback( lambda: self.playback_episode( current_channel, current_podcast))
|
||||
else:
|
||||
gpe.set_download_callback( lambda: self.download_podcast_by_url( url, want_message_dialog, None))
|
||||
|
||||
|
@ -1141,6 +1155,7 @@ class Gpodderproperties(SimpleGladeApp):
|
|||
self.chooserDownloadTo.set_filename( gl.downloaddir)
|
||||
self.updateonstartup.set_active(gl.update_on_startup)
|
||||
self.downloadnew.set_active(gl.download_after_update)
|
||||
self.showplayed.set_active(gl.show_played)
|
||||
if tagging_supported():
|
||||
self.updatetags.set_active(gl.update_tags)
|
||||
else:
|
||||
|
@ -1347,6 +1362,7 @@ class Gpodderproperties(SimpleGladeApp):
|
|||
|
||||
gl.update_on_startup = self.updateonstartup.get_active()
|
||||
gl.download_after_update = self.downloadnew.get_active()
|
||||
gl.show_played = self.showplayed.get_active()
|
||||
gl.update_tags = self.updatetags.get_active()
|
||||
device_type = self.comboboxDeviceType.get_active()
|
||||
if device_type == 0:
|
||||
|
|
|
@ -69,6 +69,7 @@ from stat import ST_MODE
|
|||
from librssreader import rssReader
|
||||
from libpodcasts import podcastChannel
|
||||
from libpodcasts import DownloadHistory
|
||||
from libpodcasts import PlaybackHistory
|
||||
from libplayers import dotdesktop_command
|
||||
|
||||
from gtk.gdk import PixbufLoader
|
||||
|
@ -123,6 +124,7 @@ class gPodderLibClass( object):
|
|||
self.opml_url = ""
|
||||
self.update_on_startup = False
|
||||
self.download_after_update = False
|
||||
self.show_played = False
|
||||
self.update_tags = False
|
||||
self.desktop_link = _("gPodder downloads")
|
||||
self.device_type = None
|
||||
|
@ -132,6 +134,7 @@ class gPodderLibClass( object):
|
|||
self.main_window_y = 0
|
||||
self.mp3_player_folder = ""
|
||||
self.__download_history = DownloadHistory( self.get_download_history_filename())
|
||||
self.__playback_history = PlaybackHistory( self.get_playback_history_filename())
|
||||
self.loadConfig()
|
||||
|
||||
def createIfNecessary( self, path):
|
||||
|
@ -154,6 +157,9 @@ class gPodderLibClass( object):
|
|||
def get_download_history_filename( self):
|
||||
return self.gpodderdir + 'download-history.txt'
|
||||
|
||||
def get_playback_history_filename( self):
|
||||
return self.gpodderdir + 'playback-history.txt'
|
||||
|
||||
def propertiesChanged( self):
|
||||
# set new environment variables for subprocesses to use,
|
||||
# but only if we are not told to passthru the env vars
|
||||
|
@ -192,6 +198,7 @@ class gPodderLibClass( object):
|
|||
self.write_to_parser( parser, 'ipod_mount', self.ipod_mount)
|
||||
self.write_to_parser( parser, 'update_on_startup', self.update_on_startup)
|
||||
self.write_to_parser( parser, 'download_after_update', self.download_after_update)
|
||||
self.write_to_parser( parser, 'show_played', self.show_played)
|
||||
self.write_to_parser( parser, 'update_tags', self.update_tags)
|
||||
self.write_to_parser( parser, 'opml_url', self.opml_url)
|
||||
self.write_to_parser( parser, 'download_dir', self.downloaddir)
|
||||
|
@ -253,7 +260,10 @@ class gPodderLibClass( object):
|
|||
downloaddir = property(fget=get_download_dir,fset=set_download_dir)
|
||||
|
||||
def history_mark_downloaded( self, url):
|
||||
self.__download_history.mark_downloaded( url)
|
||||
self.__download_history.add_item( url)
|
||||
|
||||
def history_mark_played( self, url):
|
||||
self.__playback_history.add_item( url)
|
||||
|
||||
def can_write_directory( self, directory):
|
||||
return isdir( directory) and access( directory, W_OK)
|
||||
|
@ -261,6 +271,9 @@ class gPodderLibClass( object):
|
|||
def history_is_downloaded( self, url):
|
||||
return (url in self.__download_history)
|
||||
|
||||
def history_is_played( self, url):
|
||||
return (url in self.__playback_history)
|
||||
|
||||
def get_from_parser( self, parser, option, default = ''):
|
||||
try:
|
||||
result = parser.get( self.gpodderconf_section, option)
|
||||
|
@ -314,6 +327,7 @@ class gPodderLibClass( object):
|
|||
self.ipod_mount = self.get_from_parser( parser, 'ipod_mount', '/media/ipod')
|
||||
self.update_on_startup = self.get_boolean_from_parser(parser, 'update_on_startup', default=False)
|
||||
self.download_after_update = self.get_boolean_from_parser(parser, 'download_after_update', default=False)
|
||||
self.show_played = self.get_boolean_from_parser(parser, 'show_played', default=False)
|
||||
self.update_tags = self.get_boolean_from_parser(parser, 'update_tags', default=False)
|
||||
self.downloaddir = self.get_from_parser( parser, 'download_dir', expanduser('~/gpodder-downloads'))
|
||||
self.device_type = self.get_from_parser( parser, 'device_type', 'none')
|
||||
|
@ -350,7 +364,10 @@ class gPodderLibClass( object):
|
|||
if was_oldstyle:
|
||||
self.saveConfig()
|
||||
|
||||
def openFilename( self, filename):
|
||||
def playback_episode( self, channel, episode):
|
||||
self.history_mark_played( episode.url)
|
||||
filename = channel.getPodcastFilename( episode.url)
|
||||
|
||||
log( 'Opening %s (with %s)', filename, self.open_app)
|
||||
|
||||
# use libplayers to create a commandline out of open_app plus filename, then exec in background ('&')
|
||||
|
|
|
@ -293,7 +293,16 @@ class gPodder_iPodSync( gPodderSyncMethod):
|
|||
return False
|
||||
for track in gpod.sw_get_playlist_tracks( pl):
|
||||
if episode.title == track.title and channel.title == track.album:
|
||||
log( '(ipodsync) Already on iPod: %s (from %s)', episode.title, track.title)
|
||||
gl = libgpodder.gPodderLib()
|
||||
|
||||
# Mark as played locally if played on iPod
|
||||
if track.playcount > 0:
|
||||
log( 'Episode has been played %d times on iPod: %s', track.playcount, episode.title, sender = self)
|
||||
gl.history_mark_played( episode.url)
|
||||
|
||||
# Mark as played on iPod if played locally (and set podcast flags)
|
||||
self.set_podcast_flags( track, episode)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -305,12 +314,17 @@ class gPodder_iPodSync( gPodderSyncMethod):
|
|||
log( '(ipodsync) Trying to remove: %s', track.title)
|
||||
self.remove_from_ipod( track, [ self.pl_podcasts ])
|
||||
|
||||
def set_podcast_flags( self, track):
|
||||
def set_podcast_flags( self, track, episode):
|
||||
if not ipod_supported():
|
||||
return False
|
||||
try:
|
||||
# Add blue bullet next to unplayed tracks on 5G iPods
|
||||
track.mark_unplayed = 0x02
|
||||
# (only if the podcast has not been played locally already
|
||||
gl = libgpodder.gPodderLib()
|
||||
if gl.history_is_played( episode.url):
|
||||
track.mark_unplayed = 0x01
|
||||
else:
|
||||
track.mark_unplayed = 0x02
|
||||
|
||||
# Podcast flags (for new iPods?)
|
||||
track.remember_playback_position = 0x01
|
||||
|
@ -408,8 +422,7 @@ class gPodder_iPodSync( gPodderSyncMethod):
|
|||
track = gpod.itdb_track_new()
|
||||
|
||||
track.artist = str(channel.title)
|
||||
if channel.is_music_channel:
|
||||
self.set_podcast_flags( track)
|
||||
self.set_podcast_flags( track, episode)
|
||||
|
||||
# Add release time to track if pubDate is parseable
|
||||
ipod_date = email.Utils.parsedate(episode.pubDate)
|
||||
|
|
|
@ -36,7 +36,9 @@ def enable_verbose():
|
|||
write_to_stdout = True
|
||||
|
||||
|
||||
def log( message, *args):
|
||||
def log( message, *args, **kwargs):
|
||||
if 'sender' in kwargs:
|
||||
message = '(%s) %s' % ( kwargs['sender'].__class__.__name__, message )
|
||||
if write_to_stdout:
|
||||
print message % args
|
||||
|
||||
|
|
|
@ -239,13 +239,20 @@ class podcastChannel(ListType):
|
|||
the URL of the episodes and returns True if the episode is currently
|
||||
being downloaded and False otherwise.
|
||||
"""
|
||||
new_model = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
|
||||
new_model = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
|
||||
gl = libgpodder.gPodderLib()
|
||||
|
||||
last_pubdate = self.newest_pubdate_downloaded()
|
||||
|
||||
index = 1
|
||||
for item in self.get_all_episodes():
|
||||
played_icon = None
|
||||
if self.is_downloaded( item) and want_color:
|
||||
if libgpodder.gPodderLib().history_is_played( item.url):
|
||||
played_icon = gtk.STOCK_YES
|
||||
#else:
|
||||
# played_icon = gtk.STOCK_NO
|
||||
|
||||
file_type = self.get_file_type( item)
|
||||
if file_type == 'audio':
|
||||
status_icon = 'audio-x-generic'
|
||||
|
@ -258,7 +265,9 @@ class podcastChannel(ListType):
|
|||
elif libgpodder.gPodderLib().history_is_downloaded( item.url) and want_color:
|
||||
status_icon = gtk.STOCK_DELETE
|
||||
elif last_pubdate and item.compare_pubdate( last_pubdate) >= 0:
|
||||
status_icon = 'document-new'
|
||||
status_icon = gtk.STOCK_NEW
|
||||
elif not last_pubdate and index <= 3:
|
||||
status_icon = gtk.STOCK_NEW
|
||||
else:
|
||||
status_icon = None
|
||||
new_iter = new_model.append()
|
||||
|
@ -270,6 +279,8 @@ class podcastChannel(ListType):
|
|||
new_model.set( new_iter, 5, item.cute_pubdate())
|
||||
new_model.set( new_iter, 6, item.one_line_description())
|
||||
new_model.set( new_iter, 7, item.description)
|
||||
new_model.set( new_iter, 8, played_icon)
|
||||
index += 1
|
||||
|
||||
return new_model
|
||||
|
||||
|
@ -534,7 +545,7 @@ class DownloadHistory( ListType):
|
|||
try:
|
||||
self.read_from_file()
|
||||
except:
|
||||
log( '(DownloadHistory) Creating new history list.')
|
||||
log( 'Creating new history list.', sender = self)
|
||||
|
||||
def read_from_file( self):
|
||||
for line in open( self.filename, 'r'):
|
||||
|
@ -546,17 +557,17 @@ class DownloadHistory( ListType):
|
|||
for url in self:
|
||||
fp.write( url + "\n")
|
||||
fp.close()
|
||||
log( '(DownloadHistory) Wrote %d history entries.', len( self))
|
||||
log( 'Wrote %d history entries.', len( self), sender = self)
|
||||
|
||||
def mark_downloaded( self, data, autosave = True):
|
||||
def add_item( self, data, autosave = True):
|
||||
affected = 0
|
||||
if data and type( data) is ListType:
|
||||
# Support passing a list of urls to this function
|
||||
for url in data:
|
||||
affected = affected + self.mark_downloaded( url, autosave = False)
|
||||
affected = affected + self.add_item( url, autosave = False)
|
||||
else:
|
||||
if data not in self:
|
||||
log( '(DownloadHistory) Marking as downloaded: %s', data)
|
||||
log( 'Adding: %s', data, sender = self)
|
||||
self.append( data)
|
||||
affected = affected + 1
|
||||
|
||||
|
@ -566,6 +577,10 @@ class DownloadHistory( ListType):
|
|||
return affected
|
||||
|
||||
|
||||
class PlaybackHistory( DownloadHistory):
|
||||
pass
|
||||
|
||||
|
||||
def channelsToModel( channels):
|
||||
new_model = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_STRING)
|
||||
|
||||
|
|
Loading…
Reference in New Issue