Merge pull request #793 from gpodder/remove-advertise
[mygpo.git] / mygpo / api / simple.py
blob50f1b20250bd4caaf46144e12827e851d16962a2
1 import json
2 import string
3 from functools import wraps
5 from django.shortcuts import render
6 from django.core.cache import cache
7 from django.http import HttpResponse, HttpResponseBadRequest
8 from django.views.decorators.cache import cache_page
9 from django.views.decorators.csrf import csrf_exempt
10 from django.views.decorators.cache import never_cache
11 from django.contrib.sites.requests import RequestSite
12 from django.utils.translation import gettext as _
14 from mygpo.api.basic_auth import require_valid_user, check_username
15 from mygpo.api.backend import get_device
16 from mygpo.podcasts.models import Podcast
17 from mygpo.api.opml import Exporter, Importer
18 from mygpo.api.httpresponse import JsonResponse
19 from mygpo.directory.models import ExamplePodcast
20 from mygpo.api.advanced.directory import podcast_data
21 from mygpo.subscriptions import get_subscribed_podcasts
22 from mygpo.subscriptions.tasks import 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
29 logger = logging.getLogger(__name__)
32 ALLOWED_FORMATS = ("txt", "opml", "json", "jsonp", "xml")
35 def check_format(fn):
36 @wraps(fn)
37 def tmp(request, format, *args, **kwargs):
38 if format not in ALLOWED_FORMATS:
39 return HttpResponseBadRequest("Invalid format")
41 return fn(request, *args, format=format, **kwargs)
43 return tmp
46 @csrf_exempt
47 @require_valid_user
48 @check_username
49 @check_format
50 @never_cache
51 @allowed_methods(["GET", "PUT", "POST"])
52 @cors_origin()
53 def subscriptions(request, username, device_uid, format):
55 user_agent = request.META.get("HTTP_USER_AGENT", "")
57 if request.method == "GET":
58 title = _("%(username)s's Subscription List") % {"username": username}
59 subscriptions = get_subscriptions(request.user, device_uid, user_agent)
60 return format_podcast_list(
61 subscriptions, format, title, jsonp_padding=request.GET.get("jsonp")
64 elif request.method in ("PUT", "POST"):
65 try:
66 body = request.body.decode("utf-8")
67 subscriptions = parse_subscription(body, format)
69 except ValueError as e:
70 return HttpResponseBadRequest("Unable to parse POST data: %s" % str(e))
72 return set_subscriptions(subscriptions, request.user, device_uid, user_agent)
75 @csrf_exempt
76 @require_valid_user
77 @check_username
78 @check_format
79 @never_cache
80 @allowed_methods(["GET"])
81 @cors_origin()
82 def all_subscriptions(request, username, format):
84 try:
85 scale = int(request.GET.get("scale_logo", 64))
86 except (TypeError, ValueError):
87 return HttpResponseBadRequest("scale_logo has to be a numeric value")
89 if scale not in range(1, 257):
90 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
92 subscriptions = get_subscribed_podcasts(request.user)
93 title = _("%(username)s's Subscription List") % {"username": username}
94 domain = RequestSite(request).domain
95 p_data = lambda p: podcast_data(p, domain, scale)
96 return format_podcast_list(
97 subscriptions,
98 format,
99 title,
100 json_map=p_data,
101 xml_template="podcasts.xml",
102 request=request,
106 def format_podcast_list(
107 obj_list,
108 format,
109 title,
110 get_podcast=None,
111 json_map=lambda x: x.url,
112 jsonp_padding=None,
113 xml_template=None,
114 request=None,
115 template_args={},
118 Formats a list of podcasts for use in a API response
120 obj_list is a list of podcasts or objects that contain podcasts
121 format is one if txt, opml or json
122 title is a label of the list
123 if obj_list is a list of objects containing podcasts, get_podcast is the
124 function used to get the podcast out of the each of these objects
125 json_map is a function returning the contents of an object (from obj_list)
126 that should be contained in the result (only used for format='json')
129 def default_get_podcast(p):
130 return p
132 get_podcast = get_podcast or default_get_podcast
134 if format == "txt":
135 podcasts = map(get_podcast, obj_list)
136 s = "\n".join([p.url for p in podcasts] + [""])
137 return HttpResponse(s, content_type="text/plain")
139 elif format == "opml":
140 podcasts = map(get_podcast, obj_list)
141 exporter = Exporter(title)
142 opml = exporter.generate(podcasts)
143 return HttpResponse(opml, content_type="text/xml")
145 elif format == "json":
146 objs = list(map(json_map, obj_list))
147 return JsonResponse(objs)
149 elif format == "jsonp":
150 ALLOWED_FUNCNAME = string.ascii_letters + string.digits + "_"
152 if not jsonp_padding:
153 return HttpResponseBadRequest(
154 "For a JSONP response, specify the name of the callback function in the jsonp parameter"
157 if any(x not in ALLOWED_FUNCNAME for x in jsonp_padding):
158 return HttpResponseBadRequest(
159 "JSONP padding can only contain the characters %(char)s"
160 % {"char": ALLOWED_FUNCNAME}
163 objs = list(map(json_map, obj_list))
164 return JsonResponse(objs, jsonp_padding=jsonp_padding)
166 elif format == "xml":
167 if None in (xml_template, request):
168 return HttpResponseBadRequest("XML is not a valid format for this request")
170 podcasts = map(json_map, obj_list)
171 template_args.update({"podcasts": podcasts})
173 return render(
174 request, xml_template, template_args, content_type="application/xml"
177 else:
178 return None
181 def get_subscriptions(user, device_uid, user_agent=None):
182 device = get_device(user, device_uid, user_agent)
183 return device.get_subscribed_podcasts()
186 def parse_subscription(raw_post_data, format):
187 """Parses the data according to the format"""
188 if format == "txt":
189 urls = raw_post_data.split("\n")
191 elif format == "opml":
192 begin = raw_post_data.find("<?xml")
193 end = raw_post_data.find("</opml>") + 7
194 i = Importer(content=raw_post_data[begin:end])
195 urls = [p["url"] for p in i.items]
197 elif format == "json":
198 begin = raw_post_data.find("[")
199 end = raw_post_data.find("]") + 1
200 urls = json.loads(raw_post_data[begin:end])
202 if not isinstance(urls, list):
203 raise ValueError("A list of feed URLs was expected")
205 else:
206 return []
208 urls = filter(None, urls)
209 urls = list(map(normalize_feed_url, urls))
210 return urls
213 def set_subscriptions(urls, user, device_uid, user_agent):
215 # remove empty urls
216 urls = list(filter(None, (u.strip() for u in urls)))
218 device = get_device(user, device_uid, user_agent, undelete=True)
220 subscriptions = dict((p.url, p) for p in device.get_subscribed_podcasts())
221 new = [p for p in urls if p not in subscriptions.keys()]
222 rem = [p for p in subscriptions.keys() if p not in urls]
224 remove_podcasts = Podcast.objects.filter(urls__url__in=rem)
225 for podcast in remove_podcasts:
226 unsubscribe(podcast.pk, user.pk, device.uid)
228 for url in new:
229 podcast = Podcast.objects.get_or_create_for_url(url).object
230 subscribe(podcast.pk, user.pk, device.uid, url)
232 # Only an empty response is a successful response
233 return HttpResponse("", content_type="text/plain")
236 @check_format
237 @allowed_methods(["GET"])
238 @cache_page(60 * 60)
239 @cors_origin()
240 def toplist(request, count, format):
241 count = parse_range(count, 1, 100, 100)
243 entries = Podcast.objects.all().toplist()[:count]
244 domain = RequestSite(request).domain
246 try:
247 scale = int(request.GET.get("scale_logo", 64))
248 except (TypeError, ValueError):
249 return HttpResponseBadRequest("scale_logo has to be a numeric value")
251 if scale not in range(1, 257):
252 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
254 def json_map(t):
255 podcast = t
256 p = podcast_data(podcast, domain, scale)
257 return p
259 title = _("gpodder.net - Top %(count)d") % {"count": len(entries)}
260 return format_podcast_list(
261 entries,
262 format,
263 title,
264 get_podcast=lambda t: t,
265 json_map=json_map,
266 jsonp_padding=request.GET.get("jsonp", ""),
267 xml_template="podcasts.xml",
268 request=request,
272 @check_format
273 @cache_page(60 * 60)
274 @allowed_methods(["GET"])
275 @cors_origin()
276 def search(request, format):
278 NUM_RESULTS = 20
280 query = request.GET.get("q", "")
282 try:
283 scale = int(request.GET.get("scale_logo", 64))
284 except (TypeError, ValueError):
285 return HttpResponseBadRequest("scale_logo has to be a numeric value")
287 if scale not in range(1, 257):
288 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
290 if not query:
291 return HttpResponseBadRequest("/search.opml|txt|json?q={query}")
293 results = search_podcasts(query)[:NUM_RESULTS]
295 title = _("gpodder.net - Search")
296 domain = RequestSite(request).domain
297 p_data = lambda p: podcast_data(p, domain, scale)
298 return format_podcast_list(
299 results,
300 format,
301 title,
302 json_map=p_data,
303 jsonp_padding=request.GET.get("jsonp", ""),
304 xml_template="podcasts.xml",
305 request=request,
309 @require_valid_user
310 @check_format
311 @never_cache
312 @allowed_methods(["GET"])
313 @cors_origin()
314 def suggestions(request, count, format):
315 count = parse_range(count, 1, 100, 100)
317 user = request.user
318 suggestions = Podcast.objects.filter(
319 podcastsuggestion__suggested_to=user, podcastsuggestion__deleted=False
321 title = _("gpodder.net - %(count)d Suggestions") % {"count": len(suggestions)}
322 domain = RequestSite(request).domain
323 p_data = lambda p: podcast_data(p, domain)
324 return format_podcast_list(
325 suggestions,
326 format,
327 title,
328 json_map=p_data,
329 jsonp_padding=request.GET.get("jsonp"),
333 @check_format
334 @allowed_methods(["GET"])
335 @cache_page(60 * 60)
336 @cors_origin()
337 def example_podcasts(request, format):
339 podcasts = cache.get("example-podcasts", None)
341 try:
342 scale = int(request.GET.get("scale_logo", 64))
343 except (TypeError, ValueError):
344 return HttpResponseBadRequest("scale_logo has to be a numeric value")
346 if scale not in range(1, 257):
347 return HttpResponseBadRequest("scale_logo has to be a number from 1 to 256")
349 if not podcasts:
350 podcasts = list(ExamplePodcast.objects.get_podcasts())
351 cache.set("example-podcasts", podcasts)
353 podcast_ad = Podcast.objects.get_advertised_podcast()
354 if podcast_ad:
355 podcasts = [podcast_ad] + podcasts
357 title = "gPodder Podcast Directory"
358 domain = RequestSite(request).domain
359 p_data = lambda p: podcast_data(p, domain, scale)
360 return format_podcast_list(
361 podcasts,
362 format,
363 title,
364 json_map=p_data,
365 xml_template="podcasts.xml",
366 request=request,