use most-subscribed podcast to represent its group
[mygpo.git] / mygpo / core / models.py
blob614f001b498f3d8678b2096d3ec3a831150f9f3e
1 import hashlib
2 from dateutil import parser
3 from couchdbkit.ext.django.schema import *
4 from mygpo.decorators import repeat_on_conflict
5 from mygpo import utils
8 class Episode(Document):
9 """
10 Represents an Episode. Can only be part of a Podcast
11 """
13 title = StringProperty()
14 description = StringProperty()
15 link = StringProperty()
16 released = DateTimeProperty()
17 author = StringProperty()
18 duration = IntegerProperty()
19 filesize = IntegerProperty()
20 language = StringProperty()
21 last_update = DateTimeProperty()
22 outdated = BooleanProperty()
23 mimetypes = StringListProperty()
24 merged_ids = StringListProperty()
25 oldid = IntegerProperty()
26 urls = StringListProperty()
27 podcast = StringProperty(required=True)
28 listeners = IntegerProperty()
29 content_types = StringListProperty()
32 @classmethod
33 def get(cls, id):
34 r = cls.view('core/episodes_by_id',
35 key=id,
36 include_docs=True,
38 return r.first() if r else None
41 @classmethod
42 def get_multi(cls, ids):
43 return cls.view('core/episodes_by_id',
44 include_docs=True,
45 keys=ids
49 @classmethod
50 def for_oldid(self, oldid):
51 r = Episode.view('core/episodes_by_oldid', key=oldid, limit=1, include_docs=True)
52 return r.one() if r else None
55 def get_old_obj(self):
56 if self.oldid:
57 from mygpo.api.models import Episode
58 return Episode.objects.get(id=self.oldid)
59 return None
62 def get_user_state(self, user):
63 from mygpo.users.models import EpisodeUserState
64 return EpisodeUserState.for_user_episode(user, self)
67 @property
68 def url(self):
69 return self.urls[0]
71 def __repr__(self):
72 return 'Episode %s' % self._id
75 def listener_count(self, start=None, end={}):
76 """ returns the number of users that have listened to this episode """
78 from mygpo.users.models import EpisodeUserState
79 r = EpisodeUserState.view('users/listeners_by_episode',
80 startkey = [self._id, start],
81 endkey = [self._id, end],
82 reduce = True,
83 group = True,
84 group_level = 1
86 return r.first()['value'] if r else 0
89 def listener_count_timespan(self, start=None, end={}):
90 """ returns (date, listener-count) tuples for all days w/ listeners """
92 from mygpo.users.models import EpisodeUserState
93 r = EpisodeUserState.view('users/listeners_by_episode',
94 startkey = [self._id, start],
95 endkey = [self._id, end],
96 reduce = True,
97 group = True,
98 group_level = 2,
101 for res in r:
102 date = parser.parse(res['key'][1]).date()
103 listeners = res['value']
104 yield (date, listeners)
107 @classmethod
108 def count(cls):
109 r = cls.view('core/episodes_by_podcast', limit=0)
110 return r.total_rows
113 @classmethod
114 def all(cls):
115 return utils.multi_request_view(cls, 'core/episodes_by_podcast',
116 include_docs=True)
118 def __eq__(self, other):
119 if other == None:
120 return False
121 return self.id == other.id
124 class SubscriberData(DocumentSchema):
125 timestamp = DateTimeProperty()
126 subscriber_count = IntegerProperty()
128 def __eq__(self, other):
129 if not isinstance(other, SubscriberData):
130 return False
132 return (self.timestamp == other.timestamp) and \
133 (self.subscriber_count == other.subscriber_count)
136 class PodcastSubscriberData(Document):
137 podcast = StringProperty()
138 subscribers = SchemaListProperty(SubscriberData)
140 @classmethod
141 def for_podcast(cls, id):
142 r = cls.view('core/subscribers_by_podcast', key=id, include_docs=True)
143 if r:
144 return r.first()
146 data = PodcastSubscriberData()
147 data.podcast = id
148 return data
150 def __repr__(self):
151 return 'PodcastSubscriberData for Podcast %s (%s)' % (self.podcast, self._id)
154 class Podcast(Document):
155 id = StringProperty()
156 title = StringProperty()
157 urls = StringListProperty()
158 description = StringProperty()
159 link = StringProperty()
160 last_update = DateTimeProperty()
161 logo_url = StringProperty()
162 author = StringProperty()
163 merged_ids = StringListProperty()
164 oldid = IntegerProperty()
165 group = StringProperty()
166 group_member_name = StringProperty()
167 related_podcasts = StringListProperty()
168 subscribers = SchemaListProperty(SubscriberData)
169 language = StringProperty()
170 content_types = StringListProperty()
171 tags = DictProperty()
174 @classmethod
175 def get(cls, id):
176 r = cls.view('core/podcasts_by_id',
177 key=id,
178 classes=[Podcast, PodcastGroup],
179 include_docs=True,
182 if not r:
183 return None
185 podcast_group = r.first()
186 return podcast_group.get_podcast_by_id(id)
189 @classmethod
190 def get_multi(cls, ids):
191 db = Podcast.get_db()
192 r = db.view('core/podcasts_by_id',
193 keys=ids,
194 include_docs=True,
197 for res in r:
198 if res['doc']['doc_type'] == 'Podcast':
199 yield Podcast.wrap(res['doc'])
200 else:
201 pg = PodcastGroup.wrap(res['doc'])
202 id = res['key']
203 yield pg.get_podcast_by_id(id)
206 @classmethod
207 def for_oldid(cls, oldid):
208 r = cls.view('core/podcasts_by_oldid',
209 key=long(oldid),
210 classes=[Podcast, PodcastGroup],
211 include_docs=True
214 if not r:
215 return None
217 podcast_group = r.first()
218 return podcast_group.get_podcast_by_oldid(oldid)
221 @classmethod
222 def for_url(cls, url):
223 r = cls.view('core/podcasts_by_url',
224 key=url,
225 classes=[Podcast, PodcastGroup],
226 include_docs=True
229 if not r:
230 return None
232 podcast_group = r.first()
233 return podcast_group.get_podcast_by_url(url)
237 def get_podcast_by_id(self, _):
238 return self
239 get_podcast_by_oldid = get_podcast_by_id
240 get_podcast_by_url = get_podcast_by_id
243 def get_id(self):
244 return self.id or self._id
246 @property
247 def display_title(self):
248 return self.title or self.url
250 def get_episodes(self):
251 return list(Episode.view('core/episodes_by_podcast', key=self.get_id(), include_docs=True))
254 @property
255 def url(self):
256 return self.urls[0]
259 def get_podcast(self):
260 return self
263 def get_logo_url(self, size):
264 if self.logo_url:
265 sha = hashlib.sha1(self.logo_url).hexdigest()
266 return '/logo/%d/%s.jpg' % (size, sha)
267 return '/media/podcast-%d.png' % (hash(self.title) % 5, )
270 def subscriber_count(self):
271 if not self.subscribers:
272 return 0
273 return self.subscribers[-1].subscriber_count
276 def prev_subscriber_count(self):
277 if len(self.subscribers) < 2:
278 return 0
279 return self.subscribers[-2].subscriber_count
282 def get_user_state(self, user):
283 from mygpo.users.models import PodcastUserState
284 return PodcastUserState.for_user_podcast(user, self)
287 def get_all_states(self):
288 from mygpo.users.models import PodcastUserState
289 return PodcastUserState.view('users/podcast_states_by_podcast',
290 startkey = [self.get_id(), None],
291 endkey = [self.get_id(), '\ufff0'],
292 include_docs=True)
295 @repeat_on_conflict()
296 def subscribe(self, device):
297 from mygpo import migrate
298 state = self.get_user_state(device.user)
299 device = migrate.get_or_migrate_device(device)
300 state.subscribe(device)
301 state.save()
304 @repeat_on_conflict()
305 def unsubscribe(self, device):
306 from mygpo import migrate
307 state = self.get_user_state(device.user)
308 device = migrate.get_or_migrate_device(device)
309 state.unsubscribe(device)
310 state.save()
313 def subscribe_targets(self, user):
315 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
316 devices/syncgroups on which the podcast is already subscribed
318 targets = []
320 from mygpo.api.models import Device
321 from mygpo import migrate
323 devices = Device.objects.filter(user=user, deleted=False)
324 for d in devices:
325 dev = migrate.get_or_migrate_device(d)
326 subscriptions = dev.get_subscribed_podcasts()
327 if self in subscriptions: continue
329 if d.sync_group:
330 if not d.sync_group in targets: targets.append(d.sync_group)
331 else:
332 targets.append(d)
334 return targets
337 def all_tags(self):
339 Returns all tags that are stored for the podcast, in decreasing order of importance
342 res = Podcast.view('directory/tags_by_podcast', startkey=[self.get_id(), None],
343 endkey=[self.get_id(), 'ZZZZZZ'], reduce=True, group=True, group_level=2)
344 tags = sorted(res.all(), key=lambda x: x['value'], reverse=True)
345 return [x['key'][1] for x in tags]
348 def listener_count(self):
349 """ returns the number of users that have listened to this podcast """
351 from mygpo.users.models import EpisodeUserState
352 r = EpisodeUserState.view('users/listeners_by_podcast',
353 startkey = [self.get_id(), None],
354 endkey = [self.get_id(), {}],
355 group = True,
356 group_level = 1,
357 reduce = True,
359 return r.first()['value']
362 def listener_count_timespan(self, start=None, end={}):
363 """ returns (date, listener-count) tuples for all days w/ listeners """
365 from mygpo.users.models import EpisodeUserState
366 r = EpisodeUserState.view('users/listeners_by_podcast',
367 startkey = [self.get_id(), start],
368 endkey = [self.get_id(), end],
369 group = True,
370 group_level = 2,
371 reduce = True,
374 for res in r:
375 date = parser.parse(res['key'][1]).date()
376 listeners = res['value']
377 yield (date, listeners)
380 def episode_listener_counts(self):
381 """ (Episode-Id, listener-count) tuples for episodes w/ listeners """
383 from mygpo.users.models import EpisodeUserState
384 r = EpisodeUserState.view('users/listeners_by_podcast_episode',
385 startkey = [self.get_id(), None, None],
386 endkey = [self.get_id(), {}, {}],
387 group = True,
388 group_level = 2,
389 reduce = True,
392 for res in r:
393 episode = res['key'][1]
394 listeners = res['value']
395 yield (episode, listeners)
398 def get_episode_states(self, user_oldid):
399 """ Returns the latest episode actions for the podcast's episodes """
401 from mygpo.users.models import EpisodeUserState
402 db = EpisodeUserState.get_db()
404 res = db.view('users/episode_states',
405 startkey= [user_oldid, self.get_id(), None],
406 endkey = [user_oldid, self.get_id(), {}]
409 for r in res:
410 action = r['value']
411 yield action
414 def get_old_obj(self):
415 if self.oldid:
416 from mygpo.api.models import Podcast
417 return Podcast.objects.get(id=self.oldid)
418 return None
421 def __hash__(self):
422 return hash(self.get_id())
425 def __repr__(self):
426 if not self._id:
427 return super(Podcast, self).__repr__()
428 elif self.oldid:
429 return '%s %s (%s)' % (self.__class__.__name__, self.get_id(), self.oldid)
430 else:
431 return '%s %s' % (self.__class__.__name__, self.get_id())
434 def save(self):
435 group = getattr(self, 'group', None)
436 if group: #we are part of a PodcastGroup
437 group = PodcastGroup.get(group)
438 podcasts = list(group.podcasts)
440 if not self in podcasts:
441 # the podcast has not been added to the group correctly
442 group.add_podcast(self)
444 else:
445 i = podcasts.index(self)
446 podcasts[i] = self
447 group.podcasts = podcasts
448 group.save()
450 i = podcasts.index(self)
451 podcasts[i] = self
452 group.podcasts = podcasts
453 group.save()
455 else:
456 super(Podcast, self).save()
459 def delete(self):
460 group = getattr(self, 'group', None)
461 if group:
462 group = PodcastGroup.get(group)
463 podcasts = list(group.podcasts)
465 if self in podcasts:
466 i = podcasts.index(self)
467 del podcasts[i]
468 group.podcasts = podcasts
469 group.save()
471 else:
472 super(Podcast, self).delete()
474 @classmethod
475 def all_podcasts_groups(cls):
476 return cls.view('core/podcasts_groups', include_docs=True,
477 classes=[Podcast, PodcastGroup]).iterator()
480 def __eq__(self, other):
481 if not self.get_id():
482 return self == other
484 if other == None:
485 return False
487 return self.get_id() == other.get_id()
490 @classmethod
491 def all_podcasts(cls):
492 from mygpo.utils import multi_request_view
494 for r in multi_request_view(cls, 'core/podcasts_by_oldid', wrap=False, include_docs=True):
495 obj = r['doc']
496 if obj['doc_type'] == 'Podcast':
497 yield Podcast.wrap(obj)
498 else:
499 oldid = r[u'key']
500 pg = PodcastGroup.wrap(obj)
501 podcast = pg.get_podcast_by_oldid(oldid)
502 yield podcast
506 class PodcastGroup(Document):
507 title = StringProperty()
508 podcasts = SchemaListProperty(Podcast)
510 def get_id(self):
511 return self._id
513 @classmethod
514 def for_oldid(cls, oldid):
515 r = cls.view('core/podcastgroups_by_oldid', \
516 key=oldid, limit=1, include_docs=True)
517 return r.first() if r else None
519 def get_podcast_by_id(self, id):
520 for podcast in self.podcasts:
521 if podcast.get_id() == id:
522 return podcast
523 if id in podcast.merged_ids:
524 return podcast
527 def get_podcast_by_oldid(self, oldid):
528 for podcast in self.podcasts:
529 if podcast.oldid == oldid:
530 return podcast
533 def get_podcast_by_url(self, url):
534 for podcast in self.podcasts:
535 if url in list(podcast.urls):
536 return podcast
539 def subscriber_count(self):
540 return sum([p.subscriber_count() for p in self.podcasts])
543 def prev_subscriber_count(self):
544 return sum([p.prev_subscriber_count() for p in self.podcasts])
546 @property
547 def display_title(self):
548 return self.title
551 def get_podcast(self):
552 # return podcast with most subscribers (bug 1390)
553 return sorted(self.podcasts, key=Podcast.subscriber_count,
554 reverse=True)[0]
557 @property
558 def logo_url(self):
559 return utils.first(p.logo_url for p in self.podcasts)
562 def get_logo_url(self, size):
563 if self.logo_url:
564 sha = hashlib.sha1(self.logo_url).hexdigest()
565 return '/logo/%d/%s.jpg' % (size, sha)
566 return '/media/podcast-%d.png' % (hash(self.title) % 5, )
569 def add_podcast(self, podcast):
570 if not podcast.id:
571 podcast.id = podcast._id
573 if not self._id:
574 raise ValueError('group has to have an _id first')
576 podcast.delete()
577 podcast.group = self._id
578 self.podcasts.append(podcast)
579 self.save()
580 return self.podcasts[-1]
582 def get_old_obj(self):
583 from mygpo.api.models import PodcastGroup
584 return PodcastGroup.objects.get(id=self.oldid) if self.oldid else None
587 def __repr__(self):
588 if not self._id:
589 return super(PodcastGroup, self).__repr__()
590 elif self.oldid:
591 return '%s %s (%s)' % (self.__class__.__name__, self._id[:10], self.oldid)
592 else:
593 return '%s %s' % (self.__class__.__name__, self._id[:10])
596 class SanitizingRuleStub(object):
597 pass
599 class SanitizingRule(Document):
600 slug = StringProperty()
601 applies_to = StringListProperty()
602 search = StringProperty()
603 replace = StringProperty()
604 priority = IntegerProperty()
605 description = StringProperty()
608 @classmethod
609 def for_obj_type(cls, obj_type):
610 r = cls.view('core/sanitizing_rules_by_target', include_docs=True,
611 startkey=[obj_type, None], endkey=[obj_type, {}])
613 for rule in r:
614 obj = SanitizingRuleStub()
615 obj.slug = rule.slug
616 obj.applies_to = list(rule.applies_to)
617 obj.search = rule.search
618 obj.replace = rule.replace
619 obj.priority = rule.priority
620 obj.description = rule.description
621 yield obj
624 @classmethod
625 def for_slug(cls, slug):
626 r = cls.view('core/sanitizing_rules_by_slug', include_docs=True,
627 key=slug)
628 return r.one() if r else None
631 def __repr__(self):
632 return 'SanitizingRule %s' % self._id