1 from itertools
import chain
, imap
as map
3 from functools
import partial
7 from django
.db
import IntegrityError
9 from mygpo
.podcasts
.models
import MergedUUID
10 from mygpo
import utils
11 from mygpo
.decorators
import repeat_on_conflict
12 from mygpo
.db
.couchdb
.podcast_state
import all_podcast_states
, \
13 delete_podcast_state
, update_podcast_state_podcast
, merge_podcast_states
14 from mygpo
.db
.couchdb
.episode_state
import all_episode_states
, \
15 update_episode_state_object
, add_episode_actions
, delete_episode_state
, \
19 logger
= logging
.getLogger(__name__
)
22 class IncorrectMergeException(Exception):
26 class PodcastMerger(object):
27 """ Merges podcasts and their related objects """
29 def __init__(self
, podcasts
, actions
, groups
):
30 """ Prepares to merge podcasts[1:] into podcasts[0] """
32 for n
, podcast1
in enumerate(podcasts
):
33 for m
, podcast2
in enumerate(podcasts
):
34 if podcast1
== podcast2
and n
!= m
:
35 raise IncorrectMergeException(
36 "can't merge podcast %s into itself %s" % (podcast1
.get_id(), podcast2
.get_id()))
38 self
.podcasts
= podcasts
39 self
.actions
= actions
43 """ Carries out the actual merging """
45 logger
.info('Start merging of podcasts: %r', self
.podcasts
)
47 podcast1
= self
.podcasts
.pop(0)
48 logger
.info('Merge target: %r', podcast1
)
52 for podcast2
in self
.podcasts
:
53 logger
.info('Merging %r into target', podcast2
)
54 self
.merge_states(podcast1
, podcast2
)
55 self
.reassign_episodes(podcast1
, podcast2
)
56 self
._merge
_objs
(podcast1
=podcast1
, podcast2
=podcast2
)
57 logger
.info('Deleting %r', podcast2
)
59 self
.actions
['merge-podcast'] += 1
63 def merge_episodes(self
):
64 """ Merges the episodes according to the groups """
66 for n
, episodes
in self
.groups
:
70 episode
= episodes
.pop(0)
72 em
= EpisodeMerger(episode
, ep
, self
.actions
)
75 def _merge_objs(self
, podcast1
, podcast2
):
76 reassign_merged_uuids(podcast1
, podcast2
)
77 reassign_slugs(podcast1
, podcast2
)
78 reassign_urls(podcast1
, podcast2
)
79 podcast1
.content_types
= ','.join(podcast1
.content_types
.split(',') +
80 podcast2
.content_types
.split(','))
84 def reassign_episodes(self
, podcast1
, podcast2
):
86 logger
.info('Re-assigning episodes of %r into %r', podcast2
, podcast1
)
88 # re-assign episodes to new podcast
89 # if necessary, they will be merged later anyway
90 for e
in podcast2
.episode_set
.all():
91 self
.actions
['reassign-episode'] += 1
93 for s
in all_episode_states(e
):
94 self
.actions
['reassign-episode-state'] += 1
96 update_episode_state_object(s
, podcast1
.get_id())
98 # TODO: change scopes?
102 def merge_states(self
, podcast1
, podcast2
):
103 """Merges the Podcast states that are associated with the two Podcasts.
105 This should be done after two podcasts are merged
108 key
= lambda x
: x
.user
109 states1
= sorted(all_podcast_states(podcast1
), key
=key
)
110 states2
= sorted(all_podcast_states(podcast2
), key
=key
)
112 logger
.info('Merging %d podcast states of %r into %r', len(states2
),
115 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
121 self
.actions
['move-podcast-state'] += 1
122 update_podcast_state_podcast(state2
, podcast1
.get_id(),
129 psm
= PodcastStateMerger(state
, state2
, self
.actions
)
133 class EpisodeMerger(object):
134 """ Merges two episodes """
136 def __init__(self
, episode1
, episode2
, actions
):
137 """ episode2 will be merged into episode1 """
139 if episode1
== episode2
:
140 raise IncorrectMergeException("can't merge episode into itself")
142 self
.episode1
= episode1
143 self
.episode2
= episode2
144 self
.actions
= actions
147 logger
.info('Merging episode %r into %r', self
.episode2
, self
.episode1
)
148 self
._merge
_objs
(episode1
=self
.episode1
, episode2
=self
.episode2
)
149 self
.merge_states(self
.episode1
, self
.episode2
)
150 logger
.info('Deleting %r', self
.episode2
)
151 self
.episode2
.delete()
152 self
.actions
['merge-episode'] += 1
154 def _merge_objs(self
, episode1
, episode2
):
155 reassign_urls(episode1
, episode2
)
156 reassign_merged_uuids(episode1
, episode2
)
157 reassign_slugs(episode1
, episode2
)
160 def merge_states(self
, episode
, episode2
):
161 key
= lambda x
: x
.user
162 states1
= sorted(all_episode_states(self
.episode1
), key
=key
)
163 states2
= sorted(all_episode_states(self
.episode2
), key
=key
)
165 logger
.info('Merging %d episode states of %r into %r', len(states2
),
168 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
173 self
.actions
['move-episode-state'] += 1
174 update_episode_state_object(state2
,
175 self
.episode1
.podcast
.get_id(),
176 self
.episode1
.get_id())
182 esm
= EpisodeStateMerger(state
, state2
, self
.actions
)
186 class PodcastStateMerger(object):
187 """Merges the two given podcast states"""
189 def __init__(self
, state
, state2
, actions
):
191 if state
._id
== state2
._id
:
192 raise IncorrectMergeException(
193 "can't merge podcast state into itself")
195 if state
.user
!= state2
.user
:
196 raise IncorrectMergeException(
197 "states don't belong to the same user")
201 self
.actions
= actions
204 merge_podcast_states(self
.state
, self
.state2
)
205 self
._add
_actions
(state
=self
.state
, actions
=self
.state2
.actions
)
206 delete_podcast_state(self
.state2
)
207 self
.actions
['merged-podcast-state'] += 1
210 def _add_actions(self
, state
, actions
):
212 add_episode_actions(state
, actions
)
213 except restkit
.Unauthorized
:
214 # the merge could result in an invalid list of
215 # subscribe/unsubscribe actions -- we ignore it and
216 # just use the actions from state
220 class EpisodeStateMerger(object):
221 """ Merges state2 in state """
223 def __init__(self
, state
, state2
, actions
):
225 if state
._id
== state2
._id
:
226 raise IncorrectMergeException(
227 "can't merge episode state into itself")
229 if state
.user
!= state2
.user
:
230 raise IncorrectMergeException(
231 "states don't belong to the same user")
235 self
.actions
= actions
238 merge_episode_states(self
.state
, self
.state2
)
239 delete_episode_state(self
.state2
)
240 self
.actions
['merge-episode-state'] += 1
243 def reassign_urls(obj1
, obj2
):
244 # Reassign all URLs of obj2 to obj1
245 max_order
= max([0] + [u
.order
for u
in obj1
.urls
.all()])
247 for n
, url
in enumerate(obj2
.urls
.all(), max_order
+1):
248 url
.content_object
= obj1
250 url
.scope
= obj1
.scope
253 except IntegrityError
as ie
:
254 logger
.warn('Moving URL failed: %s. Deleting.', str(ie
))
257 def reassign_merged_uuids(obj1
, obj2
):
258 # Reassign all IDs of obj2 to obj1
259 MergedUUID
.objects
.create(uuid
=obj2
.id, content_object
=obj1
)
260 for m
in obj2
.merged_uuids
.all():
261 m
.content_object
= obj1
264 def reassign_slugs(obj1
, obj2
):
265 # Reassign all Slugs of obj2 to obj1
266 max_order
= max([0] + [s
.order
for s
in obj1
.slugs
.all()])
267 for n
, slug
in enumerate(obj2
.slugs
.all(), max_order
+1):
268 slug
.content_object
= obj1
270 slug
.scope
= obj1
.scope
273 except IntegrityError
as ie
:
274 logger
.warn('Moving Slug failed: %s. Deleting', str(ie
))