1 from __future__
import division
4 from random
import random
5 from datetime
import timedelta
7 from couchdbkit
.ext
.django
.schema
import *
8 from restkit
.errors
import Unauthorized
10 from mygpo
.decorators
import repeat_on_conflict
11 from mygpo
import utils
12 from mygpo
.core
.proxy
import DocumentABCMeta
13 from mygpo
.core
.slugs
import SlugMixin
14 from mygpo
.core
.oldid
import OldIdMixin
16 # make sure this code is executed at startup
17 from mygpo
.core
.signals
import *
20 # default podcast update interval in hours
21 DEFAULT_UPDATE_INTERVAL
= 7 * 24
23 # minium podcast update interval in hours
24 MIN_UPDATE_INTERVAL
= 5
26 # every podcast should be updated at least once a month
27 MAX_UPDATE_INTERVAL
= 24 * 30
30 class SubscriptionException(Exception):
34 class MergedIdException(Exception):
35 """ raised when an object is accessed through one of its merged_ids """
37 def __init__(self
, obj
, current_id
):
39 self
.current_id
= current_id
42 class Episode(Document
, SlugMixin
, OldIdMixin
):
44 Represents an Episode. Can only be part of a Podcast
47 __metaclass__
= DocumentABCMeta
49 title
= StringProperty()
50 guid
= StringProperty()
51 description
= StringProperty(default
="")
52 subtitle
= StringProperty()
53 content
= StringProperty(default
="")
54 link
= StringProperty()
55 released
= DateTimeProperty()
56 author
= StringProperty()
57 duration
= IntegerProperty()
58 filesize
= IntegerProperty()
59 language
= StringProperty()
60 last_update
= DateTimeProperty()
61 outdated
= BooleanProperty(default
=False)
62 mimetypes
= StringListProperty()
63 merged_ids
= StringListProperty()
64 urls
= StringListProperty()
65 podcast
= StringProperty(required
=True)
66 listeners
= IntegerProperty()
67 content_types
= StringListProperty()
68 flattr_url
= StringProperty()
69 created_timestamp
= IntegerProperty()
70 license
= StringProperty()
79 return 'Episode %s' % self
._id
83 def get_short_title(self
, common_title
):
84 if not self
.title
or not common_title
:
87 title
= self
.title
.replace(common_title
, '').strip()
88 title
= re
.sub(r
'^[\W\d]+', '', title
)
92 def get_episode_number(self
, common_title
):
93 if not self
.title
or not common_title
:
96 title
= self
.title
.replace(common_title
, '').strip()
97 match
= re
.search(r
'^\W*(\d+)', title
)
101 return int(match
.group(1))
105 return set([self
._id
] + self
.merged_ids
)
109 def needs_update(self
):
110 """ Indicates if the object requires an updated from its feed """
111 return not self
.title
and not self
.outdated
113 def __eq__(self
, other
):
116 return self
._id
== other
._id
120 return hash(self
._id
)
123 def __unicode__(self
):
124 return u
'<{cls} {title} ({id})>'.format(cls
=self
.__class
__.__name
__,
125 title
=self
.title
, id=self
._id
)
129 class SubscriberData(DocumentSchema
):
130 timestamp
= DateTimeProperty()
131 subscriber_count
= IntegerProperty()
133 def __eq__(self
, other
):
134 if not isinstance(other
, SubscriberData
):
137 return (self
.timestamp
== other
.timestamp
) and \
138 (self
.subscriber_count
== other
.subscriber_count
)
141 return hash(frozenset([self
.timestamp
, self
.subscriber_count
]))
144 class PodcastSubscriberData(Document
):
145 podcast
= StringProperty()
146 subscribers
= SchemaListProperty(SubscriberData
)
150 return 'PodcastSubscriberData for Podcast %s (%s)' % (self
.podcast
, self
._id
)
153 class Podcast(Document
, SlugMixin
, OldIdMixin
):
155 __metaclass__
= DocumentABCMeta
157 id = StringProperty()
158 title
= StringProperty()
159 urls
= StringListProperty()
160 description
= StringProperty()
161 subtitle
= StringProperty()
162 link
= StringProperty()
163 last_update
= DateTimeProperty()
164 logo_url
= StringProperty()
165 author
= StringProperty()
166 merged_ids
= StringListProperty()
167 group
= StringProperty()
168 group_member_name
= StringProperty()
169 related_podcasts
= StringListProperty()
170 subscribers
= SchemaListProperty(SubscriberData
)
171 language
= StringProperty()
172 content_types
= StringListProperty()
173 tags
= DictProperty()
174 restrictions
= StringListProperty()
175 common_episode_title
= StringProperty()
176 new_location
= StringProperty()
177 latest_episode_timestamp
= DateTimeProperty()
178 episode_count
= IntegerProperty()
179 random_key
= FloatProperty(default
=random
)
180 flattr_url
= StringProperty()
181 outdated
= BooleanProperty(default
=False)
182 created_timestamp
= IntegerProperty()
183 hub
= StringProperty()
184 license
= StringProperty()
186 # avg time between podcast updates (eg new episodes) in hours
187 update_interval
= IntegerProperty(default
=DEFAULT_UPDATE_INTERVAL
)
190 def get_podcast_by_id(self
, id, current_id
=False):
191 if current_id
and id != self
.get_id():
192 raise MergedIdException(self
, self
.get_id())
197 get_podcast_by_oldid
= get_podcast_by_id
198 get_podcast_by_url
= get_podcast_by_id
202 return self
.id or self
._id
205 return set([self
.get_id()] + self
.merged_ids
)
208 def display_title(self
):
209 return self
.title
or self
.url
216 def get_podcast(self
):
220 def subscriber_change(self
):
221 prev
= self
.prev_subscriber_count()
225 return self
.subscriber_count() / prev
228 def subscriber_count(self
):
229 if not self
.subscribers
:
231 return self
.subscribers
[-1].subscriber_count
234 def prev_subscriber_count(self
):
235 if len(self
.subscribers
) < 2:
237 return self
.subscribers
[-2].subscriber_count
241 def needs_update(self
):
242 """ Indicates if the object requires an updated from its feed """
243 return not self
.title
and not self
.outdated
246 def next_update(self
):
247 return self
.last_update
+ timedelta(hours
=self
.update_interval
)
250 return hash(self
.get_id())
255 return super(Podcast
, self
).__repr
__()
257 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
259 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
263 group
= getattr(self
, 'group', None)
264 if group
: # we are part of a PodcastGroup
265 group
= PodcastGroup
.get(group
)
266 podcasts
= list(group
.podcasts
)
268 if not self
in podcasts
:
269 # the podcast has not been added to the group correctly
270 group
.add_podcast(self
)
273 i
= podcasts
.index(self
)
275 group
.podcasts
= podcasts
278 i
= podcasts
.index(self
)
280 group
.podcasts
= podcasts
284 super(Podcast
, self
).save()
288 group
= getattr(self
, 'group', None)
290 group
= PodcastGroup
.get(group
)
291 podcasts
= list(group
.podcasts
)
294 i
= podcasts
.index(self
)
296 group
.podcasts
= podcasts
300 super(Podcast
, self
).delete()
303 def __eq__(self
, other
):
304 if not self
.get_id():
310 return self
.get_id() == other
.get_id()
314 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
315 title
= StringProperty()
316 podcasts
= SchemaListProperty(Podcast
)
322 def get_podcast_by_id(self
, id, current_id
=False):
323 for podcast
in self
.podcasts
:
324 if podcast
.get_id() == id:
327 if id in podcast
.merged_ids
:
329 raise MergedIdException(podcast
, podcast
.get_id())
334 def get_podcast_by_oldid(self
, oldid
):
335 for podcast
in list(self
.podcasts
):
336 if podcast
.oldid
== oldid
or oldid
in podcast
.merged_oldids
:
340 def get_podcast_by_url(self
, url
):
341 for podcast
in self
.podcasts
:
342 if url
in list(podcast
.urls
):
346 def subscriber_change(self
):
347 prev
= self
.prev_subscriber_count()
351 return self
.subscriber_count() / prev
354 def subscriber_count(self
):
355 return sum([p
.subscriber_count() for p
in self
.podcasts
])
358 def prev_subscriber_count(self
):
359 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
362 def display_title(self
):
367 return utils
.first(p
.license
for p
in self
.podcasts
)
371 def needs_update(self
):
372 """ Indicates if the object requires an updated from its feed """
373 # A PodcastGroup has been manually created and therefore never
377 def get_podcast(self
):
378 # return podcast with most subscribers (bug 1390)
379 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
385 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
388 def logo_url(self
, value
):
389 self
.podcasts
[0].logo_url
= value
392 def add_podcast(self
, podcast
, member_name
):
395 raise ValueError('group has to have an _id first')
398 raise ValueError('podcast needs to have an _id first')
401 podcast
.id = podcast
._id
404 podcast
.group
= self
._id
405 podcast
.group_member_name
= member_name
406 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
407 key
=Podcast
.subscriber_count
, reverse
=True)
413 return super(PodcastGroup
, self
).__repr
__()
415 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
417 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])