Add 2010 to the years in copyright notice
[gpodder.git] / src / gpodder / opml.py
blob2607f6472a262b0e2d3b8947827a866dd4703d0b
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 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 xml.dom.minidom
43 import urllib
44 import urllib2
45 import os.path
46 import os
47 import platform
48 import shutil
50 from email.Utils import formatdate
51 import gpodder
54 class Importer(object):
55 """
56 Helper class to import an OPML feed from protocols
57 supported by urllib2 (e.g. HTTP) and return a GTK
58 ListStore that can be displayed in the GUI.
60 This class should support standard OPML feeds and
61 contains workarounds to support odeo.com feeds.
62 """
64 VALID_TYPES = ( 'rss', 'link' )
66 def __init__( self, url):
67 """
68 Parses the OPML feed from the given URL into
69 a local data structure containing channel metadata.
70 """
71 self.items = []
72 try:
73 if os.path.exists(url):
74 doc = xml.dom.minidom.parse(url)
75 else:
76 doc = xml.dom.minidom.parseString(util.urlopen(url).read())
78 for outline in doc.getElementsByTagName('outline'):
79 if outline.getAttribute('type') in self.VALID_TYPES and outline.getAttribute('xmlUrl') or outline.getAttribute('url'):
80 channel = {
81 'url': outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
82 'title': outline.getAttribute('title') or outline.getAttribute('text') or outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
83 'description': outline.getAttribute('text') or outline.getAttribute('xmlUrl') or outline.getAttribute('url'),
86 if channel['description'] == channel['title']:
87 channel['description'] = channel['url']
89 for attr in ( 'url', 'title', 'description' ):
90 channel[attr] = channel[attr].strip()
92 self.items.append( channel)
93 if not len(self.items):
94 log( 'OPML import finished, but no items found: %s', url, sender = self)
95 except:
96 log( 'Cannot import OPML from URL: %s', url, traceback=True, sender = self)
100 class Exporter(object):
102 Helper class to export a list of channel objects
103 to a local file in OPML 1.1 format.
105 See www.opml.org for the OPML specification.
108 FEED_TYPE = 'rss'
110 def __init__( self, filename):
111 if filename.endswith( '.opml') or filename.endswith( '.xml'):
112 self.filename = filename
113 else:
114 self.filename = '%s.opml' % ( filename, )
116 def create_node( self, doc, name, content):
118 Creates a simple XML Element node in a document
119 with tag name "name" and text content "content",
120 as in <name>content</name> and returns the element.
122 node = doc.createElement( name)
123 node.appendChild( doc.createTextNode( content))
124 return node
126 def create_outline( self, doc, channel):
128 Creates a OPML outline as XML Element node in a
129 document for the supplied channel.
131 outline = doc.createElement( 'outline')
132 outline.setAttribute( 'title', channel.title)
133 outline.setAttribute( 'text', channel.description)
134 outline.setAttribute( 'xmlUrl', channel.url)
135 outline.setAttribute( 'type', self.FEED_TYPE)
136 return outline
138 def write( self, channels):
140 Creates a XML document containing metadata for each
141 channel object in the "channels" parameter, which
142 should be a list of channel objects.
144 OPML 2.0 specification: http://www.opml.org/spec2
146 Returns True on success or False when there was an
147 error writing the file.
149 doc = xml.dom.minidom.Document()
151 opml = doc.createElement('opml')
152 opml.setAttribute('version', '2.0')
153 doc.appendChild(opml)
155 head = doc.createElement( 'head')
156 head.appendChild( self.create_node( doc, 'title', 'gPodder subscriptions'))
157 head.appendChild( self.create_node( doc, 'dateCreated', formatdate(localtime=True)))
158 opml.appendChild( head)
160 body = doc.createElement( 'body')
161 for channel in channels:
162 body.appendChild( self.create_outline( doc, channel))
163 opml.appendChild( body)
165 try:
166 data = doc.toprettyxml(encoding='utf-8', indent=' ', newl=os.linesep)
167 # We want to have at least 512 KiB free disk space after
168 # saving the opml data, if this is not possible, don't
169 # try to save the new file, but keep the old one so we
170 # don't end up with a clobbed, empty opml file.
171 FREE_DISK_SPACE_AFTER = 1024*512
172 available = util.get_free_disk_space(os.path.dirname(self.filename))
173 if available < 2*len(data)+FREE_DISK_SPACE_AFTER and not gpodder.win32:
174 # FIXME: get_free_disk_space still unimplemented for win32
175 log('Not enough free disk space to save channel list to %s', self.filename, sender = self)
176 return False
177 fp = open(self.filename+'.tmp', 'w')
178 fp.write(data)
179 fp.close()
180 if gpodder.win32:
181 # Win32 does not support atomic rename with os.rename
182 shutil.move(self.filename+'.tmp', self.filename)
183 else:
184 os.rename(self.filename+'.tmp', self.filename)
185 except:
186 log('Could not open file for writing: %s', self.filename, sender=self, traceback=True)
187 return False
189 return True