remove unused variable
[mygpo.git] / mygpo / api / simple.py
blob6faf48171acda7fec2d936ef15d6f159384f81d7
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 import string
19 from itertools import islice
21 from couchdbkit.exceptions import ResourceNotFound
23 from django.shortcuts import render_to_response
24 from django.template import RequestContext
25 from django.core.cache import cache
27 from mygpo.api.basic_auth import require_valid_user, check_username
28 from django.http import HttpResponse, HttpResponseBadRequest
29 from django.views.decorators.cache import cache_page
30 from mygpo.core import models
31 from mygpo.users.models import Suggestions
32 from mygpo.api.models import Device, Podcast
33 from mygpo.api.opml import Exporter, Importer
34 from mygpo.api.httpresponse import JsonResponse
35 from mygpo.api.sanitizing import sanitize_urls
36 from mygpo.directory.toplist import PodcastToplist
37 from mygpo.directory.models import ExamplePodcasts
38 from mygpo.api.advanced.directory import podcast_data
39 from django.views.decorators.csrf import csrf_exempt
40 from django.shortcuts import get_object_or_404
41 from django.contrib.sites.models import RequestSite
42 from mygpo.directory.search import search_podcasts
43 from mygpo.log import log
44 from django.utils.translation import ugettext as _
45 from mygpo.decorators import allowed_methods
46 from mygpo.utils import parse_range
47 from mygpo import migrate
50 try:
51 import simplejson as json
52 except ImportError:
53 import json
56 ALLOWED_FORMATS = ('txt', 'opml', 'json', 'jsonp', 'xml')
58 def check_format(fn):
59 def tmp(request, format, *args, **kwargs):
60 if not format in ALLOWED_FORMATS:
61 return HttpResponseBadRequest('Invalid format')
63 return fn(request, *args, format=format, **kwargs)
64 return tmp
67 @csrf_exempt
68 @require_valid_user
69 @check_username
70 @check_format
71 @allowed_methods(['GET', 'PUT', 'POST'])
72 def subscriptions(request, username, device_uid, format):
74 if request.method == 'GET':
75 title = _('%(username)s\'s Subscription List') % {'username': username}
76 subscriptions = get_subscriptions(request.user, device_uid)
77 return format_podcast_list(subscriptions, format, title, jsonp_padding=request.GET.get('jsonp'))
79 elif request.method in ('PUT', 'POST'):
80 subscriptions = parse_subscription(request.raw_post_data, format)
81 return set_subscriptions(subscriptions, request.user, device_uid)
84 @csrf_exempt
85 @require_valid_user
86 @check_username
87 @check_format
88 @allowed_methods(['GET'])
89 def all_subscriptions(request, username, format):
90 user = migrate.get_or_migrate_user(request.user)
91 subscriptions = user.get_subscribed_podcasts()
92 title = _('%(username)s\'s Subscription List') % {'username': username}
93 return format_podcast_list(subscriptions, format, title)
96 def format_podcast_list(obj_list, format, title, get_podcast=None,
97 json_map=lambda x: x.url, jsonp_padding=None,
98 xml_template=None, request=None, template_args={}):
99 """
100 Formats a list of podcasts for use in a API response
102 obj_list is a list of podcasts or objects that contain podcasts
103 format is one if txt, opml or json
104 title is a label of the list
105 if obj_list is a list of objects containing podcasts, get_podcast is the
106 function used to get the podcast out of the each of these objects
107 json_map is a function returning the contents of an object (from obj_list)
108 that should be contained in the result (only used for format='json')
111 def default_get_podcast(p):
112 return p.get_podcast()
114 get_podcast = get_podcast or default_get_podcast
116 if format == 'txt':
117 podcasts = map(get_podcast, obj_list)
118 s = '\n'.join([p.url for p in podcasts] + [''])
119 return HttpResponse(s, mimetype='text/plain')
121 elif format == 'opml':
122 podcasts = map(get_podcast, obj_list)
123 exporter = Exporter(title)
124 opml = exporter.generate(podcasts)
125 return HttpResponse(opml, mimetype='text/xml')
127 elif format == 'json':
128 objs = map(json_map, obj_list)
129 return JsonResponse(objs)
131 elif format == 'jsonp':
132 ALLOWED_FUNCNAME = string.letters + string.digits + '_'
134 if not jsonp_padding:
135 return HttpResponseBadRequest('For a JSONP response, specify the name of the callback function in the jsonp parameter')
137 if any(x not in ALLOWED_FUNCNAME for x in jsonp_padding):
138 return HttpResponseBadRequest('JSONP padding can only contain the characters %(char)s' % {'char': ALLOWED_FUNCNAME})
140 objs = map(json_map, obj_list)
141 return JsonResponse(objs, jsonp_padding=jsonp_padding)
143 elif format == 'xml':
144 if None in (xml_template, request):
145 return HttpResponseBadRequest('XML is not a valid format for this request')
147 podcasts = map(json_map, obj_list)
148 template_args.update({'podcasts': podcasts})
150 return render_to_response(xml_template, template_args, context_instance=RequestContext(request),
151 mimetype='application/xml')
153 else:
154 return None
157 def get_subscriptions(user, device_uid):
158 device = get_object_or_404(Device, uid=device_uid, user=user, deleted=False)
159 device = migrate.get_or_migrate_device(device)
161 return device.get_subscribed_podcasts()
164 def parse_subscription(raw_post_data, format):
165 if format == 'txt':
166 urls = raw_post_data.split('\n')
168 elif format == 'opml':
169 begin = raw_post_data.find('<?xml')
170 end = raw_post_data.find('</opml>') + 7
171 i = Importer(content=raw_post_data[begin:end])
172 urls = [p['url'] for p in i.items]
174 elif format == 'json':
175 begin = raw_post_data.find('[')
176 end = raw_post_data.find(']') + 1
177 urls = json.loads(raw_post_data[begin:end])
179 else:
180 return []
183 urls = sanitize_urls(urls)
184 urls = filter(None, urls)
185 urls = set(urls)
186 return urls
189 def set_subscriptions(urls, user, device_uid):
190 device, created = Device.objects.get_or_create(user=user, uid=device_uid)
192 # undelete a previously deleted device
193 if device.deleted:
194 device.deleted = False
195 device.save()
197 dev = migrate.get_or_migrate_device(device)
198 old = [p.url for p in dev.get_subscribed_podcasts()]
199 new = [p for p in urls if p not in old]
200 rem = [p for p in old if p not in urls]
202 for r in rem:
203 p, created = Podcast.objects.get_or_create(url=r)
204 p = migrate.get_or_migrate_podcast(p)
205 try:
206 p.unsubscribe(device)
207 except Exception as e:
208 log('Simple API: %(username)s: Could not remove subscription for podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
209 {'username': user.username, 'podcast_url': r, 'device_id': device.id, 'exception': e})
211 for n in new:
212 p, created = Podcast.objects.get_or_create(url=n)
213 p = migrate.get_or_migrate_podcast(p)
214 try:
215 p.subscribe(device)
216 except Exception as e:
217 log('Simple API: %(username)s: Could not add subscription for podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
218 {'username': user.username, 'podcast_url': n, 'device_id': device.id, 'exception': e})
220 # Only an empty response is a successful response
221 return HttpResponse('', mimetype='text/plain')
224 @check_format
225 @allowed_methods(['GET'])
226 @cache_page(60 * 60)
227 def toplist(request, count, format):
228 count = parse_range(count, 1, 100, 100)
230 toplist = PodcastToplist()
231 entries = toplist[:count]
232 domain = RequestSite(request).domain
234 try:
235 scale = int(request.GET.get('scale_logo', 64))
236 except (TypeError, ValueError):
237 return HttpResponseBadRequest('scale_logo has to be a numeric value')
239 if scale not in range(1, 257):
240 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
243 def get_podcast(t):
244 old_pos, podcast = t
245 return podcast.get_podcast()
247 def json_map(t):
248 old_pos, podcast = t
249 podcast.old_pos = old_pos
251 p = podcast_data(podcast, domain, scale)
252 p.update(dict(
253 subscribers= podcast.subscriber_count(),
254 subscribers_last_week= podcast.prev_subscriber_count(),
255 position_last_week= podcast.old_pos,
257 return p
259 title = _('gpodder.net - Top %(count)d') % {'count': len(entries)}
260 return format_podcast_list(entries,
261 format,
262 title,
263 get_podcast=get_podcast,
264 json_map=json_map,
265 jsonp_padding=request.GET.get('jsonp', ''),
266 xml_template='podcasts.xml',
267 request=request,
271 @check_format
272 @allowed_methods(['GET'])
273 def search(request, format):
275 NUM_RESULTS = 20
277 query = request.GET.get('q', '').encode('utf-8')
279 try:
280 scale = int(request.GET.get('scale_logo', 64))
281 except (TypeError, ValueError):
282 return HttpResponseBadRequest('scale_logo has to be a numeric value')
284 if scale not in range(1, 257):
285 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
287 if not query:
288 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
290 results, total = search_podcasts(q=query, limit=NUM_RESULTS)
292 title = _('gpodder.net - Search')
293 domain = RequestSite(request).domain
294 p_data = lambda p: podcast_data(p, domain, scale)
295 return format_podcast_list(results, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp', ''), xml_template='podcasts.xml', request=request)
298 @require_valid_user
299 @check_format
300 @allowed_methods(['GET'])
301 def suggestions(request, count, format):
302 count = parse_range(count, 1, 100, 100)
304 suggestion_obj = Suggestions.for_user_oldid(request.user.id)
305 suggestions = suggestion_obj.get_podcasts(count)
306 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
307 domain = RequestSite(request).domain
308 p_data = lambda p: podcast_data(p, domain)
309 return format_podcast_list(suggestions, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp'))
312 @check_format
313 @allowed_methods(['GET'])
314 @cache_page(60 * 60)
315 def example_podcasts(request, format):
317 podcasts = cache.get('example-podcasts', None)
319 try:
320 scale = int(request.GET.get('scale_logo', 64))
321 except (TypeError, ValueError):
322 return HttpResponseBadRequest('scale_logo has to be a numeric value')
324 if scale not in range(1, 257):
325 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
328 if not podcasts:
330 try:
331 examples = ExamplePodcasts.get('example_podcasts')
332 ids = examples.podcast_ids
333 podcasts = list(models.Podcast.get_multi(ids))
334 cache.set('example-podcasts', podcasts)
336 except ResourceNotFound:
337 podcasts = []
339 title = 'gPodder Podcast Directory'
340 domain = RequestSite(request).domain
341 p_data = lambda p: podcast_data(p, domain, scale)
342 return format_podcast_list(
343 podcasts,
344 format,
345 title,
346 json_map=p_data,
347 xml_template='podcasts.xml',
348 request=request,