pygpod 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygpod/__init__.py +353 -0
- pygpod/cli.py +973 -0
- pygpod/db/__init__.py +0 -0
- pygpod/db/artwork_parser.py +368 -0
- pygpod/db/artwork_writer.py +67 -0
- pygpod/db/constants.py +215 -0
- pygpod/db/itunessd.py +185 -0
- pygpod/db/parser.py +651 -0
- pygpod/db/photodb.py +654 -0
- pygpod/db/playcounts.py +398 -0
- pygpod/db/writer.py +243 -0
- pygpod/device/__init__.py +0 -0
- pygpod/device/artwork_formats.py +142 -0
- pygpod/device/device.py +580 -0
- pygpod/device/fileutils.py +129 -0
- pygpod/device/models.py +971 -0
- pygpod/device/mountpoint.py +246 -0
- pygpod/device/usb.py +622 -0
- pygpod/exceptions.py +55 -0
- pygpod/hash/__init__.py +1 -0
- pygpod/hash/checksum.py +47 -0
- pygpod/hash/hash58.py +189 -0
- pygpod/hash/hash72.py +414 -0
- pygpod/model/__init__.py +0 -0
- pygpod/model/artwork.py +487 -0
- pygpod/model/artwork_manager.py +529 -0
- pygpod/model/chapterdata.py +254 -0
- pygpod/model/config.py +79 -0
- pygpod/model/database.py +1712 -0
- pygpod/model/photo.py +25 -0
- pygpod/model/playlist.py +125 -0
- pygpod/model/smartplaylist.py +748 -0
- pygpod/model/sysinfo.py +192 -0
- pygpod/model/track.py +754 -0
- pygpod/tags.py +299 -0
- pygpod/utils/__init__.py +0 -0
- pygpod/utils/compat.py +63 -0
- pygpod/utils/datetime.py +54 -0
- pygpod/utils/encoding.py +79 -0
- pygpod/utils/plist.py +27 -0
- pygpod/utils/timezone.py +353 -0
- pygpod-0.0.4.dist-info/METADATA +456 -0
- pygpod-0.0.4.dist-info/RECORD +47 -0
- pygpod-0.0.4.dist-info/WHEEL +5 -0
- pygpod-0.0.4.dist-info/entry_points.txt +2 -0
- pygpod-0.0.4.dist-info/licenses/LICENSE +501 -0
- pygpod-0.0.4.dist-info/top_level.txt +1 -0
pygpod/__init__.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""pygpod - Pure Python iPod database library.
|
|
2
|
+
|
|
3
|
+
A complete Python rewrite of libgpod for managing iPods that mount as
|
|
4
|
+
USB mass storage devices.
|
|
5
|
+
|
|
6
|
+
Basic usage:
|
|
7
|
+
import pygpod
|
|
8
|
+
|
|
9
|
+
# Open existing iPod
|
|
10
|
+
db = pygpod.Database("/mnt/ipod")
|
|
11
|
+
|
|
12
|
+
# List tracks
|
|
13
|
+
for track in db.tracks:
|
|
14
|
+
print(f"{track.artist} - {track.title}")
|
|
15
|
+
|
|
16
|
+
# Add a track
|
|
17
|
+
db.add_track("/path/to/song.mp3")
|
|
18
|
+
|
|
19
|
+
# Save
|
|
20
|
+
db.save()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
__version__ = "0.0.4"
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
from .db.photodb import Photo, PhotoAlbum, PhotoDB
|
|
32
|
+
from .db.playcounts import read_otg_playlists, read_play_counts
|
|
33
|
+
from .device.device import Device
|
|
34
|
+
from .device.mountpoint import init_ipod, validate_mountpoint
|
|
35
|
+
from .exceptions import (
|
|
36
|
+
ArtworkError,
|
|
37
|
+
DatabaseError,
|
|
38
|
+
DependencyError,
|
|
39
|
+
DeviceError,
|
|
40
|
+
HashError,
|
|
41
|
+
MountPointError,
|
|
42
|
+
ParseError,
|
|
43
|
+
PlaylistError,
|
|
44
|
+
PyGpodError,
|
|
45
|
+
TrackError,
|
|
46
|
+
UnsupportedFormatError,
|
|
47
|
+
WriteError,
|
|
48
|
+
)
|
|
49
|
+
from .model.artwork import Artwork
|
|
50
|
+
from .model.chapterdata import Chapter, ChapterData
|
|
51
|
+
from .model.config import DatabaseConfig
|
|
52
|
+
from .model.database import Database
|
|
53
|
+
from .model.playlist import Playlist
|
|
54
|
+
from .model.smartplaylist import (
|
|
55
|
+
SPLAction,
|
|
56
|
+
SPLField,
|
|
57
|
+
SPLLimitSort,
|
|
58
|
+
SPLLimitType,
|
|
59
|
+
SPLMatch,
|
|
60
|
+
SPLPrefs,
|
|
61
|
+
SPLRule,
|
|
62
|
+
apply_limit,
|
|
63
|
+
evaluate_smart_playlist,
|
|
64
|
+
)
|
|
65
|
+
from .model.track import Track
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def discover():
|
|
69
|
+
"""Auto-discover connected iPods.
|
|
70
|
+
|
|
71
|
+
Scans mounted volumes for iPods, then always attempts USB detection
|
|
72
|
+
to find iPods at non-standard mount points or not yet mounted.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of (mountpoint, Device) tuples. Mountpoint is None for
|
|
76
|
+
USB-detected devices that are not mounted.
|
|
77
|
+
"""
|
|
78
|
+
import pathlib
|
|
79
|
+
import platform
|
|
80
|
+
|
|
81
|
+
results = []
|
|
82
|
+
found_mountpoints: set[str] = set()
|
|
83
|
+
|
|
84
|
+
# Scan common mount points
|
|
85
|
+
system = platform.system()
|
|
86
|
+
mounts = []
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if system == "Linux":
|
|
90
|
+
for base in [pathlib.Path("/media"), pathlib.Path("/run/media")]:
|
|
91
|
+
try:
|
|
92
|
+
if base.exists():
|
|
93
|
+
for p in base.glob("*/*"):
|
|
94
|
+
try:
|
|
95
|
+
if p.is_dir():
|
|
96
|
+
mounts.append(p)
|
|
97
|
+
except (PermissionError, OSError):
|
|
98
|
+
pass
|
|
99
|
+
except (PermissionError, OSError):
|
|
100
|
+
pass
|
|
101
|
+
mnt = pathlib.Path("/mnt")
|
|
102
|
+
try:
|
|
103
|
+
if mnt.exists():
|
|
104
|
+
for p in mnt.iterdir():
|
|
105
|
+
try:
|
|
106
|
+
if p.is_dir():
|
|
107
|
+
mounts.append(p)
|
|
108
|
+
except (PermissionError, OSError):
|
|
109
|
+
pass
|
|
110
|
+
except (PermissionError, OSError):
|
|
111
|
+
pass
|
|
112
|
+
elif system == "Darwin":
|
|
113
|
+
volumes = pathlib.Path("/Volumes")
|
|
114
|
+
try:
|
|
115
|
+
if volumes.exists():
|
|
116
|
+
for p in volumes.iterdir():
|
|
117
|
+
try:
|
|
118
|
+
if p.is_dir():
|
|
119
|
+
mounts.append(p)
|
|
120
|
+
except (PermissionError, OSError):
|
|
121
|
+
pass
|
|
122
|
+
except (PermissionError, OSError):
|
|
123
|
+
pass
|
|
124
|
+
elif system == "Windows":
|
|
125
|
+
import string
|
|
126
|
+
|
|
127
|
+
for letter in string.ascii_uppercase:
|
|
128
|
+
drive = pathlib.Path(f"{letter}:\\")
|
|
129
|
+
try:
|
|
130
|
+
if drive.exists():
|
|
131
|
+
mounts.append(drive)
|
|
132
|
+
except (PermissionError, OSError):
|
|
133
|
+
pass
|
|
134
|
+
except Exception:
|
|
135
|
+
logger.debug("Mount point scanning failed", exc_info=True)
|
|
136
|
+
|
|
137
|
+
for mp in mounts:
|
|
138
|
+
if validate_mountpoint(str(mp)):
|
|
139
|
+
try:
|
|
140
|
+
dev = Device.from_mountpoint(str(mp))
|
|
141
|
+
results.append((str(mp), dev))
|
|
142
|
+
found_mountpoints.add(str(mp))
|
|
143
|
+
except Exception:
|
|
144
|
+
logger.debug("Failed to read device at %s", mp, exc_info=True)
|
|
145
|
+
|
|
146
|
+
# Always try USB detection - find iPods at non-standard mounts or unmounted
|
|
147
|
+
try:
|
|
148
|
+
from .device.usb import detect_ipod_usb
|
|
149
|
+
|
|
150
|
+
usb_devices = detect_ipod_usb()
|
|
151
|
+
if usb_devices:
|
|
152
|
+
# Try to resolve mount points for USB devices
|
|
153
|
+
usb_mounts = _resolve_usb_mount_points(system)
|
|
154
|
+
for usb_mp in usb_mounts:
|
|
155
|
+
if usb_mp not in found_mountpoints and validate_mountpoint(usb_mp):
|
|
156
|
+
try:
|
|
157
|
+
dev = Device.from_mountpoint(usb_mp)
|
|
158
|
+
results.append((usb_mp, dev))
|
|
159
|
+
found_mountpoints.add(usb_mp)
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.debug("Failed to read device at %s", usb_mp, exc_info=True)
|
|
162
|
+
|
|
163
|
+
# If no mounted iPods found, include USB-only devices
|
|
164
|
+
if not results:
|
|
165
|
+
for usb_dev in usb_devices:
|
|
166
|
+
try:
|
|
167
|
+
dev = Device.from_usb(usb_dev)
|
|
168
|
+
results.append((None, dev))
|
|
169
|
+
except Exception:
|
|
170
|
+
logger.debug("Failed to create device from USB info", exc_info=True)
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.debug("USB-based iPod discovery failed", exc_info=True)
|
|
173
|
+
|
|
174
|
+
return results
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _resolve_usb_mount_points(system: str) -> list[str]:
|
|
178
|
+
"""Find mount points for USB mass storage devices.
|
|
179
|
+
|
|
180
|
+
Uses platform-specific methods to discover mount points that may
|
|
181
|
+
not be in the standard locations checked by discover().
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
system: Platform name from platform.system().
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of mount point paths for USB storage devices.
|
|
188
|
+
"""
|
|
189
|
+
mount_points: list[str] = []
|
|
190
|
+
|
|
191
|
+
if system == "Linux":
|
|
192
|
+
mount_points.extend(_resolve_linux_usb_mounts())
|
|
193
|
+
elif system == "Darwin":
|
|
194
|
+
mount_points.extend(_resolve_macos_usb_mounts())
|
|
195
|
+
elif system == "Windows":
|
|
196
|
+
mount_points.extend(_resolve_windows_usb_mounts())
|
|
197
|
+
|
|
198
|
+
return mount_points
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _resolve_linux_usb_mounts() -> list[str]:
|
|
202
|
+
"""Find USB storage mount points on Linux via /proc/mounts."""
|
|
203
|
+
import pathlib
|
|
204
|
+
|
|
205
|
+
mount_points: list[str] = []
|
|
206
|
+
|
|
207
|
+
# Check /dev/disk/by-id for Apple iPod devices and resolve to mount points
|
|
208
|
+
by_id = pathlib.Path("/dev/disk/by-id")
|
|
209
|
+
if by_id.exists():
|
|
210
|
+
apple_devs: set[str] = set()
|
|
211
|
+
for link in by_id.iterdir():
|
|
212
|
+
name_lower = link.name.lower()
|
|
213
|
+
if "apple" in name_lower or "ipod" in name_lower:
|
|
214
|
+
try:
|
|
215
|
+
real = str(link.resolve())
|
|
216
|
+
apple_devs.add(real)
|
|
217
|
+
# Also add partition variants (e.g. /dev/sdb -> /dev/sdb1)
|
|
218
|
+
parent = pathlib.Path(real)
|
|
219
|
+
if parent.parent == pathlib.Path("/dev"):
|
|
220
|
+
for part in pathlib.Path("/dev").glob(parent.name + "[0-9]*"):
|
|
221
|
+
apple_devs.add(str(part))
|
|
222
|
+
except OSError:
|
|
223
|
+
logger.debug("Cannot resolve symlink %s", link, exc_info=True)
|
|
224
|
+
|
|
225
|
+
if apple_devs:
|
|
226
|
+
proc_mounts = pathlib.Path("/proc/mounts")
|
|
227
|
+
if proc_mounts.exists():
|
|
228
|
+
try:
|
|
229
|
+
for line in proc_mounts.read_text().splitlines():
|
|
230
|
+
parts = line.split()
|
|
231
|
+
if len(parts) >= 2 and parts[0] in apple_devs:
|
|
232
|
+
mount_points.append(parts[1])
|
|
233
|
+
except OSError:
|
|
234
|
+
logger.debug("Failed to read /proc/mounts", exc_info=True)
|
|
235
|
+
|
|
236
|
+
return mount_points
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _resolve_macos_usb_mounts() -> list[str]:
|
|
240
|
+
"""Find USB storage mount points on macOS via diskutil."""
|
|
241
|
+
import subprocess
|
|
242
|
+
|
|
243
|
+
mount_points: list[str] = []
|
|
244
|
+
try:
|
|
245
|
+
import plistlib
|
|
246
|
+
|
|
247
|
+
output = subprocess.check_output(
|
|
248
|
+
["diskutil", "list", "-plist", "external"],
|
|
249
|
+
timeout=10,
|
|
250
|
+
)
|
|
251
|
+
plist = plistlib.loads(output)
|
|
252
|
+
disk_ids = plist.get("AllDisksAndPartitions", [])
|
|
253
|
+
for disk in disk_ids:
|
|
254
|
+
mp = disk.get("MountPoint")
|
|
255
|
+
if mp:
|
|
256
|
+
mount_points.append(mp)
|
|
257
|
+
for part in disk.get("Partitions", []):
|
|
258
|
+
mp = part.get("MountPoint")
|
|
259
|
+
if mp:
|
|
260
|
+
mount_points.append(mp)
|
|
261
|
+
except Exception:
|
|
262
|
+
logger.debug("macOS diskutil mount point discovery failed", exc_info=True)
|
|
263
|
+
|
|
264
|
+
return mount_points
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _resolve_windows_usb_mounts() -> list[str]:
|
|
268
|
+
"""Find USB storage mount points on Windows via WMI."""
|
|
269
|
+
mount_points: list[str] = []
|
|
270
|
+
try:
|
|
271
|
+
import wmi # type: ignore
|
|
272
|
+
|
|
273
|
+
c = wmi.WMI()
|
|
274
|
+
for disk in c.Win32_DiskDrive(InterfaceType="USB"):
|
|
275
|
+
for partition in disk.associators("Win32_DiskDriveToDiskPartition"):
|
|
276
|
+
for logical in partition.associators("Win32_LogicalDiskToPartition"):
|
|
277
|
+
mp = logical.DeviceID # e.g. "E:"
|
|
278
|
+
if mp:
|
|
279
|
+
mount_points.append(mp + "\\")
|
|
280
|
+
except Exception:
|
|
281
|
+
logger.debug("Windows WMI mount point discovery failed", exc_info=True)
|
|
282
|
+
|
|
283
|
+
return mount_points
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
__all__ = [
|
|
287
|
+
"DatabaseConfig",
|
|
288
|
+
"Database",
|
|
289
|
+
"Track",
|
|
290
|
+
"Playlist",
|
|
291
|
+
"Artwork",
|
|
292
|
+
"ChapterData",
|
|
293
|
+
"Chapter",
|
|
294
|
+
"SPLRule",
|
|
295
|
+
"SPLPrefs",
|
|
296
|
+
"SPLField",
|
|
297
|
+
"SPLAction",
|
|
298
|
+
"SPLMatch",
|
|
299
|
+
"SPLLimitSort",
|
|
300
|
+
"SPLLimitType",
|
|
301
|
+
"evaluate_smart_playlist",
|
|
302
|
+
"apply_limit",
|
|
303
|
+
"Device",
|
|
304
|
+
"PhotoDB",
|
|
305
|
+
"Photo",
|
|
306
|
+
"PhotoAlbum",
|
|
307
|
+
"read_play_counts",
|
|
308
|
+
"read_otg_playlists",
|
|
309
|
+
"init_ipod",
|
|
310
|
+
"validate_mountpoint",
|
|
311
|
+
"discover",
|
|
312
|
+
"PyGpodError",
|
|
313
|
+
"DatabaseError",
|
|
314
|
+
"ParseError",
|
|
315
|
+
"WriteError",
|
|
316
|
+
"DeviceError",
|
|
317
|
+
"MountPointError",
|
|
318
|
+
"HashError",
|
|
319
|
+
"TrackError",
|
|
320
|
+
"PlaylistError",
|
|
321
|
+
"ArtworkError",
|
|
322
|
+
"DependencyError",
|
|
323
|
+
"UnsupportedFormatError",
|
|
324
|
+
"__version__",
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Check for optional dependencies and warn about missing ones
|
|
329
|
+
def _check_optional_deps() -> None:
|
|
330
|
+
import warnings
|
|
331
|
+
|
|
332
|
+
missing = []
|
|
333
|
+
try:
|
|
334
|
+
import mutagen # noqa: F401
|
|
335
|
+
except ImportError:
|
|
336
|
+
missing.append("mutagen (tag reading: pip install pygpod[tags])")
|
|
337
|
+
try:
|
|
338
|
+
import PIL # noqa: F401
|
|
339
|
+
except ImportError:
|
|
340
|
+
missing.append("Pillow (artwork: pip install pygpod[artwork])")
|
|
341
|
+
try:
|
|
342
|
+
import usb # noqa: F401
|
|
343
|
+
except ImportError:
|
|
344
|
+
missing.append("pyusb (USB detection: pip install pygpod[usb])")
|
|
345
|
+
if missing:
|
|
346
|
+
warnings.warn(
|
|
347
|
+
"pygpod: optional dependencies not installed: " + ", ".join(missing),
|
|
348
|
+
stacklevel=3,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_check_optional_deps()
|
|
353
|
+
del _check_optional_deps
|