simplify API device list
[mygpo.git] / mygpo / api / advanced / __init__.py
blobef3b45ea891bbc3bcea69e45d806f39e970a1ff5
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, HttpResponseForbidden
20 from mygpo.api.models import Device, Podcast, SubscriptionAction, Episode, EpisodeAction, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, EPISODE_ACTION_TYPES, DEVICE_TYPES, Subscription
21 from mygpo.api.models.users import EpisodeFavorite
22 from mygpo.api.httpresponse import JsonResponse
23 from mygpo.api.sanitizing import sanitize_url
24 from mygpo.api.advanced.directory import episode_data, podcast_data
25 from mygpo.api.backend import get_all_subscriptions
26 from django.core import serializers
27 from django.shortcuts import get_object_or_404
28 from time import mktime, gmtime, strftime
29 from datetime import datetime, timedelta
30 import dateutil.parser
31 from mygpo.log import log
32 from mygpo.utils import parse_time, parse_bool
33 from mygpo.decorators import allowed_methods
34 from django.db import IntegrityError
35 import re
36 from django.views.decorators.csrf import csrf_exempt
38 try:
39 #try to import the JSON module (if we are on Python 2.6)
40 import json
42 # Python 2.5 seems to have a different json module
43 if not 'dumps' in dir(json):
44 raise ImportError
46 except ImportError:
47 # No JSON module available - fallback to simplejson (Python < 2.6)
48 print "No JSON module available - fallback to simplejson (Python < 2.6)"
49 import simplejson as json
52 @csrf_exempt
53 @require_valid_user
54 @check_username
55 @allowed_methods(['GET', 'POST'])
56 def subscriptions(request, username, device_uid):
58 now = datetime.now()
59 now_ = int(mktime(now.timetuple()))
61 if request.method == 'GET':
62 d = get_object_or_404(Device, user=request.user, uid=device_uid, deleted=False)
64 try:
65 since_ = request.GET['since']
66 except KeyError:
67 return HttpResponseBadRequest('parameter since missing')
69 since = datetime.fromtimestamp(float(since_))
71 changes = get_subscription_changes(request.user, d, since, now)
73 return JsonResponse(changes)
75 elif request.method == 'POST':
76 d, created = Device.objects.get_or_create(user=request.user, uid=device_uid, defaults = {'type': 'other', 'name': 'New Device'})
78 if d.deleted:
79 d.deleted = False
80 d.save()
82 actions = json.loads(request.raw_post_data)
83 add = actions['add'] if 'add' in actions else []
84 rem = actions['remove'] if 'remove' in actions else []
86 try:
87 update_urls = update_subscriptions(request.user, d, add, rem)
88 except IntegrityError, e:
89 return HttpResponseBadRequest(e)
91 return JsonResponse({
92 'timestamp': now_,
93 'update_urls': update_urls,
97 def update_subscriptions(user, device, add, remove):
98 updated_urls = []
99 add_sanitized = []
100 rem_sanitized = []
102 for a in add:
103 if a in remove:
104 raise IntegrityError('can not add and remove %s at the same time' % a)
106 for u in add:
107 us = sanitize_url(u)
108 if u != us: updated_urls.append( (u, us) )
109 if us != '': add_sanitized.append(us)
111 for u in remove:
112 us = sanitize_url(u)
113 if u != us: updated_urls.append( (u, us) )
114 if us != '' and us not in add_sanitized:
115 rem_sanitized.append(us)
117 for a in add_sanitized:
118 p, p_created = Podcast.objects.get_or_create(url=a)
119 try:
120 s = SubscriptionAction.objects.create(podcast=p,device=device,action=SUBSCRIBE_ACTION)
121 except IntegrityError, e:
122 log('can\'t add subscription %s for user %s: %s' % (a, user, e))
124 for r in rem_sanitized:
125 p, p_created = Podcast.objects.get_or_create(url=r)
126 try:
127 s = SubscriptionAction.objects.create(podcast=p,device=device,action=UNSUBSCRIBE_ACTION)
128 except IntegrityError, e:
129 log('can\'t remove subscription %s for user %s: %s' % (r, user, e))
131 return updated_urls
133 def get_subscription_changes(user, device, since, until):
134 actions = {}
135 for a in SubscriptionAction.objects.filter(device=device, timestamp__gt=since, timestamp__lte=until).order_by('timestamp'):
136 #ordered by ascending date; newer entries overwriter older ones
137 actions[a.podcast] = a
139 add = []
140 remove = []
142 for a in actions.values():
143 if a.action == SUBSCRIBE_ACTION:
144 add.append(a.podcast.url)
145 elif a.action == UNSUBSCRIBE_ACTION:
146 remove.append(a.podcast.url)
148 until_ = int(mktime(until.timetuple()))
149 return {'add': add, 'remove': remove, 'timestamp': until_}
152 @csrf_exempt
153 @require_valid_user
154 @check_username
155 @allowed_methods(['GET', 'POST'])
156 def episodes(request, username, version=1):
158 version = int(version)
159 now = datetime.now()
160 now_ = int(mktime(now.timetuple()))
162 if request.method == 'POST':
163 try:
164 actions = json.loads(request.raw_post_data)
165 except KeyError, e:
166 log('could not parse episode update info for user %s: %s' % (username, e))
167 return HttpResponseBadRequest()
169 try:
170 update_urls = update_episodes(request.user, actions)
171 except Exception, e:
172 log('could not update episodes for user %s: %s' % (username, e))
173 return HttpResponseBadRequest(e)
175 return JsonResponse({'timestamp': now_, 'update_urls': update_urls})
177 elif request.method == 'GET':
178 podcast_url= request.GET.get('podcast', None)
179 device_uid = request.GET.get('device', None)
180 since_ = request.GET.get('since', None)
181 aggregated = parse_bool(request.GET.get('aggregated', False))
183 since = datetime.fromtimestamp(float(since_)) if since_ else None
185 podcast = get_object_or_404(Podcast, url=podcast_url) if podcast_url else None
186 device = get_object_or_404(Device, user=request.user,uid=device_uid, deleted=False) if device_uid else None
188 return JsonResponse(get_episode_changes(request.user, podcast, device, since, now, aggregated, version))
191 def get_episode_changes(user, podcast, device, since, until, aggregated, version):
192 if aggregated:
193 actions = {}
194 else:
195 actions = []
196 eactions = EpisodeAction.objects.filter(user=user, timestamp__lte=until)
198 if podcast:
199 eactions = eactions.filter(episode__podcast=podcast)
201 if device:
202 eactions = eactions.filter(device=device)
204 if since: # we can't use None with __gt
205 eactions = eactions.filter(timestamp__gt=since)
207 if aggregated:
208 eactions = eactions.order_by('timestamp')
210 for a in eactions:
211 action = {
212 'podcast': a.episode.podcast.url,
213 'episode': a.episode.url,
214 'action': a.action,
215 'timestamp': a.timestamp.strftime('%Y-%m-%dT%H:%M:%S') #2009-12-12T09:00:00
218 if a.action == 'play' and a.playmark:
219 if version == 1:
220 t = gmtime(a.playmark)
221 action['position'] = strftime('%H:%M:%S', t)
222 elif None in (a.playmark, a.started, a.total):
223 log('Ignoring broken episode action in DB: %r' % (a,))
224 continue
225 else:
226 action['position'] = int(a.playmark)
227 action['started'] = int(a.started)
228 action['total'] = int(a.total)
230 if aggregated:
231 actions[a.episode] = action
232 else:
233 actions.append(action)
235 until_ = int(mktime(until.timetuple()))
237 if aggregated:
238 actions = list(actions.itervalues())
240 return {'actions': actions, 'timestamp': until_}
243 def update_episodes(user, actions):
244 update_urls = []
246 for e in actions:
247 u = e['podcast']
248 us = sanitize_url(u)
249 if u != us: update_urls.append( (u, us) )
250 if us == '': continue
252 podcast, p_created = Podcast.objects.get_or_create(url=us)
254 eu = e['episode']
255 eus = sanitize_url(eu, podcast=False, episode=True)
256 if eu != eus: update_urls.append( (eu, eus) )
257 if eus == '': continue
259 episode, e_created = Episode.objects.get_or_create(podcast=podcast, url=eus)
260 action = e['action']
261 if not valid_episodeaction(action):
262 raise Exception('invalid action %s' % action)
264 if 'device' in e:
265 device, created = Device.objects.get_or_create(user=user, uid=e['device'], defaults={'name': 'Unknown', 'type': 'other'})
267 # undelete a previously deleted device
268 if device.deleted:
269 device.deleted = False
270 device.save()
272 else:
273 device, created = None, False
275 timestamp = dateutil.parser.parse(e['timestamp']) if 'timestamp' in e and e['timestamp'] else datetime.now()
277 # Time values for play actions and their keys in JSON
278 PLAY_ACTION_KEYS = ('position', 'started', 'total')
279 time_values = {}
281 for key in PLAY_ACTION_KEYS:
282 if key in e:
283 if action != 'play':
284 # Key found, but must not be supplied (no play action!)
285 return HttpResponseBadRequest('%s only allowed in play actions' % key)
287 try:
288 time_values[key] = parse_time(e[key])
289 except ValueError:
290 log('could not parse %s parameter (value: %s) for user %s' % (key, repr(e[key]), user))
291 return HttpResponseBadRequest('Wrong format for %s: %s' % (key, repr(e[key])))
292 else:
293 # Value not supplied by client
294 time_values[key] = None
296 # Sanity check: If started or total are given, require position
297 if (time_values['started'] is not None or \
298 time_values['total'] is not None) and \
299 time_values['position'] is None:
300 return HttpResponseBadRequest('started and total require position')
302 # Sanity check: total and position can only appear together
303 if (time_values['total'] or time_values['started']) and \
304 not (time_values['total'] and time_values['started']):
305 return HttpResponseBadRequest('total and started parameters can only appear together')
307 try:
308 EpisodeAction.objects.create(user=user, episode=episode, device=device, action=action, timestamp=timestamp,
309 playmark=time_values['position'], started=time_values['started'], total=time_values['total'])
310 except Exception, e:
311 log('error while adding episode action (user %s, episode %s, device %s, action %s, timestamp %s): %s' % (user, episode, device, action, timestamp, e))
313 return update_urls
316 @csrf_exempt
317 @require_valid_user
318 @check_username
319 # Workaround for mygpoclient 1.0: It uses "PUT" requests
320 # instead of "POST" requests for uploading device settings
321 @allowed_methods(['POST', 'PUT'])
322 def device(request, username, device_uid):
324 d, created = Device.objects.get_or_create(user=request.user, uid=device_uid)
326 #undelete a previously deleted device
327 if d.deleted:
328 d.deleted = False
329 d.save()
331 data = json.loads(request.raw_post_data)
333 if 'caption' in data:
334 d.name = data['caption']
336 if 'type' in data:
337 if not valid_devicetype(data['type']):
338 return HttpResponseBadRequest('invalid device type %s' % data['type'])
339 d.type = data['type']
341 d.save()
343 return HttpResponse()
346 def valid_devicetype(type):
347 for t in DEVICE_TYPES:
348 if t[0] == type:
349 return True
350 return False
352 def valid_episodeaction(type):
353 for t in EPISODE_ACTION_TYPES:
354 if t[0] == type:
355 return True
356 return False
359 @csrf_exempt
360 @require_valid_user
361 @check_username
362 @allowed_methods(['GET'])
363 def devices(request, username):
364 devices = Device.objects.filter(user=request.user, deleted=False)
365 devices = map(device_data, devices)
367 return JsonResponse(devices)
370 def device_data(device):
371 return {
372 'id': d.uid,
373 'caption': d.name,
374 'type': d.type,
375 'subscriptions': Subscription.objects.filter(device=d).count()
379 @csrf_exempt
380 @require_valid_user
381 @check_username
382 def updates(request, username, device_uid):
383 now = datetime.now()
384 now_ = int(mktime(now.timetuple()))
386 device = get_object_or_404(Device, user=request.user, uid=device_uid)
388 try:
389 since_ = request.GET['since']
390 except KeyError:
391 return HttpResponseBadRequest('parameter since missing')
393 since = datetime.fromtimestamp(float(since_))
395 ret = get_subscription_changes(request.user, device, since, now)
397 # replace added urls with details
398 podcast_details = []
399 for url in ret['add']:
400 podcast = Podcast.objects.get(url=url)
401 podcast_details.append(podcast_data(podcast))
403 ret['add'] = podcast_details
406 # add episode details
407 subscriptions = get_all_subscriptions(request.user)
408 episode_status = {}
409 for e in Episode.objects.filter(podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
410 episode_status[e] = 'new'
411 for a in EpisodeAction.objects.filter(user=request.user, episode__podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
412 episode_status[a.episode] = a.action
414 updates = []
415 for episode, status in episode_status.iteritems():
416 t = episode_data(episode)
417 t['released'] = e.timestamp.strftime('%Y-%m-%dT%H:%M:%S')
418 t['status'] = status
419 updates.append(t)
421 ret['updates'] = updates
423 return JsonResponse(ret)
426 @require_valid_user
427 @check_username
428 def favorites(request, username):
429 favorites = [x.episode for x in EpisodeFavorite.objects.filter(user=request.user).order_by('-created')]
430 ret = map(episode_data, favorites)
431 return JsonResponse(ret)