Split libpodcasts in model and gtkui.model modules

Create "gpodder.gtkui" package that will contain all
modules that support the GTK+-based user interface.

libpodcasts has been renamed to "model" and is now
GTK+-clean, so it does not require the GTK+ module.
This commit is contained in:
Thomas Perl 2009-08-13 23:36:18 +02:00
parent ab482ce200
commit ef90e09b90
7 changed files with 271 additions and 230 deletions

View File

@ -143,7 +143,7 @@ remove-git-menuitem:
clean:
python setup.py clean
rm -f src/gpodder/*.pyc src/gpodder/*.pyo src/gpodder/*.bak MANIFEST PKG-INFO $(UIFILES_H) data/messages.pot~ data/gpodder-??x??.png $(ROSETTA_ARCHIVE) .coverage
rm -f src/gpodder/*.pyc src/gpodder/gtkui/*.pyc src/gpodder/*.pyo src/gpodder/*.bak MANIFEST PKG-INFO $(UIFILES_H) data/messages.pot~ data/gpodder-??x??.png $(ROSETTA_ARCHIVE) .coverage
rm -rf build
make -C data/po clean

View File

@ -26,7 +26,7 @@ integrate podcast functionality into their applications.
import gpodder
from gpodder import util
from gpodder import opml
from gpodder.libpodcasts import PodcastChannel
from gpodder.model import PodcastChannel
from gpodder.libgpodder import db
from gpodder.libgpodder import gl
from gpodder import download

View File

@ -22,7 +22,7 @@ import sys
import gpodder
from gpodder import sync
from gpodder.libpodcasts import PodcastChannel
from gpodder.model import PodcastChannel
_ = gpodder.gettext

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

238
src/gpodder/gtkui/model.py Normal file
View File

@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# gpodder.gtkui.model - GUI model classes for gPodder (2009-08-13)
# Based on code from libpodcasts.py (thp, 2005-10-29)
#
import gpodder
_ = gpodder.gettext
from gpodder import util
from gpodder import services
from gpodder import draw
import gtk
import xml.sax.saxutils
class EpisodeListModel(gtk.ListStore):
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
C_PUBLISHED_TEXT, C_DESCRIPTION, C_DESCRIPTION_STRIPPED, \
= range(8)
def __init__(self):
gtk.ListStore.__init__(self, str, str, str, object, \
gtk.gdk.Pixbuf, str, str, str)
self._icon_cache = {}
if gpodder.interface == gpodder.MAEMO:
self.ICON_AUDIO_FILE = 'gnome-mime-audio-mp3'
self.ICON_VIDEO_FILE = 'gnome-mime-video-mp4'
self.ICON_GENERIC_FILE = 'text-x-generic'
self.ICON_DOWNLOADING = 'qgn_toolb_messagin_moveto'
self.ICON_DELETED = 'qgn_toolb_gene_deletebutton'
self.ICON_NEW = 'qgn_list_gene_favor'
else:
self.ICON_AUDIO_FILE = 'audio-x-generic'
self.ICON_VIDEO_FILE = 'video-x-generic'
self.ICON_GENERIC_FILE = 'text-x-generic'
self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
self.ICON_DELETED = gtk.STOCK_DELETE
self.ICON_NEW = gtk.STOCK_ABOUT
def _format_filesize(self, episode):
if episode.length > 0:
return util.format_filesize(episode.length, 1)
else:
return None
def update_from_channel(self, channel, downloading=None, \
include_description=False, finish_callback=None):
"""
Return a gtk.ListStore containing episodes for the given channel.
Downloading should be a callback.
include_description should be a boolean value (True if description
is to be added to the episode row, or False if not)
"""
self.clear()
for episode in channel.get_all_episodes():
description = episode.format_episode_row_markup(include_description)
description_stripped = util.remove_html_tags(episode.description)
iter = self.append()
self.set(iter, \
self.C_URL, episode.url, \
self.C_TITLE, episode.title, \
self.C_FILESIZE_TEXT, self._format_filesize(episode), \
self.C_EPISODE, episode, \
self.C_PUBLISHED_TEXT, episode.cute_pubdate(), \
self.C_DESCRIPTION, description, \
self.C_DESCRIPTION_STRIPPED, description_stripped)
self.update_by_iter(iter, downloading, include_description)
if finish_callback is not None:
finish_callback()
def update_by_urls(self, urls, downloading=None, include_description=False):
for row in self:
if row[self.C_URL] in urls:
self.update_by_iter(row.iter, downloading, include_description)
def update_by_iter(self, iter, downloading=None, include_description=False):
episode = self.get_value(iter, self.C_EPISODE)
if include_description:
icon_size = 32
else:
icon_size = 16
show_bullet = False
show_padlock = False
show_missing = False
status_icon = None
if downloading is not None and downloading(episode):
status_icon = self.ICON_DOWNLOADING
else:
if episode.state == gpodder.STATE_DELETED:
status_icon = self.ICON_DELETED
elif episode.state == gpodder.STATE_NORMAL and \
not episode.is_played:
status_icon = self.ICON_NEW
elif episode.state == gpodder.STATE_DOWNLOADED:
show_bullet = not episode.is_played
show_padlock = episode.is_locked
show_missing = not episode.file_exists()
file_type = episode.file_type()
if file_type == 'audio':
status_icon = self.ICON_AUDIO_FILE
elif file_type == 'video':
status_icon = self.ICON_VIDEO_FILE
else:
status_icon = self.ICON_GENERIC_FILE
if status_icon is not None:
status_icon = util.get_tree_icon(status_icon, show_bullet, \
show_padlock, show_missing, self._icon_cache, icon_size)
self.set(iter, self.C_STATUS_ICON, status_icon)
class PodcastListModel(gtk.ListStore):
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
C_COVER, C_ERROR, C_PILL_VISIBLE = range(8)
def __init__(self, max_image_side):
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
object, gtk.gdk.Pixbuf, str, bool)
self._cover_cache = {}
self._max_image_side = max_image_side
def _resize_pixbuf(self, url, pixbuf):
if pixbuf is None:
return None
return util.resize_pixbuf_keep_ratio(pixbuf, \
self._max_image_side, self._max_image_side, \
url, self._cover_cache) or pixbuf
def _get_cover_image(self, channel):
pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True)
return self._resize_pixbuf(channel.url, pixbuf)
def _get_pill_image(self, channel):
count_downloaded = channel.stat(state=gpodder.STATE_DOWNLOADED)
count_unplayed = channel.stat(state=gpodder.STATE_DOWNLOADED, is_played=False)
if count_unplayed > 0 or count_downloaded > 0:
return draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded))
else:
return None
def _format_description(self, channel):
count_new = channel.stat(state=gpodder.STATE_NORMAL, is_played=False)
title_markup = xml.sax.saxutils.escape(channel.title)
description_markup = xml.sax.saxutils.escape(util.get_first_line(channel.description) or ' ')
d = []
if count_new:
d.append('<span weight="bold">')
d.append(title_markup)
if count_new:
d.append('</span>')
return ''.join(d+['\n', '<small>', description_markup, '</small>'])
def _format_error(self, channel):
if channel.parse_error:
return str(channel.parse_error)
else:
return None
def set_channels(self, channels):
# Clear the model and update the list of podcasts
self.clear()
for channel in channels:
iter = self.append()
self.set(iter, \
self.C_URL, channel.url, \
self.C_CHANNEL, channel, \
self.C_COVER, self._get_cover_image(channel))
self.update_by_iter(iter)
def update_by_urls(self, urls):
# Given a list of URLs, update each matching row
for row in self:
if row[self.C_URL] in urls:
self.update_by_iter(row.iter)
def update_by_iter(self, iter):
# Given a GtkTreeIter, update volatile information
channel = self.get_value(iter, self.C_CHANNEL)
pill_image = self._get_pill_image(channel)
self.set(iter, \
self.C_TITLE, channel.title, \
self.C_DESCRIPTION, self._format_description(channel), \
self.C_ERROR, self._format_error(channel), \
self.C_PILL, pill_image, \
self.C_PILL_VISIBLE, pill_image != None)
def add_cover_by_url(self, url, pixbuf):
# Resize and add the new cover image
pixbuf = self._resize_pixbuf(url, pixbuf)
for row in self:
if row[self.C_URL] == url:
row[self.C_COVER] = pixbuf
break
def delete_cover_by_url(self, url):
# Remove the cover from the model
for row in self:
if row[self.C_URL] == url:
row[self.C_COVER] = None
break
# Remove the cover from the cache
key = (url, self._max_image_side, self._max_image_side)
if key in self._cover_cache:
del self._cover_cache[key]

View File

@ -90,9 +90,10 @@ except Exception, exc:
log('Warning: This probably means your PyGTK installation is too old!')
have_trayicon = False
from libpodcasts import PodcastChannel
from libpodcasts import PodcastListModel
from libpodcasts import EpisodeListModel
from gpodder.model import PodcastChannel
from gpodder.gtkui.model import PodcastListModel
from gpodder.gtkui.model import EpisodeListModel
from gpodder.libgpodder import db
from gpodder.libgpodder import gl
@ -1661,11 +1662,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
key = key.lower()
# columns, as defined in libpodcasts' get model method
# 1 = episode title, 7 = description
columns = (1, 7)
for column in columns:
for column in (EpisodeListModel.C_TITLE, EpisodeListModel.C_DESCRIPTION_STRIPPED):
value = model.get_value( iter, column).lower()
if value.find( key) != -1:
return False

View File

@ -19,41 +19,30 @@
#
# libpodcasts.py -- data classes for gpodder
# thomas perl <thp@perli.net> 20051029
# gpodder.model - Core model classes for gPodder (2009-08-13)
# Based on libpodcasts.py (thp, 2005-10-29)
#
# Contains code based on:
# liblocdbwriter.py (2006-01-09)
# liblocdbreader.py (2006-01-10)
#
import gtk
import gpodder
from gpodder import util
from gpodder import feedcore
from gpodder import services
from gpodder import draw
from gpodder import resolver
from gpodder import corestats
from gpodder.liblogger import log
import os.path
import os
import glob
import shutil
import sys
import urllib
import urlparse
import time
import datetime
import rfc822
import hashlib
import xml.dom.minidom
import feedparser
from xml.sax import saxutils
import xml.sax.saxutils
_ = gpodder.gettext
@ -717,10 +706,10 @@ class PodcastEpisode(PodcastModelObject):
def format_episode_row_markup(self, include_description=False):
if include_description:
return '%s\n<small>%s</small>' % (saxutils.escape(self.title),
saxutils.escape(self.one_line_description()))
return '%s\n<small>%s</small>' % (xml.sax.saxutils.escape(self.title),
xml.sax.saxutils.escape(self.one_line_description()))
else:
return saxutils.escape(self.title)
return xml.sax.saxutils.escape(self.title)
@property
def title_markup(self):
@ -983,205 +972,3 @@ class PodcastEpisode(PodcastModelObject):
return True
return False
class EpisodeListModel(gtk.ListStore):
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
C_PUBLISHED_TEXT, C_DESCRIPTION, C_DESCRIPTION_STRIPPED, \
= range(8)
def __init__(self):
gtk.ListStore.__init__(self, str, str, str, object, \
gtk.gdk.Pixbuf, str, str, str)
self._icon_cache = {}
if gpodder.interface == gpodder.MAEMO:
self.ICON_AUDIO_FILE = 'gnome-mime-audio-mp3'
self.ICON_VIDEO_FILE = 'gnome-mime-video-mp4'
self.ICON_GENERIC_FILE = 'text-x-generic'
self.ICON_DOWNLOADING = 'qgn_toolb_messagin_moveto'
self.ICON_DELETED = 'qgn_toolb_gene_deletebutton'
self.ICON_NEW = 'qgn_list_gene_favor'
else:
self.ICON_AUDIO_FILE = 'audio-x-generic'
self.ICON_VIDEO_FILE = 'video-x-generic'
self.ICON_GENERIC_FILE = 'text-x-generic'
self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
self.ICON_DELETED = gtk.STOCK_DELETE
self.ICON_NEW = gtk.STOCK_ABOUT
def _format_filesize(self, episode):
if episode.length > 0:
return util.format_filesize(episode.length, 1)
else:
return None
def update_from_channel(self, channel, downloading=None, \
include_description=False, finish_callback=None):
"""
Return a gtk.ListStore containing episodes for the given channel.
Downloading should be a callback.
include_description should be a boolean value (True if description
is to be added to the episode row, or False if not)
"""
self.clear()
for episode in channel.get_all_episodes():
description = episode.format_episode_row_markup(include_description)
description_stripped = util.remove_html_tags(episode.description)
iter = self.append()
self.set(iter, \
self.C_URL, episode.url, \
self.C_TITLE, episode.title, \
self.C_FILESIZE_TEXT, self._format_filesize(episode), \
self.C_EPISODE, episode, \
self.C_PUBLISHED_TEXT, episode.cute_pubdate(), \
self.C_DESCRIPTION, description, \
self.C_DESCRIPTION_STRIPPED, description_stripped)
self.update_by_iter(iter, downloading, include_description)
if finish_callback is not None:
finish_callback()
def update_by_urls(self, urls, downloading=None, include_description=False):
for row in self:
if row[self.C_URL] in urls:
self.update_by_iter(row.iter, downloading, include_description)
def update_by_iter(self, iter, downloading=None, include_description=False):
episode = self.get_value(iter, self.C_EPISODE)
if include_description:
icon_size = 32
else:
icon_size = 16
show_bullet = False
show_padlock = False
show_missing = False
status_icon = None
if downloading is not None and downloading(episode):
status_icon = self.ICON_DOWNLOADING
else:
if episode.state == gpodder.STATE_DELETED:
status_icon = self.ICON_DELETED
elif episode.state == gpodder.STATE_NORMAL and \
not episode.is_played:
status_icon = self.ICON_NEW
elif episode.state == gpodder.STATE_DOWNLOADED:
show_bullet = not episode.is_played
show_padlock = episode.is_locked
show_missing = not episode.file_exists()
file_type = episode.file_type()
if file_type == 'audio':
status_icon = self.ICON_AUDIO_FILE
elif file_type == 'video':
status_icon = self.ICON_VIDEO_FILE
else:
status_icon = self.ICON_GENERIC_FILE
if status_icon is not None:
status_icon = util.get_tree_icon(status_icon, show_bullet, \
show_padlock, show_missing, self._icon_cache, icon_size)
self.set(iter, self.C_STATUS_ICON, status_icon)
class PodcastListModel(gtk.ListStore):
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
C_COVER, C_ERROR, C_PILL_VISIBLE = range(8)
def __init__(self, max_image_side):
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
object, gtk.gdk.Pixbuf, str, bool)
self._cover_cache = {}
self._max_image_side = max_image_side
def _resize_pixbuf(self, url, pixbuf):
if pixbuf is None:
return None
return util.resize_pixbuf_keep_ratio(pixbuf, \
self._max_image_side, self._max_image_side, \
url, self._cover_cache) or pixbuf
def _get_cover_image(self, channel):
pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True)
return self._resize_pixbuf(channel.url, pixbuf)
def _get_pill_image(self, channel):
count_downloaded = channel.stat(state=gpodder.STATE_DOWNLOADED)
count_unplayed = channel.stat(state=gpodder.STATE_DOWNLOADED, is_played=False)
if count_unplayed > 0 or count_downloaded > 0:
return draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded))
else:
return None
def _format_description(self, channel):
count_new = channel.stat(state=gpodder.STATE_NORMAL, is_played=False)
title_markup = saxutils.escape(channel.title)
description_markup = saxutils.escape(util.get_first_line(channel.description) or ' ')
d = []
if count_new:
d.append('<span weight="bold">')
d.append(title_markup)
if count_new:
d.append('</span>')
return ''.join(d+['\n', '<small>', description_markup, '</small>'])
def _format_error(self, channel):
if channel.parse_error:
return str(channel.parse_error)
else:
return None
def set_channels(self, channels):
# Clear the model and update the list of podcasts
self.clear()
for channel in channels:
iter = self.append()
self.set(iter, \
self.C_URL, channel.url, \
self.C_CHANNEL, channel, \
self.C_COVER, self._get_cover_image(channel))
self.update_by_iter(iter)
def update_by_urls(self, urls):
# Given a list of URLs, update each matching row
for row in self:
if row[self.C_URL] in urls:
self.update_by_iter(row.iter)
def update_by_iter(self, iter):
# Given a GtkTreeIter, update volatile information
channel = self.get_value(iter, self.C_CHANNEL)
pill_image = self._get_pill_image(channel)
self.set(iter, \
self.C_TITLE, channel.title, \
self.C_DESCRIPTION, self._format_description(channel), \
self.C_ERROR, self._format_error(channel), \
self.C_PILL, pill_image, \
self.C_PILL_VISIBLE, pill_image != None)
def add_cover_by_url(self, url, pixbuf):
# Resize and add the new cover image
pixbuf = self._resize_pixbuf(url, pixbuf)
for row in self:
if row[self.C_URL] == url:
row[self.C_COVER] = pixbuf
break
def delete_cover_by_url(self, url):
# Remove the cover from the model
for row in self:
if row[self.C_URL] == url:
row[self.C_COVER] = None
break
# Remove the cover from the cache
key = (url, self._max_image_side, self._max_image_side)
if key in self._cover_cache:
del self._cover_cache[key]