2 import uuid
, collections
3 from datetime
import datetime
5 from itertools
import imap
6 from operator
import itemgetter
8 from couchdbkit
import ResourceNotFound
9 from couchdbkit
.ext
.django
.schema
import *
11 from django_couchdb_utils
.registration
.models
import User
as BaseUser
13 from mygpo
.core
.proxy
import proxy_object
, DocumentABCMeta
14 from mygpo
.core
.models
import Podcast
, Episode
15 from mygpo
.utils
import linearize
, get_to_dict
, iterate_together
16 from mygpo
.decorators
import repeat_on_conflict
17 from mygpo
.users
.ratings
import RatingMixin
18 from mygpo
.users
.sync
import SyncedDevicesMixin
19 from mygpo
.log
import log
22 RE_DEVICE_UID
= re
.compile(r
'^[\w.-]+$')
25 class DeviceUIDException(Exception):
30 class Suggestions(Document
, RatingMixin
):
31 user
= StringProperty(required
=True)
32 user_oldid
= IntegerProperty()
33 podcasts
= StringListProperty()
34 blacklist
= StringListProperty()
37 def for_user(cls
, user
):
38 r
= cls
.view('users/suggestions_by_user', key
=user
._id
, \
48 def get_podcasts(self
, count
=None):
49 user
= User
.get(self
.user
)
50 subscriptions
= user
.get_subscribed_podcast_ids()
52 ids
= filter(lambda x
: not x
in self
.blacklist
+ subscriptions
, self
.podcasts
)
55 return filter(lambda x
: x
and x
.title
, Podcast
.get_multi(ids
))
60 return super(Suggestions
, self
).__repr
__()
62 return '%d Suggestions for %s (%s)' % \
63 (len(self
.podcasts
), self
.user
, self
._id
)
66 class EpisodeAction(DocumentSchema
):
68 One specific action to an episode. Must
69 always be part of a EpisodeUserState
72 action
= StringProperty(required
=True)
73 timestamp
= DateTimeProperty(required
=True, default
=datetime
.utcnow
)
74 device_oldid
= IntegerProperty(required
=False)
75 device
= StringProperty()
76 started
= IntegerProperty()
77 playmark
= IntegerProperty()
78 total
= IntegerProperty()
80 def __eq__(self
, other
):
81 if not isinstance(other
, EpisodeAction
):
83 vals
= ('action', 'timestamp', 'device', 'started', 'playmark',
85 return all([getattr(self
, v
, None) == getattr(other
, v
, None) for v
in vals
])
88 def to_history_entry(self
):
89 entry
= HistoryEntry()
90 entry
.action
= self
.action
91 entry
.timestamp
= self
.timestamp
92 entry
.device_id
= self
.device
93 entry
.started
= self
.started
94 entry
.position
= self
.playmark
95 entry
.total
= self
.total
100 def filter(user_id
, since
=None, until
={}, podcast_id
=None,
102 """ Returns Episode Actions for the given criteria"""
104 since_str
= since
.strftime('%Y-%m-%dT%H:%M:%S') if since
else None
105 until_str
= until
.strftime('%Y-%m-%dT%H:%M:%S') if until
else {}
107 # further parts of the key are filled in below
108 startkey
= [user_id
, since_str
, None, None]
109 endkey
= [user_id
, until_str
, {}, {}]
111 # additional filter that are carried out by the
112 # application, not by the database
115 if isinstance(podcast_id
, basestring
):
116 if until
is not None: # filter in database
117 startkey
[2] = podcast_id
118 endkey
[2] = podcast_id
120 add_filters
.append( lambda x
: x
['podcast_id'] == podcast_id
)
122 elif isinstance(podcast_id
, list):
123 add_filters
.append( lambda x
: x
['podcast_id'] in podcast_id
)
125 elif podcast_id
is not None:
126 raise ValueError('podcast_id can be either None, basestring '
127 'or a list of basestrings')
131 if None not in (until
, podcast_id
): # filter in database
132 startkey
[3] = device_id
133 endkey
[3] = device_id
135 dev_filter
= lambda x
: x
.get('device_id', None) == device_id
136 add_filters
.append(dev_filter
)
139 db
= EpisodeUserState
.get_db()
140 res
= db
.view('users/episode_actions',
147 state
= EpisodeUserState
.wrap(r
['doc'])
148 index
= int(r
['value'])
149 action
= HistoryEntry
.from_action_dict(state
, index
)
150 if all( f(action
) for f
in add_filters
):
154 def validate_time_values(self
):
155 """ Validates allowed combinations of time-values """
157 PLAY_ACTION_KEYS
= ('playmark', 'started', 'total')
159 # Key found, but must not be supplied (no play action!)
160 if self
.action
!= 'play':
161 for key
in PLAY_ACTION_KEYS
:
162 if getattr(self
, key
, None) is not None:
163 raise ValueError('%s only allowed in play actions' % key
)
165 # Sanity check: If started or total are given, require playmark
166 if ((self
.started
is not None) or (self
.total
is not None)) and \
167 self
.playmark
is None:
168 raise ValueError('started and total require position')
170 # Sanity check: total and playmark can only appear together
171 if ((self
.total
is not None) or (self
.started
is not None)) and \
172 ((self
.total
is None) or (self
.started
is None)):
173 raise ValueError('total and started can only appear together')
177 return '%s-Action on %s at %s (in %s)' % \
178 (self
.action
, self
.device
, self
.timestamp
, self
._id
)
182 return hash(frozenset([self
.action
, self
.timestamp
, self
.device
,
183 self
.started
, self
.playmark
, self
.total
]))
186 class Chapter(Document
):
187 """ A user-entered episode chapter """
189 device
= StringProperty()
190 created
= DateTimeProperty()
191 start
= IntegerProperty(required
=True)
192 end
= IntegerProperty(required
=True)
193 label
= StringProperty()
194 advertisement
= BooleanProperty()
197 def for_episode(cls
, episode_id
):
199 r
= db
.view('users/chapters_by_episode',
200 startkey
= [episode_id
, None],
201 endkey
= [episode_id
, {}],
207 chapter
= Chapter
.wrap(res
['value'])
208 yield (user
, chapter
)
212 return '<%s %s (%d-%d)>' % (self
.__class
__.__name
__, self
.label
,
213 self
.start
, self
.end
)
216 class EpisodeUserState(Document
):
218 Contains everything a user has done with an Episode
221 episode
= StringProperty(required
=True)
222 actions
= SchemaListProperty(EpisodeAction
)
223 settings
= DictProperty()
224 user_oldid
= IntegerProperty()
225 user
= StringProperty(required
=True)
226 ref_url
= StringProperty(required
=True)
227 podcast_ref_url
= StringProperty(required
=True)
228 merged_ids
= StringListProperty()
229 chapters
= SchemaListProperty(Chapter
)
230 podcast
= StringProperty(required
=True)
234 def for_user_episode(cls
, user
, episode
):
235 r
= cls
.view('users/episode_states_by_user_episode',
236 key
=[user
.id, episode
._id
], include_docs
=True)
242 podcast
= Podcast
.get(episode
.podcast
)
244 state
= EpisodeUserState()
245 state
.episode
= episode
._id
246 state
.podcast
= episode
.podcast
247 state
.user
= user
._id
248 state
.ref_url
= episode
.url
249 state
.podcast_ref_url
= podcast
.url
254 def for_ref_urls(cls
, user
, podcast_url
, episode_url
):
255 res
= cls
.view('users/episode_states_by_ref_urls',
256 key
= [user
.id, podcast_url
, episode_url
], limit
=1, include_docs
=True)
259 state
.ref_url
= episode_url
260 state
.podcast_ref_url
= podcast_url
264 podcast
= Podcast
.for_url(podcast_url
, create
=True)
265 episode
= Episode
.for_podcast_id_url(podcast
.get_id(), episode_url
,
268 return episode
.get_user_state(user
)
273 r
= cls
.view('users/episode_states_by_user_episode',
278 def add_actions(self
, actions
):
279 map(EpisodeAction
.validate_time_values
, actions
)
280 self
.actions
= list(self
.actions
) + actions
281 self
.actions
= list(set(self
.actions
))
282 self
.actions
= sorted(self
.actions
, key
=lambda x
: x
.timestamp
)
285 def is_favorite(self
):
286 return self
.settings
.get('is_favorite', False)
289 def set_favorite(self
, set_to
=True):
290 self
.settings
['is_favorite'] = set_to
293 def update_chapters(self
, add
=[], rem
=[]):
294 """ Updates the Chapter list
296 * add contains the chapters to be added
298 * rem contains tuples of (start, end) times. Chapters that match
299 both endpoints will be removed
302 @repeat_on_conflict(['state'])
305 self
.chapters
= self
.chapters
+ [chapter
]
307 for start
, end
in rem
:
308 keep
= lambda c
: c
.start
!= start
or c
.end
!= end
309 self
.chapters
= filter(keep
, self
.chapters
)
316 def get_history_entries(self
):
317 return imap(EpisodeAction
.to_history_entry
, self
.actions
)
321 return 'Episode-State %s (in %s)' % \
322 (self
.episode
, self
._id
)
324 def __eq__(self
, other
):
325 if not isinstance(other
, EpisodeUserState
):
328 return (self
.episode
== other
.episode
and
329 self
.user_oldid
== other
.user_oldid
)
333 class SubscriptionAction(Document
):
334 action
= StringProperty()
335 timestamp
= DateTimeProperty(default
=datetime
.utcnow
)
336 device
= StringProperty()
339 __metaclass__
= DocumentABCMeta
342 def __cmp__(self
, other
):
343 return cmp(self
.timestamp
, other
.timestamp
)
345 def __eq__(self
, other
):
346 return self
.action
== other
.action
and \
347 self
.timestamp
== other
.timestamp
and \
348 self
.device
== other
.device
351 return hash(self
.action
) + hash(self
.timestamp
) + hash(self
.device
)
354 return '<SubscriptionAction %s on %s at %s>' % (
355 self
.action
, self
.device
, self
.timestamp
)
358 class PodcastUserState(Document
):
360 Contains everything that a user has done
361 with a specific podcast and all its episodes
364 podcast
= StringProperty(required
=True)
365 user_oldid
= IntegerProperty()
366 user
= StringProperty(required
=True)
367 settings
= DictProperty()
368 actions
= SchemaListProperty(SubscriptionAction
)
369 tags
= StringListProperty()
370 ref_url
= StringProperty(required
=True)
371 disabled_devices
= StringListProperty()
372 merged_ids
= StringListProperty()
376 def for_user_podcast(cls
, user
, podcast
):
377 r
= PodcastUserState
.view('users/podcast_states_by_podcast', \
378 key
=[podcast
.get_id(), user
.id], limit
=1, include_docs
=True)
382 p
= PodcastUserState()
383 p
.podcast
= podcast
.get_id()
385 p
.ref_url
= podcast
.url
386 p
.settings
['public_subscription'] = user
.settings
.get('public_subscriptions', True)
388 p
.set_device_state(user
.devices
)
394 def for_user(cls
, user
):
395 r
= PodcastUserState
.view('users/podcast_states_by_user',
396 startkey
= [user
._id
, None],
397 endkey
= [user
._id
, 'ZZZZ'],
404 def for_device(cls
, device_id
):
405 r
= PodcastUserState
.view('users/podcast_states_by_device',
406 startkey
=[device_id
, None], endkey
=[device_id
, {}],
411 def remove_device(self
, device
):
413 Removes all actions from the podcast state that refer to the
416 self
.actions
= filter(lambda a
: a
.device
!= device
.id, self
.actions
)
421 r
= PodcastUserState
.view('users/podcast_states_by_user',
426 def subscribe(self
, device
):
427 action
= SubscriptionAction()
428 action
.action
= 'subscribe'
429 action
.device
= device
.id
430 self
.add_actions([action
])
433 def unsubscribe(self
, device
):
434 action
= SubscriptionAction()
435 action
.action
= 'unsubscribe'
436 action
.device
= device
.id
437 self
.add_actions([action
])
440 def add_actions(self
, actions
):
441 self
.actions
= list(set(self
.actions
+ actions
))
442 self
.actions
= sorted(self
.actions
)
445 def add_tags(self
, tags
):
446 self
.tags
= list(set(self
.tags
+ tags
))
449 def set_device_state(self
, devices
):
450 disabled_devices
= [device
.id for device
in devices
if device
.deleted
]
451 self
.disabled_devices
= disabled_devices
454 def get_change_between(self
, device_id
, since
, until
):
456 Returns the change of the subscription status for the given device
457 between the two timestamps.
459 The change is given as either 'subscribe' (the podcast has been
460 subscribed), 'unsubscribed' (the podcast has been unsubscribed) or
464 device_actions
= filter(lambda x
: x
.device
== device_id
, self
.actions
)
465 before
= filter(lambda x
: x
.timestamp
<= since
, device_actions
)
466 after
= filter(lambda x
: x
.timestamp
<= until
, device_actions
)
468 # nothing happened, so there can be no change
472 then
= before
[-1] if before
else None
476 if now
.action
!= 'unsubscribe':
478 elif then
.action
!= now
.action
:
483 def get_subscribed_device_ids(self
):
484 r
= PodcastUserState
.view('users/subscriptions_by_podcast',
485 startkey
= [self
.podcast
, self
.user
, None],
486 endkey
= [self
.podcast
, self
.user
, {}],
489 return (res
['key'][2] for res
in r
)
493 return self
.settings
.get('public_subscription', True)
496 def __eq__(self
, other
):
500 return self
.podcast
== other
.podcast
and \
501 self
.user
== other
.user
504 return 'Podcast %s for User %s (%s)' % \
505 (self
.podcast
, self
.user
, self
._id
)
508 class Device(Document
):
509 id = StringProperty(default
=lambda: uuid
.uuid4().hex)
510 oldid
= IntegerProperty(required
=False)
511 uid
= StringProperty(required
=True)
512 name
= StringProperty(required
=True, default
='New Device')
513 type = StringProperty(required
=True, default
='other')
514 settings
= DictProperty()
515 deleted
= BooleanProperty(default
=False)
518 def for_oldid(cls
, oldid
):
519 r
= cls
.view('users/devices_by_oldid', key
=oldid
)
520 return r
.first() if r
else None
523 def get_subscription_changes(self
, since
, until
):
525 Returns the subscription changes for the device as two lists.
526 The first lists contains the Ids of the podcasts that have been
527 subscribed to, the second list of those that have been unsubscribed
532 podcast_states
= PodcastUserState
.for_device(self
.id)
533 for p_state
in podcast_states
:
534 change
= p_state
.get_change_between(self
.id, since
, until
)
535 if change
== 'subscribe':
536 add
.append( p_state
.podcast
)
537 elif change
== 'unsubscribe':
538 rem
.append( p_state
.podcast
)
543 def get_latest_changes(self
):
544 podcast_states
= PodcastUserState
.for_device(self
.id)
545 for p_state
in podcast_states
:
546 actions
= filter(lambda x
: x
.device
== self
.id, reversed(p_state
.actions
))
548 yield (p_state
.podcast
, actions
[0])
551 def get_subscribed_podcast_ids(self
):
552 r
= self
.view('users/subscribed_podcasts_by_device',
553 startkey
= [self
.id, None],
554 endkey
= [self
.id, {}]
556 return [res
['key'][1] for res
in r
]
559 def get_subscribed_podcasts(self
):
560 return Podcast
.get_multi(self
.get_subscribed_podcast_ids())
564 return hash(frozenset([self
.uid
, self
.name
, self
.type, self
.deleted
]))
567 def __eq__(self
, other
):
568 return self
.id == other
.id
572 return '<{cls} {id}>'.format(cls
=self
.__class
__.__name
__, id=self
.id)
579 def token_generator(length
=32):
580 import random
, string
581 return "".join(random
.sample(string
.letters
+string
.digits
, length
))
584 class User(BaseUser
, SyncedDevicesMixin
):
585 oldid
= IntegerProperty()
586 settings
= DictProperty()
587 devices
= SchemaListProperty(Device
)
588 published_objects
= StringListProperty()
589 deleted
= BooleanProperty(default
=False)
590 suggestions_up_to_date
= BooleanProperty(default
=False)
592 # token for accessing subscriptions of this use
593 subscriptions_token
= StringProperty(default
=token_generator
)
595 # token for accessing the favorite-episodes feed of this user
596 favorite_feeds_token
= StringProperty(default
=token_generator
)
598 # token for automatically updating feeds published by this user
599 publisher_update_token
= StringProperty(default
=token_generator
)
605 def for_oldid(cls
, oldid
):
606 r
= cls
.view('users/users_by_oldid', key
=oldid
, limit
=1, include_docs
=True)
607 return r
.one() if r
else None
610 def create_new_token(self
, token_name
, length
=32):
611 setattr(self
, token_name
, token_generator(length
))
615 def active_devices(self
):
616 not_deleted
= lambda d
: not d
.deleted
617 return filter(not_deleted
, self
.devices
)
621 def inactive_devices(self
):
622 deleted
= lambda d
: d
.deleted
623 return filter(deleted
, self
.devices
)
626 def get_device(self
, id):
627 for device
in self
.devices
:
634 def get_device_by_uid(self
, uid
):
635 for device
in self
.devices
:
636 if device
.uid
== uid
:
640 def get_device_by_oldid(self
, oldid
):
641 for device
in self
.devices
:
642 if device
.oldid
== oldid
:
646 @repeat_on_conflict(['self'])
647 def update_device(self
, device
):
648 """ Sets the device and saves the user """
649 self
.set_device(device
)
653 def set_device(self
, device
):
655 if not RE_DEVICE_UID
.match(device
.uid
):
656 raise DeviceUIDException("'{uid} is not a valid device ID".format(
659 devices
= list(self
.devices
)
660 ids
= [x
.id for x
in devices
]
661 if not device
.id in ids
:
662 devices
.append(device
)
663 self
.devices
= devices
666 index
= ids
.index(device
.id)
668 devices
.insert(index
, device
)
669 self
.devices
= devices
672 def remove_device(self
, device
):
673 devices
= list(self
.devices
)
674 ids
= [x
.id for x
in devices
]
675 if not device
.id in ids
:
678 index
= ids
.index(device
.id)
680 self
.devices
= devices
682 if self
.is_synced(device
):
683 self
.unsync_device(device
)
687 def get_subscriptions(self
, public
=None):
689 Returns a list of (podcast-id, device-id) tuples for all
690 of the users subscriptions
693 r
= PodcastUserState
.view('users/subscribed_podcasts_by_user',
694 startkey
= [self
._id
, public
, None, None],
695 endkey
= [self
._id
+'ZZZ', None, None, None],
698 return [res
['key'][1:] for res
in r
]
701 def get_subscriptions_by_device(self
, public
=None):
702 get_dev
= itemgetter(2)
703 groups
= collections
.defaultdict(list)
704 subscriptions
= self
.get_subscriptions(public
=public
)
705 subscriptions
= sorted(subscriptions
, key
=get_dev
)
707 for public
, podcast_id
, device_id
in subscriptions
:
708 groups
[device_id
].append(podcast_id
)
713 def get_subscribed_podcast_ids(self
, public
=None):
715 Returns the Ids of all subscribed podcasts
717 return list(set(x
[1] for x
in self
.get_subscriptions(public
=public
)))
720 def get_subscribed_podcasts(self
, public
=None):
721 return Podcast
.get_multi(self
.get_subscribed_podcast_ids(public
=public
))
724 def get_subscription_history(self
, device_id
=None, reverse
=False, public
=None):
725 """ Returns chronologically ordered subscription history entries
727 Setting device_id restricts the actions to a certain device
730 def action_iter(state
):
731 for action
in sorted(state
.actions
, reverse
=reverse
):
732 if device_id
is not None and device_id
!= action
.device
:
735 if public
is not None and state
.is_public() != public
:
738 entry
= HistoryEntry()
739 entry
.timestamp
= action
.timestamp
740 entry
.action
= action
.action
741 entry
.podcast_id
= state
.podcast
742 entry
.device_id
= action
.device
745 if device_id
is None:
746 podcast_states
= PodcastUserState
.for_user(self
)
748 podcast_states
= PodcastUserState
.for_device(device_id
)
750 # create an action_iter for each PodcastUserState
751 subscription_action_lists
= [action_iter(x
) for x
in podcast_states
]
753 action_cmp_key
= lambda x
: x
.timestamp
755 # Linearize their subscription-actions
756 return linearize(action_cmp_key
, subscription_action_lists
, reverse
)
759 def get_global_subscription_history(self
, public
=None):
760 """ Actions that added/removed podcasts from the subscription list
762 Returns an iterator of all subscription actions that either
763 * added subscribed a podcast that hasn't been subscribed directly
764 before the action (but could have been subscribed) earlier
765 * removed a subscription of the podcast is not longer subscribed
769 subscriptions
= collections
.defaultdict(int)
771 for entry
in self
.get_subscription_history(public
=public
):
772 if entry
.action
== 'subscribe':
773 subscriptions
[entry
.podcast_id
] += 1
775 # a new subscription has been added
776 if subscriptions
[entry
.podcast_id
] == 1:
779 elif entry
.action
== 'unsubscribe':
780 subscriptions
[entry
.podcast_id
] -= 1
782 # the last subscription has been removed
783 if subscriptions
[entry
.podcast_id
] == 0:
788 def get_newest_episodes(self
, max_date
, max_per_podcast
=5):
789 """ Returns the newest episodes of all subscribed podcasts
791 Only max_per_podcast episodes per podcast are loaded. Episodes with
792 release dates above max_date are discarded.
794 This method returns a generator that produces the newest episodes.
796 The number of required DB queries is equal to the number of (distinct)
797 podcasts of all consumed episodes (max: number of subscribed podcasts),
798 plus a constant number of initial queries (when the first episode is
801 cmp_key
= lambda episode
: episode
.released
or datetime(2000, 01, 01)
803 podcasts
= list(self
.get_subscribed_podcasts())
804 podcasts
= filter(lambda p
: p
.latest_episode_timestamp
, podcasts
)
805 podcasts
= sorted(podcasts
, key
=lambda p
: p
.latest_episode_timestamp
,
808 podcast_dict
= dict((p
.get_id(), p
) for p
in podcasts
)
810 # contains the un-yielded episodes, newest first
813 for podcast
in podcasts
:
817 for episode
in episodes
:
818 # determine for which episodes there won't be a new episodes
819 # that is newer; those can be yielded
820 if episode
.released
> podcast
.latest_episode_timestamp
:
821 p
= podcast_dict
.get(episode
.podcast
, None)
822 yield proxy_object(episode
, podcast
=p
)
823 yielded_episodes
+= 1
827 # remove the episodes that have been yielded before
828 episodes
= episodes
[yielded_episodes
:]
830 # fetch and merge episodes for the next podcast
831 new_episodes
= list(podcast
.get_episodes(since
=1, until
=max_date
,
832 descending
=True, limit
=max_per_podcast
))
833 episodes
= sorted(episodes
+new_episodes
, key
=cmp_key
, reverse
=True)
836 # yield the remaining episodes
837 for episode
in episodes
:
838 podcast
= podcast_dict
.get(episode
.podcast
, None)
839 yield proxy_object(episode
, podcast
=podcast
)
843 def save(self
, *args
, **kwargs
):
844 super(User
, self
).save(*args
, **kwargs
)
846 podcast_states
= PodcastUserState
.for_user(self
)
847 for state
in podcast_states
:
848 @repeat_on_conflict(['state'])
849 def _update_state(state
):
850 old_devs
= set(state
.disabled_devices
)
851 state
.set_device_state(self
.devices
)
853 if old_devs
!= set(state
.disabled_devices
):
856 _update_state(state
=state
)
861 def __eq__(self
, other
):
865 return self
._id
== other
._id
869 return 'User %s' % self
._id
872 class History(object):
874 def __init__(self
, user
, device
):
877 self
._db
= EpisodeUserState
.get_db()
880 self
._view
= 'users/device_history'
881 self
._startkey
= [self
.user
._id
, device
.id, None]
882 self
._endkey
= [self
.user
._id
, device
.id, {}]
884 self
._view
= 'users/history'
885 self
._startkey
= [self
.user
._id
, None]
886 self
._endkey
= [self
.user
._id
, {}]
889 def __getitem__(self
, key
):
891 if isinstance(key
, slice):
892 start
= key
.start
or 0
893 length
= key
.stop
- start
898 res
= self
._db
.view(self
._view
,
900 startkey
= self
._endkey
,
901 endkey
= self
._startkey
,
908 state_doc
= action
['doc']
909 index
= int(action
['value'])
911 if state_doc
['doc_type'] == 'EpisodeUserState':
912 state
= EpisodeUserState
.wrap(state_doc
)
914 state
= PodcastUserState
.wrap(state_doc
)
916 yield HistoryEntry
.from_action_dict(state
, index
)
920 class HistoryEntry(object):
921 """ A class that can represent subscription and episode actions """
925 def from_action_dict(cls
, state
, index
):
927 entry
= HistoryEntry()
928 action
= state
.actions
[index
]
930 if isinstance(state
, EpisodeUserState
):
931 entry
.type = 'Episode'
932 entry
.podcast_url
= state
.podcast_ref_url
933 entry
.episode_url
= state
.ref_url
934 entry
.podcast_id
= state
.podcast
935 entry
.episode_id
= state
.episode
937 entry
.device_id
= action
.device
939 entry
.started
= action
.started
941 entry
.position
= action
.playmark
943 entry
.total
= action
.total
946 entry
.type = 'Subscription'
947 entry
.podcast_url
= state
.ref_url
948 entry
.podcast_id
= state
.podcast
951 entry
.action
= action
.action
952 entry
.timestamp
= action
.timestamp
959 return getattr(self
, 'position', None)
963 def fetch_data(cls
, user
, entries
,
964 podcasts
=None, episodes
=None):
965 """ Efficiently loads additional data for a number of entries """
969 podcast_ids
= [getattr(x
, 'podcast_id', None) for x
in entries
]
970 podcast_ids
= filter(None, podcast_ids
)
971 podcasts
= get_to_dict(Podcast
, podcast_ids
, get_id
=Podcast
.get_id
)
975 episode_ids
= [getattr(x
, 'episode_id', None) for x
in entries
]
976 episode_ids
= filter(None, episode_ids
)
977 episodes
= get_to_dict(Episode
, episode_ids
)
980 # does not need pre-populated data because no db-access is required
981 device_ids
= [getattr(x
, 'device_id', None) for x
in entries
]
982 device_ids
= filter(None, device_ids
)
983 devices
= dict([ (id, user
.get_device(id)) for id in device_ids
])
986 for entry
in entries
:
987 podcast_id
= getattr(entry
, 'podcast_id', None)
988 entry
.podcast
= podcasts
.get(podcast_id
, None)
990 episode_id
= getattr(entry
, 'episode_id', None)
991 entry
.episode
= episodes
.get(episode_id
, None)
994 device
= devices
.get(getattr(entry
, 'device_id', None), None)
995 entry
.device
= device