Properly update existing episodes (bug 211)
[gpodder.git] / src / gpodder / opml.py
blobde5caa2b7ae95bf67a1ee01cfac4f71f175a7c81
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # opml.py -- OPML import and export functionality
23 # Thomas Perl <thp@perli.net> 2007-08-19
25 # based on: libopmlreader.py (2006-06-13)
26 # libopmlwriter.py (2005-12-08)
29 """OPML import and export functionality
31 This module contains helper classes to import subscriptions
32 from OPML files on the web and to export a list of channel
33 objects to valid OPML 1.1 files that can be used to backup
34 or distribute gPodder's channel subscriptions.
35 """
37 from gpodder.liblogger import log
39 from gpodder import util
41 import gtk
42 import gobject
44 import xml.dom.minidom
45 import xml.sax.saxutils
47 import urllib
48 import urllib2
49 import os.path
50 import os
52 import datetime
53 import gpodder
56 class Importer(object):
57 """
58 Helper class to import an OPML feed from protocols
59 supported by urllib2 (e.g. HTTP) and return a GTK
60 ListStore that can be displayed in the GUI.
62 This class should support standard OPML feeds and
63 contains workarounds to support odeo.com feeds.
64 """
66 VALID_TYPES = ( 'rss', 'link' )
68 def read_url( self, url):
69 request = urllib2.Request( url, headers = {'User-agent': gpodder.user_agent})
70 return urllib2.urlopen( request).read()
72 def __init__( self, url):
73 """
74 Parses the OPML feed from the given URL into
75 a local data structure containing channel metadata.
76 """
77 self.items = []
78 try:
79 if url.startswith('/'):
80 # assume local filename
81 if os.path.exists(url):
82 doc = xml.dom.minidom.parse( url)
83 else:
84 log('Empty/non-existing OPML file', sender=self)
85 return
86 else:
87 doc = xml.dom.minidom.parseString( self.read_url( url))
89 for outline in doc.getElementsByTagName('outline'):
90 if outline.getAttribute('type') in self.VALID_TYPES and outline.getAttribute('xmlUrl') or outline.getAttribute('url'):
91 channel = {
92 'url': outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
93 'title': outline.getAttribute('title') or outline.getAttribute('text') or outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
94 'description': outline.getAttribute('text') or outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
97 if channel['description'] == channel['title']:
98 channel['description'] = channel['url']
100 for attr in ( 'url', 'title', 'description' ):
101 channel[attr] = channel[attr].strip()
103 self.items.append( channel)
104 if not len(self.items):
105 log( 'OPML import finished, but no items found: %s', url, sender = self)
106 except:
107 log( 'Cannot import OPML from URL: %s', url, traceback=True, sender = self)
109 def format_channel( self, channel):
111 Formats a channel dictionary (as populated by the
112 constructor) into a Pango markup string, suitable
113 for output in GTK widgets.
115 The resulting string contains the title and description.
117 return '<b>%s</b>\n<span size="small">%s</span>' % ( xml.sax.saxutils.escape( urllib.unquote_plus( channel['title'])), xml.sax.saxutils.escape( channel['description']), )
119 def get_model( self):
121 Returns a gtk.ListStore with three columns:
123 - a bool that is initally set to False
124 - a descriptive Pango markup string created
125 by calling self.format_channel()
126 - the URL of the channel as string
128 model = gtk.ListStore( gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING)
130 for channel in self.items:
131 model.append( [ False, self.format_channel( channel), channel['url'] ])
133 return model
137 class Exporter(object):
139 Helper class to export a list of channel objects
140 to a local file in OPML 1.1 format.
142 See www.opml.org for the OPML specification.
145 FEED_TYPE = 'rss'
147 def __init__( self, filename):
148 if filename.endswith( '.opml') or filename.endswith( '.xml'):
149 self.filename = filename
150 else:
151 self.filename = '%s.opml' % ( filename, )
153 def create_node( self, doc, name, content):
155 Creates a simple XML Element node in a document
156 with tag name "name" and text content "content",
157 as in <name>content</name> and returns the element.
159 node = doc.createElement( name)
160 node.appendChild( doc.createTextNode( content))
161 return node
163 def create_outline( self, doc, channel):
165 Creates a OPML outline as XML Element node in a
166 document for the supplied channel.
168 outline = doc.createElement( 'outline')
169 outline.setAttribute( 'title', channel.title)
170 outline.setAttribute( 'text', channel.description)
171 outline.setAttribute( 'xmlUrl', channel.url)
172 outline.setAttribute( 'type', self.FEED_TYPE)
173 return outline
175 def write( self, channels):
177 Creates a XML document containing metadata for each
178 channel object in the "channels" parameter, which
179 should be a list of channel objects.
181 Returns True on success or False when there was an
182 error writing the file.
184 doc = xml.dom.minidom.Document()
186 opml = doc.createElement( 'opml')
187 opml.setAttribute( 'version', '1.1')
188 doc.appendChild( opml)
190 head = doc.createElement( 'head')
191 head.appendChild( self.create_node( doc, 'title', 'gPodder subscriptions'))
192 head.appendChild( self.create_node( doc, 'dateCreated', datetime.datetime.now().ctime()))
193 opml.appendChild( head)
195 body = doc.createElement( 'body')
196 for channel in channels:
197 body.appendChild( self.create_outline( doc, channel))
198 opml.appendChild( body)
200 try:
201 data = doc.toprettyxml(encoding='utf-8', indent=' ', newl=os.linesep)
202 # We want to have at least 512 KiB free disk space after
203 # saving the opml data, if this is not possible, don't
204 # try to save the new file, but keep the old one so we
205 # don't end up with a clobbed, empty opml file.
206 FREE_DISK_SPACE_AFTER = 1024*512
207 if util.get_free_disk_space(os.path.dirname(self.filename)) < 2*len(data)+FREE_DISK_SPACE_AFTER:
208 log('Not enough free disk space to save channel list to %s', self.filename, sender = self)
209 return False
210 fp = open(self.filename+'.tmp', 'w')
211 fp.write(data)
212 fp.close()
213 os.rename(self.filename+'.tmp', self.filename)
214 except:
215 log( 'Could not open file for writing: %s', self.filename, sender = self)
216 return False
218 return True