[Migration] remove podcasts_to_dict
[mygpo.git] / mygpo / users / sync.py
blob2a3fa2cac199b509b962cf9a555bc93a4a615469
1 from collections import namedtuple
3 from couchdbkit.ext.django.schema import *
5 from mygpo.core.models import Podcast, SubscriptionException
7 import logging
8 logger = logging.getLogger(__name__)
11 GroupedDevices = namedtuple('GroupedDevices', 'is_synced devices')
15 class SyncedDevicesMixin(DocumentSchema):
16 """ Contains the device-syncing functionality of a user """
18 sync_groups = ListProperty()
21 def get_grouped_devices(self):
22 """ Returns groups of synced devices and a unsynced group """
24 indexed_devices = dict( (dev.id, dev) for dev in self.active_devices )
26 for group in self.sync_groups:
28 devices = [indexed_devices.pop(device_id, None) for device_id in group]
29 devices = filter(None, devices)
30 if not devices:
31 continue
33 yield GroupedDevices(
34 True,
35 devices
38 # un-synced devices
39 yield GroupedDevices(
40 False,
41 indexed_devices.values()
45 def sync_devices(self, device1, device2):
46 """ Puts two devices in a common sync group"""
48 devices = set([device1, device2])
49 if not devices.issubset(set(self.devices)):
50 raise ValueError('the devices do not belong to the user')
52 sg1 = self.get_device_sync_group(device1)
53 sg2 = self.get_device_sync_group(device2)
55 if sg1 is not None and sg2 is not None:
56 # merge sync_groups
57 self.sync_groups[sg1].extend(self.sync_groups[sg2])
58 self.sync_groups.pop(sg2)
60 elif sg1 is None and sg2 is None:
61 self.sync_groups.append([device1.id, device2.id])
63 elif sg1 is not None:
64 self.sync_groups[sg1].append(device2.id)
66 elif sg2 is not None:
67 self.sync_groups[sg2].append(device1.id)
70 def unsync_device(self, device):
71 """ Removts the device from its sync-group
73 Raises a ValueError if the device is not synced """
75 sg = self.get_device_sync_group(device)
77 if sg is None:
78 raise ValueError('the device is not synced')
80 group = self.sync_groups[sg]
82 if len(group) <= 2:
83 self.sync_groups.pop(sg)
85 else:
86 group.remove(device.id)
89 def get_device_sync_group(self, device):
90 """ Returns the sync-group Id of the device """
92 for n, group in enumerate(self.sync_groups):
93 if device.id in group:
94 return n
97 def is_synced(self, device):
98 return self.get_device_sync_group(device) is not None
101 def get_synced(self, device):
102 """ Returns the devices that are synced with the given one """
104 sg = self.get_device_sync_group(device)
106 if sg is None:
107 return []
109 devices = self.get_devices_in_group(sg)
110 devices.remove(device)
111 return devices
115 def get_sync_targets(self, device):
116 """ Returns the devices and groups with which the device can be synced
118 Groups are represented as lists of devices """
120 sg = self.get_device_sync_group(device)
122 for n, group in enumerate(self.get_grouped_devices()):
124 if sg == n:
125 # the device's group can't be a sync-target
126 continue
128 elif group.is_synced:
129 yield group.devices
131 else:
132 # every unsynced device is a sync-target
133 for dev in group.devices:
134 if not dev == device:
135 yield dev
138 def get_devices_in_group(self, sg):
139 """ Returns the devices in the group with the given Id """
141 ids = self.sync_groups[sg]
142 return map(self.get_device, ids)
145 def sync_group(self, device):
146 """ Sync the group of the device """
148 group_index = self.get_device_sync_group(device)
150 if group_index is None:
151 return
153 group_state = self.get_group_state(group_index)
155 for device in self.get_devices_in_group(group_index):
156 sync_actions = self.get_sync_actions(device, group_state)
157 self.apply_sync_actions(device, sync_actions)
160 def apply_sync_actions(self, device, sync_actions):
161 """ Applies the sync-actions to the device """
163 from mygpo.db.couchdb.podcast_state import subscribe, unsubscribe
164 add, rem = sync_actions
166 podcasts = Podcast.objects.filter(id__in=(add+rem))
167 podcasts = {podcast.id: podcast for podcast in podcasts}
169 for podcast_id in add:
170 podcast = podcasts.get(podcast_id, None)
171 if podcast is None:
172 continue
173 try:
174 subscribe(podcast, self, device)
175 except SubscriptionException as e:
176 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
177 dict(username=self.username, error=repr(e)))
179 for podcast_id in rem:
180 podcast = podcasts.get(podcast_id, None)
181 if not podcast:
182 continue
184 try:
185 unsubscribe(podcast, self, device)
186 except SubscriptionException as e:
187 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
188 dict(username=self.username, error=repr(e)))
191 def get_group_state(self, group_index):
192 """ Returns the group's subscription state
194 The state is represented by the latest actions for each podcast """
196 device_ids = self.sync_groups[group_index]
197 devices = self.get_devices(device_ids)
199 state = {}
201 for d in devices:
202 actions = dict(d.get_latest_changes())
203 for podcast_id, action in actions.items():
204 if not podcast_id in state or \
205 action.timestamp > state[podcast_id].timestamp:
206 state[podcast_id] = action
208 return state
211 def get_sync_actions(self, device, group_state):
212 """ Get the actions required to bring the device to the group's state
214 After applying the actions the device reflects the group's state """
216 sg = self.get_device_sync_group(device)
217 if sg is None:
218 return [], []
220 # Filter those that describe actual changes to the current state
221 add, rem = [], []
222 current_state = dict(device.get_latest_changes())
224 for podcast_id, action in group_state.items():
226 # Sync-Actions must be newer than current state
227 if podcast_id in current_state and \
228 action.timestamp <= current_state[podcast_id].timestamp:
229 continue
231 # subscribe only what hasn't been subscribed before
232 if action.action == 'subscribe' and \
233 (podcast_id not in current_state or \
234 current_state[podcast_id].action == 'unsubscribe'):
235 add.append(podcast_id)
237 # unsubscribe only what has been subscribed before
238 elif action.action == 'unsubscribe' and \
239 podcast_id in current_state and \
240 current_state[podcast_id].action == 'subscribe':
241 rem.append(podcast_id)
243 return add, rem