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
, 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 mygpo
.api
.sanitizing
import sanitize_url
23 from django
.core
import serializers
24 from time
import mktime
, gmtime
, strftime
25 from datetime
import datetime
, timedelta
26 import dateutil
.parser
27 from mygpo
.log
import log
28 from mygpo
.utils
import parse_time
, parse_bool
29 from django
.db
import IntegrityError
31 from django
.views
.decorators
.csrf
import csrf_exempt
34 #try to import the JSON module (if we are on Python 2.6)
37 # Python 2.5 seems to have a different json module
38 if not 'dumps' in dir(json
):
42 # No JSON module available - fallback to simplejson (Python < 2.6)
43 print "No JSON module available - fallback to simplejson (Python < 2.6)"
44 import simplejson
as json
50 def subscriptions(request
, username
, device_uid
):
53 now_
= int(mktime(now
.timetuple()))
55 if request
.method
== 'GET':
57 d
= Device
.objects
.get(user
=request
.user
, uid
=device_uid
, deleted
=False)
58 except Device
.DoesNotExist
:
59 raise Http404('device %s does not exist' % device_uid
)
62 since_
= request
.GET
['since']
64 return HttpResponseBadRequest('parameter since missing')
66 since
= datetime
.fromtimestamp(float(since_
))
68 changes
= get_subscription_changes(request
.user
, d
, since
, now
)
70 return JsonResponse(changes
)
72 elif request
.method
== 'POST':
73 d
, created
= Device
.objects
.get_or_create(user
=request
.user
, uid
=device_uid
, defaults
= {'type': 'other', 'name': 'New Device'})
79 actions
= json
.loads(request
.raw_post_data
)
80 add
= actions
['add'] if 'add' in actions
else []
81 rem
= actions
['remove'] if 'remove' in actions
else []
84 update_urls
= update_subscriptions(request
.user
, d
, add
, rem
)
85 except IntegrityError
, e
:
86 return HttpResponseBadRequest(e
)
90 'update_urls': update_urls
,
94 return HttpResponseNotAllowed(['GET', 'POST'])
97 def update_subscriptions(user
, device
, add
, remove
):
104 raise IntegrityError('can not add and remove %s at the same time' % a
)
108 if u
!= us
: updated_urls
.append( (u
, us
) )
109 if us
!= '': add_sanitized
.append(us
)
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
)
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
)
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
))
133 def get_subscription_changes(user
, device
, since
, until
):
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
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_
}
155 def episodes(request
, username
, version
=1):
157 version
= int(version
)
159 now_
= int(mktime(now
.timetuple()))
161 if request
.method
== 'POST':
163 actions
= json
.loads(request
.raw_post_data
)
165 log('could not parse episode update info for user %s: %s' % (username
, e
))
166 return HttpResponseBadRequest()
169 update_urls
= update_episodes(request
.user
, actions
)
171 log('could not update episodes for user %s: %s' % (username
, e
))
172 return HttpResponseBadRequest(e
)
174 return JsonResponse({'timestamp': now_
, 'update_urls': update_urls
})
176 elif request
.method
== 'GET':
177 podcast_url
= request
.GET
.get('podcast', None)
178 device_uid
= request
.GET
.get('device', None)
179 since_
= request
.GET
.get('since', None)
180 aggregated
= parse_bool(request
.GET
.get('aggregated', False))
182 since
= datetime
.fromtimestamp(float(since_
)) if since_
else None
185 podcast
= Podcast
.objects
.get(url
=podcast_url
) if podcast_url
else None
186 device
= Device
.objects
.get(user
=request
.user
,uid
=device_uid
, deleted
=False) if device_uid
else None
192 return JsonResponse(get_episode_changes(request
.user
, podcast
, device
, since
, now
, aggregated
, version
))
195 return HttpResponseNotAllowed(['POST', 'GET'])
198 def get_episode_changes(user
, podcast
, device
, since
, until
, aggregated
, version
):
203 eactions
= EpisodeAction
.objects
.filter(user
=user
, timestamp__lte
=until
)
206 eactions
= eactions
.filter(episode__podcast
=podcast
)
209 eactions
= eactions
.filter(device
=device
)
211 if since
: # we can't use None with __gt
212 eactions
= eactions
.filter(timestamp__gt
=since
)
215 eactions
= eactions
.order_by('timestamp')
219 'podcast': a
.episode
.podcast
.url
,
220 'episode': a
.episode
.url
,
222 'timestamp': a
.timestamp
.strftime('%Y-%m-%dT%H:%M:%S') #2009-12-12T09:00:00
225 if a
.action
== 'play' and a
.playmark
:
227 t
= gmtime(a
.playmark
)
228 action
['position'] = strftime('%H:%M:%S', t
)
230 action
['position'] = int(a
.playmark
)
231 action
['started'] = int(a
.started
)
232 action
['total'] = int(a
.total
)
235 actions
[a
.episode
] = action
237 actions
.append(action
)
239 until_
= int(mktime(until
.timetuple()))
242 actions
= list(actions
.itervalues())
244 return {'actions': actions
, 'timestamp': until_
}
247 def update_episodes(user
, actions
):
253 if u
!= us
: update_urls
.append( (u
, us
) )
254 if us
== '': continue
256 podcast
, p_created
= Podcast
.objects
.get_or_create(url
=us
)
259 eus
= sanitize_url(eu
, podcast
=False, episode
=True)
260 if eu
!= eus
: update_urls
.append( (eu
, eus
) )
261 if eus
== '': continue
263 episode
, e_created
= Episode
.objects
.get_or_create(podcast
=podcast
, url
=eus
)
265 if not valid_episodeaction(action
):
266 raise Exception('invalid action %s' % action
)
269 device
, created
= Device
.objects
.get_or_create(user
=user
, uid
=e
['device'], defaults
={'name': 'Unknown', 'type': 'other'})
271 # undelete a previously deleted device
273 device
.deleted
= False
277 device
, created
= None, False
278 timestamp
= dateutil
.parser
.parse(e
['timestamp']) if 'timestamp' in e
else datetime
.now()
280 # Time values for play actions and their keys in JSON
281 PLAY_ACTION_KEYS
= ('position', 'started', 'total')
284 for key
in PLAY_ACTION_KEYS
:
287 # Key found, but must not be supplied (no play action!)
288 return HttpResponseBadRequest('%s only allowed in play actions' % key
)
291 time_values
[key
] = parse_time(e
[key
])
293 log('could not parse %s parameter (value: %s) for user %s' % (key
, repr(e
[key
]), user
))
294 return HttpResponseBadRequest('Wrong format for %s: %s' % (key
, repr(e
[key
])))
296 # Value not supplied by client
297 time_values
[key
] = None
299 # Sanity check: If started or total are given, require position
300 if (time_values
['started'] is not None or \
301 time_values
['total'] is not None) and \
302 time_values
['position'] is None:
303 return HttpResponseBadRequest('started and total require position')
306 EpisodeAction
.objects
.create(user
=user
, episode
=episode
, device
=device
, action
=action
, timestamp
=timestamp
,
307 playmark
=time_values
['position'], started
=time_values
['started'], total
=time_values
['total'])
309 log('error while adding episode action (user %s, episode %s, device %s, action %s, timestamp %s): %s' % (user
, episode
, device
, action
, timestamp
, e
))
317 def device(request
, username
, device_uid
):
319 # Workaround for mygpoclient 1.0: It uses "PUT" requests
320 # instead of "POST" requests for uploading device settings
321 if request
.method
in ('POST', 'PUT'):
322 d
, created
= Device
.objects
.get_or_create(user
=request
.user
, uid
=device_uid
)
324 #undelete a previously deleted device
329 data
= json
.loads(request
.raw_post_data
)
331 if 'caption' in data
:
332 d
.name
= data
['caption']
335 if not valid_devicetype(data
['type']):
336 return HttpResponseBadRequest('invalid device type %s' % data
['type'])
337 d
.type = data
['type']
341 return HttpResponse()
344 return HttpResponseNotAllowed(['POST'])
346 def valid_devicetype(type):
347 for t
in DEVICE_TYPES
:
352 def valid_episodeaction(type):
353 for t
in EPISODE_ACTION_TYPES
:
362 def devices(request
, username
):
364 if request
.method
== 'GET':
366 for d
in Device
.objects
.filter(user
=request
.user
, deleted
=False):
371 'subscriptions': Subscription
.objects
.filter(device
=d
).count()
374 return JsonResponse(devices
)
377 return HttpResponseNotAllowed(['GET'])