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
28 from gpodder
import model
29 from gpodder
import util
32 # For Python < 2.6, we use the "simplejson" add-on module
33 # XXX: Mark as dependency
34 import simplejson
as json
36 # Python 2.6 already ships with a nice "json" module
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).
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.
67 msg
= email
.message_from_string(s
)
69 value
= msg
.get_param(param
, header
=header
)
70 decoded_list
= email
.Header
.decode_header(value
)
72 for part
, encoding
in decoded_list
:
74 value
.append(part
.decode(encoding
))
76 value
.append(unicode(part
))
77 return u
''.join(value
)
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.
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
))
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
):
104 self
.cache
= json
.load(open(self
.cache_file
, 'r'))
110 def commit_cache(self
):
111 json
.dump(self
.cache
, open(self
.cache_file
, 'w'))
113 def get_coverart(self
):
115 key
= ':'.join((self
.username
, 'avatar_url'))
116 if key
in self
.cache
:
117 return self
.cache
[key
]
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
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."""
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'])
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
:
147 self
.cache
[url
] = get_metadata(url
)
150 filesize
, filetype
, filename
= self
.cache
[url
]
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')),
157 'length': int(filesize
),
158 'mimetype': filetype
,
159 'guid': track
.get('permalink', track
.get('id')),
160 'pubDate': soundcloud_parsedate(track
.get('created_at', None)),
165 class SoundcloudFeed(object):
166 URL_REGEX
= re
.compile('http://([a-z]+\.)?soundcloud\.com/([^/]+)$', re
.I
)
169 def handle_url(cls
, url
):
170 m
= cls
.URL_REGEX
.match(url
)
172 subdomain
, username
= m
.groups()
175 def __init__(self
, username
):
176 self
.username
= username
177 self
.sc_user
= SoundcloudUser(username
)
180 return _('%s on Soundcloud') % self
.username
183 return self
.sc_user
.get_coverart()
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
]
196 episode
= model
.PodcastEpisode(channel
)
197 episode
.update_from_dict(track
)
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
)
210 return _('%s\'s favorites on Soundcloud') % self
.username
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
]
223 episode
= model
.PodcastEpisode(channel
)
224 episode
.update_from_dict(track
)
229 # Register our URL handlers
230 model
.register_custom_handler(SoundcloudFeed
)
231 model
.register_custom_handler(SoundcloudFavFeed
)