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
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
, get_device
26 from django
.shortcuts
import get_object_or_404
27 from time
import mktime
, gmtime
, strftime
28 from datetime
import datetime
29 import dateutil
.parser
30 from mygpo
.log
import log
31 from mygpo
.utils
import parse_time
, parse_bool
32 from mygpo
.decorators
import allowed_methods
33 from django
.db
import IntegrityError
34 from django
.views
.decorators
.csrf
import csrf_exempt
37 #try to import the JSON module (if we are on Python 2.6)
40 # Python 2.5 seems to have a different json module
41 if not 'dumps' in dir(json
):
45 # No JSON module available - fallback to simplejson (Python < 2.6)
46 print "No JSON module available - fallback to simplejson (Python < 2.6)"
47 import simplejson
as json
53 @allowed_methods(['GET', 'POST'])
54 def subscriptions(request
, username
, device_uid
):
57 now_
= int(mktime(now
.timetuple()))
59 if request
.method
== 'GET':
60 d
= get_object_or_404(Device
, user
=request
.user
, uid
=device_uid
, deleted
=False)
62 since_
= request
.GET
.get('since', None)
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
= get_device(request
.user
, device_uid
)
75 actions
= json
.loads(request
.raw_post_data
)
76 add
= actions
['add'] if 'add' in actions
else []
77 rem
= actions
['remove'] if 'remove' in actions
else []
80 update_urls
= update_subscriptions(request
.user
, d
, add
, rem
)
81 except IntegrityError
, e
:
82 return HttpResponseBadRequest(e
)
86 'update_urls': update_urls
,
90 def update_subscriptions(user
, device
, add
, remove
):
97 raise IntegrityError('can not add and remove %s at the same time' % a
)
100 us
= sanitize_append(u
, updated_urls
)
101 if us
!= '': add_sanitized
.append(us
)
104 us
= sanitize_append(u
, updated_urls
))
105 if us
!= '' and us
not in add_sanitized
:
106 rem_sanitized
.append(us
)
108 for a
in add_sanitized
:
109 p
, p_created
= Podcast
.objects
.get_or_create(url
=a
)
112 except IntegrityError
, e
:
113 log('can\'t add subscription %s for user %s: %s' % (a
, user
, e
))
115 for r
in rem_sanitized
:
116 p
, p_created
= Podcast
.objects
.get_or_create(url
=r
)
118 p
.unsubscribe(device
)
119 except IntegrityError
, e
:
120 log('can\'t remove subscription %s for user %s: %s' % (r
, user
, e
))
124 def get_subscription_changes(user
, device
, since
, until
):
125 #ordered by ascending date; newer entries overwriter older ones
126 query
= SubscriptionAction
.objects
.filter(device
=device
,
127 timestamp__gt
=since
, timestamp__lte
=until
).order_by('timestamp'):
128 actions
= dict([(a
.podcast
, a
) for a
in query
])
130 add
= filter(lambda a
: a
.action
== SUBSCRIBE_ACTION
, actions
)
131 rem
= filter(lambda a
: a
.action
== UNSUBSCRIBE_ACTION
, actions
)
133 until_
= int(mktime(until
.timetuple()))
134 return {'add': add
, 'remove': remove
, 'timestamp': until_
}
140 @allowed_methods(['GET', 'POST'])
141 def episodes(request
, username
, version
=1):
143 version
= int(version
)
145 now_
= int(mktime(now
.timetuple()))
147 if request
.method
== 'POST':
149 actions
= json
.loads(request
.raw_post_data
)
151 log('could not parse episode update info for user %s: %s' % (username
, e
))
152 return HttpResponseBadRequest()
155 update_urls
= update_episodes(request
.user
, actions
)
157 log('could not update episodes for user %s: %s' % (username
, e
))
158 return HttpResponseBadRequest(e
)
160 return JsonResponse({'timestamp': now_
, 'update_urls': update_urls
})
162 elif request
.method
== 'GET':
163 podcast_url
= request
.GET
.get('podcast', None)
164 device_uid
= request
.GET
.get('device', None)
165 since_
= request
.GET
.get('since', None)
166 aggregated
= parse_bool(request
.GET
.get('aggregated', False))
168 since
= datetime
.fromtimestamp(float(since_
)) if since_
else None
170 podcast
= get_object_or_404(Podcast
, url
=podcast_url
) if podcast_url
else None
171 device
= get_object_or_404(Device
, user
=request
.user
,uid
=device_uid
, deleted
=False) if device_uid
else None
173 return JsonResponse(get_episode_changes(request
.user
, podcast
, device
, since
, now
, aggregated
, version
))
176 def get_episode_changes(user
, podcast
, device
, since
, until
, aggregated
, version
):
181 eactions
= EpisodeAction
.objects
.filter(user
=user
, timestamp__lte
=until
)
184 eactions
= eactions
.filter(episode__podcast
=podcast
)
187 eactions
= eactions
.filter(device
=device
)
189 if since
: # we can't use None with __gt
190 eactions
= eactions
.filter(timestamp__gt
=since
)
193 eactions
= eactions
.order_by('timestamp')
197 'podcast': a
.episode
.podcast
.url
,
198 'episode': a
.episode
.url
,
200 'timestamp': a
.timestamp
.strftime('%Y-%m-%dT%H:%M:%S') #2009-12-12T09:00:00
203 if a
.action
== 'play' and a
.playmark
:
205 t
= gmtime(a
.playmark
)
206 action
['position'] = strftime('%H:%M:%S', t
)
207 elif None in (a
.playmark
, a
.started
, a
.total
):
208 log('Ignoring broken episode action in DB: %r' % (a
,))
211 action
['position'] = int(a
.playmark
)
212 action
['started'] = int(a
.started
)
213 action
['total'] = int(a
.total
)
216 actions
[a
.episode
] = action
218 actions
.append(action
)
220 until_
= int(mktime(until
.timetuple()))
223 actions
= list(actions
.itervalues())
225 return {'actions': actions
, 'timestamp': until_
}
228 def update_episodes(user
, actions
):
232 us
= sanitize_append(e
['podcast'], update_urls
)
233 if us
== '': continue
235 podcast
, p_created
= Podcast
.objects
.get_or_create(url
=us
)
237 eus
= sanitize_append(e
['episode'], update_urls
)
238 if eus
== '': continue
240 episode
, e_created
= Episode
.objects
.get_or_create(podcast
=podcast
, url
=eus
)
242 if not valid_episodeaction(action
):
243 raise Exception('invalid action %s' % action
)
246 device
= get_device(user
, e
['device'])
250 timestamp
= dateutil
.parser
.parse(e
['timestamp']) if e
.get('timestamp', None) else datetime
.now()
252 time_values
= check_time_values(e
)
255 EpisodeAction
.objects
.create(user
=user
, episode
=episode
,
256 device
=device
, action
=action
, timestamp
=timestamp
,
257 playmark
=time_values
.get('position', None),
258 started
=time_values
.get('started', None),
259 total
=time_values
.get('total', None))
261 log('error while adding episode action (user %s, episode %s, device %s, action %s, timestamp %s): %s' % (user
, episode
, device
, action
, timestamp
, e
))
269 # Workaround for mygpoclient 1.0: It uses "PUT" requests
270 # instead of "POST" requests for uploading device settings
271 @allowed_methods(['POST', 'PUT'])
272 def device(request
, username
, device_uid
):
273 d
= get_device(request
.user
, device_uid
)
275 data
= json
.loads(request
.raw_post_data
)
277 if 'caption' in data
:
278 d
.name
= data
['caption']
281 if not valid_devicetype(data
['type']):
282 return HttpResponseBadRequest('invalid device type %s' % data
['type'])
283 d
.type = data
['type']
287 return HttpResponse()
290 def valid_devicetype(type):
291 for t
in DEVICE_TYPES
:
296 def valid_episodeaction(type):
297 for t
in EPISODE_ACTION_TYPES
:
306 @allowed_methods(['GET'])
307 def devices(request
, username
):
308 devices
= Device
.objects
.filter(user
=request
.user
, deleted
=False)
309 devices
= map(device_data
, devices
)
311 return JsonResponse(devices
)
314 def device_data(device
):
317 caption
= device
.name
,
319 subscription
= Subscription
.objects
.filter(device
=device
).count()
326 def updates(request
, username
, device_uid
):
328 now_
= int(mktime(now
.timetuple()))
330 device
= get_object_or_404(Device
, user
=request
.user
, uid
=device_uid
)
332 since_
= request
.GET
.get('since', None)
334 return HttpResponseBadRequest('parameter since missing')
336 since
= datetime
.fromtimestamp(float(since_
))
338 ret
= get_subscription_changes(request
.user
, device
, since
, now
)
340 # replace added urls with details
342 for url
in ret
['add']:
343 podcast
= Podcast
.objects
.get(url
=url
)
344 podcast_details
.append(podcast_data(podcast
))
346 ret
['add'] = podcast_details
349 # add episode details
350 subscriptions
= get_all_subscriptions(request
.user
)
352 for e
in Episode
.objects
.filter(podcast__in
=subscriptions
, timestamp__gte
=since
).order_by('timestamp'):
353 episode_status
[e
] = 'new'
354 for a
in EpisodeAction
.objects
.filter(user
=request
.user
, episode__podcast__in
=subscriptions
, timestamp__gte
=since
).order_by('timestamp'):
355 episode_status
[a
.episode
] = a
.action
358 for episode
, status
in episode_status
.iteritems():
359 t
= episode_data(episode
)
360 t
['released'] = e
.timestamp
.strftime('%Y-%m-%dT%H:%M:%S')
364 ret
['updates'] = updates
366 return JsonResponse(ret
)
371 def favorites(request
, username
):
372 favorites
= [x
.episode
for x
in EpisodeFavorite
.objects
.filter(user
=request
.user
).order_by('-created')]
373 ret
= map(episode_data
, favorites
)
374 return JsonResponse(ret
)
377 def sanitize_append(url
, sanitized_list
):
378 urls
= sanitize_url(url
)
380 sanitized_list
.append( (url
, urls
) )
384 def check_time_values(action
)
385 PLAY_ACTION_KEYS
= ('position', 'started', 'total')
387 # Key found, but must not be supplied (no play action!)
388 if action
['action'] != 'play':
389 for key
in PLAY_ACTION_KEYS
:
391 raise ValueError('%s only allowed in play actions' % key
)
393 supplied_keys
= filter(lambda: x
: x
in e
, PLAY_ACTION_KEYS
)
394 time_values
= map(lambda x
: parse_time(e
[x
]), supplied_keys
)
396 # Sanity check: If started or total are given, require position
397 if (('started' in time_values
) or \
398 ('total' in time_values
)) and \
399 (not 'position' in time_values
):
400 raise ValueError('started and total require position')
402 # Sanity check: total and position can only appear together
403 if (('total' in time_values
) or ('started' in time_values
)) and \
404 not (('total' in time_values
) and ('started' in time_values
)):
405 raise HttpResponseBadRequest('total and started parameters can only appear together')