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 logo_shortname(self
):
85 return hashlib
.sha1(self
.logo_url
).hexdigest()
87 def subscribe_targets(self
, user
):
89 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
90 devices/syncgroups on which the podcast is already subscribed
94 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
96 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
97 if self
in subscriptions
: continue
100 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
107 def group_with(self
, other
, grouptitle
, myname
, othername
):
108 if self
.group
== other
.group
and self
.group
!= None:
111 if self
.group
!= None:
112 if other
.group
== None:
113 self
.group
.add(other
, othername
)
116 raise ValueError('the podcasts are already in different groups')
118 if other
.group
== None:
119 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
121 g
.add(other
, othername
)
127 if self
.group
== None:
128 raise ValueError('the podcast currently isn\'t in any group')
134 podcasts
= Podcast
.objects
.filter(group
=g
)
135 if podcasts
.count() == 1:
140 def get_similar(self
):
141 from mygpo
.data
.models
import RelatedPodcast
142 return [r
.rel_podcast
for r
in RelatedPodcast
.objects
.filter(ref_podcast
=self
)]
144 def __unicode__(self
):
145 return self
.title
if self
.title
!= '' else self
.url
151 class PodcastGroup(models
.Model
):
152 title
= models
.CharField(max_length
=100, blank
=False)
154 def add(self
, podcast
, membername
):
155 if podcast
.group
== self
:
156 podcast
.group_member_name
= membername
158 elif podcast
.group
!= None:
162 podcast
.group_member_name
= membername
166 return Podcast
.objects
.filter(group
=self
)
168 def subscriptions(self
):
170 returns the public subscriptions to podcasts in the group
172 return Subscription
.objects
.public_subscriptions(self
.podcasts())
174 def subscription_count(self
):
175 return self
.subscriptions().count()
177 def subscriber_count(self
):
179 Returns the number of public subscriptions to podcasts of this group
181 subscriptions
= self
.subscriptions()
182 return subscriptions
.values('user').distinct().count()
185 def __unicode__(self
):
189 db_table
= 'podcast_groups'
192 class ToplistEntryManager(models
.Manager
):
194 def get_query_set(self
):
195 return super(ToplistEntryManager
, self
).get_query_set().order_by('-subscriptions')
198 class ToplistEntry(models
.Model
):
199 podcast
= models
.ForeignKey(Podcast
, null
=True)
200 podcast_group
= models
.ForeignKey(PodcastGroup
, null
=True)
201 oldplace
= models
.IntegerField(db_column
='old_place')
202 subscriptions
= models
.IntegerField(db_column
='subscription_count')
204 objects
= ToplistEntryManager()
211 return self
.podcast_group
213 def get_podcast(self
):
215 Returns a podcast which is representative for this toplist-entry
216 If the entry is a non-grouped podcast, it is returned
217 If the entry is a podcast group, one of its podcasts is returned
222 return self
.podcast_group
.podcasts()[0]
224 def __unicode__(self
):
225 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
231 class EpisodeToplistEntryManager(models
.Manager
):
233 def get_query_set(self
):
234 return super(EpisodeToplistEntryManager
, self
).get_query_set().order_by('-listeners')
237 class EpisodeToplistEntry(models
.Model
):
238 episode
= models
.ForeignKey('Episode')
239 listeners
= models
.PositiveIntegerField()
241 objects
= EpisodeToplistEntryManager()
243 def __unicode__(self
):
244 return '%s (%s)' % (self
.episode
, self
.listeners
)
247 db_table
= 'episode_toplist'
250 class SuggestionEntryManager(models
.Manager
):
252 def for_user(self
, user
):
253 from mygpo
.data
.models
import SuggestionBlacklist
255 suggestions
= SuggestionEntry
.objects
.filter(user
=user
).order_by('-priority')
257 subscriptions
= [x
.podcast
for x
in Subscription
.objects
.filter(user
=user
)]
258 suggestions
= filter(lambda x
: x
.podcast
not in subscriptions
, suggestions
)
260 blacklist
= [x
.podcast
for x
in SuggestionBlacklist
.objects
.filter(user
=user
)]
261 suggestions
= filter(lambda x
: x
.podcast
not in blacklist
, suggestions
)
266 class SuggestionEntry(models
.Model
):
267 podcast
= models
.ForeignKey(Podcast
)
268 user
= models
.ForeignKey(User
)
269 priority
= models
.IntegerField()
271 objects
= SuggestionEntryManager()
273 def __unicode__(self
):
274 return '%s (%s)' % (self
.podcast
, self
.priority
)
277 db_table
= 'suggestion'
280 class Episode(models
.Model
):
281 podcast
= models
.ForeignKey(Podcast
)
282 url
= models
.URLField(verify_exists
=False)
283 title
= models
.CharField(max_length
=100, blank
=True)
284 description
= models
.TextField(null
=True, blank
=True)
285 link
= models
.URLField(null
=True, blank
=True, verify_exists
=False)
286 timestamp
= models
.DateTimeField(null
=True, blank
=True)
287 author
= models
.CharField(max_length
=100, null
=True, blank
=True)
288 duration
= models
.PositiveIntegerField(null
=True, blank
=True)
289 filesize
= models
.PositiveIntegerField(null
=True, blank
=True)
290 language
= models
.CharField(max_length
=10, null
=True, blank
=True)
291 last_update
= models
.DateTimeField(auto_now
=True)
292 outdated
= models
.BooleanField(default
=False) #set to true after episode hasn't been found in feed
293 mimetype
= models
.CharField(max_length
=30, blank
=True, null
=True)
296 m
= re
.search('\D*(\d+)\D+', self
.title
)
301 s
= s
.replace(self
.podcast
.title
, '')
302 s
= s
.replace(self
.number(), '')
303 s
= re
.search('\W*(.+)', s
).group(1)
307 def listener_count(self
):
308 from mygpo
.data
.models
import Listener
309 return Listener
.objects
.filter(episode
=self
).values('user').distinct().count()
311 def __unicode__(self
):
312 return '%s (%s)' % (self
.shortname(), self
.podcast
)
316 unique_together
= ('podcast', 'url')
318 class SyncGroup(models
.Model
):
320 Devices that should be synced with each other need to be grouped
323 SyncGroups are automatically created by calling
324 device.sync_with(other_device), but can also be created manually.
326 device.sync() synchronizes the device for which the method is called
327 with the other devices in its SyncGroup.
329 user
= models
.ForeignKey(User
)
331 def __unicode__(self
):
332 devices
= [d
.name
for d
in Device
.objects
.filter(sync_group
=self
)]
333 return ', '.join(devices
)
336 return Device
.objects
.filter(sync_group
=self
)
338 def add(self
, device
):
339 if device
.sync_group
== self
: return
340 if device
.sync_group
!= None:
343 device
.sync_group
= self
347 db_table
= 'sync_group'
350 class Device(models
.Model
):
351 user
= models
.ForeignKey(User
)
352 uid
= models
.SlugField(max_length
=50)
353 name
= models
.CharField(max_length
=100, blank
=True)
354 type = models
.CharField(max_length
=10, choices
=DEVICE_TYPES
)
355 sync_group
= models
.ForeignKey(SyncGroup
, blank
=True, null
=True)
356 deleted
= models
.BooleanField(default
=False)
357 settings
= JSONField(default
={})
359 def __unicode__(self
):
360 return self
.name
if self
.name
else _('Unnamed Device (%s)' % self
.uid
)
362 def get_subscriptions(self
):
364 return Subscription
.objects
.filter(device
=self
)
367 for s
in self
.get_sync_actions():
369 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
371 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e
), repr(self
), repr(s
.podcast
), repr(s
.action
)))
373 def sync_targets(self
):
375 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
377 sync_targets
= list(Device
.objects
.filter(user
=self
.user
, sync_group
=None, deleted
=False).exclude(pk
=self
.id))
379 sync_groups
= SyncGroup
.objects
.filter(user
=self
.user
)
380 if self
.sync_group
!= None: sync_groups
= sync_groups
.exclude(pk
=self
.sync_group
.id)
382 sync_targets
.extend( list(sync_groups
) )
386 def get_sync_actions(self
):
388 returns the SyncGroupSubscriptionActions correspond to the
389 SubscriptionActions that need to be saved for the current device
390 to synchronize it with its SyncGroup
392 if self
.sync_group
== None:
395 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
397 sync_actions
= self
.latest_actions()
400 a
= d
.latest_actions()
402 if not sync_actions
.has_key(s
):
403 if a
[s
].action
== SUBSCRIBE_ACTION
:
404 sync_actions
[s
] = a
[s
]
405 elif a
[s
].newer_than(sync_actions
[s
]) and (sync_actions
[s
].action
!= a
[s
].action
):
406 sync_actions
[s
] = a
[s
]
408 #remove actions that did not change
409 current_state
= self
.latest_actions()
410 for podcast
in current_state
.keys():
411 if podcast
in current_state
and podcast
in sync_actions
and sync_actions
[podcast
] == current_state
[podcast
]:
412 del sync_actions
[podcast
]
414 return sync_actions
.values()
416 def latest_actions(self
):
418 returns the latest action for each podcast
419 that has an action on this device
421 #all podcasts that have an action on this device
422 podcasts
= [sa
.podcast
for sa
in SubscriptionAction
.objects
.filter(device
=self
)]
423 podcasts
= list(set(podcasts
)) #remove duplicates
427 actions
[p
] = self
.latest_action(p
)
431 def latest_action(self
, podcast
):
433 returns the latest action for the given podcast on this device
435 actions
= SubscriptionAction
.objects
.filter(podcast
=podcast
,device
=self
).order_by('-timestamp', '-id')
436 if actions
.count() == 0:
441 def sync_with(self
, other
):
443 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
444 this method places them in the same SyncGroup. get_sync_actions() can
445 then return the SyncGroupSubscriptionActions for brining the device
446 in sync with its group
448 if self
.user
!= other
.user
:
449 raise ValueError('the devices belong to different users')
451 if isinstance(other
, SyncGroup
):
456 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
459 if self
.sync_group
!= None:
460 if other
.sync_group
== None:
461 self
.sync_group
.add(other
)
464 raise ValueError('the devices are in different sync groups')
467 if other
.sync_group
== None:
468 g
= SyncGroup
.objects
.create(user
=self
.user
)
473 oter
.sync_group
.add(self
)
477 stops synchronizing the device
478 this method removes the device from its SyncGroup. If only one
479 device remains in the SyncGroup, it is removed so the device can
480 be used in other groups.
482 if self
.sync_group
== None:
483 raise ValueError('the device is not synced')
486 self
.sync_group
= None
489 devices
= Device
.objects
.filter(sync_group
=g
)
490 if devices
.count() == 1:
499 class EpisodeAction(models
.Model
):
500 user
= models
.ForeignKey(User
)
501 episode
= models
.ForeignKey(Episode
)
502 device
= models
.ForeignKey(Device
,null
=True)
503 action
= models
.CharField(max_length
=10, choices
=EPISODE_ACTION_TYPES
)
504 timestamp
= models
.DateTimeField(default
=datetime
.now
)
505 started
= models
.IntegerField(null
=True, blank
=True)
506 playmark
= models
.IntegerField(null
=True, blank
=True)
507 total
= models
.IntegerField(null
=True, blank
=True)
509 def __unicode__(self
):
510 return '%s %s %s' % (self
.user
, self
.action
, self
.episode
)
512 def playmark_time(self
):
513 return datetime
.fromtimestamp(float(self
.playmark
))
515 def started_time(self
):
516 return datetime
.fromtimestamp(float(self
.started
))
519 db_table
= 'episode_log'
522 class SubscriptionManager(models
.Manager
):
524 def public_subscriptions(self
, podcasts
=None):
526 Returns either all public subscriptions or those for the given podcasts
529 subscriptions
= self
.filter(podcast__in
=podcasts
) if podcasts
else self
.all()
531 # remove users with private profiles
532 subscriptions
= subscriptions
.exclude(user__userprofile__public_profile
=False)
534 # remove inactive (eg deleted) users
535 subscriptions
= subscriptions
.exclude(user__is_active
=False)
538 # remove uers that have marked their subscription to this podcast as private
539 private_users
= SubscriptionMeta
.objects
.filter(podcast__in
=podcasts
, public
=False).values('user')
540 subscriptions
= subscriptions
.exclude(user__in
=private_users
)
545 class Subscription(models
.Model
):
546 device
= models
.ForeignKey(Device
, primary_key
=True)
547 podcast
= models
.ForeignKey(Podcast
)
548 user
= models
.ForeignKey(User
)
549 subscribed_since
= models
.DateTimeField()
551 objects
= SubscriptionManager()
553 def __unicode__(self
):
554 return '%s - %s on %s' % (self
.device
.user
, self
.podcast
, self
.device
)
557 #this is different than get_or_create because it does not necessarily create a new meta-object
558 qs
= SubscriptionMeta
.objects
.filter(user
=self
.user
, podcast
=self
.podcast
)
561 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
565 #this method has to be overwritten, if not it tries to delete a view
570 db_table
= 'current_subscription'
571 #not available in Django 1.0 (Debian stable)
575 class SubscriptionMeta(models
.Model
):
576 user
= models
.ForeignKey(User
)
577 podcast
= models
.ForeignKey(Podcast
)
578 public
= models
.BooleanField(default
=True)
579 settings
= JSONField(default
={})
581 def __unicode__(self
):
582 return '%s - %s - %s' % (self
.user
, self
.podcast
, self
.public
)
584 def save(self
, *args
, **kwargs
):
585 self
.public
= self
.settings
.get('public_subscription', True)
586 super(SubscriptionMeta
, self
).save(*args
, **kwargs
)
590 db_table
= 'subscription'
591 unique_together
= ('user', 'podcast')
594 class EpisodeSettings(models
.Model
):
595 user
= models
.ForeignKey(User
)
596 episode
= models
.ForeignKey(Episode
)
597 settings
= JSONField(default
={})
599 def save(self
, *args
, **kwargs
):
600 super(EpisodeSettings
, self
).save(*args
, **kwargs
)
602 from mygpo
.api
.models
.users
import EpisodeFavorite
603 fav
= self
.settings
.get('is_favorite', False)
605 EpisodeFavorite
.objects
.get_or_create(user
=self
.user
, episode
=self
.episode
)
607 EpisodeFavorite
.objects
.filter(user
=self
.user
, episode
=self
.episode
).delete()
611 db_table
= 'episode_settings'
612 unique_together
= ('user', 'episode')
615 class SubscriptionAction(models
.Model
):
616 device
= models
.ForeignKey(Device
)
617 podcast
= models
.ForeignKey(Podcast
)
618 action
= models
.IntegerField(choices
=SUBSCRIPTION_ACTION_TYPES
)
619 timestamp
= models
.DateTimeField(blank
=True, default
=datetime
.now
)
621 def action_string(self
):
622 return 'subscribe' if self
.action
== SUBSCRIBE_ACTION
else 'unsubscribe'
624 def newer_than(self
, action
):
625 return self
.timestamp
> action
.timestamp
627 def __unicode__(self
):
628 return '%s %s %s %s' % (self
.device
.user
, self
.device
, self
.action_string(), self
.podcast
)
631 db_table
= 'subscription_log'
632 unique_together
= ('device', 'podcast', 'timestamp')
635 class URLSanitizingRule(models
.Model
):
636 use_podcast
= models
.BooleanField()
637 use_episode
= models
.BooleanField()
638 search
= models
.CharField(max_length
=100)
639 search_precompiled
= None
640 replace
= models
.CharField(max_length
=100, null
=False, blank
=True)
641 priority
= models
.PositiveIntegerField()
642 description
= models
.TextField(null
=False, blank
=True)
645 db_table
= 'sanitizing_rules'
647 def __unicode__(self
):
648 return '%s -> %s' % (self
.search
, self
.replace
)
651 from mygpo
.search
.signals
import update_podcast_entry
, update_podcast_group_entry
, remove_podcast_entry
, remove_podcast_group_entry
652 from django
.db
.models
.signals
import post_save
, pre_delete
654 post_save
.connect(update_podcast_entry
, sender
=Podcast
)
655 pre_delete
.connect(remove_podcast_entry
, sender
=Podcast
)
657 post_save
.connect(update_podcast_group_entry
, sender
=PodcastGroup
)
658 pre_delete
.connect(remove_podcast_group_entry
, sender
=PodcastGroup
)