1 from itertools
import chain
, imap
as map
3 from functools
import partial
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
, \
13 from mygpo
.db
.couchdb
.episode_state
import all_episode_states
16 logger
= logging
.getLogger(__name__
)
19 class IncorrectMergeException(Exception):
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
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
)
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
59 def merge_episodes(self
):
60 """ Merges the episodes according to the groups """
62 for n
, episodes
in self
.groups
:
67 episode
= episodes
.pop(0)
70 em
= EpisodeMerger(episode
, ep
, self
.actions
)
73 @repeat_on_conflict(['podcast1', 'podcast2'])
74 def _merge_objs(self
, podcast1
, podcast2
):
76 podcast1
.merged_ids
= set_filter(podcast1
.get_id(),
81 podcast1
.merged_slugs
= set_filter(podcast1
.slug
,
82 podcast1
.merged_slugs
,
84 podcast2
.merged_slugs
)
86 podcast1
.merged_oldids
= set_filter(podcast1
.oldid
,
87 podcast1
.merged_oldids
,
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
)
103 @repeat_on_conflict(['s'])
104 def _save_state(self
, s
, podcast1
):
105 s
.podcast
= podcast1
.get_id()
108 @repeat_on_conflict(['e'])
109 def _save_episode(self
, e
, podcast1
):
110 e
.podcast
= podcast1
.get_id()
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
),
142 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
148 self
.actions
['move-podcast-state'] += 1
149 self
._move
_state
(state2
=state2
, new_id
=podcast1
.get_id(),
150 new_url
=podcast1
.url
)
156 psm
= PodcastStateMerger(state
, state2
, self
.actions
)
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
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
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
,
196 episode2
.merged_slugs
)
200 @repeat_on_conflict(['e'])
201 def _delete(self
, e
):
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
),
213 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
219 self
.actions
['move-episode-state'] += 1
220 self
._move
(state2
=state2
, podcast_id
=self
.episode1
.podcast
,
221 episode_id
=self
.episode1
._id
)
227 esm
= EpisodeStateMerger(state
, state2
, self
.actions
)
230 @repeat_on_conflict(['state2'])
231 def _move(self
, state2
, podcast_id
, episode_id
):
232 state2
.podcast
= podcast_id
233 state2
.episode
= episode_id
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")
252 self
.actions
= actions
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
)
278 @repeat_on_conflict(['state'])
279 def _add_actions(self
, state
, actions
):
281 state
.add_actions(actions
)
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
289 @repeat_on_conflict(['state2'])
290 def _delete(self
, state2
):
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")
309 self
.actions
= actions
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
))
333 def set_filter(orig
, *args
):
334 """ chain args, and remove falsy values and orig """
335 s
= set(chain
.from_iterable(args
))