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
31 #try to import the JSON module (if we are on Python 2.6)
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
39 def subscriptions(request
, username
, device_uid
):
41 if request
.user
.username
!= username
:
42 return HttpResponseForbidden()
45 now_
= int(mktime(now
.timetuple()))
47 if request
.method
== 'GET':
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
)
54 since_
= request
.GET
['since']
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 []
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_
})
80 return HttpResponseNotAllowed(['GET', 'POST'])
83 def update_subscriptions(user
, device
, add
, remove
):
85 p
, p_created
= Podcast
.objects
.get_or_create(url
=a
)
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
))
92 p
, p_created
= Podcast
.objects
.get_or_create(url
=r
)
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
):
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
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_
}
119 def episodes(request
, username
):
121 if request
.user
.username
!= username
:
122 return HttpResponseForbidden()
125 now_
= int(mktime(now
.timetuple()))
127 if request
.method
== 'POST':
129 actions
= json
.loads(request
.POST
['data'])
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)
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
148 return JsonResponse(get_episode_changes(request
.user
, podcast
, device
, since
, now
))
151 return HttpResponseNotAllowed(['POST', 'GET'])
154 def get_episode_changes(user
, podcast
, device
, since
, until
):
156 eactions
= EpisodeAction
.objects
.filter(user
=user
, timestamp__lte
=until
)
159 eactions
= eactions
.filter(episode__podcast
=podcast
)
162 eactions
= eactions
.filter(device
=device
)
164 if since
: # we can't use None with __gt
165 eactions
= eactions
.filter(timestamp__gt
=since
)
169 'podcast': a
.episode
.podcast
.url
,
170 'episode': a
.episode
.url
,
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
):
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'])
190 if not valid_episodeaction(action
):
191 return HttpResponseBadRequest('invalid action %s' % action
)
193 return HttpResponseBadRequest('not all required fields (podcast, episode, action) given')
196 device
, created
= Device
.objects
.get_or_create(user
=user
, uid
=e
['device'], defaults
={'name': 'Unknown', 'type': 'other'})
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
)
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']
225 if not valid_devicetype(data
['type']):
226 return HttpResponseBadRequest('invalid device type %s' % data
['type'])
227 d
.type = data
['type']
231 return HttpResponse()
234 return HttpResponseNotAllowed(['POST'])
236 def valid_devicetype(type):
237 for t
in DEVICE_TYPES
:
242 def valid_episodeaction(type):
243 for t
in EPISODE_ACTION_TYPES
:
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".
262 r
'((?P<days>\d+) days, )?(?P<hours>\d+):'
263 r
'(?P<minutes>\d+):(?P<seconds>\d+)',
265 return timedelta(**dict(( (key
, int(value
))
266 for key
, value
in d
.items() )))
269 def devices(request
, username
):
271 if request
.user
.username
!= username
:
272 return HttpResponseForbidden()
274 if request
.method
== 'GET':
276 for d
in Device
.objects
.filter(user
=request
.user
):
281 'subscriptions': Subscription
.objects
.filter(device
=d
).count()
284 return JsonResponse(devices
)
287 return HttpResponseNotAllowed(['GET'])