-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprofile_manager.py
More file actions
263 lines (219 loc) · 8.57 KB
/
Copy pathprofile_manager.py
File metadata and controls
263 lines (219 loc) · 8.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Profile management for the Link Farm plugin.
Handles CRUD operations for sync profiles.
"""
import uuid
from datetime import datetime
from typing import Dict, List, Optional
from calibre.utils.config import JSONConfig
from calibre_plugins.linkfarm.common import is_valid_directory
# Initialize plugin preferences
prefs = JSONConfig('plugins/linkfarm')
# Set default preferences
prefs.defaults['profiles'] = {}
prefs.defaults['selection_method'] = 'custom_column'
prefs.defaults['custom_column_name'] = '#sync_profiles'
prefs.defaults['tag_prefix'] = 'sync:'
prefs.defaults['show_status_icons'] = True
prefs.defaults['use_relative_symlinks'] = True
prefs.defaults['confirm_deletions'] = True
prefs.defaults['default_formats'] = ['EPUB', 'MOBI', 'AZW3', 'PDF']
class SyncProfile:
"""Represents a single sync profile."""
def __init__(self, profile_id: Optional[str] = None, name: str = '',
target_directory: str = '', format_priority: Optional[List[str]] = None,
preserve_structure: bool = False, active: bool = True,
created: Optional[str] = None, modified: Optional[str] = None):
"""
Initialize a sync profile.
Args:
profile_id: Unique profile identifier (generated if not provided)
name: Profile name (e.g., "Kindle", "Tablet")
target_directory: Target directory for symlinks
format_priority: Ordered list of preferred formats
preserve_structure: Whether to preserve author subdirectories
active: Whether profile is active
created: ISO format creation timestamp
modified: ISO format modification timestamp
"""
self.id = profile_id or str(uuid.uuid4())
self.name = name
self.target_directory = target_directory
self.format_priority = format_priority or prefs['default_formats'].copy()
self.preserve_structure = preserve_structure
self.active = active
self.created = created or datetime.now().isoformat()
self.modified = modified or datetime.now().isoformat()
def to_dict(self) -> Dict:
"""Convert profile to dictionary for storage."""
return {
'name': self.name,
'target_directory': self.target_directory,
'format_priority': self.format_priority,
'preserve_structure': self.preserve_structure,
'active': self.active,
'created': self.created,
'modified': self.modified
}
@classmethod
def from_dict(cls, profile_id: str, data: Dict) -> 'SyncProfile':
"""Create profile from dictionary."""
return cls(
profile_id=profile_id,
name=data.get('name', ''),
target_directory=data.get('target_directory', ''),
format_priority=data.get('format_priority', prefs['default_formats'].copy()),
preserve_structure=data.get('preserve_structure', False),
active=data.get('active', True),
created=data.get('created'),
modified=data.get('modified')
)
def validate(self) -> tuple[bool, Optional[str]]:
"""
Validate profile configuration.
Returns:
Tuple of (is_valid, error_message)
"""
if not self.name or not self.name.strip():
return False, "Profile name cannot be empty"
if not self.target_directory or not self.target_directory.strip():
return False, "Target directory cannot be empty"
if not is_valid_directory(self.target_directory):
return False, f"Target directory is not accessible: {self.target_directory}"
if not self.format_priority or len(self.format_priority) == 0:
return False, "At least one format must be specified"
return True, None
def update(self, **kwargs):
"""Update profile fields and set modified timestamp."""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
self.modified = datetime.now().isoformat()
class ProfileManager:
"""Manages sync profiles with CRUD operations."""
@staticmethod
def get_all_profiles() -> Dict[str, SyncProfile]:
"""Get all sync profiles."""
profiles_dict = prefs['profiles']
return {
profile_id: SyncProfile.from_dict(profile_id, data)
for profile_id, data in profiles_dict.items()
}
@staticmethod
def get_active_profiles() -> Dict[str, SyncProfile]:
"""Get only active sync profiles."""
all_profiles = ProfileManager.get_all_profiles()
return {
profile_id: profile
for profile_id, profile in all_profiles.items()
if profile.active
}
@staticmethod
def get_profile(profile_id: str) -> Optional[SyncProfile]:
"""Get a specific profile by ID."""
profiles_dict = prefs['profiles']
if profile_id in profiles_dict:
return SyncProfile.from_dict(profile_id, profiles_dict[profile_id])
return None
@staticmethod
def create_profile(name: str, target_directory: str,
format_priority: Optional[List[str]] = None,
preserve_structure: bool = False,
active: bool = True) -> SyncProfile:
"""
Create a new sync profile.
Args:
name: Profile name
target_directory: Target directory for symlinks
format_priority: Ordered list of preferred formats
preserve_structure: Whether to preserve author subdirectories
active: Whether profile is active
Returns:
Created SyncProfile
Raises:
ValueError: If profile validation fails
"""
profile = SyncProfile(
name=name,
target_directory=target_directory,
format_priority=format_priority,
preserve_structure=preserve_structure,
active=active
)
# Validate before saving
is_valid, error = profile.validate()
if not is_valid:
raise ValueError(error)
# Save to preferences
profiles = prefs['profiles']
profiles[profile.id] = profile.to_dict()
prefs['profiles'] = profiles
return profile
@staticmethod
def update_profile(profile_id: str, **kwargs) -> Optional[SyncProfile]:
"""
Update an existing profile.
Args:
profile_id: Profile ID to update
**kwargs: Fields to update
Returns:
Updated SyncProfile, or None if not found
Raises:
ValueError: If updated profile fails validation
"""
profile = ProfileManager.get_profile(profile_id)
if not profile:
return None
# Update fields
profile.update(**kwargs)
# Validate
is_valid, error = profile.validate()
if not is_valid:
raise ValueError(error)
# Save
profiles = prefs['profiles']
profiles[profile_id] = profile.to_dict()
prefs['profiles'] = profiles
return profile
@staticmethod
def delete_profile(profile_id: str) -> bool:
"""
Delete a profile.
Args:
profile_id: Profile ID to delete
Returns:
True if deleted, False if not found
"""
profiles = prefs['profiles']
if profile_id in profiles:
del profiles[profile_id]
prefs['profiles'] = profiles
return True
return False
@staticmethod
def get_profile_by_name(name: str) -> Optional[SyncProfile]:
"""Get a profile by name (case-insensitive)."""
all_profiles = ProfileManager.get_all_profiles()
name_lower = name.lower()
for profile in all_profiles.values():
if profile.name.lower() == name_lower:
return profile
return None
@staticmethod
def profile_name_exists(name: str, exclude_id: Optional[str] = None) -> bool:
"""
Check if a profile name already exists.
Args:
name: Profile name to check
exclude_id: Profile ID to exclude from check (for updates)
Returns:
True if name exists
"""
all_profiles = ProfileManager.get_all_profiles()
name_lower = name.lower()
for profile_id, profile in all_profiles.items():
if profile_id != exclude_id and profile.name.lower() == name_lower:
return True
return False