Remove unused license preamble
[mygpo.git] / mygpo / api / simple.py
blob00970e35a0869f6cfce9911bb30ea6af916748a7
1 import json
2 import string
3 from itertools import islice
4 from functools import wraps
6 from django.shortcuts import render
7 from django.core.cache import cache
8 from django.http import HttpResponse, HttpResponseBadRequest
9 from django.views.decorators.cache import cache_page
10 from django.views.decorators.csrf import csrf_exempt
11 from django.views.decorators.cache import never_cache
12 from django.contrib.sites.requests import RequestSite
13 from django.utils.translation import ugettext as _
15 from mygpo.api.basic_auth import require_valid_user, check_username
16 from mygpo.api.backend import get_device
17 from mygpo.podcasts.models import Podcast
18 from mygpo.api.opml import Exporter, Importer
19 from mygpo.api.httpresponse import JsonResponse
20 from mygpo.directory.models import ExamplePodcast
21 from mygpo.api.advanced.directory import podcast_data
22 from mygpo.subscriptions import get_subscribed_podcasts, subscribe, unsubscribe
23 from mygpo.directory.search import search_podcasts
24 from mygpo.decorators import allowed_methods, cors_origin
25 from mygpo.utils import parse_range, normalize_feed_url
27 import logging
28 logger = logging.getLogger(__name__)
31 ALLOWED_FORMATS = ('txt', 'opml', 'json', 'jsonp', 'xml')
33 def check_format(fn):
34 @wraps(fn)
35 def tmp(request, format, *args, **kwargs):
36 if not format in ALLOWED_FORMATS:
37 return HttpResponseBadRequest('Invalid format')
39 return fn(request, *args, format=format, **kwargs)
40 return tmp
43 @csrf_exempt
44 @require_valid_user
45 @check_username
46 @check_format
47 @never_cache
48 @allowed_methods(['GET', 'PUT', 'POST'])
49 @cors_origin()
50 def subscriptions(request, username, device_uid, format):
52 user_agent = request.META.get('HTTP_USER_AGENT', '')
54 if request.method == 'GET':
55 title = _('%(username)s\'s Subscription List') % {'username': username}
56 subscriptions = get_subscriptions(request.user, device_uid, user_agent)
57 return format_podcast_list(subscriptions, format, title, jsonp_padding=request.GET.get('jsonp'))
59 elif request.method in ('PUT', 'POST'):
60 try:
61 body = request.body.decode('utf-8')
62 subscriptions = parse_subscription(body, format)
64 except ValueError as e:
65 return HttpResponseBadRequest('Unable to parse POST data: %s' % str(e))
67 return set_subscriptions(subscriptions, request.user, device_uid,
68 user_agent)
71 @csrf_exempt
72 @require_valid_user
73 @check_username
74 @check_format
75 @never_cache
76 @allowed_methods(['GET'])
77 @cors_origin()
78 def all_subscriptions(request, username, format):
80 try:
81 scale = int(request.GET.get('scale_logo', 64))
82 except (TypeError, ValueError):
83 return HttpResponseBadRequest('scale_logo has to be a numeric value')
85 if scale not in range(1, 257):
86 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
89 subscriptions = get_subscribed_podcasts(request.user)
90 title = _('%(username)s\'s Subscription List') % {'username': username}
91 domain = RequestSite(request).domain
92 p_data = lambda p: podcast_data(p, domain, scale)
93 return format_podcast_list(subscriptions, format, title,
94 json_map=p_data, xml_template='podcasts.xml', request=request)
97 def format_podcast_list(obj_list, format, title, get_podcast=None,
98 json_map=lambda x: x.url, jsonp_padding=None,
99 xml_template=None, request=None, template_args={}):
101 Formats a list of podcasts for use in a API response
103 obj_list is a list of podcasts or objects that contain podcasts
104 format is one if txt, opml or json
105 title is a label of the list
106 if obj_list is a list of objects containing podcasts, get_podcast is the
107 function used to get the podcast out of the each of these objects
108 json_map is a function returning the contents of an object (from obj_list)
109 that should be contained in the result (only used for format='json')
112 def default_get_podcast(p):
113 return p
115 get_podcast = get_podcast or default_get_podcast
117 if format == 'txt':
118 podcasts = map(get_podcast, obj_list)
119 s = '\n'.join([p.url for p in podcasts] + [''])
120 return HttpResponse(s, content_type='text/plain')
122 elif format == 'opml':
123 podcasts = map(get_podcast, obj_list)
124 exporter = Exporter(title)
125 opml = exporter.generate(podcasts)
126 return HttpResponse(opml, content_type='text/xml')
128 elif format == 'json':
129 objs = list(map(json_map, obj_list))
130 return JsonResponse(objs)
132 elif format == 'jsonp':
133 ALLOWED_FUNCNAME = string.ascii_letters + string.digits + '_'
135 if not jsonp_padding:
136 return HttpResponseBadRequest('For a JSONP response, specify the name of the callback function in the jsonp parameter')
138 if any(x not in ALLOWED_FUNCNAME for x in jsonp_padding):
139 return HttpResponseBadRequest('JSONP padding can only contain the characters %(char)s' % {'char': ALLOWED_FUNCNAME})
141 objs = map(json_map, obj_list)
142 return JsonResponse(objs, jsonp_padding=jsonp_padding)
144 elif format == 'xml':
145 if None in (xml_template, request):
146 return HttpResponseBadRequest('XML is not a valid format for this request')
148 podcasts = map(json_map, obj_list)
149 template_args.update({'podcasts': podcasts})
151 return render(request, xml_template, template_args,
152 content_type='application/xml')
154 else:
155 return None
158 def get_subscriptions(user, device_uid, user_agent=None):
159 device = get_device(user, device_uid, user_agent)
160 return device.get_subscribed_podcasts()
163 def parse_subscription(raw_post_data, format):
164 """ Parses the data according to the 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 []
182 urls = filter(None, urls)
183 urls = list(map(normalize_feed_url, urls))
184 return urls
187 def set_subscriptions(urls, user, device_uid, user_agent):
189 # remove empty urls
190 urls = list(filter(None, (u.strip() for u in urls)))
192 device = get_device(user, device_uid, user_agent, undelete=True)
194 subscriptions = dict( (p.url, p) for p in device.get_subscribed_podcasts())
195 new = [p for p in urls if p not in subscriptions.keys()]
196 rem = [p for p in subscriptions.keys() if p not in urls]
198 remove_podcasts = Podcast.objects.filter(urls__url__in=rem)
199 for podcast in remove_podcasts:
200 unsubscribe(podcast, user, device)
202 for url in new:
203 podcast = Podcast.objects.get_or_create_for_url(url)
204 subscribe(podcast, user, device, url)
206 # Only an empty response is a successful response
207 return HttpResponse('', content_type='text/plain')
210 @check_format
211 @allowed_methods(['GET'])
212 @cache_page(60 * 60)
213 @cors_origin()
214 def toplist(request, count, format):
215 count = parse_range(count, 1, 100, 100)
217 entries = Podcast.objects.all().toplist()[:count]
218 domain = RequestSite(request).domain
220 try:
221 scale = int(request.GET.get('scale_logo', 64))
222 except (TypeError, ValueError):
223 return HttpResponseBadRequest('scale_logo has to be a numeric value')
225 if scale not in range(1, 257):
226 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
229 def get_podcast(t):
230 return t
232 def json_map(t):
233 podcast = t
234 p = podcast_data(podcast, domain, scale)
235 return p
237 title = _('gpodder.net - Top %(count)d') % {'count': len(entries)}
238 return format_podcast_list(entries,
239 format,
240 title,
241 get_podcast=get_podcast,
242 json_map=json_map,
243 jsonp_padding=request.GET.get('jsonp', ''),
244 xml_template='podcasts.xml',
245 request=request,
249 @check_format
250 @cache_page(60 * 60)
251 @allowed_methods(['GET'])
252 @cors_origin()
253 def search(request, format):
255 NUM_RESULTS = 20
257 query = request.GET.get('q', '')
259 try:
260 scale = int(request.GET.get('scale_logo', 64))
261 except (TypeError, ValueError):
262 return HttpResponseBadRequest('scale_logo has to be a numeric value')
264 if scale not in range(1, 257):
265 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
267 if not query:
268 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
270 results = search_podcasts(query)[:NUM_RESULTS]
272 title = _('gpodder.net - Search')
273 domain = RequestSite(request).domain
274 p_data = lambda p: podcast_data(p, domain, scale)
275 return format_podcast_list(results, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp', ''), xml_template='podcasts.xml', request=request)
278 @require_valid_user
279 @check_format
280 @never_cache
281 @allowed_methods(['GET'])
282 @cors_origin()
283 def suggestions(request, count, format):
284 count = parse_range(count, 1, 100, 100)
286 user = request.user
287 suggestions = Podcast.objects.filter(podcastsuggestion__suggested_to=user,
288 podcastsuggestion__deleted=False)
289 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
290 domain = RequestSite(request).domain
291 p_data = lambda p: podcast_data(p, domain)
292 return format_podcast_list(suggestions, format, title, json_map=p_data, jsonp_padding=request.GET.get('jsonp'))
295 @check_format
296 @allowed_methods(['GET'])
297 @cache_page(60 * 60)
298 @cors_origin()
299 def example_podcasts(request, format):
301 podcasts = cache.get('example-podcasts', None)
303 try:
304 scale = int(request.GET.get('scale_logo', 64))
305 except (TypeError, ValueError):
306 return HttpResponseBadRequest('scale_logo has to be a numeric value')
308 if scale not in range(1, 257):
309 return HttpResponseBadRequest('scale_logo has to be a number from 1 to 256')
311 if not podcasts:
312 podcasts = list(ExamplePodcast.objects.get_podcasts())
313 cache.set('example-podcasts', podcasts)
315 podcast_ad = Podcast.objects.get_advertised_podcast()
316 if podcast_ad:
317 podcasts = [podcast_ad] + podcasts
319 title = 'gPodder Podcast Directory'
320 domain = RequestSite(request).domain
321 p_data = lambda p: podcast_data(p, domain, scale)
322 return format_podcast_list(
323 podcasts,
324 format,
325 title,
326 json_map=p_data,
327 xml_template='podcasts.xml',
328 request=request,