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')
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
)
65 device
= user
.client_set
.get(uid
=device_uid
)
66 except Client
.DoesNotExist
as e
:
67 return HttpResponseNotFound(str(e
))
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
,
81 updates
= self
.get_episode_changes(user
, subscriptions
, domain
,
82 include_actions
, since
)
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
,
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
:
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
)
161 def get_since(self
, request
):
162 """ parses the "since" parameter """
163 since_
= request
.GET
.get('since', None)
165 raise ValueError('parameter since missing')
167 return datetime
.fromtimestamp(float(since_
))
168 except ValueError as e
:
169 raise ValueError("'since' is not a valid timestamp: %s" % str(e
))