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 _
25 from mygpo
.api
.constants
import EPISODE_ACTION_TYPES
, DEVICE_TYPES
, SUBSCRIBE_ACTION
, UNSUBSCRIBE_ACTION
, SUBSCRIPTION_ACTION_TYPES
26 from mygpo
.log
import log
28 class UserProfile(models
.Model
):
29 user
= models
.ForeignKey(User
, unique
=True, db_column
='user_ptr_id')
31 public_profile
= models
.BooleanField(default
=True)
32 generated_id
= models
.BooleanField(default
=False)
33 deleted
= models
.BooleanField(default
=False)
35 def __unicode__(self
):
36 return '%s (%s, %s)' % (self
.user
.username
, self
.public_profile
, self
.generated_id
)
41 class Podcast(models
.Model
):
42 url
= models
.URLField(unique
=True, verify_exists
=False)
43 title
= models
.CharField(max_length
=100, blank
=True)
44 description
= models
.TextField(blank
=True, null
=True)
45 link
= models
.URLField(blank
=True, null
=True, verify_exists
=False)
46 last_update
= models
.DateTimeField(null
=True,blank
=True)
47 logo_url
= models
.CharField(max_length
=1000,null
=True,blank
=True)
48 author
= models
.CharField(max_length
=100, null
=True, blank
=True)
49 language
= models
.CharField(max_length
=10, null
=True, blank
=True)
50 group
= models
.ForeignKey('PodcastGroup', null
=True)
51 group_member_name
= models
.CharField(max_length
=20, default
=None, null
=True, blank
=False)
53 def subscriptions(self
):
54 return Subscription
.objects
.filter(podcast
=self
)
56 def subscription_count(self
):
57 return self
.subscriptions().count()
59 def subscriber_count(self
):
60 return self
.subscriptions().values('user').distinct().count()
62 def listener_count(self
):
63 from mygpo
.data
.models
import Listener
64 return Listener
.objects
.filter(podcast
=self
).values('user').distinct().count()
66 def logo_shortname(self
):
67 return hashlib
.sha1(self
.logo_url
).hexdigest()
69 def subscribe_targets(self
, user
):
71 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
72 devices/syncgroups on which the podcast is already subscribed
76 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
78 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
79 if self
in subscriptions
: continue
82 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
89 def group_with(self
, other
, grouptitle
, myname
, othername
):
90 if self
.group
== other
.group
and self
.group
!= None:
93 if self
.group
!= None:
94 if other
.group
== None:
95 self
.group
.add(other
, myname
)
98 raise ValueError('the podcasts are already in different groups')
100 if other
.group
== None:
101 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
103 g
.add(other
, othername
)
109 if self
.group
== None:
110 raise ValueError('the podcast currently isn\'t in any group')
116 podcasts
= Podcast
.objects
.filter(group
=g
)
117 if podcasts
.count() == 1:
123 def __unicode__(self
):
124 return self
.title
if self
.title
!= '' else self
.url
130 class PodcastGroup(models
.Model
):
131 title
= models
.CharField(max_length
=100, blank
=False)
133 def add(self
, podcast
, membername
):
134 if podcast
.group
== self
:
135 podcast
.group_member_name
= membername
137 elif podcast
.group
!= None:
141 podcast
.group_member_name
= membername
145 return Podcast
.objects
.filter(group
=self
)
147 def __unicode__(self
):
151 db_table
= 'podcast_groups'
154 class ToplistEntry(models
.Model
):
155 podcast
= models
.ForeignKey(Podcast
, null
=True)
156 podcast_group
= models
.ForeignKey(PodcastGroup
, null
=True)
157 oldplace
= models
.IntegerField(db_column
='old_place')
158 subscriptions
= models
.IntegerField(db_column
='subscription_count')
164 return self
.podcast_group
166 def get_podcast(self
):
168 Returns a podcast which is representative for this toplist-entry
169 If the entry is a non-grouped podcast, it is returned
170 If the entry is a podcast group, one of its podcasts is returned
175 return self
.podcast_group
.podcasts()[0]
177 def __unicode__(self
):
178 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
183 class EpisodeToplistEntry(models
.Model
):
184 episode
= models
.ForeignKey('Episode')
185 listeners
= models
.PositiveIntegerField()
187 def __unicode__(self
):
188 return '%s (%s)' % (self
.episode
, self
.listeners
)
191 db_table
= 'episode_toplist'
193 class SuggestionEntry(models
.Model
):
194 podcast
= models
.ForeignKey(Podcast
)
195 user
= models
.ForeignKey(User
)
196 priority
= models
.IntegerField()
200 subscriptions
= [x
.podcast
for x
in Subscription
.objects
.filter(user
=user
)]
201 suggestions
= SuggestionEntry
.objects
.filter(user
=user
).order_by('-priority')
202 return [s
for s
in suggestions
if s
.podcast
not in subscriptions
]
204 def __unicode__(self
):
205 return '%s (%s)' % (self
.podcast
, self
.priority
)
208 db_table
= 'suggestion'
211 class Episode(models
.Model
):
212 podcast
= models
.ForeignKey(Podcast
)
213 url
= models
.URLField(verify_exists
=False)
214 title
= models
.CharField(max_length
=100, blank
=True)
215 description
= models
.TextField(null
=True, blank
=True)
216 link
= models
.URLField(null
=True, blank
=True, verify_exists
=False)
217 timestamp
= models
.DateTimeField(null
=True, blank
=True)
218 author
= models
.CharField(max_length
=100, null
=True, blank
=True)
219 duration
= models
.PositiveIntegerField(null
=True, blank
=True)
220 filesize
= models
.PositiveIntegerField(null
=True, blank
=True)
221 language
= models
.CharField(max_length
=10, null
=True, blank
=True)
222 last_update
= models
.DateTimeField(auto_now
=True)
223 outdated
= models
.BooleanField(default
=False) #set to true after episode hasn't been found in feed
226 m
= re
.search('\D*(\d+)\D+', self
.title
)
231 s
= s
.replace(self
.podcast
.title
, '')
232 s
= s
.replace(self
.number(), '')
233 s
= re
.search('\W*(.+)', s
).group(1)
237 def listener_count(self
):
238 from mygpo
.data
.models
import Listener
239 return Listener
.objects
.filter(episode
=self
).values('user').distinct().count()
241 def __unicode__(self
):
242 return '%s (%s)' % (self
.shortname(), self
.podcast
)
246 unique_together
= ('podcast', 'url')
248 class SyncGroup(models
.Model
):
250 Devices that should be synced with each other need to be grouped
253 SyncGroups are automatically created by calling
254 device.sync_with(other_device), but can also be created manually.
256 device.sync() synchronizes the device for which the method is called
257 with the other devices in its SyncGroup.
259 user
= models
.ForeignKey(User
)
261 def __unicode__(self
):
262 devices
= [d
.name
for d
in Device
.objects
.filter(sync_group
=self
)]
263 return ', '.join(devices
)
266 return Device
.objects
.filter(sync_group
=self
)
268 def add(self
, device
):
269 if device
.sync_group
== self
: return
270 if device
.sync_group
!= None:
273 device
.sync_group
= self
277 db_table
= 'sync_group'
280 class Device(models
.Model
):
281 user
= models
.ForeignKey(User
)
282 uid
= models
.SlugField(max_length
=50)
283 name
= models
.CharField(max_length
=100, blank
=True)
284 type = models
.CharField(max_length
=10, choices
=DEVICE_TYPES
)
285 sync_group
= models
.ForeignKey(SyncGroup
, blank
=True, null
=True)
286 deleted
= models
.BooleanField(default
=False)
288 def __unicode__(self
):
289 return self
.name
if self
.name
else _('Unnamed Device (%s)' % self
.uid
)
291 def get_subscriptions(self
):
293 return Subscription
.objects
.filter(device
=self
)
296 for s
in self
.get_sync_actions():
298 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
300 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e
), repr(self
), repr(s
.podcast
), repr(s
.action
)))
302 def sync_targets(self
):
304 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
306 sync_targets
= list(Device
.objects
.filter(user
=self
.user
, sync_group
=None, deleted
=False).exclude(pk
=self
.id))
308 sync_groups
= SyncGroup
.objects
.filter(user
=self
.user
)
309 if self
.sync_group
!= None: sync_groups
= sync_groups
.exclude(pk
=self
.sync_group
.id)
311 sync_targets
.extend( list(sync_groups
) )
315 def get_sync_actions(self
):
317 returns the SyncGroupSubscriptionActions correspond to the
318 SubscriptionActions that need to be saved for the current device
319 to synchronize it with its SyncGroup
321 if self
.sync_group
== None:
324 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
326 sync_actions
= self
.latest_actions()
329 a
= d
.latest_actions()
331 if not sync_actions
.has_key(s
):
332 if a
[s
].action
== SUBSCRIBE_ACTION
:
333 sync_actions
[s
] = a
[s
]
334 elif a
[s
].newer_than(sync_actions
[s
]) and (sync_actions
[s
].action
!= a
[s
].action
):
335 sync_actions
[s
] = a
[s
]
337 #remove actions that did not change
338 current_state
= self
.latest_actions()
339 for podcast
in current_state
.keys():
340 if sync_actions
[podcast
] == current_state
[podcast
]:
341 del sync_actions
[podcast
]
343 return sync_actions
.values()
345 def latest_actions(self
):
347 returns the latest action for each podcast
348 that has an action on this device
350 #all podcasts that have an action on this device
351 podcasts
= [sa
.podcast
for sa
in SubscriptionAction
.objects
.filter(device
=self
)]
352 podcasts
= list(set(podcasts
)) #remove duplicates
356 actions
[p
] = self
.latest_action(p
)
360 def latest_action(self
, podcast
):
362 returns the latest action for the given podcast on this device
364 actions
= SubscriptionAction
.objects
.filter(podcast
=podcast
,device
=self
).order_by('-timestamp', '-id')
365 if actions
.count() == 0:
370 def sync_with(self
, other
):
372 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
373 this method places them in the same SyncGroup. get_sync_actions() can
374 then return the SyncGroupSubscriptionActions for brining the device
375 in sync with its group
377 if self
.user
!= other
.user
:
378 raise ValueError('the devices belong to different users')
380 if isinstance(other
, SyncGroup
):
385 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
388 if self
.sync_group
!= None:
389 if other
.sync_group
== None:
390 self
.sync_group
.add(other
)
393 raise ValueError('the devices are in different sync groups')
396 if other
.sync_group
== None:
397 g
= SyncGroup
.objects
.create(user
=self
.user
)
402 oter
.sync_group
.add(self
)
406 stops synchronizing the device
407 this method removes the device from its SyncGroup. If only one
408 device remains in the SyncGroup, it is removed so the device can
409 be used in other groups.
411 if self
.sync_group
== None:
412 raise ValueError('the device is not synced')
416 self
.sync_group
= None
419 devices
= Device
.objects
.filter(sync_group
=g
)
420 if devices
.count() == 1:
429 class EpisodeAction(models
.Model
):
430 user
= models
.ForeignKey(User
)
431 episode
= models
.ForeignKey(Episode
)
432 device
= models
.ForeignKey(Device
,null
=True)
433 action
= models
.CharField(max_length
=10, choices
=EPISODE_ACTION_TYPES
)
434 timestamp
= models
.DateTimeField(default
=datetime
.now
)
435 playmark
= models
.IntegerField(null
=True, blank
=True)
437 def __unicode__(self
):
438 return '%s %s %s' % (self
.user
, self
.action
, self
.episode
)
440 def playmark_time(self
):
441 return datetime
.fromtimestamp(float(self
.playmark
))
444 db_table
= 'episode_log'
447 class Subscription(models
.Model
):
448 device
= models
.ForeignKey(Device
, primary_key
=True)
449 podcast
= models
.ForeignKey(Podcast
)
450 user
= models
.ForeignKey(User
)
451 subscribed_since
= models
.DateTimeField()
453 def __unicode__(self
):
454 return '%s - %s on %s' % (self
.device
.user
, self
.podcast
, self
.device
)
457 #this is different than get_or_create because it does not necessarily create a new meta-object
458 qs
= SubscriptionMeta
.objects
.filter(user
=self
.user
, podcast
=self
.podcast
)
461 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
465 #this method has to be overwritten, if not it tries to delete a view
470 db_table
= 'current_subscription'
471 #not available in Django 1.0 (Debian stable)
475 class SubscriptionMeta(models
.Model
):
476 user
= models
.ForeignKey(User
)
477 podcast
= models
.ForeignKey(Podcast
)
478 public
= models
.BooleanField(default
=True)
480 def __unicode__(self
):
481 return '%s - %s - %s' % (self
.user
, self
.podcast
, self
.public
)
484 db_table
= 'subscription'
485 unique_together
= ('user', 'podcast')
488 class SubscriptionAction(models
.Model
):
489 device
= models
.ForeignKey(Device
)
490 podcast
= models
.ForeignKey(Podcast
)
491 action
= models
.IntegerField(choices
=SUBSCRIPTION_ACTION_TYPES
)
492 timestamp
= models
.DateTimeField(blank
=True, default
=datetime
.now
)
494 def action_string(self
):
495 return 'subscribe' if self
.action
== SUBSCRIBE_ACTION
else 'unsubscribe'
497 def newer_than(self
, action
):
498 return self
.timestamp
> action
.timestamp
500 def __unicode__(self
):
501 return '%s %s %s %s' % (self
.device
.user
, self
.device
, self
.action_string(), self
.podcast
)
504 db_table
= 'subscription_log'
505 unique_together
= ('device', 'podcast', 'timestamp')
508 class URLSanitizingRule(models
.Model
):
509 use_podcast
= models
.BooleanField()
510 use_episode
= models
.BooleanField()
511 search
= models
.CharField(max_length
=100)
512 search_precompiled
= None
513 replace
= models
.CharField(max_length
=100, null
=False, blank
=True)
514 priority
= models
.PositiveIntegerField()
515 description
= models
.TextField(null
=False, blank
=True)
518 db_table
= 'sanitizing_rules'
520 def __unicode__(self
):
521 return '%s -> %s' % (self
.search
, self
.replace
)