[Migration] don't migrate episodes with invalid IDs
[mygpo.git] / mygpo / core / models.py
blob4a80adeb50d8753d097e52ebe85605007efb15a0
1 from __future__ import division
3 import re
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):
31 pass
34 class MergedIdException(Exception):
35 """ raised when an object is accessed through one of its merged_ids """
37 def __init__(self, obj, current_id):
38 self.obj = obj
39 self.current_id = current_id
42 class Episode(Document, SlugMixin, OldIdMixin):
43 """
44 Represents an Episode. Can only be part of a Podcast
45 """
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()
74 @property
75 def url(self):
76 return self.urls[0]
78 def __repr__(self):
79 return 'Episode %s' % self._id
83 def get_short_title(self, common_title):
84 if not self.title or not common_title:
85 return None
87 title = self.title.replace(common_title, '').strip()
88 title = re.sub(r'^[\W\d]+', '', title)
89 return title
92 def get_episode_number(self, common_title):
93 if not self.title or not common_title:
94 return None
96 title = self.title.replace(common_title, '').strip()
97 match = re.search(r'^\W*(\d+)', title)
98 if not match:
99 return None
101 return int(match.group(1))
104 def get_ids(self):
105 return set([self._id] + self.merged_ids)
108 @property
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):
114 if other is None:
115 return False
116 return self._id == other._id
119 def __hash__(self):
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):
135 return False
137 return (self.timestamp == other.timestamp) and \
138 (self.subscriber_count == other.subscriber_count)
140 def __hash__(self):
141 return hash(frozenset([self.timestamp, self.subscriber_count]))
144 class PodcastSubscriberData(Document):
145 podcast = StringProperty()
146 subscribers = SchemaListProperty(SubscriberData)
149 def __repr__(self):
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())
194 return self
197 get_podcast_by_oldid = get_podcast_by_id
198 get_podcast_by_url = get_podcast_by_id
201 def get_id(self):
202 return self.id or self._id
204 def get_ids(self):
205 return set([self.get_id()] + self.merged_ids)
207 @property
208 def display_title(self):
209 return self.title or self.url
211 @property
212 def url(self):
213 return self.urls[0]
216 def get_podcast(self):
217 return self
220 def subscriber_change(self):
221 prev = self.prev_subscriber_count()
222 if prev <= 0:
223 return 0
225 return self.subscriber_count() / prev
228 def subscriber_count(self):
229 if not self.subscribers:
230 return 0
231 return self.subscribers[-1].subscriber_count
234 def prev_subscriber_count(self):
235 if len(self.subscribers) < 2:
236 return 0
237 return self.subscribers[-2].subscriber_count
240 @property
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
245 @property
246 def next_update(self):
247 return self.last_update + timedelta(hours=self.update_interval)
249 def __hash__(self):
250 return hash(self.get_id())
253 def __repr__(self):
254 if not self._id:
255 return super(Podcast, self).__repr__()
256 elif self.oldid:
257 return '%s %s (%s)' % (self.__class__.__name__, self.get_id(), self.oldid)
258 else:
259 return '%s %s' % (self.__class__.__name__, self.get_id())
262 def save(self):
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)
272 else:
273 i = podcasts.index(self)
274 podcasts[i] = self
275 group.podcasts = podcasts
276 group.save()
278 i = podcasts.index(self)
279 podcasts[i] = self
280 group.podcasts = podcasts
281 group.save()
283 else:
284 super(Podcast, self).save()
287 def delete(self):
288 group = getattr(self, 'group', None)
289 if group:
290 group = PodcastGroup.get(group)
291 podcasts = list(group.podcasts)
293 if self in podcasts:
294 i = podcasts.index(self)
295 del podcasts[i]
296 group.podcasts = podcasts
297 group.save()
299 else:
300 super(Podcast, self).delete()
303 def __eq__(self, other):
304 if not self.get_id():
305 return self == other
307 if other is None:
308 return False
310 return self.get_id() == other.get_id()
314 class PodcastGroup(Document, SlugMixin, OldIdMixin):
315 title = StringProperty()
316 podcasts = SchemaListProperty(Podcast)
318 def get_id(self):
319 return self._id
322 def get_podcast_by_id(self, id, current_id=False):
323 for podcast in self.podcasts:
324 if podcast.get_id() == id:
325 return podcast
327 if id in podcast.merged_ids:
328 if current_id:
329 raise MergedIdException(podcast, podcast.get_id())
331 return podcast
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:
337 return podcast
340 def get_podcast_by_url(self, url):
341 for podcast in self.podcasts:
342 if url in list(podcast.urls):
343 return podcast
346 def subscriber_change(self):
347 prev = self.prev_subscriber_count()
348 if not prev:
349 return 0
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])
361 @property
362 def display_title(self):
363 return self.title
365 @property
366 def license(self):
367 return utils.first(p.license for p in self.podcasts)
370 @property
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
374 # requires an update
375 return False
377 def get_podcast(self):
378 # return podcast with most subscribers (bug 1390)
379 return sorted(self.podcasts, key=Podcast.subscriber_count,
380 reverse=True)[0]
383 @property
384 def logo_url(self):
385 return utils.first(p.logo_url for p in self.podcasts)
387 @logo_url.setter
388 def logo_url(self, value):
389 self.podcasts[0].logo_url = value
392 def add_podcast(self, podcast, member_name):
394 if not self._id:
395 raise ValueError('group has to have an _id first')
397 if not podcast._id:
398 raise ValueError('podcast needs to have an _id first')
400 if not podcast.id:
401 podcast.id = podcast._id
403 podcast.delete()
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)
408 self.save()
411 def __repr__(self):
412 if not self._id:
413 return super(PodcastGroup, self).__repr__()
414 elif self.oldid:
415 return '%s %s (%s)' % (self.__class__.__name__, self._id[:10], self.oldid)
416 else:
417 return '%s %s' % (self.__class__.__name__, self._id[:10])