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
20 from datetime
import datetime
21 from django
.utils
.translation
import ugettext
as _
22 from mygpo
.api
.fields
import SeparatedValuesField
, JSONField
26 from mygpo
.api
.constants
import EPISODE_ACTION_TYPES
, DEVICE_TYPES
, UNSUBSCRIBE_ACTION
, SUBSCRIBE_ACTION
, SUBSCRIPTION_ACTION_TYPES
27 from mygpo
.log
import log
29 class UserProfile(models
.Model
):
30 user
= models
.ForeignKey(User
, unique
=True, db_column
='user_ptr_id')
32 public_profile
= models
.BooleanField(default
=True)
33 generated_id
= models
.BooleanField(default
=False)
34 deleted
= models
.BooleanField(default
=False)
35 suggestion_up_to_date
= models
.BooleanField(default
=False)
36 settings
= JSONField(default
={})
38 def __unicode__(self
):
39 return '%s (%s, %s)' % (self
.user
.username
, self
.public_profile
, self
.generated_id
)
41 def save(self
, *args
, **kwargs
):
42 self
.public_profile
= self
.settings
.get('public_profile', True)
43 super(UserProfile
, self
).save(*args
, **kwargs
)
48 class Podcast(models
.Model
):
49 url
= models
.URLField(unique
=True, verify_exists
=False)
50 title
= models
.CharField(max_length
=100, blank
=True)
51 description
= models
.TextField(blank
=True, null
=True)
52 link
= models
.URLField(blank
=True, null
=True, verify_exists
=False)
53 last_update
= models
.DateTimeField(null
=True,blank
=True)
54 logo_url
= models
.CharField(max_length
=1000,null
=True,blank
=True)
55 author
= models
.CharField(max_length
=100, null
=True, blank
=True)
56 language
= models
.CharField(max_length
=10, null
=True, blank
=True)
57 group
= models
.ForeignKey('PodcastGroup', null
=True)
58 group_member_name
= models
.CharField(max_length
=20, default
=None, null
=True, blank
=False)
59 content_types
= SeparatedValuesField(null
=True, blank
=True)
62 def subscribe(self
, device
):
64 Subscribe to the current Podcast on the given Device
66 SubscriptionAction
.objects
.create(podcast
=self
, action
=SUBSCRIBE_ACTION
, device
=device
)
69 def unsubscribe(self
, device
):
71 Unsubscribe the current Podcast from the given Device
73 SubscriptionAction
.objects
.create(podcast
=self
, action
=UNSUBSCRIBE_ACTION
, device
=device
)
76 def subscriptions(self
):
78 returns all public subscriptions to this podcast
80 return Subscription
.objects
.public_subscriptions([self
])
83 def subscription_count(self
):
84 return self
.subscriptions().count()
86 def subscriber_count(self
):
88 Returns the number of public subscriptions to this podcast
90 subscriptions
= self
.subscriptions()
91 return subscriptions
.values('user').distinct().count()
94 def listener_count(self
):
95 from mygpo
.data
.models
import Listener
96 return Listener
.objects
.filter(podcast
=self
).values('user').distinct().count()
98 def listener_count_timespan(self
, start
, end
):
99 return EpisodeAction
.objects
.filter(episode__podcast
=self
,
100 timestamp__range
=(start
, end
),
101 action
='play').values('user_id').distinct().count()
103 def logo_shortname(self
):
104 return hashlib
.sha1(self
.logo_url
).hexdigest()
106 def subscribe_targets(self
, user
):
108 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
109 devices/syncgroups on which the podcast is already subscribed
113 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
115 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
116 if self
in subscriptions
: continue
119 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
126 def group_with(self
, other
, grouptitle
, myname
, othername
):
127 if self
.group
== other
.group
and self
.group
!= None:
130 if self
.group
!= None:
131 if other
.group
== None:
132 self
.group
.add(other
, othername
)
135 raise ValueError('the podcasts are already in different groups')
137 if other
.group
== None:
138 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
140 g
.add(other
, othername
)
146 if self
.group
== None:
147 raise ValueError('the podcast currently isn\'t in any group')
153 podcasts
= Podcast
.objects
.filter(group
=g
)
154 if podcasts
.count() == 1:
159 def get_similar(self
):
160 from mygpo
.data
.models
import RelatedPodcast
161 return [r
.rel_podcast
for r
in RelatedPodcast
.objects
.filter(ref_podcast
=self
)]
163 def get_episodes(self
):
164 return Episode
.objects
.filter(podcast
=self
)
166 def __unicode__(self
):
167 return self
.title
if self
.title
!= '' else self
.url
173 class PodcastGroup(models
.Model
):
174 title
= models
.CharField(max_length
=100, blank
=False)
176 def add(self
, podcast
, membername
):
177 if podcast
.group
== self
:
178 podcast
.group_member_name
= membername
180 elif podcast
.group
!= None:
184 podcast
.group_member_name
= membername
188 return Podcast
.objects
.filter(group
=self
)
190 def subscriptions(self
):
192 returns the public subscriptions to podcasts in the group
194 return Subscription
.objects
.public_subscriptions(self
.podcasts())
196 def subscription_count(self
):
197 return self
.subscriptions().count()
199 def subscriber_count(self
):
201 Returns the number of public subscriptions to podcasts of this group
203 subscriptions
= self
.subscriptions()
204 return subscriptions
.values('user').distinct().count()
207 def __unicode__(self
):
211 db_table
= 'podcast_groups'
214 class ToplistEntryManager(models
.Manager
):
216 def get_query_set(self
):
217 return super(ToplistEntryManager
, self
).get_query_set().order_by('-subscriptions')
220 class ToplistEntry(models
.Model
):
221 podcast
= models
.ForeignKey(Podcast
, null
=True)
222 podcast_group
= models
.ForeignKey(PodcastGroup
, null
=True)
223 oldplace
= models
.IntegerField(db_column
='old_place')
224 subscriptions
= models
.IntegerField(db_column
='subscription_count')
226 objects
= ToplistEntryManager()
233 return self
.podcast_group
235 def get_podcast(self
):
237 Returns a podcast which is representative for this toplist-entry
238 If the entry is a non-grouped podcast, it is returned
239 If the entry is a podcast group, one of its podcasts is returned
244 return self
.podcast_group
.podcasts()[0]
246 def __unicode__(self
):
247 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
253 class EpisodeToplistEntryManager(models
.Manager
):
255 def get_query_set(self
):
256 return super(EpisodeToplistEntryManager
, self
).get_query_set().order_by('-listeners')
259 class EpisodeToplistEntry(models
.Model
):
260 episode
= models
.ForeignKey('Episode')
261 listeners
= models
.PositiveIntegerField()
263 objects
= EpisodeToplistEntryManager()
265 def __unicode__(self
):
266 return '%s (%s)' % (self
.episode
, self
.listeners
)
269 db_table
= 'episode_toplist'
272 class SuggestionEntryManager(models
.Manager
):
274 def for_user(self
, user
):
275 from mygpo
.data
.models
import SuggestionBlacklist
277 suggestions
= SuggestionEntry
.objects
.filter(user
=user
).order_by('-priority')
279 subscriptions
= [x
.podcast
for x
in Subscription
.objects
.filter(user
=user
)]
280 suggestions
= filter(lambda x
: x
.podcast
not in subscriptions
, suggestions
)
282 blacklist
= [x
.podcast
for x
in SuggestionBlacklist
.objects
.filter(user
=user
)]
283 suggestions
= filter(lambda x
: x
.podcast
not in blacklist
, suggestions
)
288 class SuggestionEntry(models
.Model
):
289 podcast
= models
.ForeignKey(Podcast
)
290 user
= models
.ForeignKey(User
)
291 priority
= models
.IntegerField()
293 objects
= SuggestionEntryManager()
295 def __unicode__(self
):
296 return '%s (%s)' % (self
.podcast
, self
.priority
)
299 db_table
= 'suggestion'
302 class Episode(models
.Model
):
303 podcast
= models
.ForeignKey(Podcast
)
304 url
= models
.URLField(verify_exists
=False)
305 title
= models
.CharField(max_length
=100, blank
=True)
306 description
= models
.TextField(null
=True, blank
=True)
307 link
= models
.URLField(null
=True, blank
=True, verify_exists
=False)
308 timestamp
= models
.DateTimeField(null
=True, blank
=True)
309 author
= models
.CharField(max_length
=100, null
=True, blank
=True)
310 duration
= models
.PositiveIntegerField(null
=True, blank
=True)
311 filesize
= models
.PositiveIntegerField(null
=True, blank
=True)
312 language
= models
.CharField(max_length
=10, null
=True, blank
=True)
313 last_update
= models
.DateTimeField(auto_now
=True)
314 outdated
= models
.BooleanField(default
=False) #set to true after episode hasn't been found in feed
315 mimetype
= models
.CharField(max_length
=30, blank
=True, null
=True)
318 m
= re
.search('\D*(\d+)\D+', self
.title
)
319 return m
.group(1) if m
else ''
323 s
= s
.replace(self
.podcast
.title
, '')
324 s
= s
.replace(self
.number(), '')
325 m
= re
.search('\W*(.+)', s
)
326 s
= m
.group(1) if m
else s
330 def listener_count(self
):
331 from mygpo
.data
.models
import Listener
332 return Listener
.objects
.filter(episode
=self
).values('user').distinct().count()
334 def listener_count_timespan(self
, start
, end
):
335 return EpisodeAction
.objects
.filter(episode
=self
,
336 timestamp__range
=(start
, end
),
337 action
='play').values('user_id').distinct().count()
339 def __unicode__(self
):
340 return '%s (%s)' % (self
.shortname(), self
.podcast
)
344 unique_together
= ('podcast', 'url')
346 class SyncGroup(models
.Model
):
348 Devices that should be synced with each other need to be grouped
351 SyncGroups are automatically created by calling
352 device.sync_with(other_device), but can also be created manually.
354 device.sync() synchronizes the device for which the method is called
355 with the other devices in its SyncGroup.
357 user
= models
.ForeignKey(User
)
359 def __unicode__(self
):
360 devices
= [d
.name
for d
in Device
.objects
.filter(sync_group
=self
)]
361 return ', '.join(devices
)
364 return Device
.objects
.filter(sync_group
=self
)
366 def add(self
, device
):
367 if device
.sync_group
== self
: return
368 if device
.sync_group
!= None:
371 device
.sync_group
= self
375 db_table
= 'sync_group'
378 class Device(models
.Model
):
379 user
= models
.ForeignKey(User
)
380 uid
= models
.SlugField(max_length
=50)
381 name
= models
.CharField(max_length
=100, blank
=True)
382 type = models
.CharField(max_length
=10, choices
=DEVICE_TYPES
)
383 sync_group
= models
.ForeignKey(SyncGroup
, blank
=True, null
=True)
384 deleted
= models
.BooleanField(default
=False)
385 settings
= JSONField(default
={})
387 def __unicode__(self
):
388 return self
.name
if self
.name
else _('Unnamed Device (%s)' % self
.uid
)
390 def get_subscriptions(self
):
392 return Subscription
.objects
.filter(device
=self
)
395 for s
in self
.get_sync_actions():
397 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
399 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e
), repr(self
), repr(s
.podcast
), repr(s
.action
)))
401 def sync_targets(self
):
403 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
405 sync_targets
= list(Device
.objects
.filter(user
=self
.user
, sync_group
=None, deleted
=False).exclude(pk
=self
.id))
407 sync_groups
= SyncGroup
.objects
.filter(user
=self
.user
)
408 if self
.sync_group
!= None: sync_groups
= sync_groups
.exclude(pk
=self
.sync_group
.id)
410 sync_targets
.extend( list(sync_groups
) )
414 def get_sync_actions(self
):
416 returns the SyncGroupSubscriptionActions correspond to the
417 SubscriptionActions that need to be saved for the current device
418 to synchronize it with its SyncGroup
420 if self
.sync_group
== None:
423 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
425 sync_actions
= self
.latest_actions()
428 a
= d
.latest_actions()
430 if not sync_actions
.has_key(s
):
431 if a
[s
].action
== SUBSCRIBE_ACTION
:
432 sync_actions
[s
] = a
[s
]
433 elif a
[s
].newer_than(sync_actions
[s
]) and (sync_actions
[s
].action
!= a
[s
].action
):
434 sync_actions
[s
] = a
[s
]
436 #remove actions that did not change
437 current_state
= self
.latest_actions()
438 for podcast
in current_state
.keys():
439 if podcast
in current_state
and podcast
in sync_actions
and sync_actions
[podcast
] == current_state
[podcast
]:
440 del sync_actions
[podcast
]
442 return sync_actions
.values()
444 def latest_actions(self
):
446 returns the latest action for each podcast
447 that has an action on this device
449 #all podcasts that have an action on this device
450 podcasts
= [sa
.podcast
for sa
in SubscriptionAction
.objects
.filter(device
=self
)]
451 podcasts
= list(set(podcasts
)) #remove duplicates
455 actions
[p
] = self
.latest_action(p
)
459 def latest_action(self
, podcast
):
461 returns the latest action for the given podcast on this device
463 actions
= SubscriptionAction
.objects
.filter(podcast
=podcast
,device
=self
).order_by('-timestamp', '-id')
464 if actions
.count() == 0:
469 def sync_with(self
, other
):
471 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
472 this method places them in the same SyncGroup. get_sync_actions() can
473 then return the SyncGroupSubscriptionActions for brining the device
474 in sync with its group
476 if self
.user
!= other
.user
:
477 raise ValueError('the devices belong to different users')
479 if isinstance(other
, SyncGroup
):
484 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
487 if self
.sync_group
!= None:
488 if other
.sync_group
== None:
489 self
.sync_group
.add(other
)
492 raise ValueError('the devices are in different sync groups')
495 if other
.sync_group
== None:
496 g
= SyncGroup
.objects
.create(user
=self
.user
)
501 oter
.sync_group
.add(self
)
505 stops synchronizing the device
506 this method removes the device from its SyncGroup. If only one
507 device remains in the SyncGroup, it is removed so the device can
508 be used in other groups.
510 if self
.sync_group
== None:
511 raise ValueError('the device is not synced')
514 self
.sync_group
= None
517 devices
= Device
.objects
.filter(sync_group
=g
)
518 if devices
.count() == 1:
527 class EpisodeAction(models
.Model
):
528 user
= models
.ForeignKey(User
)
529 episode
= models
.ForeignKey(Episode
)
530 device
= models
.ForeignKey(Device
,null
=True)
531 action
= models
.CharField(max_length
=10, choices
=EPISODE_ACTION_TYPES
)
532 timestamp
= models
.DateTimeField(default
=datetime
.now
)
533 started
= models
.IntegerField(null
=True, blank
=True)
534 playmark
= models
.IntegerField(null
=True, blank
=True)
535 total
= models
.IntegerField(null
=True, blank
=True)
537 def __unicode__(self
):
538 return '%s %s %s' % (self
.user
, self
.action
, self
.episode
)
540 def playmark_time(self
):
541 return datetime
.fromtimestamp(float(self
.playmark
))
543 def started_time(self
):
544 return datetime
.fromtimestamp(float(self
.started
))
547 db_table
= 'episode_log'
550 class SubscriptionManager(models
.Manager
):
552 def public_subscriptions(self
, podcasts
=None):
554 Returns either all public subscriptions or those for the given podcasts
557 subscriptions
= self
.filter(podcast__in
=podcasts
) if podcasts
else self
.all()
559 # remove users with private profiles
560 subscriptions
= subscriptions
.exclude(user__userprofile__public_profile
=False)
562 # remove inactive (eg deleted) users
563 subscriptions
= subscriptions
.exclude(user__is_active
=False)
566 # remove uers that have marked their subscription to this podcast as private
567 private_users
= SubscriptionMeta
.objects
.filter(podcast__in
=podcasts
, public
=False).values('user')
568 subscriptions
= subscriptions
.exclude(user__in
=private_users
)
573 class Subscription(models
.Model
):
574 device
= models
.ForeignKey(Device
, primary_key
=True)
575 podcast
= models
.ForeignKey(Podcast
)
576 user
= models
.ForeignKey(User
)
577 subscribed_since
= models
.DateTimeField()
579 objects
= SubscriptionManager()
581 def __unicode__(self
):
582 return '%s - %s on %s' % (self
.device
.user
, self
.podcast
, self
.device
)
585 #this is different than get_or_create because it does not necessarily create a new meta-object
586 qs
= SubscriptionMeta
.objects
.filter(user
=self
.user
, podcast
=self
.podcast
)
589 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
593 #this method has to be overwritten, if not it tries to delete a view
598 db_table
= 'current_subscription'
599 #not available in Django 1.0 (Debian stable)
603 class SubscriptionMeta(models
.Model
):
604 user
= models
.ForeignKey(User
)
605 podcast
= models
.ForeignKey(Podcast
)
606 public
= models
.BooleanField(default
=True)
607 settings
= JSONField(default
={})
609 def __unicode__(self
):
610 return '%s - %s - %s' % (self
.user
, self
.podcast
, self
.public
)
612 def save(self
, *args
, **kwargs
):
613 self
.public
= self
.settings
.get('public_subscription', True)
614 super(SubscriptionMeta
, self
).save(*args
, **kwargs
)
618 db_table
= 'subscription'
619 unique_together
= ('user', 'podcast')
622 class EpisodeSettings(models
.Model
):
623 user
= models
.ForeignKey(User
)
624 episode
= models
.ForeignKey(Episode
)
625 settings
= JSONField(default
={})
627 def save(self
, *args
, **kwargs
):
628 super(EpisodeSettings
, self
).save(*args
, **kwargs
)
630 from mygpo
.api
.models
.users
import EpisodeFavorite
631 fav
= self
.settings
.get('is_favorite', False)
633 EpisodeFavorite
.objects
.get_or_create(user
=self
.user
, episode
=self
.episode
)
635 EpisodeFavorite
.objects
.filter(user
=self
.user
, episode
=self
.episode
).delete()
639 db_table
= 'episode_settings'
640 unique_together
= ('user', 'episode')
643 class SubscriptionAction(models
.Model
):
644 device
= models
.ForeignKey(Device
)
645 podcast
= models
.ForeignKey(Podcast
)
646 action
= models
.IntegerField(choices
=SUBSCRIPTION_ACTION_TYPES
)
647 timestamp
= models
.DateTimeField(blank
=True, default
=datetime
.now
)
649 def action_string(self
):
650 return 'subscribe' if self
.action
== SUBSCRIBE_ACTION
else 'unsubscribe'
652 def newer_than(self
, action
):
653 return self
.timestamp
> action
.timestamp
655 def __unicode__(self
):
656 return '%s %s %s %s' % (self
.device
.user
, self
.device
, self
.action_string(), self
.podcast
)
659 db_table
= 'subscription_log'
660 unique_together
= ('device', 'podcast', 'timestamp')
663 class URLSanitizingRule(models
.Model
):
664 use_podcast
= models
.BooleanField()
665 use_episode
= models
.BooleanField()
666 search
= models
.CharField(max_length
=100)
667 search_precompiled
= None
668 replace
= models
.CharField(max_length
=100, null
=False, blank
=True)
669 priority
= models
.PositiveIntegerField()
670 description
= models
.TextField(null
=False, blank
=True)
673 db_table
= 'sanitizing_rules'
675 def __unicode__(self
):
676 return '%s -> %s' % (self
.search
, self
.replace
)
679 from mygpo
.search
.signals
import update_podcast_entry
, update_podcast_group_entry
, remove_podcast_entry
, remove_podcast_group_entry
680 from django
.db
.models
.signals
import post_save
, pre_delete
682 post_save
.connect(update_podcast_entry
, sender
=Podcast
)
683 pre_delete
.connect(remove_podcast_entry
, sender
=Podcast
)
685 post_save
.connect(update_podcast_group_entry
, sender
=PodcastGroup
)
686 pre_delete
.connect(remove_podcast_group_entry
, sender
=PodcastGroup
)