fix IndexError when merging episodes of single podcast
[mygpo.git] / mygpo / maintenance / merge.py
blobb7bfd42c0b8febd9c1bae9643d00788f48f53b2a
1 from itertools import chain, imap as map
2 import logging
3 from functools import partial
5 import restkit
7 from mygpo import utils
8 from mygpo.decorators import repeat_on_conflict
9 from mygpo.db.couchdb.podcast import delete_podcast
10 from mygpo.db.couchdb.episode import episodes_for_podcast_uncached
11 from mygpo.db.couchdb.podcast_state import all_podcast_states, \
12 delete_podcast_state
13 from mygpo.db.couchdb.episode_state import all_episode_states
16 class IncorrectMergeException(Exception):
17 pass
20 class PodcastMerger(object):
21 """ Merges podcasts and their related objects """
23 def __init__(self, podcasts, actions, groups):
24 """ Prepares to merge podcasts[1:] into podcasts[0] """
26 for n, podcast1 in enumerate(podcasts):
27 for m, podcast2 in enumerate(podcasts):
28 if podcast1 == podcast2 and n != m:
29 raise IncorrectMergeException(
30 "can't merge podcast into itself")
32 self.podcasts = podcasts
33 self.actions = actions
34 self.groups = groups
36 def merge(self):
37 """ Carries out the actual merging """
39 podcast1 = self.podcasts.pop(0)
41 self.merge_episodes()
43 for podcast2 in self.podcasts:
44 self.merge_states(podcast1, podcast2)
45 self.reassign_episodes(podcast1, podcast2)
46 self._merge_objs(podcast1=podcast1, podcast2=podcast2)
47 delete_podcast(podcast2)
48 self.actions['merge-podcast'] += 1
50 return podcast1
52 def merge_episodes(self):
53 """ Merges the episodes according to the groups """
55 for n, episodes in self.groups:
57 if not episodes:
58 continue
60 episode = episodes.pop(0)
62 for ep in episodes:
63 em = EpisodeMerger(episode, ep, self.actions)
64 em.merge()
66 @repeat_on_conflict(['podcast1', 'podcast2'])
67 def _merge_objs(self, podcast1, podcast2):
69 podcast1.merged_ids = set_filter(podcast1.get_id(),
70 podcast1.merged_ids,
71 [podcast2.get_id()],
72 podcast2.merged_ids)
74 podcast1.merged_slugs = set_filter(podcast1.slug,
75 podcast1.merged_slugs,
76 [podcast2.slug],
77 podcast2.merged_slugs)
79 podcast1.merged_oldids = set_filter(podcast1.oldid,
80 podcast1.merged_oldids,
81 [podcast2.oldid],
82 podcast2.merged_oldids)
84 # the first URL in the list represents the podcast main URL
85 main_url = podcast1.url
86 podcast1.urls = set_filter(None, podcast1.urls, podcast2.urls)
87 # so we insert it as the first again
88 podcast1.urls.remove(main_url)
89 podcast1.urls.insert(0, main_url)
91 podcast1.content_types = set_filter(None, podcast1.content_types,
92 podcast2.content_types)
94 podcast1.save()
96 @repeat_on_conflict(['s'])
97 def _save_state(self, s, podcast1):
98 s.podcast = podcast1.get_id()
99 s.save()
101 @repeat_on_conflict(['e'])
102 def _save_episode(self, e, podcast1):
103 e.podcast = podcast1.get_id()
104 e.save()
106 def reassign_episodes(self, podcast1, podcast2):
107 # re-assign episodes to new podcast
108 # if necessary, they will be merged later anyway
109 for e in episodes_for_podcast_uncached(podcast2):
110 self.actions['reassign-episode'] += 1
112 for s in all_episode_states(e):
113 self.actions['reassign-episode-state'] += 1
115 self._save_state(s=s, podcast1=podcast1)
117 self._save_episode(e=e, podcast1=podcast1)
119 def merge_states(self, podcast1, podcast2):
120 """Merges the Podcast states that are associated with the two Podcasts.
122 This should be done after two podcasts are merged
125 key = lambda x: x.user
126 states1 = sorted(all_podcast_states(podcast1), key=key)
127 states2 = sorted(all_podcast_states(podcast2), key=key)
129 for state, state2 in utils.iterate_together([states1, states2], key):
131 if state == state2:
132 continue
134 if state is None:
135 self.actions['move-podcast-state'] += 1
136 self._move_state(state2=state2, new_id=podcast1.get_id(),
137 new_url=podcast1.url)
139 elif state2 is None:
140 continue
142 else:
143 psm = PodcastStateMerger(state, state2, self.actions)
144 psm.merge()
146 @repeat_on_conflict(['state2'])
147 def _move_state(self, state2, new_id, new_url):
148 state2.ref_url = new_url
149 state2.podcast = new_id
150 state2.save()
153 class EpisodeMerger(object):
155 def __init__(self, episode1, episode2, actions):
156 if episode1 == episode2:
157 raise IncorrectMergeException("can't merge episode into itself")
159 self.episode1 = episode1
160 self.episode2 = episode2
161 self.actions = actions
163 def merge(self):
164 self._merge_objs(episode1=self.episode1, episode2=self.episode2)
165 self.merge_states(self.episode1, self.episode2)
166 self._delete(e=self.episode2)
167 self.actions['merge-episode'] += 1
169 @repeat_on_conflict(['episode1'])
170 def _merge_objs(self, episode1, episode2):
172 episode1.urls = set_filter(None, episode1.urls, episode2.urls)
174 episode1.merged_ids = set_filter(episode1._id, episode1.merged_ids,
175 [episode2._id], episode2.merged_ids)
177 episode1.merged_slugs = set_filter(episode1.slug,
178 episode1.merged_slugs,
179 [episode2.slug],
180 episode2.merged_slugs)
182 episode1.save()
184 @repeat_on_conflict(['e'])
185 def _delete(self, e):
186 e.delete()
188 def merge_states(self, episode, episode2):
190 key = lambda x: x.user
191 states1 = sorted(all_episode_states(self.episode1), key=key)
192 states2 = sorted(all_episode_states(self.episode2), key=key)
194 for state, state2 in utils.iterate_together([states1, states2], key):
196 if state == state2:
197 continue
199 if state is None:
200 self.actions['move-episode-state'] += 1
201 self._move(state2=state2, podcast_id=self.episode1.podcast,
202 episode_id=self.episode1._id)
204 elif state2 is None:
205 continue
207 else:
208 esm = EpisodeStateMerger(state, state2, self.actions)
209 esm.merge()
211 @repeat_on_conflict(['state2'])
212 def _move(self, state2, podcast_id, episode_id):
213 state2.podcast = podcast_id
214 state2.episode = episode_id
215 state2.save()
218 class PodcastStateMerger(object):
219 """Merges the two given podcast states"""
221 def __init__(self, state, state2, actions):
223 if state._id == state2._id:
224 raise IncorrectMergeException(
225 "can't merge podcast state into itself")
227 if state.user != state2.user:
228 raise IncorrectMergeException(
229 "states don't belong to the same user")
231 self.state = state
232 self.state2 = state2
233 self.actions = actions
235 def merge(self):
236 self._do_merge(state=self.state, state2=self.state2)
237 self._add_actions(state=self.state, actions=self.state2.actions)
238 self._delete(state2=self.state2)
239 self.actions['merged-podcast-state'] += 1
241 @repeat_on_conflict(['state'])
242 def _do_merge(self, state, state2):
244 # overwrite settings in state2 with state's settings
245 settings = state2.settings
246 settings.update(state.settings)
247 state.settings = settings
249 state.disabled_devices = set_filter(None, state.disabled_devices,
250 state2.disabled_devices)
252 state.merged_ids = set_filter(state._id, state.merged_ids,
253 [state2._id], state2.merged_ids)
255 state.tags = set_filter(None, state.tags, state2.tags)
257 state.save()
259 @repeat_on_conflict(['state'])
260 def _add_actions(self, state, actions):
261 try:
262 state.add_actions(actions)
263 state.save()
264 except restkit.Unauthorized:
265 # the merge could result in an invalid list of
266 # subscribe/unsubscribe actions -- we ignore it and
267 # just use the actions from state
268 return
270 @repeat_on_conflict(['state2'])
271 def _delete(self, state2):
272 state2.delete()
275 class EpisodeStateMerger(object):
276 """ Merges state2 in state """
278 def __init__(self, state, state2, actions):
280 if state._id == state2._id:
281 raise IncorrectMergeException(
282 "can't merge episode state into itself")
284 if state.user != state2.user:
285 raise IncorrectMergeException(
286 "states don't belong to the same user")
288 self.state = state
289 self.state2 = state2
290 self.actions = actions
292 def merge(self):
293 self._merge_obj(state=self.state, state2=self.state2)
294 delete_podcast_state(self.state2)
295 self.actions['merge-episode-state'] += 1
297 @repeat_on_conflict(['state'])
298 def _merge_obj(self, state, state2):
299 state.add_actions(state2.actions)
301 # overwrite settings in state2 with state's settings
302 settings = state2.settings
303 settings.update(state.settings)
304 state.settings = settings
306 merged_ids = set(state.merged_ids + [state2._id] + state2.merged_ids)
307 state.merged_ids = filter(None, merged_ids)
309 state.chapters = list(set(state.chapters + state2.chapters))
311 state.save()
314 def set_filter(orig, *args):
315 """ chain args, and remove falsy values and orig """
316 s = set(chain.from_iterable(args))
317 s = s - set([orig])
318 s = filter(None, s)
319 return s