testing, bugfixing episodes in advanced api
[mygpo.git] / mygpo / api / advanced.py
blobdc4ccbdc6c8a42d874f792958be4af13ced77737
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
19 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, HttpResponseNotAllowed
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.httpresponse import JsonResponse
22 from django.core import serializers
23 from time import mktime
24 from datetime import datetime, timedelta
25 import dateutil.parser
26 from mygpo.logging import log
27 from django.db import IntegrityError
28 import re
30 try:
31 #try to import the JSON module (if we are on Python 2.6)
32 import json
33 except ImportError:
34 # No JSON module available - fallback to simplejson (Python < 2.6)
35 print "No JSON module available - fallback to simplejson (Python < 2.6)"
36 import simplejson as json
38 @require_valid_user
39 def subscriptions(request, username, device_uid):
41 if request.user.username != username:
42 return HttpResponseForbidden()
44 now = datetime.now()
45 now_ = int(mktime(now.timetuple()))
47 if request.method == 'GET':
48 try:
49 d = Device.objects.get(user=request.user, uid=device_uid)
50 except Device.DoesNotExist:
51 raise Http404('device %s does not exist' % device_uid)
53 try:
54 since_ = request.GET['since']
55 except KeyError:
56 return HttpResponseBadRequest('parameter since missing')
58 since = datetime.fromtimestamp(float(since_))
60 changes = get_subscription_changes(request.user, d, since, now)
62 return JsonResponse(changes)
64 elif request.method == 'POST':
65 d, created = Device.objects.get_or_create(user=request.user, uid=device_uid, defaults = {'type': 'other', 'name': 'New Device'})
67 actions = json.loads(request.POST['data'])
68 add = actions['add'] if 'add' in actions else []
69 rem = actions['remove'] if 'remove' in actions else []
71 for a in add:
72 if a in rem:
73 return HttpResponseBadRequest('can not add and remove %s at the same time' % a)
75 update_subscriptions(request.user, d, add, rem)
77 return JsonResponse({'timestamp': now_})
79 else:
80 return HttpResponseNotAllowed(['GET', 'POST'])
83 def update_subscriptions(user, device, add, remove):
84 for a in add:
85 p, p_created = Podcast.objects.get_or_create(url=a)
86 try:
87 s = SubscriptionAction.objects.create(podcast=p,device=device,action=SUBSCRIBE_ACTION)
88 except IntegrityError, e:
89 log('can\'t add subscription %s for user %s: %s' % (a, user, e))
91 for r in remove:
92 p, p_created = Podcast.objects.get_or_create(url=r)
93 try:
94 s = SubscriptionAction.objects.create(podcast=p,device=device,action=UNSUBSCRIBE_ACTION)
95 except IntegrityError, e:
96 log('can\'t remove subscription %s for user %s: %s' % (r, user, e))
99 def get_subscription_changes(user, device, since, until):
100 actions = {}
101 for a in SubscriptionAction.objects.filter(device=device, timestamp__gt=since, timestamp__lte=until).order_by('timestamp'):
102 #ordered by ascending date; newer entries overwriter older ones
103 actions[a.podcast] = a
105 add = []
106 remove = []
108 for a in actions.values():
109 if a.action == SUBSCRIBE_ACTION:
110 add.append(a.podcast.url)
111 elif a.action == UNSUBSCRIBE_ACTION:
112 remove.append(a.podcast.url)
114 until_ = int(mktime(until.timetuple()))
115 return {'add': add, 'remove': remove, 'timestamp': until_}
118 @require_valid_user
119 def episodes(request, username):
121 if request.user.username != username:
122 return HttpResponseForbidden()
124 now = datetime.now()
125 now_ = int(mktime(now.timetuple()))
127 if request.method == 'POST':
128 try:
129 actions = json.loads(request.POST['data'])
130 except KeyError:
131 return HttpResponseBadRequest()
133 update_episodes(request.user, actions)
135 return JsonResponse({'timestamp': now_})
137 elif request.method == 'GET':
138 podcast_id = request.GET.get('podcast', None)
139 device_id = request.GET.get('device', None)
140 since = request.GET.get('since', None)
142 try:
143 podcast = Podcast.objects.get(pk=podcast_id) if podcast_id else None
144 device = Device.objects.get(pk=device_id) if device_id else None
145 except:
146 raise Http404
148 return JsonResponse(get_episode_changes(request.user, podcast, device, since, now))
150 else:
151 return HttpResponseNotAllowed(['POST', 'GET'])
154 def get_episode_changes(user, podcast, device, since, until):
155 actions = []
156 eactions = EpisodeAction.objects.filter(user=user, timestamp__lte=until)
158 if podcast:
159 eactions = eactions.filter(episode__podcast=podcast)
161 if device:
162 eactions = eactions.filter(device=device)
164 if since: # we can't use None with __gt
165 eactions = eactions.filter(timestamp__gt=since)
167 for a in eactions:
168 action = {
169 'podcast': a.episode.podcast.url,
170 'episode': a.episode.url,
171 'action': a.action,
172 'timestamp': a.timestamp
175 if a.action == 'play': action['time'] = a.playmark
177 actions.append(action)
179 until_ = int(mktime(until.timetuple()))
181 return {'actions': actions, 'timestamp': until_}
184 def update_episodes(user, actions):
185 for e in actions:
186 try:
187 podcast, p_created = Podcast.objects.get_or_create(url=e['podcast'])
188 episode, e_created = Episode.objects.get_or_create(podcast=podcast, url=e['episode'])
189 action = e['action']
190 if not valid_episodeaction(action):
191 return HttpResponseBadRequest('invalid action %s' % action)
192 except:
193 return HttpResponseBadRequest('not all required fields (podcast, episode, action) given')
195 if 'device' in e:
196 device, created = Device.objects.get_or_create(user=user, uid=e['device'], defaults={'name': 'Unknown', 'type': 'other'})
197 else:
198 device, created = None, False
199 timestamp = dateutil.parser.parse(e['timestamp']) if 'timestamp' in e else datetime.now()
200 position = parseTimeDelta(e['position']) if 'position' in e else None
201 playmark = position.seconds if position else None
203 if position and action != 'play':
204 return HttpResponseBadRequest('parameter position can only be used with action play')
206 EpisodeAction.objects.create(user=user, episode=episode, device=device, action=action, timestamp=timestamp, playmark=playmark)
210 @require_valid_user
211 def device(request, username, device_uid):
213 if request.user.username != username:
214 return HttpResponseForbidden()
216 if request.method == 'POST':
217 d, created = Device.objects.get_or_create(user=request.user, uid=device_uid)
219 data = json.loads(request.POST['data'])
221 if 'caption' in data:
222 d.name = data['caption']
224 if 'type' in data:
225 if not valid_devicetype(data['type']):
226 return HttpResponseBadRequest('invalid device type %s' % data['type'])
227 d.type = data['type']
229 d.save()
231 return HttpResponse()
233 else:
234 return HttpResponseNotAllowed(['POST'])
236 def valid_devicetype(type):
237 for t in DEVICE_TYPES:
238 if t[0] == type:
239 return True
240 return False
242 def valid_episodeaction(type):
243 for t in EPISODE_ACTION_TYPES:
244 if t[0] == type:
245 return True
246 return False
248 # http://kbyanc.blogspot.com/2007/08/python-reconstructing-timedeltas-from.html
249 def parseTimeDelta(s):
250 """Create timedelta object representing time delta
251 expressed in a string
253 Takes a string in the format produced by calling str() on
254 a python timedelta object and returns a timedelta instance
255 that would produce that string.
257 Acceptable formats are: "X days, HH:MM:SS" or "HH:MM:SS".
259 if s is None:
260 return None
261 d = re.match(
262 r'((?P<days>\d+) days, )?(?P<hours>\d+):'
263 r'(?P<minutes>\d+):(?P<seconds>\d+)',
264 str(s)).groupdict(0)
265 return timedelta(**dict(( (key, int(value))
266 for key, value in d.items() )))
268 @require_valid_user
269 def devices(request, username):
271 if request.user.username != username:
272 return HttpResponseForbidden()
274 if request.method == 'GET':
275 devices = []
276 for d in Device.objects.filter(user=request.user):
277 devices.append({
278 'id': d.uid,
279 'caption': d.name,
280 'type': d.type,
281 'subscriptions': Subscription.objects.filter(device=d).count()
284 return JsonResponse(devices)
286 else:
287 return HttpResponseNotAllowed(['GET'])