[Directory] migrate example podcasts to Django ORM
[mygpo.git] / mygpo / api / simple.py
blobe22205c3e397215f7a035e8132d73c958ec4bd21
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
20 from functools import wraps
22 from couchdbkit.exceptions import ResourceNotFound
24 from django.shortcuts import render
25 from django.core.cache import cache
26 from django.http import HttpResponse, HttpResponseBadRequest
27 from django.views.decorators.cache import cache_page
28 from django.views.decorators.csrf import csrf_exempt
29 from django.views.decorators.cache import never_cache
30 from django.contrib.sites.models import RequestSite
31 from django.utils.translation import ugettext as _
33 from mygpo.api.basic_auth import require_valid_user, check_username
34 from mygpo.api.backend import get_device
35 from mygpo.podcasts.models import Podcast
36 from mygpo.api.opml import Exporter, Importer
37 from mygpo.api.httpresponse import JsonResponse
38 from mygpo.directory.models import ExamplePodcast
39 from mygpo.api.advanced.directory import podcast_data
40 from mygpo.subscriptions import get_subscribed_podcasts, subscribe, unsubscribe
41 from mygpo.directory.search import search_podcasts
42 from mygpo.decorators import allowed_methods, cors_origin
43 from mygpo.utils import parse_range, normalize_feed_url
44 from mygpo.core.json import json, JSONDecodeError
46 import logging
47 logger = logging.getLogger(__name__)
50 ALLOWED_FORMATS = ('txt', 'opml', 'json', 'jsonp', 'xml')
52 def check_format(fn):
53 @wraps(fn)
54 def tmp(request, format, *args, **kwargs):
55 if not format in ALLOWED_FORMATS:
56 return HttpResponseBadRequest('Invalid format')
58 return fn(request, *args, format=format, **kwargs)
59 return tmp
62 @csrf_exempt
63 @require_valid_user
64 @check_username
65 @check_format
66 @never_cache
67 @allowed_methods(['GET', 'PUT', 'POST'])
68 @cors_origin()
69 def subscriptions(request, username, device_uid, format):
71 user_agent = request.META.get('HTTP_USER_AGENT', '')
73 if request.method == 'GET':
74 title = _('%(username)s\'s Subscription List') % {'username': username}
75 subscriptions = get_subscriptions(request.user, device_uid, user_agent)
76 return format_podcast_list(subscriptions, format, title, jsonp_padding=request.GET.get('jsonp'))
78 elif request.method in ('PUT', 'POST'):
79 try:
80 subscriptions = parse_subscription(request.body, format)
82 except JSONDecodeError as e:
83 return HttpResponseBadRequest('Unable to parse POST data: %s' % str(e))
85 return set_subscriptions(subscriptions, request.user, device_uid,
86 user_agent)
89 @csrf_exempt
90 @require_valid_user
91 @check_username
92 @check_format
93 @never_cache
94 @allowed_methods(['GET'])
95 @cors_origin()
96 def all_subscriptions(request, username, format):
98 try:
99 scale = int(request.GET.get('scale_logo', 64))
100 except (TypeError, ValueError):
101 return HttpResponseBadRequest('scale_logo has to be a numeric value')
103 if scale not in range(1, 257):
104 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
107 subscriptions = get_subscribed_podcasts(request.user)
108 title = _('%(username)s\'s Subscription List') % {'username': username}
109 domain = RequestSite(request).domain
110 p_data = lambda p: podcast_data(p, domain, scale)
111 return format_podcast_list(subscriptions, format, title,
112 json_map=p_data, xml_template='podcasts.xml', request=request)
115 def format_podcast_list(obj_list, format, title, get_podcast=None,
116 json_map=lambda x: x.url, jsonp_padding=None,
117 xml_template=None, request=None, template_args={}):
119 Formats a list of podcasts for use in a API response
121 obj_list is a list of podcasts or objects that contain podcasts
122 format is one if txt, opml or json
123 title is a label of the list
124 if obj_list is a list of objects containing podcasts, get_podcast is the
125 function used to get the podcast out of the each of these objects
126 json_map is a function returning the contents of an object (from obj_list)
127 that should be contained in the result (only used for format='json')
130 def default_get_podcast(p):
131 return p
133 get_podcast = get_podcast or default_get_podcast
135 if format == 'txt':
136 podcasts = map(get_podcast, obj_list)
137 s = '\n'.join([p.url for p in podcasts] + [''])
138 return HttpResponse(s, content_type='text/plain')
140 elif format == 'opml':
141 podcasts = map(get_podcast, obj_list)
142 exporter = Exporter(title)
143 opml = exporter.generate(podcasts)
144 return HttpResponse(opml, content_type='text/xml')
146 elif format == 'json':
147 objs = map(json_map, obj_list)
148 return JsonResponse(objs)
150 elif format == 'jsonp':
151 ALLOWED_FUNCNAME = string.letters + string.digits + '_'
153 if not jsonp_padding:
154 return HttpResponseBadRequest('For a JSONP response, specify the name of the callback function in the jsonp parameter')
156 if any(x not in ALLOWED_FUNCNAME for x in jsonp_padding):
157 return HttpResponseBadRequest('JSONP padding can only contain the characters %(char)s' % {'char': ALLOWED_FUNCNAME})
159 objs = map(json_map, obj_list)
160 return JsonResponse(objs, jsonp_padding=jsonp_padding)
162 elif format == 'xml':
163 if None in (xml_template, request):
164 return HttpResponseBadRequest('XML is not a valid format for this request')
166 podcasts = map(json_map, obj_list)
167 template_args.update({'podcasts': podcasts})
169 return render(request, xml_template, template_args,
170 content_type='application/xml')
172 else:
173 return None
176 def get_subscriptions(user, device_uid, user_agent=None):
177 device = get_device(user, device_uid, user_agent)
178 return device.get_subscribed_podcasts()
181 def parse_subscription(raw_post_data, format):
182 if format == 'txt':
183 urls = raw_post_data.split('\n')
185 elif format == 'opml':
186 begin = raw_post_data.find('<?xml')
187 end = raw_post_data.find('</opml>') + 7
188 i = Importer(content=raw_post_data[begin:end])
189 urls = [p['url'] for p in i.items]
191 elif format == 'json':
192 begin = raw_post_data.find('[')
193 end = raw_post_data.find(']') + 1
194 urls = json.loads(raw_post_data[begin:end])
196 else:
197 return []
200 urls = map(normalize_feed_url, urls)
201 urls = filter(None, urls)
202 urls = set(urls)
203 return urls
206 def set_subscriptions(urls, user, device_uid, user_agent):
208 device = get_device(user, device_uid, user_agent, undelete=True)
210 subscriptions = dict( (p.url, p) for p in device.get_subscribed_podcasts())
211 new = [p for p in urls if p not in subscriptions.keys()]
212 rem = [p for p in subscriptions.keys() if p not in urls]
214 remove_podcasts = Podcast.objects.filter(urls__url__in=rem)
215 for podcast in remove_podcasts:
216 unsubscribe(podcast, user, device)
218 for url in new:
219 podcast = Podcast.objects.get_or_create_for_url(url)
220 subscribe(podcast, user, device, url)
222 # Only an empty response is a successful response
223 return HttpResponse('', content_type='text/plain')
226 @check_format
227 @allowed_methods(['GET'])
228 @cache_page(60 * 60)
229 @cors_origin()
230 def toplist(request, count, format):
231 count = parse_range(count, 1, 100, 100)
233 entries = Podcast.objects.all().toplist()[:count]
234 domain = RequestSite(request).domain
236 try:
237 scale = int(request.GET.get('scale_logo', 64))
238 except (TypeError, ValueError):
239 return HttpResponseBadRequest('scale_logo has to be a numeric value')
241 if scale not in range(1, 257):
242 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
245 def get_podcast(t):
246 return t
248 def json_map(t):
249 podcast = t
250 p = podcast_data(podcast, domain, scale)
251 return p
253 title = _('gpodder.net - Top %(count)d') % {'count': len(entries)}
254 return format_podcast_list(entries,
255 format,
256 title,
257 get_podcast=get_podcast,
258 json_map=json_map,
259 jsonp_padding=request.GET.get('jsonp', ''),
260 xml_template='podcasts.xml',
261 request=request,
265 @check_format
266 @cache_page(60 * 60)
267 @allowed_methods(['GET'])
268 @cors_origin()
269 def search(request, format):
271 NUM_RESULTS = 20
273 query = request.GET.get('q', '').encode('utf-8')
275 try:
276 scale = int(request.GET.get('scale_logo', 64))
277 except (TypeError, ValueError):
278 return HttpResponseBadRequest('scale_logo has to be a numeric value')
280 if scale not in range(1, 257):
281 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
283 if not query:
284 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
286 results = search_podcasts(query)[:NUM_RESULTS]
288 title = _('gpodder.net - Search')
289 domain = RequestSite(request).domain
290 p_data = lambda p: podcast_data(p, domain, scale)
291 return format_podcast_list(results, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp', ''), xml_template='podcasts.xml', request=request)
294 @require_valid_user
295 @check_format
296 @never_cache
297 @allowed_methods(['GET'])
298 @cors_origin()
299 def suggestions(request, count, format):
300 count = parse_range(count, 1, 100, 100)
302 user = request.user
303 suggestions = Podcast.objects.filter(podcastsuggestion__suggested_to=user,
304 podcastsuggestion__deleted=False)
305 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
306 domain = RequestSite(request).domain
307 p_data = lambda p: podcast_data(p, domain)
308 return format_podcast_list(suggestions, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp'))
311 @check_format
312 @allowed_methods(['GET'])
313 @cache_page(60 * 60)
314 @cors_origin()
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')
327 if not podcasts:
328 podcasts = ExamplePodcast.objects.get_podcasts()
329 cache.set('example-podcasts', podcasts)
331 title = 'gPodder Podcast Directory'
332 domain = RequestSite(request).domain
333 p_data = lambda p: podcast_data(p, domain, scale)
334 return format_podcast_list(
335 podcasts,
336 format,
337 title,
338 json_map=p_data,
339 xml_template='podcasts.xml',
340 request=request,