Delay live search to be more responsive
[gpodder.git] / src / gpodder / soundcloud.py
blob13a0dd705940a89a64110c4403a5c00bda846127
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # gPodder - A media aggregator and podcast client
5 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
7 # gPodder is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # gPodder is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # Soundcloud.com API client module for gPodder
22 # Thomas Perl <thpinfo.com>; 2009-11-03
24 import gpodder
26 _ = gpodder.gettext
28 from gpodder import model
29 from gpodder import util
31 try:
32 # For Python < 2.6, we use the "simplejson" add-on module
33 # XXX: Mark as dependency
34 import simplejson as json
35 except ImportError:
36 # Python 2.6 already ships with a nice "json" module
37 import json
39 import os
40 import time
42 import re
43 import email
44 import email.Header
47 # gPodder's consumer key for the Soundcloud API
48 CONSUMER_KEY = 'zrweghtEtnZLpXf3mlm8mQ'
51 def soundcloud_parsedate(s):
52 """Parse a string into a unix timestamp
54 Only strings provided by Soundcloud's API are
55 parsed with this function (2009/11/03 13:37:00).
56 """
57 m = re.match(r'(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2})', s)
58 return time.mktime([int(x) for x in m.groups()]+[0, 0, -1])
60 def get_param(s, param='filename', header='content-disposition'):
61 """Get a parameter from a string of headers
63 By default, this gets the "filename" parameter of
64 the content-disposition header. This works fine
65 for downloads from Soundcloud.
66 """
67 msg = email.message_from_string(s)
68 if header in msg:
69 value = msg.get_param(param, header=header)
70 decoded_list = email.Header.decode_header(value)
71 value = []
72 for part, encoding in decoded_list:
73 if encoding:
74 value.append(part.decode(encoding))
75 else:
76 value.append(unicode(part))
77 return u''.join(value)
79 return None
81 def get_metadata(url):
82 """Get file download metadata
84 Returns a (size, type, name) from the given download
85 URL. Will use the network connection to determine the
86 metadata via the HTTP header fields.
87 """
88 track_fp = util.urlopen(url)
89 headers = track_fp.info()
90 filesize = headers['content-length'] or '0'
91 filetype = headers['content-type'] or 'application/octet-stream'
92 headers_s = '\n'.join('%s:%s'%(k,v) for k, v in headers.items())
93 filename = get_param(headers_s) or os.path.basename(os.path.dirname(url))
94 track_fp.close()
95 return filesize, filetype, filename
98 class SoundcloudUser(object):
99 def __init__(self, username):
100 self.username = username
101 self.cache_file = os.path.join(gpodder.home, 'soundcloud.cache')
102 if os.path.exists(self.cache_file):
103 try:
104 self.cache = json.load(open(self.cache_file, 'r'))
105 except:
106 self.cache = {}
107 else:
108 self.cache = {}
110 def commit_cache(self):
111 json.dump(self.cache, open(self.cache_file, 'w'))
113 def get_coverart(self):
114 global CONSUMER_KEY
115 key = ':'.join((self.username, 'avatar_url'))
116 if key in self.cache:
117 return self.cache[key]
119 image = None
120 try:
121 json_url = 'http://api.soundcloud.com/users/%s.json?consumer_key=%s' % (self.username, CONSUMER_KEY)
122 user_info = json.load(util.urlopen(json_url))
123 image = user_info.get('avatar_url', None)
124 self.cache[key] = image
125 finally:
126 self.commit_cache()
128 return image
130 def get_tracks(self, feed):
131 """Get a generator of tracks from a SC user
133 The generator will give you a dictionary for every
134 track it can find for its user."""
135 global CONSUMER_KEY
136 try:
137 json_url = 'http://api.soundcloud.com/users/%(user)s/%(feed)s.json?filter=downloadable&consumer_key=%(consumer_key)s' \
138 % { "user":self.username, "feed":feed, "consumer_key": CONSUMER_KEY }
139 tracks = (track for track in json.load(util.urlopen(json_url)) \
140 if track['downloadable'])
142 for track in tracks:
143 # Prefer stream URL (MP3), fallback to download URL
144 url = track.get('stream_url', track['download_url'])
145 if url not in self.cache:
146 try:
147 self.cache[url] = get_metadata(url)
148 except:
149 continue
150 filesize, filetype, filename = self.cache[url]
152 yield {
153 'title': track.get('title', track.get('permalink', _('Unknown track'))),
154 'link': track.get('permalink_url', 'http://soundcloud.com/'+self.username),
155 'description': track.get('description', _('No description available')),
156 'url': url,
157 'length': int(filesize),
158 'mimetype': filetype,
159 'guid': track.get('permalink', track.get('id')),
160 'pubDate': soundcloud_parsedate(track.get('created_at', None)),
162 finally:
163 self.commit_cache()
165 class SoundcloudFeed(object):
166 URL_REGEX = re.compile('http://([a-z]+\.)?soundcloud\.com/([^/]+)$', re.I)
168 @classmethod
169 def handle_url(cls, url):
170 m = cls.URL_REGEX.match(url)
171 if m is not None:
172 subdomain, username = m.groups()
173 return cls(username)
175 def __init__(self, username):
176 self.username = username
177 self.sc_user = SoundcloudUser(username)
179 def get_title(self):
180 return _('%s on Soundcloud') % self.username
182 def get_image(self):
183 return self.sc_user.get_coverart()
185 def get_link(self):
186 return 'http://soundcloud.com/%s' % self.username
188 def get_description(self):
189 return _('Tracks published by %s on Soundcloud.') % self.username
191 def get_new_episodes(self, channel, guids):
192 tracks = [t for t in self.sc_user.get_tracks('tracks') \
193 if t['guid'] not in guids]
195 for track in tracks:
196 episode = model.PodcastEpisode(channel)
197 episode.update_from_dict(track)
198 episode.save()
200 return len(tracks)
202 class SoundcloudFavFeed(SoundcloudFeed):
203 URL_REGEX = re.compile('http://([a-z]+\.)?soundcloud\.com/([^/]+)/favorites', re.I)
206 def __init__(self, username):
207 super(SoundcloudFavFeed,self).__init__(username)
209 def get_title(self):
210 return _('%s\'s favorites on Soundcloud') % self.username
212 def get_link(self):
213 return 'http://soundcloud.com/%s/favorites' % self.username
215 def get_description(self):
216 return _('Tracks favorited by %s on Soundcloud.') % self.username
218 def get_new_episodes(self, channel, guids):
219 tracks = [t for t in self.sc_user.get_tracks('favorites') \
220 if t['guid'] not in guids]
222 for track in tracks:
223 episode = model.PodcastEpisode(channel)
224 episode.update_from_dict(track)
225 episode.save()
227 return len(tracks)
229 # Register our URL handlers
230 model.register_custom_handler(SoundcloudFeed)
231 model.register_custom_handler(SoundcloudFavFeed)