fix typo
[mygpo.git] / mygpo / maintenance / merge.py
blob6cc189a3dcac011760a88d7e2bb1681c1eb22b74
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, reload_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, update_podcast_state_podcast, merge_podcast_states
13 from mygpo.db.couchdb.episode_state import all_episode_states, \
14 update_episode_state_object, add_episode_actions, delete_episode_state, \
15 merge_episode_states
17 import logging
18 logger = logging.getLogger(__name__)
21 class IncorrectMergeException(Exception):
22 pass
25 class PodcastMerger(object):
26 """ Merges podcasts and their related objects """
28 def __init__(self, podcasts, actions, groups):
29 """ Prepares to merge podcasts[1:] into podcasts[0] """
31 for n, podcast1 in enumerate(podcasts):
32 for m, podcast2 in enumerate(podcasts):
33 if podcast1 == podcast2 and n != m:
34 raise IncorrectMergeException(
35 "can't merge podcast %s into itself %s" % (podcast1.get_id(), podcast2.get_id()))
37 self.podcasts = podcasts
38 self.actions = actions
39 self.groups = groups
41 def merge(self):
42 """ Carries out the actual merging """
44 logger.info('Start merging of podcasts: %s', self.podcasts)
46 podcast1 = self.podcasts.pop(0)
47 logger.info('Merge target: %s', podcast1)
49 self.merge_episodes()
51 for podcast2 in self.podcasts:
52 logger.info('Merging %s into target', podcast2)
53 self.merge_states(podcast1, podcast2)
54 self.reassign_episodes(podcast1, podcast2)
55 self._merge_objs(podcast1=podcast1, podcast2=podcast2)
56 delete_podcast(podcast2)
57 self.actions['merge-podcast'] += 1
59 return podcast1
61 def merge_episodes(self):
62 """ Merges the episodes according to the groups """
64 for n, episodes in self.groups:
66 if not episodes:
67 continue
69 episode = episodes.pop(0)
71 for ep in episodes:
72 em = EpisodeMerger(episode, ep, self.actions)
73 em.merge()
75 @repeat_on_conflict(['podcast1', 'podcast2'], reload_f=reload_podcast)
76 def _merge_objs(self, podcast1, podcast2):
78 podcast1.merged_ids = set_filter(podcast1.get_id(),
79 podcast1.merged_ids,
80 [podcast2.get_id()],
81 podcast2.merged_ids)
83 podcast1.merged_slugs = set_filter(podcast1.slug,
84 podcast1.merged_slugs,
85 [podcast2.slug],
86 podcast2.merged_slugs)
88 podcast1.merged_oldids = set_filter(podcast1.oldid,
89 podcast1.merged_oldids,
90 [podcast2.oldid],
91 podcast2.merged_oldids)
93 # the first URL in the list represents the podcast main URL
94 main_url = podcast1.url
95 podcast1.urls = set_filter(None, podcast1.urls, podcast2.urls)
96 # so we insert it as the first again
97 podcast1.urls.remove(main_url)
98 podcast1.urls.insert(0, main_url)
100 podcast1.content_types = set_filter(None, podcast1.content_types,
101 podcast2.content_types)
103 podcast1.save()
105 @repeat_on_conflict(['e'])
106 def _save_episode(self, e, podcast1):
107 e.podcast = podcast1.get_id()
108 e.save()
110 def reassign_episodes(self, podcast1, podcast2):
112 logger.info('Re-assigning episodes of %s into %s', podcast2, podcast1)
114 # re-assign episodes to new podcast
115 # if necessary, they will be merged later anyway
116 for e in episodes_for_podcast_uncached(podcast2):
117 self.actions['reassign-episode'] += 1
119 for s in all_episode_states(e):
120 self.actions['reassign-episode-state'] += 1
122 update_episode_state_object(s, podcast1.get_id())
124 self._save_episode(e=e, podcast1=podcast1)
126 def merge_states(self, podcast1, podcast2):
127 """Merges the Podcast states that are associated with the two Podcasts.
129 This should be done after two podcasts are merged
132 key = lambda x: x.user
133 states1 = sorted(all_podcast_states(podcast1), key=key)
134 states2 = sorted(all_podcast_states(podcast2), key=key)
136 logger.info('Merging %d podcast states of %s into %s', len(states2),
137 podcast2, podcast1)
139 for state, state2 in utils.iterate_together([states1, states2], key):
141 if state == state2:
142 continue
144 if state is None:
145 self.actions['move-podcast-state'] += 1
146 update_podcast_state_podcast(state2, podcast1.get_id(),
147 podcast1.url)
149 elif state2 is None:
150 continue
152 else:
153 psm = PodcastStateMerger(state, state2, self.actions)
154 psm.merge()
157 class EpisodeMerger(object):
159 def __init__(self, episode1, episode2, actions):
160 if episode1 == episode2:
161 raise IncorrectMergeException("can't merge episode into itself")
163 self.episode1 = episode1
164 self.episode2 = episode2
165 self.actions = actions
167 def merge(self):
169 logger.info('Merging episode %s into %s', self.episode2, self.episode1)
171 self._merge_objs(episode1=self.episode1, episode2=self.episode2)
172 self.merge_states(self.episode1, self.episode2)
173 self._delete(e=self.episode2)
174 self.actions['merge-episode'] += 1
176 @repeat_on_conflict(['episode1'])
177 def _merge_objs(self, episode1, episode2):
179 episode1.urls = set_filter(None, episode1.urls, episode2.urls)
181 episode1.merged_ids = set_filter(episode1._id, episode1.merged_ids,
182 [episode2._id], episode2.merged_ids)
184 episode1.merged_slugs = set_filter(episode1.slug,
185 episode1.merged_slugs,
186 [episode2.slug],
187 episode2.merged_slugs)
189 episode1.save()
191 @repeat_on_conflict(['e'])
192 def _delete(self, e):
193 e.delete()
195 def merge_states(self, episode, episode2):
197 key = lambda x: x.user
198 states1 = sorted(all_episode_states(self.episode1), key=key)
199 states2 = sorted(all_episode_states(self.episode2), key=key)
201 logger.info('Merging %d episode states of %s into %s', len(states2),
202 episode2, episode)
204 for state, state2 in utils.iterate_together([states1, states2], key):
206 if state == state2:
207 continue
209 if state is None:
210 self.actions['move-episode-state'] += 1
211 update_episode_state_object(state2, self.episode1.podcast,
212 self.episode1._id)
214 elif state2 is None:
215 continue
217 else:
218 esm = EpisodeStateMerger(state, state2, self.actions)
219 esm.merge()
222 class PodcastStateMerger(object):
223 """Merges the two given podcast states"""
225 def __init__(self, state, state2, actions):
227 if state._id == state2._id:
228 raise IncorrectMergeException(
229 "can't merge podcast state into itself")
231 if state.user != state2.user:
232 raise IncorrectMergeException(
233 "states don't belong to the same user")
235 self.state = state
236 self.state2 = state2
237 self.actions = actions
239 def merge(self):
240 merge_podcast_states(self.state, self.state2)
241 self._add_actions(state=self.state, actions=self.state2.actions)
242 delete_podcast_state(self.state2)
243 self.actions['merged-podcast-state'] += 1
246 def _add_actions(self, state, actions):
247 try:
248 add_episode_actions(state, actions)
249 except restkit.Unauthorized:
250 # the merge could result in an invalid list of
251 # subscribe/unsubscribe actions -- we ignore it and
252 # just use the actions from state
253 return
256 class EpisodeStateMerger(object):
257 """ Merges state2 in state """
259 def __init__(self, state, state2, actions):
261 if state._id == state2._id:
262 raise IncorrectMergeException(
263 "can't merge episode state into itself")
265 if state.user != state2.user:
266 raise IncorrectMergeException(
267 "states don't belong to the same user")
269 self.state = state
270 self.state2 = state2
271 self.actions = actions
273 def merge(self):
274 merge_episode_states(self.state, self.state2)
275 delete_episode_state(self.state2)
276 self.actions['merge-episode-state'] += 1
279 def set_filter(orig, *args):
280 """ chain args, and remove falsy values and orig """
281 s = set(chain.from_iterable(args))
282 s = s - set([orig])
283 s = filter(None, s)
284 return s