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()
66 return 'Episode %s' % self
._id
70 def get_short_title(self
, common_title
):
71 if not self
.title
or not common_title
:
74 title
= self
.title
.replace(common_title
, '').strip()
75 title
= re
.sub(r
'^[\W\d]+', '', title
)
79 def get_episode_number(self
, common_title
):
80 if not self
.title
or not common_title
:
83 title
= self
.title
.replace(common_title
, '').strip()
84 match
= re
.search(r
'^\W*(\d+)', title
)
88 return int(match
.group(1))
92 return set([self
._id
] + self
.merged_ids
)
95 def __eq__(self
, other
):
98 return self
._id
== other
._id
102 return hash(self
._id
)
106 return '<{cls} {title} ({id})>'.format(cls
=self
.__class
__.__name
__,
107 title
=self
.title
, id=self
._id
)
112 class SubscriberData(DocumentSchema
):
113 timestamp
= DateTimeProperty()
114 subscriber_count
= IntegerProperty()
116 def __eq__(self
, other
):
117 if not isinstance(other
, SubscriberData
):
120 return (self
.timestamp
== other
.timestamp
) and \
121 (self
.subscriber_count
== other
.subscriber_count
)
124 return hash(frozenset([self
.timestamp
, self
.subscriber_count
]))
127 class PodcastSubscriberData(Document
):
128 podcast
= StringProperty()
129 subscribers
= SchemaListProperty(SubscriberData
)
133 return 'PodcastSubscriberData for Podcast %s (%s)' % (self
.podcast
, self
._id
)
136 class Podcast(Document
, SlugMixin
, OldIdMixin
):
138 __metaclass__
= DocumentABCMeta
140 id = StringProperty()
141 title
= StringProperty()
142 urls
= StringListProperty()
143 description
= StringProperty()
144 link
= StringProperty()
145 last_update
= DateTimeProperty()
146 logo_url
= StringProperty()
147 author
= StringProperty()
148 merged_ids
= StringListProperty()
149 group
= StringProperty()
150 group_member_name
= StringProperty()
151 related_podcasts
= StringListProperty()
152 subscribers
= SchemaListProperty(SubscriberData
)
153 language
= StringProperty()
154 content_types
= StringListProperty()
155 tags
= DictProperty()
156 restrictions
= StringListProperty()
157 common_episode_title
= StringProperty()
158 new_location
= StringProperty()
159 latest_episode_timestamp
= DateTimeProperty()
160 episode_count
= IntegerProperty()
161 random_key
= FloatProperty(default
=random
)
165 def get_podcast_by_id(self
, id, current_id
=False):
166 if current_id
and id != self
.get_id():
167 raise MergedIdException(self
, self
.get_id())
172 get_podcast_by_oldid
= get_podcast_by_id
173 get_podcast_by_url
= get_podcast_by_id
177 return self
.id or self
._id
180 return set([self
.get_id()] + self
.merged_ids
)
183 def display_title(self
):
184 return self
.title
or self
.url
187 def group_with(self
, other
, grouptitle
, myname
, othername
):
189 if self
.group
and (self
.group
== other
.group
):
190 # they are already grouped
193 group1
= PodcastGroup
.get(self
.group
) if self
.group
else None
194 group2
= PodcastGroup
.get(other
.group
) if other
.group
else None
196 if group1
and group2
:
197 raise ValueError('both podcasts already are in different groups')
199 elif not (group1
or group2
):
200 group
= PodcastGroup(title
=grouptitle
)
202 group
.add_podcast(self
, myname
)
203 group
.add_podcast(other
, othername
)
207 group1
.add_podcast(other
, othername
)
211 group2
.add_podcast(self
, myname
)
216 def get_common_episode_title(self
, num_episodes
=100):
218 if self
.common_episode_title
:
219 return self
.common_episode_title
221 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
222 episodes
= episodes_for_podcast(self
, descending
=True, limit
=num_episodes
)
224 # We take all non-empty titles
225 titles
= filter(None, (e
.title
for e
in episodes
))
226 # get the longest common substring
227 common_title
= utils
.longest_substr(titles
)
229 # but consider only the part up to the first number. Otherwise we risk
230 # removing part of the number (eg if a feed contains episodes 100-199)
231 common_title
= re
.search(r
'^\D*', common_title
).group(0)
233 if len(common_title
.strip()) < 2:
239 @cache_result(timeout
=60*60)
240 def get_latest_episode(self
):
241 # since = 1 ==> has a timestamp
243 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
244 episodes
= episodes_for_podcast(self
, since
=1, descending
=True, limit
=1)
245 return next(iter(episodes
), None)
248 def get_episode_before(self
, episode
):
249 if not episode
.released
:
252 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
253 prevs
= episodes_for_podcast(self
, until
=episode
.released
,
254 descending
=True, limit
=1)
256 return next(iter(prevs
), None)
259 def get_episode_after(self
, episode
):
260 if not episode
.released
:
263 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
264 nexts
= episodes_for_podcast(self
, since
=episode
.released
, limit
=1)
266 return next(iter(nexts
), None)
274 def get_podcast(self
):
278 def get_logo_url(self
, size
):
280 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
282 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
284 prefix
= CoverArt
.get_prefix(filename
)
286 return reverse('logo', args
=[size
, prefix
, filename
])
289 def subscriber_change(self
):
290 prev
= self
.prev_subscriber_count()
294 return self
.subscriber_count() / prev
297 def subscriber_count(self
):
298 if not self
.subscribers
:
300 return self
.subscribers
[-1].subscriber_count
303 def prev_subscriber_count(self
):
304 if len(self
.subscribers
) < 2:
306 return self
.subscribers
[-2].subscriber_count
310 @repeat_on_conflict()
311 def subscribe(self
, user
, device
):
312 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
313 state
= podcast_state_for_user_podcast(user
, self
)
314 state
.subscribe(device
)
318 except Unauthorized
as ex
:
319 raise SubscriptionException(ex
)
322 @repeat_on_conflict()
323 def unsubscribe(self
, user
, device
):
324 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
325 state
= podcast_state_for_user_podcast(user
, self
)
326 state
.unsubscribe(device
)
330 except Unauthorized
as ex
:
331 raise SubscriptionException(ex
)
334 def subscribe_targets(self
, user
):
336 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
337 devices/syncgroups on which the podcast is already subscribed
341 subscriptions_by_devices
= user
.get_subscriptions_by_device()
343 for group
in user
.get_grouped_devices():
347 dev
= group
.devices
[0]
349 if not self
.get_id() in subscriptions_by_devices
[dev
.id]:
350 targets
.append(group
.devices
)
353 for device
in group
.devices
:
354 if not self
.get_id() in subscriptions_by_devices
[device
.id]:
355 targets
.append(device
)
361 return hash(self
.get_id())
366 return super(Podcast
, self
).__repr
__()
368 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
370 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
374 group
= getattr(self
, 'group', None)
375 if group
: #we are part of a PodcastGroup
376 group
= PodcastGroup
.get(group
)
377 podcasts
= list(group
.podcasts
)
379 if not self
in podcasts
:
380 # the podcast has not been added to the group correctly
381 group
.add_podcast(self
)
384 i
= podcasts
.index(self
)
386 group
.podcasts
= podcasts
389 i
= podcasts
.index(self
)
391 group
.podcasts
= podcasts
395 super(Podcast
, self
).save()
399 group
= getattr(self
, 'group', None)
401 group
= PodcastGroup
.get(group
)
402 podcasts
= list(group
.podcasts
)
405 i
= podcasts
.index(self
)
407 group
.podcasts
= podcasts
411 super(Podcast
, self
).delete()
414 def __eq__(self
, other
):
415 if not self
.get_id():
421 return self
.get_id() == other
.get_id()
425 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
426 title
= StringProperty()
427 podcasts
= SchemaListProperty(Podcast
)
433 def get_podcast_by_id(self
, id, current_id
=False):
434 for podcast
in self
.podcasts
:
435 if podcast
.get_id() == id:
438 if id in podcast
.merged_ids
:
440 raise MergedIdException(podcast
, podcast
.get_id())
445 def get_podcast_by_oldid(self
, oldid
):
446 for podcast
in list(self
.podcasts
):
447 if podcast
.oldid
== oldid
:
451 def get_podcast_by_url(self
, url
):
452 for podcast
in self
.podcasts
:
453 if url
in list(podcast
.urls
):
457 def subscriber_change(self
):
458 prev
= self
.prev_subscriber_count()
462 return self
.subscriber_count() / prev
465 def subscriber_count(self
):
466 return sum([p
.subscriber_count() for p
in self
.podcasts
])
469 def prev_subscriber_count(self
):
470 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
473 def display_title(self
):
477 def get_podcast(self
):
478 # return podcast with most subscribers (bug 1390)
479 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
485 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
488 def get_logo_url(self
, size
):
490 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
492 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
494 prefix
= CoverArt
.get_prefix(filename
)
496 return reverse('logo', args
=[size
, prefix
, filename
])
499 def add_podcast(self
, podcast
, member_name
):
502 raise ValueError('group has to have an _id first')
505 raise ValueError('podcast needs to have an _id first')
508 podcast
.id = podcast
._id
511 podcast
.group
= self
._id
512 podcast
.group_member_name
= member_name
513 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
514 key
=Podcast
.subscriber_count
, reverse
=True)
520 return super(PodcastGroup
, self
).__repr
__()
522 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
524 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
528 class SanitizingRule(Document
):
529 slug
= StringProperty()
530 applies_to
= StringListProperty()
531 search
= StringProperty()
532 replace
= StringProperty()
533 priority
= IntegerProperty()
534 description
= StringProperty()
538 return 'SanitizingRule %s' % self
._id