add logging to podcast and episode merging
[mygpo.git] / mygpo / maintenance / merge.py
blob4964181b39126c1bd9009dae2f91466b2b1932e5
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
15 import logging
16 logger = logging.getLogger(__name__)
19 class IncorrectMergeException(Exception):
20 pass
23 class PodcastMerger(object):
24 """ Merges podcasts and their related objects """
26 def __init__(self, podcasts, actions, groups):
27 """ Prepares to merge podcasts[1:] into podcasts[0] """
29 for n, podcast1 in enumerate(podcasts):
30 for m, podcast2 in enumerate(podcasts):
31 if podcast1 == podcast2 and n != m:
32 raise IncorrectMergeException(
33 "can't merge podcast into itself")
35 self.podcasts = podcasts
36 self.actions = actions
37 self.groups = groups
39 def merge(self):
40 """ Carries out the actual merging """
42 logger.info('Start merging of podcasts: %s', self.podcasts)
44 podcast1 = self.podcasts.pop(0)
45 logger.info('Merge target: %s', podcast1)
47 self.merge_episodes()
49 for podcast2 in self.podcasts:
50 logger.info('Merging %s into target', podcast2)
51 self.merge_states(podcast1, podcast2)
52 self.reassign_episodes(podcast1, podcast2)
53 self._merge_objs(podcast1=podcast1, podcast2=podcast2)
54 delete_podcast(podcast2)
55 self.actions['merge-podcast'] += 1
57 return podcast1
59 def merge_episodes(self):
60 """ Merges the episodes according to the groups """
62 for n, episodes in self.groups:
64 if not episodes:
65 continue
67 episode = episodes.pop(0)
69 for ep in episodes:
70 em = EpisodeMerger(episode, ep, self.actions)
71 em.merge()
73 @repeat_on_conflict(['podcast1', 'podcast2'])
74 def _merge_objs(self, podcast1, podcast2):
76 podcast1.merged_ids = set_filter(podcast1.get_id(),
77 podcast1.merged_ids,
78 [podcast2.get_id()],
79 podcast2.merged_ids)
81 podcast1.merged_slugs = set_filter(podcast1.slug,
82 podcast1.merged_slugs,
83 [podcast2.slug],
84 podcast2.merged_slugs)
86 podcast1.merged_oldids = set_filter(podcast1.oldid,
87 podcast1.merged_oldids,
88 [podcast2.oldid],
89 podcast2.merged_oldids)
91 # the first URL in the list represents the podcast main URL
92 main_url = podcast1.url
93 podcast1.urls = set_filter(None, podcast1.urls, podcast2.urls)
94 # so we insert it as the first again
95 podcast1.urls.remove(main_url)
96 podcast1.urls.insert(0, main_url)
98 podcast1.content_types = set_filter(None, podcast1.content_types,
99 podcast2.content_types)
101 podcast1.save()
103 @repeat_on_conflict(['s'])
104 def _save_state(self, s, podcast1):
105 s.podcast = podcast1.get_id()
106 s.save()
108 @repeat_on_conflict(['e'])
109 def _save_episode(self, e, podcast1):
110 e.podcast = podcast1.get_id()
111 e.save()
113 def reassign_episodes(self, podcast1, podcast2):
115 logger.info('Re-assigning episodes of %s into %s', podcast2, podcast1)
117 # re-assign episodes to new podcast
118 # if necessary, they will be merged later anyway
119 for e in episodes_for_podcast_uncached(podcast2):
120 self.actions['reassign-episode'] += 1
122 for s in all_episode_states(e):
123 self.actions['reassign-episode-state'] += 1
125 self._save_state(s=s, podcast1=podcast1)
127 self._save_episode(e=e, podcast1=podcast1)
129 def merge_states(self, podcast1, podcast2):
130 """Merges the Podcast states that are associated with the two Podcasts.
132 This should be done after two podcasts are merged
135 key = lambda x: x.user
136 states1 = sorted(all_podcast_states(podcast1), key=key)
137 states2 = sorted(all_podcast_states(podcast2), key=key)
139 logger.info('Merging %d podcast states of %s into %s', len(states2),
140 podcast2, podcast1)
142 for state, state2 in utils.iterate_together([states1, states2], key):
144 if state == state2:
145 continue
147 if state is None:
148 self.actions['move-podcast-state'] += 1
149 self._move_state(state2=state2, new_id=podcast1.get_id(),
150 new_url=podcast1.url)
152 elif state2 is None:
153 continue
155 else:
156 psm = PodcastStateMerger(state, state2, self.actions)
157 psm.merge()
159 @repeat_on_conflict(['state2'])
160 def _move_state(self, state2, new_id, new_url):
161 state2.ref_url = new_url
162 state2.podcast = new_id
163 state2.save()
166 class EpisodeMerger(object):
168 def __init__(self, episode1, episode2, actions):
169 if episode1 == episode2:
170 raise IncorrectMergeException("can't merge episode into itself")
172 self.episode1 = episode1
173 self.episode2 = episode2
174 self.actions = actions
176 def merge(self):
178 logger.info('Merging episode %s into %s', self.episode2, self.episode1)
180 self._merge_objs(episode1=self.episode1, episode2=self.episode2)
181 self.merge_states(self.episode1, self.episode2)
182 self._delete(e=self.episode2)
183 self.actions['merge-episode'] += 1
185 @repeat_on_conflict(['episode1'])
186 def _merge_objs(self, episode1, episode2):
188 episode1.urls = set_filter(None, episode1.urls, episode2.urls)
190 episode1.merged_ids = set_filter(episode1._id, episode1.merged_ids,
191 [episode2._id], episode2.merged_ids)
193 episode1.merged_slugs = set_filter(episode1.slug,
194 episode1.merged_slugs,
195 [episode2.slug],
196 episode2.merged_slugs)
198 episode1.save()
200 @repeat_on_conflict(['e'])
201 def _delete(self, e):
202 e.delete()
204 def merge_states(self, episode, episode2):
206 key = lambda x: x.user
207 states1 = sorted(all_episode_states(self.episode1), key=key)
208 states2 = sorted(all_episode_states(self.episode2), key=key)
210 logger.info('Merging %d episode states of %s into %s', len(states2),
211 episode2, episode)
213 for state, state2 in utils.iterate_together([states1, states2], key):
215 if state == state2:
216 continue
218 if state is None:
219 self.actions['move-episode-state'] += 1
220 self._move(state2=state2, podcast_id=self.episode1.podcast,
221 episode_id=self.episode1._id)
223 elif state2 is None:
224 continue
226 else:
227 esm = EpisodeStateMerger(state, state2, self.actions)
228 esm.merge()
230 @repeat_on_conflict(['state2'])
231 def _move(self, state2, podcast_id, episode_id):
232 state2.podcast = podcast_id
233 state2.episode = episode_id
234 state2.save()
237 class PodcastStateMerger(object):
238 """Merges the two given podcast states"""
240 def __init__(self, state, state2, actions):
242 if state._id == state2._id:
243 raise IncorrectMergeException(
244 "can't merge podcast state into itself")
246 if state.user != state2.user:
247 raise IncorrectMergeException(
248 "states don't belong to the same user")
250 self.state = state
251 self.state2 = state2
252 self.actions = actions
254 def merge(self):
255 self._do_merge(state=self.state, state2=self.state2)
256 self._add_actions(state=self.state, actions=self.state2.actions)
257 self._delete(state2=self.state2)
258 self.actions['merged-podcast-state'] += 1
260 @repeat_on_conflict(['state'])
261 def _do_merge(self, state, state2):
263 # overwrite settings in state2 with state's settings
264 settings = state2.settings
265 settings.update(state.settings)
266 state.settings = settings
268 state.disabled_devices = set_filter(None, state.disabled_devices,
269 state2.disabled_devices)
271 state.merged_ids = set_filter(state._id, state.merged_ids,
272 [state2._id], state2.merged_ids)
274 state.tags = set_filter(None, state.tags, state2.tags)
276 state.save()
278 @repeat_on_conflict(['state'])
279 def _add_actions(self, state, actions):
280 try:
281 state.add_actions(actions)
282 state.save()
283 except restkit.Unauthorized:
284 # the merge could result in an invalid list of
285 # subscribe/unsubscribe actions -- we ignore it and
286 # just use the actions from state
287 return
289 @repeat_on_conflict(['state2'])
290 def _delete(self, state2):
291 state2.delete()
294 class EpisodeStateMerger(object):
295 """ Merges state2 in state """
297 def __init__(self, state, state2, actions):
299 if state._id == state2._id:
300 raise IncorrectMergeException(
301 "can't merge episode state into itself")
303 if state.user != state2.user:
304 raise IncorrectMergeException(
305 "states don't belong to the same user")
307 self.state = state
308 self.state2 = state2
309 self.actions = actions
311 def merge(self):
312 self._merge_obj(state=self.state, state2=self.state2)
313 delete_podcast_state(self.state2)
314 self.actions['merge-episode-state'] += 1
316 @repeat_on_conflict(['state'])
317 def _merge_obj(self, state, state2):
318 state.add_actions(state2.actions)
320 # overwrite settings in state2 with state's settings
321 settings = state2.settings
322 settings.update(state.settings)
323 state.settings = settings
325 merged_ids = set(state.merged_ids + [state2._id] + state2.merged_ids)
326 state.merged_ids = filter(None, merged_ids)
328 state.chapters = list(set(state.chapters + state2.chapters))
330 state.save()
333 def set_filter(orig, *args):
334 """ chain args, and remove falsy values and orig """
335 s = set(chain.from_iterable(args))
336 s = s - set([orig])
337 s = filter(None, s)
338 return s