simplify Simple API
[mygpo.git] / mygpo / api / simple.py
blob8fa42e6fdfcae1d42f2440aa81136a1a5e522b5f
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 from mygpo.api.basic_auth import require_valid_user, check_username
19 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed
20 from mygpo.api.models import Device, SubscriptionAction, Podcast, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SuggestionEntry
21 from mygpo.api.opml import Exporter, Importer
22 from mygpo.api.httpresponse import JsonResponse
23 from mygpo.api.sanitizing import sanitize_url
24 from mygpo.api.backend import get_toplist, get_all_subscriptions
25 from django.views.decorators.csrf import csrf_exempt
26 from django.shortcuts import get_object_or_404
27 from mygpo.search.models import SearchEntry
29 try:
30 import json
32 # Python 2.5 seems to have a different json module
33 if not 'dumps' in dir(json):
34 raise ImportError
36 except ImportError:
37 import simplejson as json
40 ALLOWED_FORMATS = ('txt', 'opml', 'json')
42 def check_format(fn):
43 def tmp(request, format, *args, **kwargs):
44 if not format in ALLOWED_FORMATS:
45 return HttpResponseBadRequest('Invalid format')
47 return fn(request, *args, format=format, **kwargs)
48 return tmp
51 @csrf_exempt
52 @require_valid_user
53 @check_username
54 @check_format
55 def subscriptions(request, username, device_uid, format):
57 if request.method == 'GET':
58 title = _('%(username)s\'s Subscription List') % {'username': username}
59 subscriptions = get_subscriptions(request.user, device_uid)
60 return format_podcast_list(subscriptions, format, title)
62 elif request.method in ('PUT', 'POST'):
63 subscriptions = parse_subscription(request.raw_post_data, format)
64 return set_subscriptions(subscriptions, request.user, device_uid)
66 else:
67 return HttpResponseNotAllowed(['GET', 'PUT', 'POST'])
70 @csrf_exempt
71 @require_valid_user
72 @check_username
73 @check_format
74 def all_subscriptions(request, username, format):
75 if request.method != 'GET':
76 return HttpResponseNotAllowed(['GET'])
78 subscriptions = get_all_subscriptions(request.user)
79 title = _('%(username)s\'s Subscription List') % {'username': username}
80 return format_podcast_list(subscriptions, format, title)
83 def format_podcast_list(obj_list, format, title, get_podcast=lambda x: x, json_map=lambda x: x.url):
84 """
85 Formats a list of podcasts for use in a API response
87 obj_list is a list of podcasts or objects that contain podcasts
88 format is one if txt, opml or json
89 title is a label of the list
90 if obj_list is a list of objects containing podcasts, get_podcast is the
91 function used to get the podcast out of the each of these objects
92 json_map is a function returning the contents of an object (from obj_list)
93 that should be contained in the result (only used for format='json')
94 """
95 if format == 'txt':
96 podcasts = map(get_podcast, obj_list)
97 s = '\n'.join([p.url for p in podcasts] + [''])
98 return HttpResponse(s, mimetype='text/plain')
100 elif format == 'opml':
101 podcasts = map(get_podcast, obj_list)
102 exporter = Exporter(title)
103 opml = exporter.generate(podcasts)
104 return HttpResponse(opml, mimetype='text/xml')
106 elif format == 'json':
107 objs = map(json_map, obj_list)
108 return JsonResponse(objs)
110 else:
111 return None
114 def get_subscriptions(user, device_uid):
115 device = get_object_or_404(Device, uid=device_uid, user=user, deleted=False)
116 return [s.podcast for s in device.get_subscriptions()]
119 def parse_subscription(raw_post_data, format):
120 if format == 'txt':
121 urls = raw_post_data.split('\n')
123 elif format == 'opml':
124 begin = raw_post_data.find('<?xml')
125 end = raw_post_data.find('</opml>') + 7
126 i = Importer(content=raw_post_data[begin:end])
127 urls = [p['url'] for p in i.items]
129 elif format == 'json':
130 begin = raw_post_data.find('[')
131 end = raw_post_data.find(']') + 1
132 urls = json.loads(raw_post_data[begin:end])
134 else:
135 return []
137 urls = map(sanitize_url, urls)
138 urls = filter(lambda x: x, urls)
139 urls = set(urls)
140 return urls
143 def set_subscriptions(urls, user, device_uid):
144 device, created = Device.objects.get_or_create(user=user, uid=device_uid,
145 defaults = {'type': 'other', 'name': device_uid})
147 # undelete a previously deleted device
148 if device.deleted:
149 device.deleted = False
150 device.save()
152 old = [s.podcast.url for s in device.get_subscriptions()]
153 new = [p for p in urls if p not in old]
154 rem = [p for p in old if p not in urls]
156 for r in rem:
157 p = Podcast.objects.get(url=r)
158 s = SubscriptionAction(podcast=p, device=device, action=UNSUBSCRIBE_ACTION)
159 s.save()
161 for n in new:
162 p, created = Podcast.objects.get_or_create(url=n)
163 s = SubscriptionAction(podcast=p, action=SUBSCRIBE_ACTION, device=device)
164 s.save()
166 # Only an empty response is a successful response
167 return HttpResponse('', mimetype='text/plain')
170 @check_format
171 def toplist(request, count, format):
172 if request.method != 'GET':
173 return HttpResponseNotAllowed(['GET'])
175 if int(count) not in range(1,100):
176 count = 100
178 toplist = get_toplist(count)
179 json_map = lambda t: {'url': t.get_podcast().url,
180 'title':t.get_podcast().title,
181 'description':t.get_podcast().description,
182 'subscribers':t.subscriptions,
183 'subscribers_last_week':t.oldplace}
184 title = _('gpodder.net - Top %(count)d') % {'count': len(toplist)}
185 return format_podcast_list(toplist,
186 format,
187 title,
188 get_podcast=lambda x: x.get_podcast(),
189 json_map=json_map)
192 @check_format
193 def search(request, format):
194 if request.method != 'GET':
195 return HttpResponseNotAllowed(['GET'])
197 query = request.GET.get('q', '').encode('utf-8')
199 if not query:
200 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
202 results = [r.get_podcast() for r in SearchEntry.objects.search(query)[:20]]
204 json_map = lambda p: {'url':p.url, 'title':p.title, 'description':p.description}
205 title = _('gpodder.net - Search')
206 return format_podcast_list(results, format, title, json_map=json_map)
209 @require_valid_user
210 @check_format
211 def suggestions(request, count, format):
212 if request.method != 'GET':
213 return HttpResponseNotAllowed(['GET'])
215 if int(count) not in range(1,100):
216 count = 100
218 suggestions = SuggestionEntry.objects.for_user(user)[:int(count)]
219 json_map = lambda p: {'url': p.url, 'title': p.title, 'description': p.description}
220 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
221 return format_podcast_list(suggestions, format, title, json_map=json_map)