refactoring in publisher and Podcast model code
[mygpo.git] / mygpo / api / models / __init__.py
blob3a9c2f89a85b0d12098e47a81f65a50a95208a2b
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 django.db import models
19 from django.contrib.auth.models import User, UserManager
20 from datetime import datetime
21 from django.utils.translation import ugettext as _
22 from mygpo.api.fields import SeparatedValuesField, JSONField
23 import hashlib
24 import re
25 import json
27 from mygpo.api.constants import EPISODE_ACTION_TYPES, DEVICE_TYPES, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SUBSCRIPTION_ACTION_TYPES
28 from mygpo.log import log
30 class UserProfile(models.Model):
31 user = models.ForeignKey(User, unique=True, db_column='user_ptr_id')
33 public_profile = models.BooleanField(default=True)
34 generated_id = models.BooleanField(default=False)
35 deleted = models.BooleanField(default=False)
36 suggestion_up_to_date = models.BooleanField(default=False)
37 settings = JSONField(default={})
39 def __unicode__(self):
40 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
42 def save(self, *args, **kwargs):
43 self.public_profile = self.settings.get('public_profile', True)
44 super(UserProfile, self).save(*args, **kwargs)
46 class Meta:
47 db_table = 'user'
49 class Podcast(models.Model):
50 url = models.URLField(unique=True, verify_exists=False)
51 title = models.CharField(max_length=100, blank=True)
52 description = models.TextField(blank=True, null=True)
53 link = models.URLField(blank=True, null=True, verify_exists=False)
54 last_update = models.DateTimeField(null=True,blank=True)
55 logo_url = models.CharField(max_length=1000,null=True,blank=True)
56 author = models.CharField(max_length=100, null=True, blank=True)
57 language = models.CharField(max_length=10, null=True, blank=True)
58 group = models.ForeignKey('PodcastGroup', null=True)
59 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
60 content_types = SeparatedValuesField(null=True, blank=True)
62 def subscriptions(self):
63 """
64 returns all public subscriptions to this podcast
65 """
66 return Subscription.objects.public_subscriptions([self])
69 def subscription_count(self):
70 return self.subscriptions().count()
72 def subscriber_count(self):
73 """
74 Returns the number of public subscriptions to this podcast
75 """
76 subscriptions = self.subscriptions()
77 return subscriptions.values('user').distinct().count()
80 def listener_count(self):
81 from mygpo.data.models import Listener
82 return Listener.objects.filter(podcast=self).values('user').distinct().count()
84 def listener_count_timespan(self, start, end):
85 return EpisodeAction.objects.filter(episode__podcast=self,
86 timestamp__range=(start, end),
87 action='play').values('user_id').distinct().count()
89 def logo_shortname(self):
90 return hashlib.sha1(self.logo_url).hexdigest()
92 def subscribe_targets(self, user):
93 """
94 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
95 devices/syncgroups on which the podcast is already subscribed
96 """
97 targets = []
99 devices = Device.objects.filter(user=user, deleted=False)
100 for d in devices:
101 subscriptions = [x.podcast for x in d.get_subscriptions()]
102 if self in subscriptions: continue
104 if d.sync_group:
105 if not d.sync_group in targets: targets.append(d.sync_group)
106 else:
107 targets.append(d)
109 return targets
112 def group_with(self, other, grouptitle, myname, othername):
113 if self.group == other.group and self.group != None:
114 return
116 if self.group != None:
117 if other.group == None:
118 self.group.add(other, othername)
120 else:
121 raise ValueError('the podcasts are already in different groups')
122 else:
123 if other.group == None:
124 g = PodcastGroup.objects.create(title=grouptitle)
125 g.add(self, myname)
126 g.add(other, othername)
128 else:
129 oter.group.add(self)
131 def ungroup(self):
132 if self.group == None:
133 raise ValueError('the podcast currently isn\'t in any group')
135 g = self.group
136 self.group = None
137 self.save()
139 podcasts = Podcast.objects.filter(group=g)
140 if podcasts.count() == 1:
141 p = podcasts[0]
142 p.group = None
143 p.save()
145 def get_similar(self):
146 from mygpo.data.models import RelatedPodcast
147 return [r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=self)]
149 def get_episodes(self):
150 return Episode.objects.filter(podcast=self)
152 def __unicode__(self):
153 return self.title if self.title != '' else self.url
155 class Meta:
156 db_table = 'podcast'
159 class PodcastGroup(models.Model):
160 title = models.CharField(max_length=100, blank=False)
162 def add(self, podcast, membername):
163 if podcast.group == self:
164 podcast.group_member_name = membername
166 elif podcast.group != None:
167 podcast.ungroup()
169 podcast.group = self
170 podcast.group_member_name = membername
171 podcast.save()
173 def podcasts(self):
174 return Podcast.objects.filter(group=self)
176 def subscriptions(self):
178 returns the public subscriptions to podcasts in the group
180 return Subscription.objects.public_subscriptions(self.podcasts())
182 def subscription_count(self):
183 return self.subscriptions().count()
185 def subscriber_count(self):
187 Returns the number of public subscriptions to podcasts of this group
189 subscriptions = self.subscriptions()
190 return subscriptions.values('user').distinct().count()
193 def __unicode__(self):
194 return self.title
196 class Meta:
197 db_table = 'podcast_groups'
200 class ToplistEntryManager(models.Manager):
202 def get_query_set(self):
203 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
206 class ToplistEntry(models.Model):
207 podcast = models.ForeignKey(Podcast, null=True)
208 podcast_group = models.ForeignKey(PodcastGroup, null=True)
209 oldplace = models.IntegerField(db_column='old_place')
210 subscriptions = models.IntegerField(db_column='subscription_count')
212 objects = ToplistEntryManager()
215 def get_item(self):
216 if self.podcast:
217 return self.podcast
218 else:
219 return self.podcast_group
221 def get_podcast(self):
223 Returns a podcast which is representative for this toplist-entry
224 If the entry is a non-grouped podcast, it is returned
225 If the entry is a podcast group, one of its podcasts is returned
227 if self.podcast:
228 return self.podcast
229 else:
230 return self.podcast_group.podcasts()[0]
232 def __unicode__(self):
233 return '%s (%s)' % (self.podcast, self.subscriptions)
235 class Meta:
236 db_table = 'toplist'
239 class EpisodeToplistEntryManager(models.Manager):
241 def get_query_set(self):
242 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
245 class EpisodeToplistEntry(models.Model):
246 episode = models.ForeignKey('Episode')
247 listeners = models.PositiveIntegerField()
249 objects = EpisodeToplistEntryManager()
251 def __unicode__(self):
252 return '%s (%s)' % (self.episode, self.listeners)
254 class Meta:
255 db_table = 'episode_toplist'
258 class SuggestionEntryManager(models.Manager):
260 def for_user(self, user):
261 from mygpo.data.models import SuggestionBlacklist
263 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
265 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
266 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
268 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
269 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
271 return suggestions
274 class SuggestionEntry(models.Model):
275 podcast = models.ForeignKey(Podcast)
276 user = models.ForeignKey(User)
277 priority = models.IntegerField()
279 objects = SuggestionEntryManager()
281 def __unicode__(self):
282 return '%s (%s)' % (self.podcast, self.priority)
284 class Meta:
285 db_table = 'suggestion'
288 class Episode(models.Model):
289 podcast = models.ForeignKey(Podcast)
290 url = models.URLField(verify_exists=False)
291 title = models.CharField(max_length=100, blank=True)
292 description = models.TextField(null=True, blank=True)
293 link = models.URLField(null=True, blank=True, verify_exists=False)
294 timestamp = models.DateTimeField(null=True, blank=True)
295 author = models.CharField(max_length=100, null=True, blank=True)
296 duration = models.PositiveIntegerField(null=True, blank=True)
297 filesize = models.PositiveIntegerField(null=True, blank=True)
298 language = models.CharField(max_length=10, null=True, blank=True)
299 last_update = models.DateTimeField(auto_now=True)
300 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
301 mimetype = models.CharField(max_length=30, blank=True, null=True)
303 def number(self):
304 m = re.search('\D*(\d+)\D+', self.title)
305 return m.group(1) if m else ''
307 def shortname(self):
308 s = self.title
309 s = s.replace(self.podcast.title, '')
310 s = s.replace(self.number(), '')
311 m = re.search('\W*(.+)', s)
312 s = m.group(1) if m else s
313 s = s.strip()
314 return s
316 def listener_count(self):
317 from mygpo.data.models import Listener
318 return Listener.objects.filter(episode=self).values('user').distinct().count()
320 def listener_count_timespan(self, start, end):
321 return EpisodeAction.objects.filter(episode=self,
322 timestamp__range=(start, end),
323 action='play').values('user_id').distinct().count()
325 def __unicode__(self):
326 return '%s (%s)' % (self.shortname(), self.podcast)
328 class Meta:
329 db_table = 'episode'
330 unique_together = ('podcast', 'url')
332 class SyncGroup(models.Model):
334 Devices that should be synced with each other need to be grouped
335 in a SyncGroup.
337 SyncGroups are automatically created by calling
338 device.sync_with(other_device), but can also be created manually.
340 device.sync() synchronizes the device for which the method is called
341 with the other devices in its SyncGroup.
343 user = models.ForeignKey(User)
345 def __unicode__(self):
346 devices = [d.name for d in Device.objects.filter(sync_group=self)]
347 return ', '.join(devices)
349 def devices(self):
350 return Device.objects.filter(sync_group=self)
352 def add(self, device):
353 if device.sync_group == self: return
354 if device.sync_group != None:
355 device.unsync()
357 device.sync_group = self
358 device.save()
360 class Meta:
361 db_table = 'sync_group'
364 class Device(models.Model):
365 user = models.ForeignKey(User)
366 uid = models.SlugField(max_length=50)
367 name = models.CharField(max_length=100, blank=True)
368 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
369 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
370 deleted = models.BooleanField(default=False)
371 settings = JSONField(default={})
373 def __unicode__(self):
374 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
376 def get_subscriptions(self):
377 self.sync()
378 return Subscription.objects.filter(device=self)
380 def sync(self):
381 for s in self.get_sync_actions():
382 try:
383 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
384 except Exception, e:
385 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
387 def sync_targets(self):
389 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
391 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
393 sync_groups = SyncGroup.objects.filter(user=self.user)
394 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
396 sync_targets.extend( list(sync_groups) )
397 return sync_targets
400 def get_sync_actions(self):
402 returns the SyncGroupSubscriptionActions correspond to the
403 SubscriptionActions that need to be saved for the current device
404 to synchronize it with its SyncGroup
406 if self.sync_group == None:
407 return []
409 devices = self.sync_group.devices().exclude(pk=self.id)
411 sync_actions = self.latest_actions()
413 for d in devices:
414 a = d.latest_actions()
415 for s in a.keys():
416 if not sync_actions.has_key(s):
417 if a[s].action == SUBSCRIBE_ACTION:
418 sync_actions[s] = a[s]
419 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
420 sync_actions[s] = a[s]
422 #remove actions that did not change
423 current_state = self.latest_actions()
424 for podcast in current_state.keys():
425 if podcast in current_state and podcast in sync_actions and sync_actions[podcast] == current_state[podcast]:
426 del sync_actions[podcast]
428 return sync_actions.values()
430 def latest_actions(self):
432 returns the latest action for each podcast
433 that has an action on this device
435 #all podcasts that have an action on this device
436 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
437 podcasts = list(set(podcasts)) #remove duplicates
439 actions = {}
440 for p in podcasts:
441 actions[p] = self.latest_action(p)
443 return actions
445 def latest_action(self, podcast):
447 returns the latest action for the given podcast on this device
449 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
450 if actions.count() == 0:
451 return None
452 else:
453 return actions[0]
455 def sync_with(self, other):
457 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
458 this method places them in the same SyncGroup. get_sync_actions() can
459 then return the SyncGroupSubscriptionActions for brining the device
460 in sync with its group
462 if self.user != other.user:
463 raise ValueError('the devices belong to different users')
465 if isinstance(other, SyncGroup):
466 other.add(self)
467 self.save()
468 return
470 if self.sync_group == other.sync_group and self.sync_group != None:
471 return
473 if self.sync_group != None:
474 if other.sync_group == None:
475 self.sync_group.add(other)
477 else:
478 raise ValueError('the devices are in different sync groups')
480 else:
481 if other.sync_group == None:
482 g = SyncGroup.objects.create(user=self.user)
483 g.add(self)
484 g.add(other)
486 else:
487 oter.sync_group.add(self)
489 def unsync(self):
491 stops synchronizing the device
492 this method removes the device from its SyncGroup. If only one
493 device remains in the SyncGroup, it is removed so the device can
494 be used in other groups.
496 if self.sync_group == None:
497 raise ValueError('the device is not synced')
499 g = self.sync_group
500 self.sync_group = None
501 self.save()
503 devices = Device.objects.filter(sync_group=g)
504 if devices.count() == 1:
505 d = devices[0]
506 d.sync_group = None
507 d.save()
508 g.delete()
510 class Meta:
511 db_table = 'device'
513 class EpisodeAction(models.Model):
514 user = models.ForeignKey(User)
515 episode = models.ForeignKey(Episode)
516 device = models.ForeignKey(Device,null=True)
517 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
518 timestamp = models.DateTimeField(default=datetime.now)
519 started = models.IntegerField(null=True, blank=True)
520 playmark = models.IntegerField(null=True, blank=True)
521 total = models.IntegerField(null=True, blank=True)
523 def __unicode__(self):
524 return '%s %s %s' % (self.user, self.action, self.episode)
526 def playmark_time(self):
527 return datetime.fromtimestamp(float(self.playmark))
529 def started_time(self):
530 return datetime.fromtimestamp(float(self.started))
532 class Meta:
533 db_table = 'episode_log'
536 class SubscriptionManager(models.Manager):
538 def public_subscriptions(self, podcasts=None):
540 Returns either all public subscriptions or those for the given podcasts
543 subscriptions = self.filter(podcast__in=podcasts) if podcasts else self.all()
545 # remove users with private profiles
546 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
548 # remove inactive (eg deleted) users
549 subscriptions = subscriptions.exclude(user__is_active=False)
551 if podcasts:
552 # remove uers that have marked their subscription to this podcast as private
553 private_users = SubscriptionMeta.objects.filter(podcast__in=podcasts, public=False).values('user')
554 subscriptions = subscriptions.exclude(user__in=private_users)
556 return subscriptions
559 class Subscription(models.Model):
560 device = models.ForeignKey(Device, primary_key=True)
561 podcast = models.ForeignKey(Podcast)
562 user = models.ForeignKey(User)
563 subscribed_since = models.DateTimeField()
565 objects = SubscriptionManager()
567 def __unicode__(self):
568 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
570 def get_meta(self):
571 #this is different than get_or_create because it does not necessarily create a new meta-object
572 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
574 if qs.count() == 0:
575 return SubscriptionMeta(user=self.user, podcast=self.podcast)
576 else:
577 return qs[0]
579 #this method has to be overwritten, if not it tries to delete a view
580 def delete(self):
581 pass
583 class Meta:
584 db_table = 'current_subscription'
585 #not available in Django 1.0 (Debian stable)
586 managed = False
589 class SubscriptionMeta(models.Model):
590 user = models.ForeignKey(User)
591 podcast = models.ForeignKey(Podcast)
592 public = models.BooleanField(default=True)
593 settings = JSONField(default={})
595 def __unicode__(self):
596 return '%s - %s - %s' % (self.user, self.podcast, self.public)
598 def save(self, *args, **kwargs):
599 self.public = self.settings.get('public_subscription', True)
600 super(SubscriptionMeta, self).save(*args, **kwargs)
603 class Meta:
604 db_table = 'subscription'
605 unique_together = ('user', 'podcast')
608 class EpisodeSettings(models.Model):
609 user = models.ForeignKey(User)
610 episode = models.ForeignKey(Episode)
611 settings = JSONField(default={})
613 def save(self, *args, **kwargs):
614 super(EpisodeSettings, self).save(*args, **kwargs)
616 from mygpo.api.models.users import EpisodeFavorite
617 fav = self.settings.get('is_favorite', False)
618 if fav:
619 EpisodeFavorite.objects.get_or_create(user=self.user, episode=self.episode)
620 else:
621 EpisodeFavorite.objects.filter(user=self.user, episode=self.episode).delete()
624 class Meta:
625 db_table = 'episode_settings'
626 unique_together = ('user', 'episode')
629 class SubscriptionAction(models.Model):
630 device = models.ForeignKey(Device)
631 podcast = models.ForeignKey(Podcast)
632 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
633 timestamp = models.DateTimeField(blank=True, default=datetime.now)
635 def action_string(self):
636 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
638 def newer_than(self, action):
639 return self.timestamp > action.timestamp
641 def __unicode__(self):
642 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
644 class Meta:
645 db_table = 'subscription_log'
646 unique_together = ('device', 'podcast', 'timestamp')
649 class URLSanitizingRule(models.Model):
650 use_podcast = models.BooleanField()
651 use_episode = models.BooleanField()
652 search = models.CharField(max_length=100)
653 search_precompiled = None
654 replace = models.CharField(max_length=100, null=False, blank=True)
655 priority = models.PositiveIntegerField()
656 description = models.TextField(null=False, blank=True)
658 class Meta:
659 db_table = 'sanitizing_rules'
661 def __unicode__(self):
662 return '%s -> %s' % (self.search, self.replace)
665 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry, remove_podcast_entry, remove_podcast_group_entry
666 from django.db.models.signals import post_save, pre_delete
668 post_save.connect(update_podcast_entry, sender=Podcast)
669 pre_delete.connect(remove_podcast_entry, sender=Podcast)
671 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
672 pre_delete.connect(remove_podcast_group_entry, sender=PodcastGroup)