2b58d6b1594f6669db1cd19fd25f378bd26e73f1
[mygpo.git] / mygpo / api / simple.py
blob2b58d6b1594f6669db1cd19fd25f378bd26e73f1
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
28 from django.utils.translation import ugettext as _
31 try:
32 import json
34 # Python 2.5 seems to have a different json module
35 if not 'dumps' in dir(json):
36 raise ImportError
38 except ImportError:
39 import simplejson as json
42 ALLOWED_FORMATS = ('txt', 'opml', 'json')
44 def check_format(fn):
45 def tmp(request, format, *args, **kwargs):
46 if not format in ALLOWED_FORMATS:
47 return HttpResponseBadRequest('Invalid format')
49 return fn(request, *args, format=format, **kwargs)
50 return tmp
53 @csrf_exempt
54 @require_valid_user
55 @check_username
56 @check_format
57 def subscriptions(request, username, device_uid, format):
59 if request.method == 'GET':
60 title = _('%(username)s\'s Subscription List') % {'username': username}
61 subscriptions = get_subscriptions(request.user, device_uid)
62 return format_podcast_list(subscriptions, format, title)
64 elif request.method in ('PUT', 'POST'):
65 subscriptions = parse_subscription(request.raw_post_data, format)
66 return set_subscriptions(subscriptions, request.user, device_uid)
68 else:
69 return HttpResponseNotAllowed(['GET', 'PUT', 'POST'])
72 @csrf_exempt
73 @require_valid_user
74 @check_username
75 @check_format
76 def all_subscriptions(request, username, format):
77 if request.method != 'GET':
78 return HttpResponseNotAllowed(['GET'])
80 subscriptions = get_all_subscriptions(request.user)
81 title = _('%(username)s\'s Subscription List') % {'username': username}
82 return format_podcast_list(subscriptions, format, title)
85 def format_podcast_list(obj_list, format, title, get_podcast=lambda x: x, json_map=lambda x: x.url):
86 """
87 Formats a list of podcasts for use in a API response
89 obj_list is a list of podcasts or objects that contain podcasts
90 format is one if txt, opml or json
91 title is a label of the list
92 if obj_list is a list of objects containing podcasts, get_podcast is the
93 function used to get the podcast out of the each of these objects
94 json_map is a function returning the contents of an object (from obj_list)
95 that should be contained in the result (only used for format='json')
96 """
97 if format == 'txt':
98 podcasts = map(get_podcast, obj_list)
99 s = '\n'.join([p.url for p in podcasts] + [''])
100 return HttpResponse(s, mimetype='text/plain')
102 elif format == 'opml':
103 podcasts = map(get_podcast, obj_list)
104 exporter = Exporter(title)
105 opml = exporter.generate(podcasts)
106 return HttpResponse(opml, mimetype='text/xml')
108 elif format == 'json':
109 objs = map(json_map, obj_list)
110 return JsonResponse(objs)
112 else:
113 return None
116 def get_subscriptions(user, device_uid):
117 device = get_object_or_404(Device, uid=device_uid, user=user, deleted=False)
118 return [s.podcast for s in device.get_subscriptions()]
121 def parse_subscription(raw_post_data, format):
122 if format == 'txt':
123 urls = raw_post_data.split('\n')
125 elif format == 'opml':
126 begin = raw_post_data.find('<?xml')
127 end = raw_post_data.find('</opml>') + 7
128 i = Importer(content=raw_post_data[begin:end])
129 urls = [p['url'] for p in i.items]
131 elif format == 'json':
132 begin = raw_post_data.find('[')
133 end = raw_post_data.find(']') + 1
134 urls = json.loads(raw_post_data[begin:end])
136 else:
137 return []
139 urls = map(sanitize_url, urls)
140 urls = filter(lambda x: x, urls)
141 urls = set(urls)
142 return urls
145 def set_subscriptions(urls, user, device_uid):
146 device, created = Device.objects.get_or_create(user=user, uid=device_uid,
147 defaults = {'type': 'other', 'name': device_uid})
149 # undelete a previously deleted device
150 if device.deleted:
151 device.deleted = False
152 device.save()
154 old = [s.podcast.url for s in device.get_subscriptions()]
155 new = [p for p in urls if p not in old]
156 rem = [p for p in old if p not in urls]
158 for r in rem:
159 p = Podcast.objects.get(url=r)
160 s = SubscriptionAction(podcast=p, device=device, action=UNSUBSCRIBE_ACTION)
161 s.save()
163 for n in new:
164 p, created = Podcast.objects.get_or_create(url=n)
165 s = SubscriptionAction(podcast=p, action=SUBSCRIBE_ACTION, device=device)
166 s.save()
168 # Only an empty response is a successful response
169 return HttpResponse('', mimetype='text/plain')
172 @check_format
173 def toplist(request, count, format):
174 if request.method != 'GET':
175 return HttpResponseNotAllowed(['GET'])
177 if int(count) not in range(1,100):
178 count = 100
180 toplist = get_toplist(count)
181 json_map = lambda t: {'url': t.get_podcast().url,
182 'title':t.get_podcast().title,
183 'description':t.get_podcast().description,
184 'subscribers':t.subscriptions,
185 'subscribers_last_week':t.oldplace}
186 title = _('gpodder.net - Top %(count)d') % {'count': len(toplist)}
187 return format_podcast_list(toplist,
188 format,
189 title,
190 get_podcast=lambda x: x.get_podcast(),
191 json_map=json_map)
194 @check_format
195 def search(request, format):
196 if request.method != 'GET':
197 return HttpResponseNotAllowed(['GET'])
199 query = request.GET.get('q', '').encode('utf-8')
201 if not query:
202 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
204 results = [r.get_podcast() for r in SearchEntry.objects.search(query)[:20]]
206 json_map = lambda p: {'url':p.url, 'title':p.title, 'description':p.description}
207 title = _('gpodder.net - Search')
208 return format_podcast_list(results, format, title, json_map=json_map)
211 @require_valid_user
212 @check_format
213 def suggestions(request, count, format):
214 if request.method != 'GET':
215 return HttpResponseNotAllowed(['GET'])
217 if int(count) not in range(1,100):
218 count = 100
220 suggestions = SuggestionEntry.objects.for_user(user)[:int(count)]
221 json_map = lambda p: {'url': p.url, 'title': p.title, 'description': p.description}
222 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
223 return format_podcast_list(suggestions, format, title, json_map=json_map)