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
.episode
import episodes_for_podcast
10 from mygpo
.db
.couchdb
.podcast_state
import all_podcast_states
11 from mygpo
.db
.couchdb
.episode_state
import all_episode_states
14 class IncorrectMergeException(Exception):
18 class PodcastMerger(object):
19 """ Merges podcast2 into podcast
21 Also merges the related podcast states, and re-assignes podcast2's episodes
22 to podcast, but does neither merge their episodes nor their episode states
26 def __init__(self
, podcasts
, actions
, groups
):
28 for n
, podcast1
in enumerate(podcasts
):
29 for m
, podcast2
in enumerate(podcasts
):
30 if podcast1
== podcast2
and n
!= m
:
31 raise IncorrectMergeException("can't merge podcast into itself")
33 self
.podcasts
= podcasts
34 self
.actions
= actions
39 podcast1
= self
.podcasts
.pop(0)
41 for podcast2
in self
.podcasts
:
42 self
._merge
_objs
(podcast1
=podcast1
, podcast2
=podcast2
)
43 self
.merge_states(podcast1
, podcast2
)
45 self
.reassign_episodes(podcast1
, podcast2
)
46 self
._delete
(podcast2
=podcast2
)
48 self
.actions
['merge-podcast'] += 1
51 def merge_episodes(self
):
52 for n
, episodes
in self
.groups
:
54 episode
= episodes
.pop(0)
58 em
= EpisodeMerger(episode
, ep
, self
.actions
)
62 @repeat_on_conflict(['podcast1', 'podcast2'])
63 def _merge_objs(self
, podcast1
, podcast2
):
65 podcast1
.merged_ids
= set_filter(podcast1
.get_id(),
66 podcast1
.merged_ids
, [podcast2
.get_id()], podcast2
.merged_ids
)
68 podcast1
.merged_slugs
= set_filter(podcast1
.slug
,
69 podcast1
.merged_slugs
, [podcast2
.slug
], podcast2
.merged_slugs
)
71 podcast1
.merged_oldids
= set_filter(podcast1
.oldid
,
72 podcast1
.merged_oldids
, [podcast2
.oldid
],
73 podcast2
.merged_oldids
)
75 # the first URL in the list represents the podcast main URL
76 main_url
= podcast1
.url
77 podcast1
.urls
= set_filter(None, podcast1
.urls
, podcast2
.urls
)
78 # so we insert it as the first again
79 podcast1
.urls
.remove(main_url
)
80 podcast1
.urls
.insert(0, main_url
)
82 # we ignore related_podcasts because
83 # * the elements should be roughly the same
84 # * element order is important but could not preserved exactly
86 podcast1
.content_types
= set_filter(None, podcast1
.content_types
,
87 podcast2
.content_types
)
89 key
= lambda x
: x
.timestamp
90 for a
, b
in utils
.iterate_together(
91 [podcast1
.subscribers
, podcast2
.subscribers
], key
):
93 if a
is None or b
is None:
96 # avoid increasing subscriber_count when merging
97 # duplicate entries of a single podcast
98 if a
.subscriber_count
== b
.subscriber_count
:
101 a
.subscriber_count
+= b
.subscriber_count
103 for src
, tags
in podcast2
.tags
.items():
104 podcast1
.tags
[src
] = set_filter(None, podcast1
.tags
.get(src
, []),
110 @repeat_on_conflict(['podcast2'])
111 def _delete(self
, podcast2
):
115 @repeat_on_conflict(['s'])
116 def _save_state(self
, s
, podcast1
):
117 s
.podcast
= podcast1
.get_id()
121 @repeat_on_conflict(['e'])
122 def _save_episode(self
, e
, podcast1
):
123 e
.podcast
= podcast1
.get_id()
128 def reassign_episodes(self
, podcast1
, podcast2
):
129 # re-assign episodes to new podcast
130 # if necessary, they will be merged later anyway
131 for e
in episodes_for_podcast(podcast2
):
132 self
.actions
['reassign-episode'] += 1
134 for s
in all_episode_states(e
):
135 self
.actions
['reassign-episode-state'] += 1
137 self
._save
_state
(s
=s
, podcast1
=podcast1
)
139 self
._save
_episode
(e
=e
, podcast1
=podcast1
)
142 def merge_states(self
, podcast1
, podcast2
):
143 """Merges the Podcast states that are associated with the two Podcasts.
145 This should be done after two podcasts are merged
148 key
= lambda x
: x
.user
149 states1
= sorted(all_podcast_states(podcast1
), key
=key
)
150 states2
= sorted(all_podcast_states(podcast2
), key
=key
)
152 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
158 self
.actions
['move-podcast-state'] += 1
159 self
._move
_state
(state2
=state2
, new_id
=podcast1
.get_id(),
160 new_url
=podcast1
.url
)
166 psm
= PodcastStateMerger(state
, state2
, self
.actions
)
170 @repeat_on_conflict(['state2'])
171 def _move_state(self
, state2
, new_id
, new_url
):
172 state2
.ref_url
= new_url
173 state2
.podcast
= new_id
176 @repeat_on_conflict(['state2'])
177 def _delete_state(state2
):
182 class EpisodeMerger(object):
185 def __init__(self
, episode1
, episode2
, actions
):
186 if episode1
== episode2
:
187 raise IncorrectMergeException("can't merge episode into itself")
189 self
.episode1
= episode1
190 self
.episode2
= episode2
191 self
.actions
= actions
195 self
._merge
_objs
(episode1
=self
.episode1
, episode2
=self
.episode2
)
196 self
.merge_states(self
.episode1
, self
.episode2
)
197 self
._delete
(e
=self
.episode2
)
198 self
.actions
['merge-episode'] += 1
201 @repeat_on_conflict(['episode1'])
202 def _merge_objs(self
, episode1
, episode2
):
204 episode1
.urls
= set_filter(None, episode1
.urls
, episode2
.urls
)
206 episode1
.merged_ids
= set_filter(episode1
._id
, episode1
.merged_ids
,
207 [episode2
._id
], episode2
.merged_ids
)
209 episode1
.merged_slugs
= set_filter(episode1
.slug
,
210 episode1
.merged_slugs
, [episode2
.slug
], episode2
.merged_slugs
)
215 @repeat_on_conflict(['e'])
216 def _delete(self
, e
):
220 def merge_states(self
, episode
, episode2
):
222 key
= lambda x
: x
.user
223 states1
= sorted(all_episode_states(self
.episode1
), key
=key
)
224 states2
= sorted(all_episode_states(self
.episode2
), key
=key
)
226 for state
, state2
in utils
.iterate_together([states1
, states2
], key
):
232 self
.actions
['move-episode-state'] += 1
233 self
._move
(state2
=state2
, podcast_id
=self
.episode1
.podcast
,
234 episode_id
=self
.episode1
._id
)
240 esm
= EpisodeStateMerger(state
, state2
, self
.actions
)
244 @repeat_on_conflict(['state2'])
245 def _move(self
, state2
, podcast_id
, episode_id
):
246 state2
.podcast
= podcast_id
247 state2
.episode
= episode_id
255 class PodcastStateMerger(object):
256 """Merges the two given podcast states"""
258 def __init__(self
, state
, state2
, actions
):
260 if state
._id
== state2
._id
:
261 raise IncorrectMergeException("can't merge podcast state into itself")
263 if state
.user
!= state2
.user
:
264 raise IncorrectMergeException("states don't belong to the same user")
268 self
.actions
= actions
272 self
._do
_merge
(state
=self
.state
, state2
=self
.state2
)
273 self
._add
_actions
(state
=self
.state
, actions
=self
.state2
.actions
)
274 self
._delete
(state2
=self
.state2
)
275 self
.actions
['merged-podcast-state'] += 1
278 @repeat_on_conflict(['state'])
279 def _do_merge(self
, state
, state2
):
281 # overwrite settings in state2 with state's settings
282 settings
= state2
.settings
283 settings
.update(state
.settings
)
284 state
.settings
= settings
286 state
.disabled_devices
= set_filter(None, state
.disabled_devices
,
287 state2
.disabled_devices
)
289 state
.merged_ids
= set_filter(state1
._id
, state
.merged_ids
,
290 [state2
._id
], state2
.merged_ids
)
292 state
.tags
= set_filter(None, state
.tags
, state2
.tags
)
297 @repeat_on_conflict(['state'])
298 def _add_actions(self
, state
, actions
):
300 state
.add_actions(actions
)
302 except restkit
.Unauthorized
:
303 # the merge could result in an invalid list of
304 # subscribe/unsubscribe actions -- we ignore it and
305 # just use the actions from state
308 @repeat_on_conflict(['state2'])
309 def _delete(self
, state2
):
316 class EpisodeStateMerger(object):
317 """ Merges state2 in state """
319 def __init__(self
, state
, state2
, actions
):
321 if state
._id
== state2
._id
:
322 raise IncorrectMergeException("can't merge episode state into itself")
324 if state
.user
!= state2
.user
:
325 raise IncorrectMergeException("states don't belong to the same user")
329 self
.actions
= actions
333 self
._merge
_obj
(state
=self
.state
, state2
=self
.state2
)
334 self
._do
_delete
(state2
=self
.state2
)
335 self
.actions
['merge-episode-state'] += 1
338 @repeat_on_conflict(['state'])
339 def _merge_obj(self
, state
, state2
):
340 state
.add_actions(state2
.actions
)
342 # overwrite settings in state2 with state's settings
343 settings
= state2
.settings
344 settings
.update(state
.settings
)
345 state
.settings
= settings
347 merged_ids
= set(state
.merged_ids
+ [state2
._id
] + state2
.merged_ids
)
348 state
.merged_ids
= filter(None, merged_ids
)
350 state
.chapters
= list(set(state
.chapters
+ state2
.chapters
))
354 @repeat_on_conflict(['state2'])
355 def _do_delete(self
, state2
):
359 def set_filter(orig
, *args
):
360 """ chain args, and remove falsy values and orig """
361 s
= set(chain
.from_iterable(args
))