115f83af3915021c0ed66a28cee690daaedd0713
[mygpo.git] / mygpo / users / sync.py
blob115f83af3915021c0ed66a28cee690daaedd0713
1 from collections import namedtuple
3 from couchdbkit.ext.django.schema import *
5 from mygpo.core.models import Podcast, SubscriptionException
6 from mygpo.log import log
7 from mygpo.db.couchdb.podcast import podcasts_to_dict
10 GroupedDevices = namedtuple('GroupedDevices', 'is_synced devices')
14 class SyncedDevicesMixin(DocumentSchema):
15 """ Contains the device-syncing functionality of a user """
17 sync_groups = ListProperty()
20 def get_grouped_devices(self):
21 """ Returns groups of synced devices and a unsynced group """
23 indexed_devices = dict( (dev.id, dev) for dev in self.active_devices )
25 for group in self.sync_groups:
27 yield GroupedDevices(
28 True,
29 [indexed_devices.pop(device_id) for device_id in group]
32 # un-synced devices
33 yield GroupedDevices(
34 False,
35 indexed_devices.values()
39 def sync_devices(self, device1, device2):
40 """ Puts two devices in a common sync group"""
42 devices = set([device1, device2])
43 if not devices.issubset(set(self.devices)):
44 raise ValueError('the devices do not belong to the user')
46 sg1 = self.get_device_sync_group(device1)
47 sg2 = self.get_device_sync_group(device2)
49 if sg1 is not None and sg2 is not None:
50 # merge sync_groups
51 self.sync_groups[sg1].extend(self.sync_groups[sg2])
52 self.sync_groups.pop(sg2)
54 elif sg1 is None and sg2 is None:
55 self.sync_groups.append([device1.id, device2.id])
57 elif sg1 is not None:
58 self.sync_groups[sg1].append(device2.id)
60 elif sg2 is not None:
61 self.sync_groups[sg2].append(device1.id)
64 def unsync_device(self, device):
65 """ Removts the device from its sync-group
67 Raises a ValueError if the device is not synced """
69 sg = self.get_device_sync_group(device)
71 if sg is None:
72 raise ValueError('the device is not synced')
74 group = self.sync_groups[sg]
76 if len(group) <= 2:
77 self.sync_groups.pop(sg)
79 else:
80 group.remove(device.id)
83 def get_device_sync_group(self, device):
84 """ Returns the sync-group Id of the device """
86 for n, group in enumerate(self.sync_groups):
87 if device.id in group:
88 return n
91 def is_synced(self, device):
92 return self.get_device_sync_group(device) is not None
95 def get_synced(self, device):
96 """ Returns the devices that are synced with the given one """
98 sg = self.get_device_sync_group(device)
100 if sg is None:
101 return []
103 devices = self.get_devices_in_group(sg)
104 devices.remove(device)
105 return devices
109 def get_sync_targets(self, device):
110 """ Returns the devices and groups with which the device can be synced
112 Groups are represented as lists of devices """
114 sg = self.get_device_sync_group(device)
116 for n, group in enumerate(self.get_grouped_devices()):
118 if sg == n:
119 # the device's group can't be a sync-target
120 continue
122 elif group.is_synced:
123 yield group.devices
125 else:
126 # every unsynced device is a sync-target
127 for dev in group.devices:
128 if not dev == device:
129 yield dev
132 def get_devices_in_group(self, sg):
133 """ Returns the devices in the group with the given Id """
135 ids = self.sync_groups[sg]
136 return map(self.get_device, ids)
139 def sync_group(self, device):
140 """ Sync the group of the device """
142 group_index = self.get_device_sync_group(device)
144 if group_index is None:
145 return
147 group_state = self.get_group_state(group_index)
149 for device in self.get_devices_in_group(group_index):
150 sync_actions = self.get_sync_actions(device, group_state)
151 self.apply_sync_actions(device, sync_actions)
154 def apply_sync_actions(self, device, sync_actions):
155 """ Applies the sync-actions to the device """
157 add, rem = sync_actions
159 podcasts = podcasts_to_dict(add + rem)
161 for podcast_id in add:
162 podcast = podcasts.get(podcast_id, None)
163 if podcast is None:
164 continue
165 try:
166 podcast.subscribe(self, device)
167 except SubscriptionException as e:
168 log('Web: %(username)s: cannot sync device: %(error)s' %
169 dict(username=self.username, error=repr(e)))
171 for podcast_id in rem:
172 podcast = podcasts.get(podcast_id, None)
173 if not podcast:
174 continue
176 try:
177 podcast.unsubscribe(self, device)
178 except SubscriptionException as e:
179 log('Web: %(username)s: cannot sync device: %(error)s' %
180 dict(username=self.username, error=repr(e)))
183 def get_group_state(self, group_index):
184 """ Returns the group's subscription state
186 The state is represented by the latest actions for each podcast """
188 device_ids = self.sync_groups[group_index]
189 devices = [self.get_device(device_id) for device_id in device_ids]
191 state = {}
193 for d in devices:
194 actions = dict(d.get_latest_changes())
195 for podcast_id, action in actions.items():
196 if not podcast_id in state or \
197 action.timestamp > state[podcast_id].timestamp:
198 state[podcast_id] = action
200 return state
203 def get_sync_actions(self, device, group_state):
204 """ Get the actions required to bring the device to the group's state
206 After applying the actions the device reflects the group's state """
208 sg = self.get_device_sync_group(device)
209 if sg is None:
210 return [], []
212 # Filter those that describe actual changes to the current state
213 add, rem = [], []
214 current_state = dict(device.get_latest_changes())
216 for podcast_id, action in group_state.items():
218 # Sync-Actions must be newer than current state
219 if podcast_id in current_state and \
220 action.timestamp <= current_state[podcast_id].timestamp:
221 continue
223 # subscribe only what hasn't been subscribed before
224 if action.action == 'subscribe' and \
225 (podcast_id not in current_state or \
226 current_state[podcast_id].action == 'unsubscribe'):
227 add.append(podcast_id)
229 # unsubscribe only what has been subscribed before
230 elif action.action == 'unsubscribe' and \
231 podcast_id in current_state and \
232 current_state[podcast_id].action == 'subscribe':
233 rem.append(podcast_id)
235 return add, rem