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 def get_episode_before(self
, episode
):
242 if not episode
.released
:
245 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
246 prevs
= episodes_for_podcast(self
, until
=episode
.released
,
247 descending
=True, limit
=1)
249 return next(iter(prevs
), None)
252 def get_episode_after(self
, episode
):
253 if not episode
.released
:
256 from mygpo
.db
.couchdb
.episode
import episodes_for_podcast
257 from datetime
import timedelta
258 nexts
= episodes_for_podcast(self
,
259 since
=episode
.released
+ timedelta(seconds
=1), limit
=1)
261 return next(iter(nexts
), None)
269 def get_podcast(self
):
273 def get_logo_url(self
, size
):
275 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
277 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
279 prefix
= CoverArt
.get_prefix(filename
)
281 return reverse('logo', args
=[size
, prefix
, filename
])
284 def subscriber_change(self
):
285 prev
= self
.prev_subscriber_count()
289 return self
.subscriber_count() / prev
292 def subscriber_count(self
):
293 if not self
.subscribers
:
295 return self
.subscribers
[-1].subscriber_count
298 def prev_subscriber_count(self
):
299 if len(self
.subscribers
) < 2:
301 return self
.subscribers
[-2].subscriber_count
305 @repeat_on_conflict()
306 def subscribe(self
, user
, device
):
307 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
308 state
= podcast_state_for_user_podcast(user
, self
)
309 state
.subscribe(device
)
313 except Unauthorized
as ex
:
314 raise SubscriptionException(ex
)
317 @repeat_on_conflict()
318 def unsubscribe(self
, user
, device
):
319 from mygpo
.db
.couchdb
.podcast_state
import podcast_state_for_user_podcast
320 state
= podcast_state_for_user_podcast(user
, self
)
321 state
.unsubscribe(device
)
325 except Unauthorized
as ex
:
326 raise SubscriptionException(ex
)
329 def subscribe_targets(self
, user
):
331 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
332 devices/syncgroups on which the podcast is already subscribed
336 subscriptions_by_devices
= user
.get_subscriptions_by_device()
338 for group
in user
.get_grouped_devices():
342 dev
= group
.devices
[0]
344 if not self
.get_id() in subscriptions_by_devices
[dev
.id]:
345 targets
.append(group
.devices
)
348 for device
in group
.devices
:
349 if not self
.get_id() in subscriptions_by_devices
[device
.id]:
350 targets
.append(device
)
356 return hash(self
.get_id())
361 return super(Podcast
, self
).__repr
__()
363 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
365 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
369 group
= getattr(self
, 'group', None)
370 if group
: #we are part of a PodcastGroup
371 group
= PodcastGroup
.get(group
)
372 podcasts
= list(group
.podcasts
)
374 if not self
in podcasts
:
375 # the podcast has not been added to the group correctly
376 group
.add_podcast(self
)
379 i
= podcasts
.index(self
)
381 group
.podcasts
= podcasts
384 i
= podcasts
.index(self
)
386 group
.podcasts
= podcasts
390 super(Podcast
, self
).save()
394 group
= getattr(self
, 'group', None)
396 group
= PodcastGroup
.get(group
)
397 podcasts
= list(group
.podcasts
)
400 i
= podcasts
.index(self
)
402 group
.podcasts
= podcasts
406 super(Podcast
, self
).delete()
409 def __eq__(self
, other
):
410 if not self
.get_id():
416 return self
.get_id() == other
.get_id()
420 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
421 title
= StringProperty()
422 podcasts
= SchemaListProperty(Podcast
)
428 def get_podcast_by_id(self
, id, current_id
=False):
429 for podcast
in self
.podcasts
:
430 if podcast
.get_id() == id:
433 if id in podcast
.merged_ids
:
435 raise MergedIdException(podcast
, podcast
.get_id())
440 def get_podcast_by_oldid(self
, oldid
):
441 for podcast
in list(self
.podcasts
):
442 if podcast
.oldid
== oldid
:
446 def get_podcast_by_url(self
, url
):
447 for podcast
in self
.podcasts
:
448 if url
in list(podcast
.urls
):
452 def subscriber_change(self
):
453 prev
= self
.prev_subscriber_count()
457 return self
.subscriber_count() / prev
460 def subscriber_count(self
):
461 return sum([p
.subscriber_count() for p
in self
.podcasts
])
464 def prev_subscriber_count(self
):
465 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
468 def display_title(self
):
472 def get_podcast(self
):
473 # return podcast with most subscribers (bug 1390)
474 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
480 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
483 def logo_url(self
, value
):
484 self
.podcasts
[0].logo_url
= value
487 def get_logo_url(self
, size
):
489 filename
= hashlib
.sha1(self
.logo_url
).hexdigest()
491 filename
= 'podcast-%d.png' % (hash(self
.title
) % 5, )
493 prefix
= CoverArt
.get_prefix(filename
)
495 return reverse('logo', args
=[size
, prefix
, filename
])
498 def add_podcast(self
, podcast
, member_name
):
501 raise ValueError('group has to have an _id first')
504 raise ValueError('podcast needs to have an _id first')
507 podcast
.id = podcast
._id
510 podcast
.group
= self
._id
511 podcast
.group_member_name
= member_name
512 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
513 key
=Podcast
.subscriber_count
, reverse
=True)
519 return super(PodcastGroup
, self
).__repr
__()
521 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
523 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
527 class SanitizingRule(Document
):
528 slug
= StringProperty()
529 applies_to
= StringListProperty()
530 search
= StringProperty()
531 replace
= StringProperty()
532 priority
= IntegerProperty()
533 description
= StringProperty()
537 return 'SanitizingRule %s' % self
._id