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
)
317 except Unauthorized
as ex
:
318 raise SubscriptionException(ex
)
321 @repeat_on_conflict()
322 def unsubscribe(self
, user
, device
):
323 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
324 state
= podcast_state_for_user_podcast(user
, self
)
325 state
.unsubscribe(device
)
328 except Unauthorized
as ex
:
329 raise SubscriptionException(ex
)
332 def subscribe_targets(self
, user
):
334 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
335 devices/syncgroups on which the podcast is already subscribed
339 subscriptions_by_devices
= user
.get_subscriptions_by_device()
341 for group
in user
.get_grouped_devices():
345 dev
= group
.devices
[0]
347 if not self
.get_id() in subscriptions_by_devices
[dev
.id]:
348 targets
.append(group
.devices
)
351 for device
in group
.devices
:
352 if not self
.get_id() in subscriptions_by_devices
[device
.id]:
353 targets
.append(device
)
359 return hash(self
.get_id())
364 return super(Podcast
, self
).__repr
__()
366 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
368 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
372 group
= getattr(self
, 'group', None)
373 if group
: #we are part of a PodcastGroup
374 group
= PodcastGroup
.get(group
)
375 podcasts
= list(group
.podcasts
)
377 if not self
in podcasts
:
378 # the podcast has not been added to the group correctly
379 group
.add_podcast(self
)
382 i
= podcasts
.index(self
)
384 group
.podcasts
= podcasts
387 i
= podcasts
.index(self
)
389 group
.podcasts
= podcasts
393 super(Podcast
, self
).save()
397 group
= getattr(self
, 'group', None)
399 group
= PodcastGroup
.get(group
)
400 podcasts
= list(group
.podcasts
)
403 i
= podcasts
.index(self
)
405 group
.podcasts
= podcasts
409 super(Podcast
, self
).delete()
412 def __eq__(self
, other
):
413 if not self
.get_id():
419 return self
.get_id() == other
.get_id()
423 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
424 title
= StringProperty()
425 podcasts
= SchemaListProperty(Podcast
)
431 def get_podcast_by_id(self
, id, current_id
=False):
432 for podcast
in self
.podcasts
:
433 if podcast
.get_id() == id:
436 if id in podcast
.merged_ids
:
438 raise MergedIdException(podcast
, podcast
.get_id())
443 def get_podcast_by_oldid(self
, oldid
):
444 for podcast
in list(self
.podcasts
):
445 if podcast
.oldid
== oldid
:
449 def get_podcast_by_url(self
, url
):
450 for podcast
in self
.podcasts
:
451 if url
in list(podcast
.urls
):
455 def subscriber_change(self
):
456 prev
= self
.prev_subscriber_count()
460 return self
.subscriber_count() / prev
463 def subscriber_count(self
):
464 return sum([p
.subscriber_count() for p
in self
.podcasts
])
467 def prev_subscriber_count(self
):
468 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
471 def display_title(self
):
475 def get_podcast(self
):
476 # return podcast with most subscribers (bug 1390)
477 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
483 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
486 def get_logo_url(self
, size
):
488 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
490 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
492 prefix
= CoverArt
.get_prefix(filename
)
494 return reverse('logo', args
=[size
, prefix
, filename
])
497 def add_podcast(self
, podcast
, member_name
):
500 raise ValueError('group has to have an _id first')
503 raise ValueError('podcast needs to have an _id first')
506 podcast
.id = podcast
._id
509 podcast
.group
= self
._id
510 podcast
.group_member_name
= member_name
511 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
512 key
=Podcast
.subscriber_count
, reverse
=True)
518 return super(PodcastGroup
, self
).__repr
__()
520 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
522 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
526 class SanitizingRule(Document
):
527 slug
= StringProperty()
528 applies_to
= StringListProperty()
529 search
= StringProperty()
530 replace
= StringProperty()
531 priority
= IntegerProperty()
532 description
= StringProperty()
536 return 'SanitizingRule %s' % self
._id