def test_merge_podcasts(self): # Create additional data that will be merged state1 = episode_state_for_user_episode(self.user, self.episode1) state2 = episode_state_for_user_episode(self.user, self.episode2) action1 = EpisodeAction(action='play', timestamp=datetime.utcnow(), upload_timestamp=get_timestamp(datetime.utcnow())) action2 = EpisodeAction(action='download', timestamp=datetime.utcnow(), upload_timestamp=get_timestamp(datetime.utcnow())) add_episode_actions(state1, [action1]) add_episode_actions(state2, [action2]) # copy of the object episode2 = episode_by_id(self.episode2._id) # decide which episodes to merge groups = [(0, [self.episode1, self.episode2])] counter = Counter() pm = PodcastMerger([self.podcast1, self.podcast2], counter, groups) pm.merge() state1 = episode_state_for_user_episode(self.user, self.episode1) state2 = episode_state_for_user_episode(self.user, episode2) self.assertIn(action1, state1.actions) self.assertIn(action2, state1.actions) self.assertEqual(state2._id, None)
def get(self, request, username, device_uid): now = datetime.utcnow() now_ = get_timestamp(now) user = request.user try: device = user.client_set.get(uid=device_uid) except Client.DoesNotExist as e: return HttpResponseNotFound(str(e)) try: since = self.get_since(request) except ValueError as e: return HttpResponseBadRequest(str(e)) include_actions = parse_bool(request.GET.get("include_actions", False)) domain = RequestSite(request).domain add, rem, subscriptions = self.get_subscription_changes( user, device, since, now, domain) updates = self.get_episode_changes(user, subscriptions, domain, include_actions, since) return JsonResponse({ "add": add, "rem": rem, "updates": updates, "timestamp": get_timestamp(now), })
def get(self, request, username, device_uid): now = datetime.utcnow() now_ = get_timestamp(now) user = request.user try: device = user.client_set.get(uid=device_uid) except Client.DoesNotExist as e: return HttpResponseNotFound(str(e)) try: since = self.get_since(request) except ValueError as e: return HttpResponseBadRequest(str(e)) include_actions = parse_bool(request.GET.get('include_actions', False)) domain = RequestSite(request).domain add, rem, subscriptions = self.get_subscription_changes(user, device, since, now, domain) updates = self.get_episode_changes(user, subscriptions, domain, include_actions, since) return JsonResponse({ 'add': add, 'rem': rem, 'updates': updates, 'timestamp': get_timestamp(now), })
def get_episode_changes(user, podcast, device, since, until, aggregated, version): devices = dict( (dev.id, dev.uid) for dev in user.devices ) args = {} if podcast is not None: args['podcast_id'] = podcast.get_id() if device is not None: args['device_id'] = device.id actions = EpisodeAction.filter(user._id, since, until, **args) if version == 1: actions = imap(convert_position, actions) clean_data = partial(clean_episode_action_data, user=user, devices=devices) actions = map(clean_data, actions) actions = filter(None, actions) if aggregated: actions = dict( (a['episode'], a) for a in actions ).values() until_ = get_timestamp(until) return {'actions': actions, 'timestamp': until_}
def create(request, username, format): """ Creates a new podcast list and links to it in the Location header """ title = request.GET.get('title', None) if not title: return HttpResponseBadRequest('Title missing') slug = slugify(title) if not slug: return HttpResponseBadRequest('Invalid title') plist = podcastlist_for_user_slug(request.user._id, slug) if plist: return HttpResponse('List already exists', status=409) urls = parse_subscription(request.body, format) podcasts = [podcast_for_url(url, create=True) for url in urls] podcast_ids = map(Podcast.get_id, podcasts) plist = PodcastList() plist.created_timestamp = get_timestamp(datetime.utcnow()) plist.title = title plist.slug = slug plist.user = request.user._id plist.podcasts = podcast_ids plist.save() response = HttpResponse(status=201) list_url = reverse('api-get-list', args=[request.user.username, slug, format]) response['Location'] = list_url return response
def post(self, request, version, username, device_uid): """ Client sends subscription updates """ now = get_timestamp(datetime.utcnow()) logger.info('Subscription Upload @{username}/{device_uid}'.format( username=request.user.username, device_uid=device_uid)) d = get_device(request.user, device_uid, request.META.get('HTTP_USER_AGENT', '')) actions = self.parsed_body(request) add = list(filter(None, actions.get('add', []))) rem = list(filter(None, actions.get('remove', []))) logger.info('Subscription Upload @{username}/{device_uid}: add ' '{num_add}, remove {num_remove}'.format( username=request.user.username, device_uid=device_uid, num_add=len(add), num_remove=len(rem))) update_urls = self.update_subscriptions(request.user, d, add, rem) return JsonResponse({ 'timestamp': now, 'update_urls': update_urls, })
def create_list(request): title = request.POST.get('title', None) if not title: messages.error(request, _('You have to specify a title.')) return HttpResponseRedirect(reverse('lists-overview')) slug = slugify(title) if not slug: messages.error(request, _('"{title}" is not a valid title').format( title=title)) return HttpResponseRedirect(reverse('lists-overview')) plist = podcastlist_for_user_slug(request.user._id, slug) if plist is None: plist = PodcastList() plist.created_timestamp = get_timestamp(datetime.utcnow()) plist.title = title plist.slug = slug plist.user = request.user._id plist.save() list_url = reverse('list-show', args=[request.user.username, slug]) return HttpResponseRedirect(list_url)
def post(self, request, version, username, device_uid): """ Client sends subscription updates """ now = get_timestamp(datetime.utcnow()) logger.info( "Subscription Upload @{username}/{device_uid}".format( username=request.user.username, device_uid=device_uid ) ) d = get_device( request.user, device_uid, request.META.get("HTTP_USER_AGENT", "") ) actions = self.parsed_body(request) add = list(filter(None, actions.get("add", []))) rem = list(filter(None, actions.get("remove", []))) logger.info( "Subscription Upload @{username}/{device_uid}: add " "{num_add}, remove {num_remove}".format( username=request.user.username, device_uid=device_uid, num_add=len(add), num_remove=len(rem), ) ) update_urls = self.update_subscriptions(request.user, d, add, rem) return JsonResponse({"timestamp": now, "update_urls": update_urls})
def post(self, request, version, username, device_uid): """ Client sends subscription updates """ now = get_timestamp(datetime.utcnow()) logger.info( 'Subscription Upload @{username}/{device_uid}'.format( username=request.user.username, device_uid=device_uid ) ) d = get_device( request.user, device_uid, request.META.get('HTTP_USER_AGENT', '') ) actions = self.parsed_body(request) add = list(filter(None, actions.get('add', []))) rem = list(filter(None, actions.get('remove', []))) logger.info( 'Subscription Upload @{username}/{device_uid}: add ' '{num_add}, remove {num_remove}'.format( username=request.user.username, device_uid=device_uid, num_add=len(add), num_remove=len(rem), ) ) update_urls = self.update_subscriptions(request.user, d, add, rem) return JsonResponse({'timestamp': now, 'update_urls': update_urls})
def parse_episode_action(action, user, update_urls, now, ua_string): action_str = action.get('action', None) if not valid_episodeaction(action_str): raise Exception('invalid action %s' % action_str) new_action = EpisodeAction() new_action.action = action['action'] if action.get('device', False): device = get_device(user, action['device'], ua_string) new_action.device = device.id if action.get('timestamp', False): new_action.timestamp = dateutil.parser.parse(action['timestamp']) else: new_action.timestamp = now new_action.timestamp = new_action.timestamp.replace(microsecond=0) new_action.upload_timestamp = get_timestamp(now) new_action.started = action.get('started', None) new_action.playmark = action.get('position', None) new_action.total = action.get('total', None) return new_action
def add_action(request, episode): device = request.user.get_device(request.POST.get('device')) action_str = request.POST.get('action') timestamp = request.POST.get('timestamp', '') if timestamp: try: timestamp = dateutil.parser.parse(timestamp) except (ValueError, AttributeError): timestamp = datetime.utcnow() else: timestamp = datetime.utcnow() action = EpisodeAction() action.timestamp = timestamp action.upload_timestamp = get_timestamp(datetime.utcnow()) action.device = device.id if device else None action.action = action_str state = episode_state_for_user_episode(request.user, episode) add_episode_actions(state, [action]) podcast = podcast_by_id(episode.podcast) return HttpResponseRedirect(get_episode_link_target(episode, podcast))
def test_limit_actions(self): """ Test that max MAX_EPISODE_ACTIONS episodes are returned """ timestamps = [] t = datetime.utcnow() for n in range(15): timestamp = t - timedelta(seconds=n) EpisodeHistoryEntry.objects.create( timestamp=timestamp, episode=self.episode, user=self.user, action=EpisodeHistoryEntry.DOWNLOAD, ) timestamps.append(timestamp) url = reverse(episodes, kwargs={'version': '2', 'username': self.user.username}) response = self.client.get(url, {'since': '0'}, **self.extra) self.assertEqual(response.status_code, 200, response.content) response_obj = json.loads(response.content.decode('utf-8')) actions = response_obj['actions'] # 10 actions should be returned self.assertEqual(len(actions), 10) timestamps = sorted(timestamps) # the first 10 actions, according to their timestamp should be returned for action, timestamp in zip(actions, timestamps): self.assertEqual(timestamp.isoformat(), action['timestamp']) # the `timestamp` field in the response should be the timestamp of the # last returned action self.assertEqual(get_timestamp(timestamps[9]), response_obj['timestamp'])
def test_merge_podcasts(self): podcast1 = podcast_by_id(self.podcast1.get_id()) podcast2 = podcast_by_id(self.podcast2.get_id()) podcast3 = podcast_by_id(self.podcast3.get_id()) # assert that the podcasts are actually grouped self.assertEqual(podcast2._id, podcast3._id) self.assertNotEqual(podcast2.get_id(), podcast2._id) self.assertNotEqual(podcast3.get_id(), podcast3._id) # Create additional data that will be merged state1 = episode_state_for_user_episode(self.user, self.episode1) state2 = episode_state_for_user_episode(self.user, self.episode2) action1 = EpisodeAction(action='play', timestamp=datetime.utcnow(), upload_timestamp=get_timestamp(datetime.utcnow())) action2 = EpisodeAction(action='download', timestamp=datetime.utcnow(), upload_timestamp=get_timestamp(datetime.utcnow())) add_episode_actions(state1, [action1]) add_episode_actions(state2, [action2]) # copy of the object episode2 = episode_by_id(self.episode2._id) # decide which episodes to merge groups = [(0, [self.episode1, self.episode2])] counter = Counter() pm = PodcastMerger([podcast2, podcast1], counter, groups) pm.merge() state1 = episode_state_for_user_episode(self.user, self.episode1) state2 = episode_state_for_user_episode(self.user, episode2) self.assertIn(action1, state1.actions) self.assertIn(action2, state1.actions) self.assertEqual(state2._id, None) episode1 = episode_by_id(self.episode1._id) # episode2 has been merged into episode1, so it must contain its # merged _id self.assertEqual(episode1.merged_ids, [episode2._id])
def subscriptions(request, username, device_uid): now = datetime.now() now_ = get_timestamp(now) if request.method == 'GET': try: device = request.user.get_device_by_uid(device_uid) except DeviceDoesNotExist as e: return HttpResponseNotFound(str(e)) since_ = request.GET.get('since', None) if since_ is None: return HttpResponseBadRequest('parameter since missing') try: since = datetime.fromtimestamp(float(since_)) except ValueError: return HttpResponseBadRequest('since-value is not a valid timestamp') changes = get_subscription_changes(request.user, device, since, now) return JsonResponse(changes) elif request.method == 'POST': d = get_device(request.user, device_uid, request.META.get('HTTP_USER_AGENT', '')) if not request.body: return HttpResponseBadRequest('POST data must not be empty') try: actions = parse_request_body(request) except (JSONDecodeError, UnicodeDecodeError, ValueError) as e: msg = (u'Could not decode subscription update POST data for ' + 'user %s: %s') % (username, request.body.decode('ascii', errors='replace')) logger.warn(msg, exc_info=True) return HttpResponseBadRequest(msg) add = actions['add'] if 'add' in actions else [] rem = actions['remove'] if 'remove' in actions else [] add = filter(None, add) rem = filter(None, rem) try: update_urls = update_subscriptions(request.user, d, add, rem) except ValueError, e: return HttpResponseBadRequest(e) return JsonResponse({ 'timestamp': now_, 'update_urls': update_urls, })
def episodes(request, username, version=1): version = int(version) now = datetime.now() now_ = get_timestamp(now) ua_string = request.META.get('HTTP_USER_AGENT', '') if request.method == 'POST': try: actions = json.loads(request.raw_post_data) except (JSONDecodeError, UnicodeDecodeError) as e: log('Advanced API: could not decode episode update POST data for user %s: %s' % (username, e)) return HttpResponseBadRequest() try: update_urls = update_episodes(request.user, actions, now, ua_string) except DeviceUIDException as e: import traceback log('could not update episodes for user %s: %s %s: %s' % (username, e, traceback.format_exc(), actions)) return HttpResponseBadRequest(str(e)) return JsonResponse({'timestamp': now_, 'update_urls': update_urls}) elif request.method == 'GET': podcast_url= request.GET.get('podcast', None) device_uid = request.GET.get('device', None) since_ = request.GET.get('since', None) aggregated = parse_bool(request.GET.get('aggregated', False)) try: since = datetime.fromtimestamp(float(since_)) if since_ else None except ValueError: return HttpResponseBadRequest('since-value is not a valid timestamp') if podcast_url: podcast = Podcast.for_url(podcast_url) if not podcast: raise Http404 else: podcast = None if device_uid: try: device = request.user.get_device_by_uid(device_uid) except DeviceDoesNotExist as e: return HttpResponseNotFound(str(e)) else: device = None changes = get_episode_changes(request.user, podcast, device, since, now, aggregated, version) return JsonResponse(changes)
def test_no_actions(self): """ Test when there are no actions to return """ t1 = get_timestamp(datetime.utcnow()) url = reverse(episodes, kwargs={'version': '2', 'username': self.user.username}) response = self.client.get(url, {'since': '0'}, **self.extra) self.assertEqual(response.status_code, 200, response.content) response_obj = json.loads(response.content.decode('utf-8')) actions = response_obj['actions'] # 10 actions should be returned self.assertEqual(len(actions), 0) returned = response_obj['timestamp'] t2 = get_timestamp(datetime.utcnow()) # the `timestamp` field in the response should be the timestamp of the # last returned action self.assertGreaterEqual(returned, t1) self.assertGreaterEqual(t2, returned)
def get_episode_changes(user, podcast, device, since, until, aggregated, version): history = EpisodeHistoryEntry.objects.filter(user=user, timestamp__lt=until) # return the earlier entries first history = history.order_by("timestamp") if since: history = history.filter(timestamp__gte=since) if podcast is not None: history = history.filter(episode__podcast=podcast) if device is not None: history = history.filter(client=device) if version == 1: history = map(convert_position, history) # Limit number of returned episode actions max_actions = dsettings.MAX_EPISODE_ACTIONS history = history[:max_actions] # evaluate query and turn into list, for negative indexing history = list(history) actions = [episode_action_json(a, user) for a in history] if aggregated: actions = list(dict((a["episode"], a) for a in actions).values()) if history: ts = get_timestamp(history[-1].timestamp) else: ts = get_timestamp(until) return {"actions": actions, "timestamp": ts}
def get_episode_changes(user, podcast, device, since, until, aggregated, version): history = EpisodeHistoryEntry.objects.filter(user=user, timestamp__lt=until) # return the earlier entries first history = history.order_by('timestamp') if since: history = history.filter(timestamp__gte=since) if podcast is not None: history = history.filter(episode__podcast=podcast) if device is not None: history = history.filter(client=device) if version == 1: history = map(convert_position, history) # Limit number of returned episode actions max_actions = dsettings.MAX_EPISODE_ACTIONS history = history[:max_actions] # evaluate query and turn into list, for negative indexing history = list(history) actions = [episode_action_json(a, user) for a in history] if aggregated: actions = list(dict((a['episode'], a) for a in actions).values()) if history: ts = get_timestamp(history[-1].timestamp) else: ts = get_timestamp(until) return {'actions': actions, 'timestamp': ts}
def get_changes(self, user, device, since, until): """ Returns subscription changes for the given device """ history = get_subscription_history(user, device, since, until) logger.info('Subscription History: {num}'.format(num=len(history))) add, rem = subscription_diff(history) logger.info('Subscription Diff: +{num_add}/-{num_remove}'.format( num_add=len(add), num_remove=len(rem))) until_ = get_timestamp(until) # TODO: we'd need to get the ref_urls here somehow add_urls = [p.url for p in add] rem_urls = [p.url for p in rem] return (add_urls, rem_urls, until_)
def post(self, request, username): """ Add / remove Chapters to/from an episode """ user = request.user now_ = get_timestamp(datetime.utcnow()) body = self.parsed_body(request) podcast_url, episode_url, update_urls = self.get_urls(body) body['podcast'] = podcast_url body['episode'] = episode_url if not podcast_url or not episode_url: raise RequestException('Invalid Podcast or Episode URL') self.update_chapters(body, user) return JsonResponse({'update_url': update_urls, 'timestamp': now_})
def subscriptions(request, username, device_uid): now = datetime.now() now_ = get_timestamp(now) if request.method == 'GET': try: device = request.user.get_device_by_uid(device_uid) except DeviceDoesNotExist as e: return HttpResponseNotFound(str(e)) since_ = request.GET.get('since', None) if since_ == None: return HttpResponseBadRequest('parameter since missing') try: since = datetime.fromtimestamp(float(since_)) except ValueError: return HttpResponseBadRequest('since-value is not a valid timestamp') changes = get_subscription_changes(request.user, device, since, now) return JsonResponse(changes) elif request.method == 'POST': d = get_device(request.user, device_uid, request.META.get('HTTP_USER_AGENT', '')) if not request.raw_post_data: return HttpResponseBadRequest('POST data must not be empty') actions = json.loads(request.raw_post_data) add = actions['add'] if 'add' in actions else [] rem = actions['remove'] if 'remove' in actions else [] add = filter(None, add) rem = filter(None, rem) try: update_urls = update_subscriptions(request.user, d, add, rem) except IntegrityError, e: return HttpResponseBadRequest(e) return JsonResponse({ 'timestamp': now_, 'update_urls': update_urls, })
def post(self, request, version, username, device_uid): """ Client sends subscription updates """ now = get_timestamp(datetime.utcnow()) d = get_device(request.user, device_uid, request.META.get('HTTP_USER_AGENT', '')) actions = self.parsed_body(request) add = filter(None, actions.get('add', [])) rem = filter(None, actions.get('remove', [])) update_urls = self.update_subscriptions(request.user, d, add, rem) return JsonResponse({ 'timestamp': now, 'update_urls': update_urls, })
def episode_for_podcast_id_url(podcast_id, episode_url, create=False): if not podcast_id: raise QueryParameterMissing('podcast_id') if not episode_url: raise QueryParameterMissing('episode_url') key = u'episode-podcastid-%s-url-%s' % ( sha1(podcast_id.encode('utf-8')).hexdigest(), sha1(episode_url.encode('utf-8')).hexdigest()) # Disabled as cache invalidation is not working properly # episode = cache.get(key) # if episode: # return episode db = get_main_database() episode = get_single_result(db, 'episodes/by_podcast_url', key = [podcast_id, episode_url], include_docs = True, reduce = False, schema = Episode, ) if episode: if episode.needs_update: incomplete_obj.send_robust(sender=episode) else: cache.set(key, episode) return episode if create: episode = Episode() episode.created_timestamp = get_timestamp(datetime.utcnow()) episode.podcast = podcast_id episode.urls = [episode_url] episode.save() incomplete_obj.send_robust(sender=episode) return episode return None
def auto_flattr_episode(user, episode_id): """ Task to auto-flattr an episode In addition to the flattring itself, it also records the event """ success, msg = flattr_thing(user, episode_id, None, False, 'Episode') if not success: return False episode = episode_by_id(episode_id) state = episode_state_for_user_episode(user, episode) action = EpisodeAction() action.action = 'flattr' action.upload_timestamp = get_timestamp(datetime.utcnow()) add_episode_actions(state, [action]) return True
def get(self, request, username): """ Get chapters for an episode """ user = request.user now_ = get_timestamp(datetime.utcnow()) podcast_url, episode_url, _update_urls = self.get_urls(request) episode = Episode.objects.filter(podcast__urls__url=podcast_url, urls__url=episode_url).get() chapters = Chapter.objects.filter(user=user, episode=episode) since = self.get_since(request) if since: chapters = chapters.filter(created__gte=since) chapters_json = map(self.chapter_to_json, chapters) return JsonResponse({'chapters': chapters_json, 'timestamp': now_})
def updates(request, username, device_uid): now = datetime.now() now_ = get_timestamp(now) try: device = request.user.get_device_by_uid(device_uid) except DeviceDoesNotExist as e: return HttpResponseNotFound(str(e)) since_ = request.GET.get('since', None) if since_ == None: return HttpResponseBadRequest('parameter since missing') try: since = datetime.fromtimestamp(float(since_)) except ValueError: return HttpResponseBadRequest('since-value is not a valid timestamp') include_actions = parse_bool(request.GET.get('include_actions', False)) ret = get_subscription_changes(request.user, device, since, now) domain = RequestSite(request).domain subscriptions = list(device.get_subscribed_podcasts()) podcasts = dict( (p.url, p) for p in subscriptions ) prepare_podcast_data = partial(get_podcast_data, podcasts, domain) ret['add'] = map(prepare_podcast_data, ret['add']) devices = dict( (dev.id, dev.uid) for dev in request.user.devices ) clean_action_data = partial(clean_episode_action_data, user=request.user, devices=devices) # index subscribed podcasts by their Id for fast access podcasts = dict( (p.get_id(), p) for p in subscriptions ) prepare_episode_data = partial(get_episode_data, podcasts, domain, clean_action_data, include_actions) episode_updates = get_episode_updates(request.user, subscriptions, since) ret['updates'] = map(prepare_episode_data, episode_updates) return JsonResponse(ret)
def get(self, request, username): """ Get chapters for an episode """ user = request.user now_ = get_timestamp(datetime.utcnow()) podcast_url, episode_url, _update_urls = self.get_urls(request) episode = Episode.objects.filter( podcast__urls__url=podcast_url, urls__url=episode_url ).get() chapters = Chapter.objects.filter(user=user, episode=episode) since = self.get_since(request) if since: chapters = chapters.filter(created__gte=since) chapters_json = map(self.chapter_to_json, chapters) return JsonResponse({'chapters': chapters_json, 'timestamp': now_})
def get_episode_changes(user, podcast, device, since, until, aggregated, version): history = EpisodeHistoryEntry.objects.filter(user=user, timestamp__lt=until) if since: history = history.filter(timestamp__gte=since) if podcast is not None: history = history.filter(episode__podcast=podcast) if device is not None: history = history.filter(client=device) if version == 1: history = map(convert_position, history) actions = [episode_action_json(a, user) for a in history] if aggregated: actions = list(dict( (a['episode'], a) for a in actions ).values()) return {'actions': actions, 'timestamp': get_timestamp(until)}
def podcast_for_url(url, create=False): if not url: raise QueryParameterMissing('url') key = 'podcast-by-url-%s' % sha1(url.encode('utf-8')).hexdigest() podcast = cache.get(key) if podcast: return podcast db = get_main_database() podcast_group = get_single_result(db, 'podcasts/by_url', key = url, include_docs = True, wrapper = _wrap_pg, ) if podcast_group: podcast = podcast_group.get_podcast_by_url(url) if podcast.needs_update: incomplete_obj.send_robust(sender=podcast) else: cache.set(key, podcast) return podcast if create: podcast = Podcast() podcast.created_timestamp = get_timestamp(datetime.utcnow()) podcast.urls = [url] podcast.save() incomplete_obj.send_robust(sender=podcast) return podcast return None
def flattr_episode(request, episode): """ Flattrs an episode, records an event and redirects to the episode """ user = request.user site = RequestSite(request) # Flattr via the tasks queue, but wait for the result task = flattr_thing.delay(user, episode._id, site.domain, request.is_secure(), 'Episode') success, msg = task.get() if success: action = EpisodeAction() action.action = 'flattr' action.upload_timestamp = get_timestamp(datetime.utcnow()) state = episode_state_for_user_episode(request.user, episode) add_episode_actions(state, [action]) messages.success(request, _("Flattr\'d")) else: messages.error(request, msg) podcast = podcast_by_id(episode.podcast) return HttpResponseRedirect(get_episode_link_target(episode, podcast))
def episodes(request, username, version=1): version = int(version) now = datetime.utcnow() now_ = get_timestamp(now) ua_string = request.META.get("HTTP_USER_AGENT", "") if request.method == "POST": try: actions = parse_request_body(request) except (UnicodeDecodeError, ValueError) as e: msg = ("Could not decode episode update POST data for " + "user %s: %s") % ( username, request.body.decode("ascii", errors="replace"), ) logger.warning(msg, exc_info=True) return HttpResponseBadRequest(msg) logger.info("start: user %s: %d actions from %s" % (request.user, len(actions), ua_string)) # handle in background if (dsettings.API_ACTIONS_MAX_NONBG is not None and len(actions) > dsettings.API_ACTIONS_MAX_NONBG): bg_handler = dsettings.API_ACTIONS_BG_HANDLER if bg_handler is not None: modname, funname = bg_handler.rsplit(".", 1) mod = import_module(modname) fun = getattr(mod, funname) fun(request.user, actions, now, ua_string) # TODO: return 202 Accepted return JsonResponse({"timestamp": now_, "update_urls": []}) try: update_urls = update_episodes(request.user, actions, now, ua_string) except ValidationError as e: logger.warning( "Validation Error while uploading episode actions " "for user %s: %s", username, str(e), ) return HttpResponseBadRequest(str(e)) except InvalidEpisodeActionAttributes as e: msg = ( "invalid episode action attributes while uploading episode actions for user %s" % (username, )) logger.warning(msg, exc_info=True) return HttpResponseBadRequest(str(e)) logger.info("done: user %s: %d actions from %s" % (request.user, len(actions), ua_string)) return JsonResponse({"timestamp": now_, "update_urls": update_urls}) elif request.method == "GET": podcast_url = request.GET.get("podcast", None) device_uid = request.GET.get("device", None) since_ = request.GET.get("since", None) aggregated = parse_bool(request.GET.get("aggregated", False)) try: since = int(since_) if since_ else None if since is not None: since = datetime.utcfromtimestamp(since) except ValueError: return HttpResponseBadRequest( "since-value is not a valid timestamp") if podcast_url: podcast = get_object_or_404(Podcast, urls__url=podcast_url) else: podcast = None if device_uid: try: user = request.user device = user.client_set.get(uid=device_uid) except Client.DoesNotExist as e: return HttpResponseNotFound(str(e)) else: device = None changes = get_episode_changes(request.user, podcast, device, since, now, aggregated, version) return JsonResponse(changes)
def get_changes(self, device, since, until): """ Returns subscription changes for the given device """ add_urls, rem_urls = device.get_subscription_changes(since, until) until_ = get_timestamp(until) return (add_urls, rem_urls, until_)
def episodes(request, username, version=1): version = int(version) now = datetime.now() now_ = get_timestamp(now) ua_string = request.META.get('HTTP_USER_AGENT', '') if request.method == 'POST': try: actions = parse_request_body(request) except (JSONDecodeError, UnicodeDecodeError, ValueError) as e: msg = ('Could not decode episode update POST data for ' + 'user %s: %s') % (username, request.body.decode('ascii', errors='replace')) logger.warn(msg, exc_info=True) return HttpResponseBadRequest(msg) logger.info('start: user %s: %d actions from %s' % (request.user._id, len(actions), ua_string)) # handle in background if len(actions) > dsettings.API_ACTIONS_MAX_NONBG: bg_handler = dsettings.API_ACTIONS_BG_HANDLER if bg_handler is not None: modname, funname = bg_handler.rsplit('.', 1) mod = import_module(modname) fun = getattr(mod, funname) fun(request.user, actions, now, ua_string) # TODO: return 202 Accepted return JsonResponse({'timestamp': now_, 'update_urls': []}) try: update_urls = update_episodes(request.user, actions, now, ua_string) except DeviceUIDException as e: logger.warn('invalid device UID while uploading episode actions for user %s', username) return HttpResponseBadRequest(str(e)) except InvalidEpisodeActionAttributes as e: msg = 'invalid episode action attributes while uploading episode actions for user %s' % (username,) logger.warn(msg, exc_info=True) return HttpResponseBadRequest(str(e)) logger.info('done: user %s: %d actions from %s' % (request.user._id, len(actions), ua_string)) return JsonResponse({'timestamp': now_, 'update_urls': update_urls}) elif request.method == 'GET': podcast_url= request.GET.get('podcast', None) device_uid = request.GET.get('device', None) since_ = request.GET.get('since', None) aggregated = parse_bool(request.GET.get('aggregated', False)) try: since = int(since_) if since_ else None except ValueError: return HttpResponseBadRequest('since-value is not a valid timestamp') if podcast_url: podcast = podcast_for_url(podcast_url) if not podcast: raise Http404 else: podcast = None if device_uid: try: device = request.user.get_device_by_uid(device_uid) except DeviceDoesNotExist as e: return HttpResponseNotFound(str(e)) else: device = None changes = get_episode_changes(request.user, podcast, device, since, now_, aggregated, version) return JsonResponse(changes)
def test_merge(self): p1 = Podcast() p1.urls = ['http://example.com/podcast1.rss'] p1.save() p2 = Podcast() p2.urls = ['http://example.com/podcast2.rss'] p2.save() e1 = Episode() e1.title = 'Episode 1' e1.podcast = p1.get_id() e1.urls = ['http://example.com/podcast1/e1.mp3'] e1.save() e2 = Episode() e2.title = 'Episode 2' e2.podcast = p1.get_id() e2.urls = ['http://example.com/podcast1/e2.mp3'] e2.save() e3 = Episode() e3.title = 'Episode 3' e3.podcast = p2.get_id() e3.urls = ['http://example.com/podcast2/e2.mp3'] e3.save() e4 = Episode() e4.title = 'Episode 4' e4.podcast = p2.get_id() e4.urls = ['http://example.com/podcast2/e3.mp3'] e4.save() user = User() user.username = '******' user.email = '*****@*****.**' user.set_password('secret') device1 = Device() device1.uid = 'dev1' device2 = Device() device2.uid = 'dev2' user.devices.append(device1) user.devices.append(device2) user.save() p1.subscribe(user, device1) time.sleep(1) p1.unsubscribe(user, device1) time.sleep(1) p1.subscribe(user, device1) p2.subscribe(user, device2) s1 = episode_state_for_user_episode(user, e1) add_episode_actions(s1, [EpisodeAction(action='play', upload_timestamp=get_timestamp(datetime.utcnow()))]) s3 = episode_state_for_user_episode(user, e3) add_episode_actions(s3, [EpisodeAction(action='play', upload_timestamp=get_timestamp(datetime.utcnow()))]) # we need that for later e3_id = e3._id actions = Counter() # decide which episodes to merge groups = [(0, [e1]), (1, [e2, e3]), (2, [e4])] # carry out the merge pm = PodcastMerger([p1, p2], actions, groups) pm.merge() e1 = episode_by_id(e1._id) es1 = episode_state_for_user_episode(user, e1) self.assertEqual(len(es1.actions), 1) # check if merged episode's id can still be accessed e3 = episode_by_id(e3_id) es3 = episode_state_for_user_episode(user, e3) self.assertEqual(len(es3.actions), 1) p1 = podcast_by_id(p1.get_id()) ps1 = podcast_state_for_user_podcast(user, p1) self.assertEqual(len(ps1.get_subscribed_device_ids()), 2) self.assertEqual(len(list(episodes_for_podcast(p1))), 3)
def episodes(request, username, version=1): version = int(version) now = datetime.utcnow() now_ = get_timestamp(now) ua_string = request.META.get('HTTP_USER_AGENT', '') if request.method == 'POST': try: actions = parse_request_body(request) except (UnicodeDecodeError, ValueError) as e: msg = ('Could not decode episode update POST data for ' + 'user %s: %s') % (username, request.body.decode('ascii', errors='replace')) logger.warn(msg, exc_info=True) return HttpResponseBadRequest(msg) logger.info('start: user %s: %d actions from %s' % (request.user, len(actions), ua_string)) # handle in background if len(actions) > dsettings.API_ACTIONS_MAX_NONBG: bg_handler = dsettings.API_ACTIONS_BG_HANDLER if bg_handler is not None: modname, funname = bg_handler.rsplit('.', 1) mod = import_module(modname) fun = getattr(mod, funname) fun(request.user, actions, now, ua_string) # TODO: return 202 Accepted return JsonResponse({'timestamp': now_, 'update_urls': []}) try: update_urls = update_episodes(request.user, actions, now, ua_string) except ValidationError as e: logger.warning( 'Validation Error while uploading episode actions ' 'for user %s: %s', username, str(e)) return HttpResponseBadRequest(str(e)) except InvalidEpisodeActionAttributes as e: msg = 'invalid episode action attributes while uploading episode actions for user %s' % ( username, ) logger.warn(msg, exc_info=True) return HttpResponseBadRequest(str(e)) logger.info('done: user %s: %d actions from %s' % (request.user, len(actions), ua_string)) return JsonResponse({'timestamp': now_, 'update_urls': update_urls}) elif request.method == 'GET': podcast_url = request.GET.get('podcast', None) device_uid = request.GET.get('device', None) since_ = request.GET.get('since', None) aggregated = parse_bool(request.GET.get('aggregated', False)) try: since = int(since_) if since_ else None if since is not None: since = datetime.utcfromtimestamp(since) except ValueError: return HttpResponseBadRequest( 'since-value is not a valid timestamp') if podcast_url: podcast = get_object_or_404(Podcast, urls__url=podcast_url) else: podcast = None if device_uid: try: user = request.user device = user.client_set.get(uid=device_uid) except Client.DoesNotExist as e: return HttpResponseNotFound(str(e)) else: device = None changes = get_episode_changes(request.user, podcast, device, since, now, aggregated, version) return JsonResponse(changes)
def get_subscription_changes(user, device, since, until): add_urls, rem_urls = device.get_subscription_changes(since, until) until_ = get_timestamp(until) return {'add': add_urls, 'remove': rem_urls, 'timestamp': until_}