1 from __future__
import division
5 from random
import random
7 from couchdbkit
.ext
.django
.schema
import *
8 from restkit
.errors
import Unauthorized
10 from django
.core
.urlresolvers
import reverse
12 from mygpo
.decorators
import repeat_on_conflict
13 from mygpo
import utils
14 from mygpo
.cache
import cache_result
15 from mygpo
.core
.proxy
import DocumentABCMeta
16 from mygpo
.core
.slugs
import SlugMixin
17 from mygpo
.core
.oldid
import OldIdMixin
18 from mygpo
.web
.logo
import CoverArt
21 class SubscriptionException(Exception):
25 class MergedIdException(Exception):
26 """ raised when an object is accessed through one of its merged_ids """
28 def __init__(self
, obj
, current_id
):
30 self
.current_id
= current_id
33 class Episode(Document
, SlugMixin
, OldIdMixin
):
35 Represents an Episode. Can only be part of a Podcast
38 __metaclass__
= DocumentABCMeta
40 title
= StringProperty()
41 guid
= StringProperty()
42 description
= StringProperty(default
="")
43 content
= StringProperty(default
="")
44 link
= StringProperty()
45 released
= DateTimeProperty()
46 author
= StringProperty()
47 duration
= IntegerProperty()
48 filesize
= IntegerProperty()
49 language
= StringProperty()
50 last_update
= DateTimeProperty()
51 outdated
= BooleanProperty(default
=False)
52 mimetypes
= StringListProperty()
53 merged_ids
= StringListProperty()
54 urls
= StringListProperty()
55 podcast
= StringProperty(required
=True)
56 listeners
= IntegerProperty()
57 content_types
= StringListProperty()
58 flattr_url
= StringProperty()
67 return 'Episode %s' % self
._id
71 def get_short_title(self
, common_title
):
72 if not self
.title
or not common_title
:
75 title
= self
.title
.replace(common_title
, '').strip()
76 title
= re
.sub(r
'^[\W\d]+', '', title
)
80 def get_episode_number(self
, common_title
):
81 if not self
.title
or not common_title
:
84 title
= self
.title
.replace(common_title
, '').strip()
85 match
= re
.search(r
'^\W*(\d+)', title
)
89 return int(match
.group(1))
93 return set([self
._id
] + self
.merged_ids
)
96 def __eq__(self
, other
):
99 return self
._id
== other
._id
103 return hash(self
._id
)
107 return '<{cls} {title} ({id})>'.format(cls
=self
.__class
__.__name
__,
108 title
=self
.title
, id=self
._id
)
113 class SubscriberData(DocumentSchema
):
114 timestamp
= DateTimeProperty()
115 subscriber_count
= IntegerProperty()
117 def __eq__(self
, other
):
118 if not isinstance(other
, SubscriberData
):
121 return (self
.timestamp
== other
.timestamp
) and \
122 (self
.subscriber_count
== other
.subscriber_count
)
125 return hash(frozenset([self
.timestamp
, self
.subscriber_count
]))
128 class PodcastSubscriberData(Document
):
129 podcast
= StringProperty()
130 subscribers
= SchemaListProperty(SubscriberData
)
134 return 'PodcastSubscriberData for Podcast %s (%s)' % (self
.podcast
, self
._id
)
137 class Podcast(Document
, SlugMixin
, OldIdMixin
):
139 __metaclass__
= DocumentABCMeta
141 id = StringProperty()
142 title
= StringProperty()
143 urls
= StringListProperty()
144 description
= StringProperty()
145 link
= StringProperty()
146 last_update
= DateTimeProperty()
147 logo_url
= StringProperty()
148 author
= StringProperty()
149 merged_ids
= StringListProperty()
150 group
= StringProperty()
151 group_member_name
= StringProperty()
152 related_podcasts
= StringListProperty()
153 subscribers
= SchemaListProperty(SubscriberData
)
154 language
= StringProperty()
155 content_types
= StringListProperty()
156 tags
= DictProperty()
157 restrictions
= StringListProperty()
158 common_episode_title
= StringProperty()
159 new_location
= StringProperty()
160 latest_episode_timestamp
= DateTimeProperty()
161 episode_count
= IntegerProperty()
162 random_key
= FloatProperty(default
=random
)
163 flattr_url
= StringProperty()
167 def get_podcast_by_id(self
, id, current_id
=False):
168 if current_id
and id != self
.get_id():
169 raise MergedIdException(self
, self
.get_id())
174 get_podcast_by_oldid
= get_podcast_by_id
175 get_podcast_by_url
= get_podcast_by_id
179 return self
.id or self
._id
182 return set([self
.get_id()] + self
.merged_ids
)
185 def display_title(self
):
186 return self
.title
or self
.url
189 def group_with(self
, other
, grouptitle
, myname
, othername
):
191 if self
.group
and (self
.group
== other
.group
):
192 # they are already grouped
195 group1
= PodcastGroup
.get(self
.group
) if self
.group
else None
196 group2
= PodcastGroup
.get(other
.group
) if other
.group
else None
198 if group1
and group2
:
199 raise ValueError('both podcasts already are in different groups')
201 elif not (group1
or group2
):
202 group
= PodcastGroup(title
=grouptitle
)
204 group
.add_podcast(self
, myname
)
205 group
.add_podcast(other
, othername
)
209 group1
.add_podcast(other
, othername
)
213 group2
.add_podcast(self
, myname
)
218 def get_common_episode_title(self
, num_episodes
=100):
220 if self
.common_episode_title
:
221 return self
.common_episode_title
223 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
224 episodes
= episodes_for_podcast(self
, descending
=True, limit
=num_episodes
)
226 # We take all non-empty titles
227 titles
= filter(None, (e
.title
for e
in episodes
))
228 # get the longest common substring
229 common_title
= utils
.longest_substr(titles
)
231 # but consider only the part up to the first number. Otherwise we risk
232 # removing part of the number (eg if a feed contains episodes 100-199)
233 common_title
= re
.search(r
'^\D*', common_title
).group(0)
235 if len(common_title
.strip()) < 2:
241 @cache_result(timeout
=60*60)
242 def get_latest_episode(self
):
243 # since = 1 ==> has a timestamp
245 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
246 episodes
= episodes_for_podcast(self
, since
=1, descending
=True, limit
=1)
247 return next(iter(episodes
), None)
250 def get_episode_before(self
, episode
):
251 if not episode
.released
:
254 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
255 prevs
= episodes_for_podcast(self
, until
=episode
.released
,
256 descending
=True, limit
=1)
258 return next(iter(prevs
), None)
261 def get_episode_after(self
, episode
):
262 if not episode
.released
:
265 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
266 nexts
= episodes_for_podcast(self
, since
=episode
.released
, limit
=1)
268 return next(iter(nexts
), None)
276 def get_podcast(self
):
280 def get_logo_url(self
, size
):
282 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
284 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
286 prefix
= CoverArt
.get_prefix(filename
)
288 return reverse('logo', args
=[size
, prefix
, filename
])
291 def subscriber_change(self
):
292 prev
= self
.prev_subscriber_count()
296 return self
.subscriber_count() / prev
299 def subscriber_count(self
):
300 if not self
.subscribers
:
302 return self
.subscribers
[-1].subscriber_count
305 def prev_subscriber_count(self
):
306 if len(self
.subscribers
) < 2:
308 return self
.subscribers
[-2].subscriber_count
312 @repeat_on_conflict()
313 def subscribe(self
, user
, device
):
314 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
315 state
= podcast_state_for_user_podcast(user
, self
)
316 state
.subscribe(device
)
320 except Unauthorized
as ex
:
321 raise SubscriptionException(ex
)
324 @repeat_on_conflict()
325 def unsubscribe(self
, user
, device
):
326 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
327 state
= podcast_state_for_user_podcast(user
, self
)
328 state
.unsubscribe(device
)
332 except Unauthorized
as ex
:
333 raise SubscriptionException(ex
)
336 def subscribe_targets(self
, user
):
338 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
339 devices/syncgroups on which the podcast is already subscribed
343 subscriptions_by_devices
= user
.get_subscriptions_by_device()
345 for group
in user
.get_grouped_devices():
349 dev
= group
.devices
[0]
351 if not self
.get_id() in subscriptions_by_devices
[dev
.id]:
352 targets
.append(group
.devices
)
355 for device
in group
.devices
:
356 if not self
.get_id() in subscriptions_by_devices
[device
.id]:
357 targets
.append(device
)
363 return hash(self
.get_id())
368 return super(Podcast
, self
).__repr
__()
370 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
372 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
376 group
= getattr(self
, 'group', None)
377 if group
: #we are part of a PodcastGroup
378 group
= PodcastGroup
.get(group
)
379 podcasts
= list(group
.podcasts
)
381 if not self
in podcasts
:
382 # the podcast has not been added to the group correctly
383 group
.add_podcast(self
)
386 i
= podcasts
.index(self
)
388 group
.podcasts
= podcasts
391 i
= podcasts
.index(self
)
393 group
.podcasts
= podcasts
397 super(Podcast
, self
).save()
401 group
= getattr(self
, 'group', None)
403 group
= PodcastGroup
.get(group
)
404 podcasts
= list(group
.podcasts
)
407 i
= podcasts
.index(self
)
409 group
.podcasts
= podcasts
413 super(Podcast
, self
).delete()
416 def __eq__(self
, other
):
417 if not self
.get_id():
423 return self
.get_id() == other
.get_id()
427 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
428 title
= StringProperty()
429 podcasts
= SchemaListProperty(Podcast
)
435 def get_podcast_by_id(self
, id, current_id
=False):
436 for podcast
in self
.podcasts
:
437 if podcast
.get_id() == id:
440 if id in podcast
.merged_ids
:
442 raise MergedIdException(podcast
, podcast
.get_id())
447 def get_podcast_by_oldid(self
, oldid
):
448 for podcast
in list(self
.podcasts
):
449 if podcast
.oldid
== oldid
:
453 def get_podcast_by_url(self
, url
):
454 for podcast
in self
.podcasts
:
455 if url
in list(podcast
.urls
):
459 def subscriber_change(self
):
460 prev
= self
.prev_subscriber_count()
464 return self
.subscriber_count() / prev
467 def subscriber_count(self
):
468 return sum([p
.subscriber_count() for p
in self
.podcasts
])
471 def prev_subscriber_count(self
):
472 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
475 def display_title(self
):
479 def get_podcast(self
):
480 # return podcast with most subscribers (bug 1390)
481 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
487 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
490 def get_logo_url(self
, size
):
492 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
494 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
496 prefix
= CoverArt
.get_prefix(filename
)
498 return reverse('logo', args
=[size
, prefix
, filename
])
501 def add_podcast(self
, podcast
, member_name
):
504 raise ValueError('group has to have an _id first')
507 raise ValueError('podcast needs to have an _id first')
510 podcast
.id = podcast
._id
513 podcast
.group
= self
._id
514 podcast
.group_member_name
= member_name
515 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
516 key
=Podcast
.subscriber_count
, reverse
=True)
522 return super(PodcastGroup
, self
).__repr
__()
524 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
526 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
530 class SanitizingRule(Document
):
531 slug
= StringProperty()
532 applies_to
= StringListProperty()
533 search
= StringProperty()
534 replace
= StringProperty()
535 priority
= IntegerProperty()
536 description
= StringProperty()
540 return 'SanitizingRule %s' % self
._id