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 def get_episode_before(self
, episode
):
240 if not episode
.released
:
243 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
244 prevs
= episodes_for_podcast(self
, until
=episode
.released
,
245 descending
=True, limit
=1)
247 return next(iter(prevs
), None)
250 def get_episode_after(self
, episode
):
251 if not episode
.released
:
254 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
255 nexts
= episodes_for_podcast(self
, since
=episode
.released
, limit
=1)
257 return next(iter(nexts
), None)
265 def get_podcast(self
):
269 def get_logo_url(self
, size
):
271 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
273 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
275 prefix
= CoverArt
.get_prefix(filename
)
277 return reverse('logo', args
=[size
, prefix
, filename
])
280 def subscriber_change(self
):
281 prev
= self
.prev_subscriber_count()
285 return self
.subscriber_count() / prev
288 def subscriber_count(self
):
289 if not self
.subscribers
:
291 return self
.subscribers
[-1].subscriber_count
294 def prev_subscriber_count(self
):
295 if len(self
.subscribers
) < 2:
297 return self
.subscribers
[-2].subscriber_count
301 @repeat_on_conflict()
302 def subscribe(self
, user
, device
):
303 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
304 state
= podcast_state_for_user_podcast(user
, self
)
305 state
.subscribe(device
)
309 except Unauthorized
as ex
:
310 raise SubscriptionException(ex
)
313 @repeat_on_conflict()
314 def unsubscribe(self
, user
, device
):
315 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
316 state
= podcast_state_for_user_podcast(user
, self
)
317 state
.unsubscribe(device
)
321 except Unauthorized
as ex
:
322 raise SubscriptionException(ex
)
325 def subscribe_targets(self
, user
):
327 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
328 devices/syncgroups on which the podcast is already subscribed
332 subscriptions_by_devices
= user
.get_subscriptions_by_device()
334 for group
in user
.get_grouped_devices():
338 dev
= group
.devices
[0]
340 if not self
.get_id() in subscriptions_by_devices
[dev
.id]:
341 targets
.append(group
.devices
)
344 for device
in group
.devices
:
345 if not self
.get_id() in subscriptions_by_devices
[device
.id]:
346 targets
.append(device
)
352 return hash(self
.get_id())
357 return super(Podcast
, self
).__repr
__()
359 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
361 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
365 group
= getattr(self
, 'group', None)
366 if group
: #we are part of a PodcastGroup
367 group
= PodcastGroup
.get(group
)
368 podcasts
= list(group
.podcasts
)
370 if not self
in podcasts
:
371 # the podcast has not been added to the group correctly
372 group
.add_podcast(self
)
375 i
= podcasts
.index(self
)
377 group
.podcasts
= podcasts
380 i
= podcasts
.index(self
)
382 group
.podcasts
= podcasts
386 super(Podcast
, self
).save()
390 group
= getattr(self
, 'group', None)
392 group
= PodcastGroup
.get(group
)
393 podcasts
= list(group
.podcasts
)
396 i
= podcasts
.index(self
)
398 group
.podcasts
= podcasts
402 super(Podcast
, self
).delete()
405 def __eq__(self
, other
):
406 if not self
.get_id():
412 return self
.get_id() == other
.get_id()
416 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
417 title
= StringProperty()
418 podcasts
= SchemaListProperty(Podcast
)
424 def get_podcast_by_id(self
, id, current_id
=False):
425 for podcast
in self
.podcasts
:
426 if podcast
.get_id() == id:
429 if id in podcast
.merged_ids
:
431 raise MergedIdException(podcast
, podcast
.get_id())
436 def get_podcast_by_oldid(self
, oldid
):
437 for podcast
in list(self
.podcasts
):
438 if podcast
.oldid
== oldid
:
442 def get_podcast_by_url(self
, url
):
443 for podcast
in self
.podcasts
:
444 if url
in list(podcast
.urls
):
448 def subscriber_change(self
):
449 prev
= self
.prev_subscriber_count()
453 return self
.subscriber_count() / prev
456 def subscriber_count(self
):
457 return sum([p
.subscriber_count() for p
in self
.podcasts
])
460 def prev_subscriber_count(self
):
461 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
464 def display_title(self
):
468 def get_podcast(self
):
469 # return podcast with most subscribers (bug 1390)
470 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
476 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
479 def get_logo_url(self
, size
):
481 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
483 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
485 prefix
= CoverArt
.get_prefix(filename
)
487 return reverse('logo', args
=[size
, prefix
, filename
])
490 def add_podcast(self
, podcast
, member_name
):
493 raise ValueError('group has to have an _id first')
496 raise ValueError('podcast needs to have an _id first')
499 podcast
.id = podcast
._id
502 podcast
.group
= self
._id
503 podcast
.group_member_name
= member_name
504 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
505 key
=Podcast
.subscriber_count
, reverse
=True)
511 return super(PodcastGroup
, self
).__repr
__()
513 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
515 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
519 class SanitizingRule(Document
):
520 slug
= StringProperty()
521 applies_to
= StringListProperty()
522 search
= StringProperty()
523 replace
= StringProperty()
524 priority
= IntegerProperty()
525 description
= StringProperty()
529 return 'SanitizingRule %s' % self
._id