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.
Files changed (47) hide show
  1. pygpod/__init__.py +353 -0
  2. pygpod/cli.py +973 -0
  3. pygpod/db/__init__.py +0 -0
  4. pygpod/db/artwork_parser.py +368 -0
  5. pygpod/db/artwork_writer.py +67 -0
  6. pygpod/db/constants.py +215 -0
  7. pygpod/db/itunessd.py +185 -0
  8. pygpod/db/parser.py +651 -0
  9. pygpod/db/photodb.py +654 -0
  10. pygpod/db/playcounts.py +398 -0
  11. pygpod/db/writer.py +243 -0
  12. pygpod/device/__init__.py +0 -0
  13. pygpod/device/artwork_formats.py +142 -0
  14. pygpod/device/device.py +580 -0
  15. pygpod/device/fileutils.py +129 -0
  16. pygpod/device/models.py +971 -0
  17. pygpod/device/mountpoint.py +246 -0
  18. pygpod/device/usb.py +622 -0
  19. pygpod/exceptions.py +55 -0
  20. pygpod/hash/__init__.py +1 -0
  21. pygpod/hash/checksum.py +47 -0
  22. pygpod/hash/hash58.py +189 -0
  23. pygpod/hash/hash72.py +414 -0
  24. pygpod/model/__init__.py +0 -0
  25. pygpod/model/artwork.py +487 -0
  26. pygpod/model/artwork_manager.py +529 -0
  27. pygpod/model/chapterdata.py +254 -0
  28. pygpod/model/config.py +79 -0
  29. pygpod/model/database.py +1712 -0
  30. pygpod/model/photo.py +25 -0
  31. pygpod/model/playlist.py +125 -0
  32. pygpod/model/smartplaylist.py +748 -0
  33. pygpod/model/sysinfo.py +192 -0
  34. pygpod/model/track.py +754 -0
  35. pygpod/tags.py +299 -0
  36. pygpod/utils/__init__.py +0 -0
  37. pygpod/utils/compat.py +63 -0
  38. pygpod/utils/datetime.py +54 -0
  39. pygpod/utils/encoding.py +79 -0
  40. pygpod/utils/plist.py +27 -0
  41. pygpod/utils/timezone.py +353 -0
  42. pygpod-0.0.4.dist-info/METADATA +456 -0
  43. pygpod-0.0.4.dist-info/RECORD +47 -0
  44. pygpod-0.0.4.dist-info/WHEEL +5 -0
  45. pygpod-0.0.4.dist-info/entry_points.txt +2 -0
  46. pygpod-0.0.4.dist-info/licenses/LICENSE +501 -0
  47. 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