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
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
)
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
):
64 returns all public subscriptions to this podcast
66 return Subscription
.objects
.public_subscriptions([self
])
69 def subscription_count(self
):
70 return self
.subscriptions().count()
72 def subscriber_count(self
):
74 Returns the number of public subscriptions to this podcast
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
):
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
99 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
101 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
102 if self
in subscriptions
: continue
105 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
112 def group_with(self
, other
, grouptitle
, myname
, othername
):
113 if self
.group
== other
.group
and self
.group
!= None:
116 if self
.group
!= None:
117 if other
.group
== None:
118 self
.group
.add(other
, othername
)
121 raise ValueError('the podcasts are already in different groups')
123 if other
.group
== None:
124 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
126 g
.add(other
, othername
)
132 if self
.group
== None:
133 raise ValueError('the podcast currently isn\'t in any group')
139 podcasts
= Podcast
.objects
.filter(group
=g
)
140 if podcasts
.count() == 1:
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
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:
170 podcast
.group_member_name
= membername
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
):
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()
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
230 return self
.podcast_group
.podcasts()[0]
232 def __unicode__(self
):
233 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
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
)
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
)
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
)
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)
304 m
= re
.search('\D*(\d+)\D+', self
.title
)
305 return m
.group(1) if m
else ''
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
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
)
330 unique_together
= ('podcast', 'url')
332 class SyncGroup(models
.Model
):
334 Devices that should be synced with each other need to be grouped
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
)
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:
357 device
.sync_group
= self
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
):
378 return Subscription
.objects
.filter(device
=self
)
381 for s
in self
.get_sync_actions():
383 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
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
) )
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:
409 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
411 sync_actions
= self
.latest_actions()
414 a
= d
.latest_actions()
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
441 actions
[p
] = self
.latest_action(p
)
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:
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
):
470 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
473 if self
.sync_group
!= None:
474 if other
.sync_group
== None:
475 self
.sync_group
.add(other
)
478 raise ValueError('the devices are in different sync groups')
481 if other
.sync_group
== None:
482 g
= SyncGroup
.objects
.create(user
=self
.user
)
487 oter
.sync_group
.add(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')
500 self
.sync_group
= None
503 devices
= Device
.objects
.filter(sync_group
=g
)
504 if devices
.count() == 1:
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
))
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)
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
)
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
)
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
)
575 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
579 #this method has to be overwritten, if not it tries to delete a view
584 db_table
= 'current_subscription'
585 #not available in Django 1.0 (Debian stable)
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
)
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)
619 EpisodeFavorite
.objects
.get_or_create(user
=self
.user
, episode
=self
.episode
)
621 EpisodeFavorite
.objects
.filter(user
=self
.user
, episode
=self
.episode
).delete()
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
)
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)
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
)