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
, 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
, \
18 logger
= logging
.getLogger(__name__
)
21 class IncorrectMergeException(Exception):
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
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
)
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
61 def merge_episodes(self
):
62 """ Merges the episodes according to the groups """
64 for n
, episodes
in self
.groups
:
69 episode
= episodes
.pop(0)
72 em
= EpisodeMerger(episode
, ep
, self
.actions
)
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(),
83 podcast1
.merged_slugs
= set_filter(podcast1
.slug
,
84 podcast1
.merged_slugs
,
86 podcast2
.merged_slugs
)
88 podcast1
.merged_oldids
= set_filter(podcast1
.oldid
,
89 podcast1
.merged_oldids
,
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
)
105 @repeat_on_conflict(['e'])
106 def _save_episode(self
, e
, podcast1
):
107 e
.podcast
= podcast1
.get_id()
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
),
139 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
145 self
.actions
['move-podcast-state'] += 1
146 update_podcast_state_podcast(state2
, podcast1
.get_id(),
153 psm
= PodcastStateMerger(state
, state2
, self
.actions
)
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
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
,
187 episode2
.merged_slugs
)
191 @repeat_on_conflict(['e'])
192 def _delete(self
, e
):
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
),
204 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
210 self
.actions
['move-episode-state'] += 1
211 update_episode_state_object(state2
, self
.episode1
.podcast
,
218 esm
= EpisodeStateMerger(state
, state2
, self
.actions
)
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")
237 self
.actions
= actions
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
):
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
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")
271 self
.actions
= actions
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
))