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
:
29 [indexed_devices
.pop(device_id
) for device_id
in group
]
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:
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])
58 self
.sync_groups
[sg1
].append(device2
.id)
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
)
72 raise ValueError('the device is not synced')
74 group
= self
.sync_groups
[sg
]
77 self
.sync_groups
.pop(sg
)
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
:
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
)
103 devices
= self
.get_devices_in_group(sg
)
104 devices
.remove(device
)
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()):
119 # the device's group can't be a sync-target
122 elif group
.is_synced
:
126 # every unsynced device is a sync-target
127 for dev
in group
.devices
:
128 if not dev
== device
:
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
)
140 """ Syncs all of the user's device groups """
142 for group
in self
.get_grouped_devices():
144 device
= group
.devices
[0]
145 self
.sync_group(device
)
148 def sync_group(self
, device
):
149 """ Sync the group of the device """
151 group_index
= self
.get_device_sync_group(device
)
153 if group_index
is None:
156 group_state
= self
.get_group_state(group_index
)
158 for device
in self
.get_devices_in_group(group_index
):
159 sync_actions
= self
.get_sync_actions(device
, group_state
)
160 self
.apply_sync_actions(device
, sync_actions
)
163 def apply_sync_actions(self
, device
, sync_actions
):
164 """ Applies the sync-actions to the device """
166 add
, rem
= sync_actions
168 podcasts
= podcasts_to_dict(add
+ rem
)
170 for podcast_id
in add
:
171 podcast
= podcasts
.get(podcast_id
, None)
175 podcast
.subscribe(self
, device
)
176 except SubscriptionException
as e
:
177 log('Web: %(username)s: cannot sync device: %(error)s' %
178 dict(username
=self
.username
, error
=repr(e
)))
180 for podcast_id
in rem
:
181 podcast
= podcasts
.get(podcast_id
, None)
186 podcast
.unsubscribe(self
, device
)
187 except SubscriptionException
as e
:
188 log('Web: %(username)s: cannot sync device: %(error)s' %
189 dict(username
=self
.username
, error
=repr(e
)))
192 def get_group_state(self
, group_index
):
193 """ Returns the group's subscription state
195 The state is represented by the latest actions for each podcast """
197 device_ids
= self
.sync_groups
[group_index
]
198 devices
= [self
.get_device(device_id
) for device_id
in device_ids
]
203 actions
= dict(d
.get_latest_changes())
204 for podcast_id
, action
in actions
.items():
205 if not podcast_id
in state
or \
206 action
.timestamp
> state
[podcast_id
].timestamp
:
207 state
[podcast_id
] = action
212 def get_sync_actions(self
, device
, group_state
):
213 """ Get the actions required to bring the device to the group's state
215 After applying the actions the device reflects the group's state """
217 sg
= self
.get_device_sync_group(device
)
221 # Filter those that describe actual changes to the current state
223 current_state
= dict(device
.get_latest_changes())
225 for podcast_id
, action
in group_state
.items():
227 # Sync-Actions must be newer than current state
228 if podcast_id
in current_state
and \
229 action
.timestamp
<= current_state
[podcast_id
].timestamp
:
232 # subscribe only what hasn't been subscribed before
233 if action
.action
== 'subscribe' and \
234 (podcast_id
not in current_state
or \
235 current_state
[podcast_id
].action
== 'unsubscribe'):
236 add
.append(podcast_id
)
238 # unsubscribe only what has been subscribed before
239 elif action
.action
== 'unsubscribe' and \
240 podcast_id
in current_state
and \
241 current_state
[podcast_id
].action
== 'subscribe':
242 rem
.append(podcast_id
)