[Core] remove gevent usage
[mygpo.git] / mygpo / api / advanced / updates.py
blobecd8b7b24c42774b79a46bfed4a32fc8d0f5baef
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 itertools import chain
19 from datetime import datetime
21 from django.http import HttpResponseBadRequest, HttpResponseNotFound
22 from django.contrib.sites.models import RequestSite
23 from django.views.decorators.csrf import csrf_exempt
24 from django.views.decorators.cache import never_cache
25 from django.utils.decorators import method_decorator
26 from django.views.generic.base import View
28 from mygpo.podcasts.models import Episode
29 from mygpo.api.httpresponse import JsonResponse
30 from mygpo.api.advanced import clean_episode_action_data
31 from mygpo.api.advanced.directory import episode_data, podcast_data
32 from mygpo.utils import parse_bool, get_timestamp
33 from mygpo.subscriptions import get_subscription_history, subscription_diff
34 from mygpo.users.models import Client
35 from mygpo.users.subscriptions import subscription_changes, podcasts_for_states
36 from mygpo.api.basic_auth import require_valid_user, check_username
37 from mygpo.decorators import cors_origin
38 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states
40 from collections import namedtuple
41 EpisodeStatus = namedtuple('EpisodeStatus', 'episode status action')
43 import logging
44 logger = logging.getLogger(__name__)
47 class DeviceUpdates(View):
48 """ returns various updates for a device
50 http://wiki.gpodder.org/wiki/Web_Services/API_2/Devices#Get_Updates """
52 @method_decorator(csrf_exempt)
53 @method_decorator(require_valid_user)
54 @method_decorator(check_username)
55 @method_decorator(never_cache)
56 @method_decorator(cors_origin())
57 def get(self, request, username, device_uid):
59 now = datetime.utcnow()
60 now_ = get_timestamp(now)
62 user = request.user
64 try:
65 device = user.client_set.get(uid=device_uid)
66 except Client.DoesNotExist as e:
67 return HttpResponseNotFound(str(e))
69 try:
70 since = self.get_since(request)
71 except ValueError as e:
72 return HttpResponseBadRequest(str(e))
74 include_actions = parse_bool(request.GET.get('include_actions', False))
76 domain = RequestSite(request).domain
78 add, rem, subscriptions = self.get_subscription_changes(user, device,
79 since, now,
80 domain)
81 updates = self.get_episode_changes(user, subscriptions, domain,
82 include_actions, since)
84 return JsonResponse({
85 'add': add,
86 'rem': rem,
87 'updates': updates,
88 'timestamp': get_timestamp(now),
92 def get_subscription_changes(self, user, device, since, now, domain):
93 """ gets new, removed and current subscriptions """
95 history = get_subscription_history(user, device, since, now)
96 add, rem = subscription_diff(history)
98 subscriptions = device.get_subscribed_podcasts()
100 add = [podcast_data(p, domain) for url in add]
101 rem = [p.url for p in rem]
103 return add, rem, subscriptions
106 def get_episode_changes(self, user, subscriptions, domain, include_actions, since):
107 devices = {dev.id.hex: dev.uid for dev in user.client_set.all()}
109 # index subscribed podcasts by their Id for fast access
110 podcasts = {p.get_id(): p for p in subscriptions}
112 episode_updates = self.get_episode_updates(user, subscriptions, since)
114 return [self.get_episode_data(status, podcasts, domain,
115 include_actions, user, devices) for status in episode_updates]
118 def get_episode_updates(self, user, subscribed_podcasts, since,
119 max_per_podcast=5):
120 """ Returns the episode updates since the timestamp """
122 episodes = Episode.objects.filter(podcast__in=subscribed_podcasts,
123 released__gt=since)[:max_per_podcast]
125 e_actions = chain.from_iterable(get_podcasts_episode_states(p,
126 user.profile.uuid.hex) for p in subscribed_podcasts)
128 # TODO: get_podcasts_episode_states could be optimized by returning
129 # only actions within some time frame
131 e_status = { e.id.hex: EpisodeStatus(e, 'new', None) for e in episodes}
133 for action in e_actions:
134 e_id = action['episode_id']
136 if not e_id in e_status:
137 continue
139 episode = e_status[e_id].episode
141 e_status[e_id] = EpisodeStatus(episode, action['action'], action)
143 return e_status.itervalues()
146 def get_episode_data(self, episode_status, podcasts, domain, include_actions, user, devices):
147 """ Get episode data for an episode status object """
149 # TODO: shouldn't the podcast_id be in the episode status?
150 podcast_id = episode_status.episode.podcast
151 podcast = podcasts.get(podcast_id, None)
152 t = episode_data(episode_status.episode, domain, podcast)
153 t['status'] = episode_status.status
155 # include latest action (bug 1419)
156 if include_actions and episode_status.action:
157 t['action'] = clean_episode_action_data(episode_status.action, user, devices)
159 return t
161 def get_since(self, request):
162 """ parses the "since" parameter """
163 since_ = request.GET.get('since', None)
164 if since_ is None:
165 raise ValueError('parameter since missing')
166 try:
167 return datetime.fromtimestamp(float(since_))
168 except ValueError as e:
169 raise ValueError("'since' is not a valid timestamp: %s" % str(e))