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 MergedIdException(Exception):
31 """ raised when an object is accessed through one of its merged_ids """
33 def __init__(self
, obj
, current_id
):
35 self
.current_id
= current_id
38 class Episode(Document
, SlugMixin
, OldIdMixin
):
40 Represents an Episode. Can only be part of a Podcast
43 __metaclass__
= DocumentABCMeta
45 title
= StringProperty()
46 guid
= StringProperty()
47 description
= StringProperty(default
="")
48 subtitle
= StringProperty()
49 content
= StringProperty(default
="")
50 link
= StringProperty()
51 released
= DateTimeProperty()
52 author
= StringProperty()
53 duration
= IntegerProperty()
54 filesize
= IntegerProperty()
55 language
= StringProperty()
56 last_update
= DateTimeProperty()
57 outdated
= BooleanProperty(default
=False)
58 mimetypes
= StringListProperty()
59 merged_ids
= StringListProperty()
60 urls
= StringListProperty()
61 podcast
= StringProperty(required
=True)
62 listeners
= IntegerProperty()
63 content_types
= StringListProperty()
64 flattr_url
= StringProperty()
65 created_timestamp
= IntegerProperty()
66 license
= StringProperty()
75 return 'Episode %s' % self
._id
79 def get_short_title(self
, common_title
):
80 if not self
.title
or not common_title
:
83 title
= self
.title
.replace(common_title
, '').strip()
84 title
= re
.sub(r
'^[\W\d]+', '', title
)
88 def get_episode_number(self
, common_title
):
89 if not self
.title
or not common_title
:
92 title
= self
.title
.replace(common_title
, '').strip()
93 match
= re
.search(r
'^\W*(\d+)', title
)
97 return int(match
.group(1))
101 return set([self
._id
] + self
.merged_ids
)
105 def needs_update(self
):
106 """ Indicates if the object requires an updated from its feed """
107 return not self
.title
and not self
.outdated
109 def __eq__(self
, other
):
112 return self
._id
== other
._id
116 return hash(self
._id
)
119 def __unicode__(self
):
120 return u
'<{cls} {title} ({id})>'.format(cls
=self
.__class
__.__name
__,
121 title
=self
.title
, id=self
._id
)
125 class SubscriberData(DocumentSchema
):
126 timestamp
= DateTimeProperty()
127 subscriber_count
= IntegerProperty()
129 def __eq__(self
, other
):
130 if not isinstance(other
, SubscriberData
):
133 return (self
.timestamp
== other
.timestamp
) and \
134 (self
.subscriber_count
== other
.subscriber_count
)
137 return hash(frozenset([self
.timestamp
, self
.subscriber_count
]))
140 class PodcastSubscriberData(Document
):
141 podcast
= StringProperty()
142 subscribers
= SchemaListProperty(SubscriberData
)
146 return 'PodcastSubscriberData for Podcast %s (%s)' % (self
.podcast
, self
._id
)
149 class Podcast(Document
, SlugMixin
, OldIdMixin
):
151 __metaclass__
= DocumentABCMeta
153 id = StringProperty()
154 title
= StringProperty()
155 urls
= StringListProperty()
156 description
= StringProperty()
157 subtitle
= StringProperty()
158 link
= StringProperty()
159 last_update
= DateTimeProperty()
160 logo_url
= StringProperty()
161 author
= StringProperty()
162 merged_ids
= StringListProperty()
163 group
= StringProperty()
164 group_member_name
= StringProperty()
165 related_podcasts
= StringListProperty()
166 subscribers
= SchemaListProperty(SubscriberData
)
167 language
= StringProperty()
168 content_types
= StringListProperty()
169 tags
= DictProperty()
170 restrictions
= StringListProperty()
171 common_episode_title
= StringProperty()
172 new_location
= StringProperty()
173 latest_episode_timestamp
= DateTimeProperty()
174 episode_count
= IntegerProperty()
175 random_key
= FloatProperty(default
=random
)
176 flattr_url
= StringProperty()
177 outdated
= BooleanProperty(default
=False)
178 created_timestamp
= IntegerProperty()
179 hub
= StringProperty()
180 license
= StringProperty()
182 # avg time between podcast updates (eg new episodes) in hours
183 update_interval
= IntegerProperty(default
=DEFAULT_UPDATE_INTERVAL
)
186 def get_podcast_by_id(self
, id, current_id
=False):
187 if current_id
and id != self
.get_id():
188 raise MergedIdException(self
, self
.get_id())
193 get_podcast_by_oldid
= get_podcast_by_id
194 get_podcast_by_url
= get_podcast_by_id
198 return self
.id or self
._id
201 return set([self
.get_id()] + self
.merged_ids
)
204 def display_title(self
):
205 return self
.title
or self
.url
212 def get_podcast(self
):
216 def subscriber_change(self
):
217 prev
= self
.prev_subscriber_count()
221 return self
.subscriber_count() / prev
224 def subscriber_count(self
):
225 if not self
.subscribers
:
227 return self
.subscribers
[-1].subscriber_count
230 def prev_subscriber_count(self
):
231 if len(self
.subscribers
) < 2:
233 return self
.subscribers
[-2].subscriber_count
237 def needs_update(self
):
238 """ Indicates if the object requires an updated from its feed """
239 return not self
.title
and not self
.outdated
242 def next_update(self
):
243 return self
.last_update
+ timedelta(hours
=self
.update_interval
)
246 return hash(self
.get_id())
251 return super(Podcast
, self
).__repr
__()
253 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
255 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
259 group
= getattr(self
, 'group', None)
260 if group
: # we are part of a PodcastGroup
261 group
= PodcastGroup
.get(group
)
262 podcasts
= list(group
.podcasts
)
264 if not self
in podcasts
:
265 # the podcast has not been added to the group correctly
266 group
.add_podcast(self
)
269 i
= podcasts
.index(self
)
271 group
.podcasts
= podcasts
274 i
= podcasts
.index(self
)
276 group
.podcasts
= podcasts
280 super(Podcast
, self
).save()
284 group
= getattr(self
, 'group', None)
286 group
= PodcastGroup
.get(group
)
287 podcasts
= list(group
.podcasts
)
290 i
= podcasts
.index(self
)
292 group
.podcasts
= podcasts
296 super(Podcast
, self
).delete()
299 def __eq__(self
, other
):
300 if not self
.get_id():
306 return self
.get_id() == other
.get_id()
310 class PodcastGroup(Document
, SlugMixin
, OldIdMixin
):
311 title
= StringProperty()
312 podcasts
= SchemaListProperty(Podcast
)
318 def get_podcast_by_id(self
, id, current_id
=False):
319 for podcast
in self
.podcasts
:
320 if podcast
.get_id() == id:
323 if id in podcast
.merged_ids
:
325 raise MergedIdException(podcast
, podcast
.get_id())
330 def get_podcast_by_oldid(self
, oldid
):
331 for podcast
in list(self
.podcasts
):
332 if podcast
.oldid
== oldid
or oldid
in podcast
.merged_oldids
:
336 def get_podcast_by_url(self
, url
):
337 for podcast
in self
.podcasts
:
338 if url
in list(podcast
.urls
):
342 def subscriber_change(self
):
343 prev
= self
.prev_subscriber_count()
347 return self
.subscriber_count() / prev
350 def subscriber_count(self
):
351 return sum([p
.subscriber_count() for p
in self
.podcasts
])
354 def prev_subscriber_count(self
):
355 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
358 def display_title(self
):
363 return utils
.first(p
.license
for p
in self
.podcasts
)
367 def needs_update(self
):
368 """ Indicates if the object requires an updated from its feed """
369 # A PodcastGroup has been manually created and therefore never
373 def get_podcast(self
):
374 # return podcast with most subscribers (bug 1390)
375 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
381 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
384 def logo_url(self
, value
):
385 self
.podcasts
[0].logo_url
= value
388 def add_podcast(self
, podcast
, member_name
):
391 raise ValueError('group has to have an _id first')
394 raise ValueError('podcast needs to have an _id first')
397 podcast
.id = podcast
._id
400 podcast
.group
= self
._id
401 podcast
.group_member_name
= member_name
402 self
.podcasts
= sorted(self
.podcasts
+ [podcast
],
403 key
=Podcast
.subscriber_count
, reverse
=True)
409 return super(PodcastGroup
, self
).__repr
__()
411 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
413 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])