ipodsync 0.1.0__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.
- ipodsync/__init__.py +27 -0
- ipodsync/__main__.py +5 -0
- ipodsync/_art_templates.py +29 -0
- ipodsync/_hashab_gen.py +1588 -0
- ipodsync/_hashab_gen2.py +838 -0
- ipodsync/_hashab_gen_rt.py +134 -0
- ipodsync/_hashab_py.py +285 -0
- ipodsync/_hashab_tables.bin +0 -0
- ipodsync/_hashab_tables.py +31 -0
- ipodsync/_lib_templates.py +16 -0
- ipodsync/artwork_writer.py +252 -0
- ipodsync/cbk.py +44 -0
- ipodsync/cli.py +314 -0
- ipodsync/export.py +91 -0
- ipodsync/hashab.py +44 -0
- ipodsync/importer.py +137 -0
- ipodsync/library.py +360 -0
- ipodsync/models.py +31 -0
- ipodsync/sysinfo.py +50 -0
- ipodsync/transport.py +165 -0
- ipodsync-0.1.0.dist-info/METADATA +114 -0
- ipodsync-0.1.0.dist-info/RECORD +25 -0
- ipodsync-0.1.0.dist-info/WHEEL +4 -0
- ipodsync-0.1.0.dist-info/entry_points.txt +2 -0
- ipodsync-0.1.0.dist-info/licenses/LICENSE +21 -0
ipodsync/sysinfo.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Read the device's FirewireGuid — needed for the hashAB signature.
|
|
2
|
+
|
|
3
|
+
FirewireGuid is an 8-byte device identifier. It lives in
|
|
4
|
+
`iPod_Control/Device/SysInfoExtended` (XML plist) or `SysInfo` (text),
|
|
5
|
+
as a hex string like `0x000A2700XXXXXXXX` (16 hex characters).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# 0x + 16 hex, or a FireWireGUID key near 16 hex chars
|
|
13
|
+
_GUID_RE = re.compile(
|
|
14
|
+
r"(?:FireWireGUID|FirewireGuid)\D{0,40}?(?:0x)?([0-9A-Fa-f]{16})",
|
|
15
|
+
re.IGNORECASE | re.DOTALL,
|
|
16
|
+
)
|
|
17
|
+
_BARE_HEX_RE = re.compile(r"0x([0-9A-Fa-f]{16})")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FirewireGuidNotFound(RuntimeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _extract(text: str) -> str | None:
|
|
25
|
+
m = _GUID_RE.search(text)
|
|
26
|
+
if m:
|
|
27
|
+
return m.group(1)
|
|
28
|
+
m = _BARE_HEX_RE.search(text)
|
|
29
|
+
if m:
|
|
30
|
+
return m.group(1)
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_firewire_guid(sysinfo_extended: Path, sysinfo: Path | None = None) -> bytes:
|
|
35
|
+
"""Return FirewireGuid as 8 bytes (big-endian, as in the hex string).
|
|
36
|
+
|
|
37
|
+
Tries SysInfoExtended, then SysInfo. errors='ignore' — the files may
|
|
38
|
+
contain binary tails.
|
|
39
|
+
"""
|
|
40
|
+
for p in (sysinfo_extended, sysinfo):
|
|
41
|
+
if p is None or not p.exists():
|
|
42
|
+
continue
|
|
43
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
44
|
+
hexs = _extract(text)
|
|
45
|
+
if hexs:
|
|
46
|
+
return bytes.fromhex(hexs)
|
|
47
|
+
raise FirewireGuidNotFound(
|
|
48
|
+
f"Could not find FireWireGUID in {sysinfo_extended}"
|
|
49
|
+
+ (f" / {sysinfo}" if sysinfo else "")
|
|
50
|
+
)
|
ipodsync/transport.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Transport: the iPod nano mounts as an ordinary mass-storage volume.
|
|
2
|
+
|
|
3
|
+
Uploading = writing files under ``iPod_Control/``. No WebUSB/MTP involved.
|
|
4
|
+
This module locates the device, hands out paths and tracks availability.
|
|
5
|
+
|
|
6
|
+
Discovery is cross-platform:
|
|
7
|
+
- macOS: ``/Volumes/*``
|
|
8
|
+
- Linux: ``/media/$USER/*``, ``/media/*``, ``/run/media/$USER/*``, ``/mnt/*``
|
|
9
|
+
- override: set ``IPODSYNC_MOUNT=/path/to/mount`` or pass ``mount=`` explicitly.
|
|
10
|
+
A candidate is an iPod if it contains ``iPod_Control/``.
|
|
11
|
+
|
|
12
|
+
Access note: on macOS the OS may return ``Operation not permitted`` (EPERM) for a
|
|
13
|
+
removable volume when the terminal lacks Full Disk Access (TCC). We tell that state
|
|
14
|
+
(NO_ACCESS) apart from "no iPod connected" (NOT_FOUND).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IPodNotFound(RuntimeError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Access(Enum):
|
|
31
|
+
READY = "ready" # volume mounted and iPod_Control/iTunes is readable
|
|
32
|
+
NO_ACCESS = "no_access" # EPERM — missing TCC permission (macOS Full Disk Access)
|
|
33
|
+
NOT_FOUND = "not_found" # no iPod connected
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NO_ACCESS_HINT = (
|
|
37
|
+
"iPod is mounted ({vols}) but not accessible (macOS TCC).\n"
|
|
38
|
+
" Quick fix: eject -> unplug -> plug back in.\n"
|
|
39
|
+
" Permanent: System Settings -> Privacy & Security -> Full Disk Access -> "
|
|
40
|
+
"add your terminal (Terminal/iTerm) and enable it."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class IPod:
|
|
46
|
+
"""A mounted iPod. All paths are absolute."""
|
|
47
|
+
root: Path
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def control(self) -> Path:
|
|
51
|
+
return self.root / "iPod_Control"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def itunes_dir(self) -> Path:
|
|
55
|
+
return self.control / "iTunes"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def itunes_cdb(self) -> Path:
|
|
59
|
+
"""Legacy mhbd named iTunesCDB (device regenerates it from SQLite)."""
|
|
60
|
+
return self.itunes_dir / "iTunesCDB"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def music_dir(self) -> Path:
|
|
64
|
+
return self.control / "Music"
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def device_dir(self) -> Path:
|
|
68
|
+
return self.control / "Device"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def sysinfo_extended(self) -> Path:
|
|
72
|
+
return self.device_dir / "SysInfoExtended"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def sysinfo(self) -> Path:
|
|
76
|
+
return self.device_dir / "SysInfo"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _candidate_roots() -> list[Path]:
|
|
80
|
+
"""Possible mount points to probe, in priority order."""
|
|
81
|
+
env = os.environ.get("IPODSYNC_MOUNT")
|
|
82
|
+
if env:
|
|
83
|
+
return [Path(env)]
|
|
84
|
+
roots: list[Path] = []
|
|
85
|
+
if sys.platform == "darwin":
|
|
86
|
+
base_dirs = ["/Volumes"]
|
|
87
|
+
else: # linux and other posix
|
|
88
|
+
user = os.environ.get("USER") or os.environ.get("LOGNAME") or ""
|
|
89
|
+
base_dirs = [f"/media/{user}", "/media", f"/run/media/{user}", "/mnt"]
|
|
90
|
+
for base in base_dirs:
|
|
91
|
+
try:
|
|
92
|
+
roots += sorted(Path(base).iterdir())
|
|
93
|
+
except OSError:
|
|
94
|
+
continue
|
|
95
|
+
return roots
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _probe(vol: Path) -> str:
|
|
99
|
+
"""'ready' — iPod_Control/iTunes readable; 'blocked' — EPERM; 'no' — not an iPod."""
|
|
100
|
+
marker = vol / "iPod_Control" / "iTunes"
|
|
101
|
+
try:
|
|
102
|
+
os.stat(marker)
|
|
103
|
+
return "ready"
|
|
104
|
+
except PermissionError:
|
|
105
|
+
return "blocked"
|
|
106
|
+
except OSError:
|
|
107
|
+
# no iPod_Control here — but the whole volume may be EPERM-locked
|
|
108
|
+
try:
|
|
109
|
+
os.stat(vol)
|
|
110
|
+
return "no"
|
|
111
|
+
except PermissionError:
|
|
112
|
+
return "blocked"
|
|
113
|
+
except OSError:
|
|
114
|
+
return "no"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def probe_ipods(name_hint: str = "iPod") -> tuple[Access, IPod | None, list[Path]]:
|
|
118
|
+
"""Return (state, IPod|None, list of blocked volumes)."""
|
|
119
|
+
ready, blocked = [], []
|
|
120
|
+
for v in _candidate_roots():
|
|
121
|
+
s = _probe(v)
|
|
122
|
+
if s == "ready":
|
|
123
|
+
ready.append(v)
|
|
124
|
+
elif s == "blocked":
|
|
125
|
+
blocked.append(v)
|
|
126
|
+
if ready:
|
|
127
|
+
pick = (next((v for v in ready if v.name == name_hint), None)
|
|
128
|
+
or next((v for v in ready if name_hint.lower() in v.name.lower()), None)
|
|
129
|
+
or ready[0])
|
|
130
|
+
return Access.READY, IPod(pick), blocked
|
|
131
|
+
if blocked:
|
|
132
|
+
return Access.NO_ACCESS, None, blocked
|
|
133
|
+
return Access.NOT_FOUND, None, []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def find_ipod(name_hint: str = "iPod") -> IPod:
|
|
137
|
+
"""Find a ready iPod. Raises with a helpful message on NO_ACCESS / NOT_FOUND."""
|
|
138
|
+
status, ipod, blocked = probe_ipods(name_hint)
|
|
139
|
+
if status is Access.READY:
|
|
140
|
+
return ipod
|
|
141
|
+
if status is Access.NO_ACCESS:
|
|
142
|
+
raise IPodNotFound(NO_ACCESS_HINT.format(vols=", ".join(v.name for v in blocked)))
|
|
143
|
+
raise IPodNotFound(
|
|
144
|
+
"iPod not found. Connect the device (enable Disk Use), or set "
|
|
145
|
+
"IPODSYNC_MOUNT=/path/to/mount."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def wait_for_ipod(timeout: float = 120, interval: float = 2,
|
|
150
|
+
name_hint: str = "iPod", on_wait=None) -> IPod:
|
|
151
|
+
"""Wait until an iPod becomes READY. timeout=0 waits forever.
|
|
152
|
+
|
|
153
|
+
on_wait(status, blocked) is called on each waiting iteration (for logging).
|
|
154
|
+
"""
|
|
155
|
+
start = time.monotonic()
|
|
156
|
+
while True:
|
|
157
|
+
status, ipod, blocked = probe_ipods(name_hint)
|
|
158
|
+
if status is Access.READY:
|
|
159
|
+
return ipod
|
|
160
|
+
if on_wait:
|
|
161
|
+
on_wait(status, blocked)
|
|
162
|
+
if timeout and (time.monotonic() - start) > timeout:
|
|
163
|
+
raise IPodNotFound(f"iPod did not become available within {timeout}s "
|
|
164
|
+
f"(last state: {status.value})")
|
|
165
|
+
time.sleep(interval)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ipodsync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Upload music to an iPod nano 6G/7G without iTunes: SQLite library, hashAB signing, cover art (pure-Python).
|
|
5
|
+
Project-URL: Homepage, https://github.com/buldiei/ipodsync
|
|
6
|
+
Project-URL: Repository, https://github.com/buldiei/ipodsync
|
|
7
|
+
Project-URL: Issues, https://github.com/buldiei/ipodsync/issues
|
|
8
|
+
Project-URL: LinkedIn, https://www.linkedin.com/in/alex-buldiei-b10a9a150/
|
|
9
|
+
Author-email: Alex Buldiei <abuldiei@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: artwork,ipod,itunes,music,nano,sync
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: mutagen>=1.47
|
|
23
|
+
Requires-Dist: pillow>=10.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ipodsync
|
|
29
|
+
|
|
30
|
+
[](https://github.com/buldiei/ipodsync/actions/workflows/ci.yml)
|
|
31
|
+
[](https://pypi.org/project/ipodsync/)
|
|
32
|
+
[](https://pypi.org/project/ipodsync/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
Upload music to an **iPod nano 6G/7G** without iTunes/Apple Music — straight from
|
|
36
|
+
the terminal, on macOS and Linux. The iPod mounts as a plain volume; `ipodsync`
|
|
37
|
+
edits its SQLite library, signs it (hashAB) and **generates cover art itself**.
|
|
38
|
+
|
|
39
|
+
> ⚠️ Alpha. Works on real hardware, but it writes to the player's binary database.
|
|
40
|
+
> **Keep backups** (the tool makes one before every edit) and don't open this iPod
|
|
41
|
+
> in iTunes.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- `list` / `export` — inspect and download tracks from the device (with tags).
|
|
46
|
+
- `add` — upload an MP3, metadata from tags, **cover art attached automatically**
|
|
47
|
+
(from an embedded APIC/covr): shown both in Now Playing and in list/Albums views.
|
|
48
|
+
- `rm` — remove a track (+file), re-sign.
|
|
49
|
+
- playlists — `playlists` / `pl-create` / `pl-add` / `pl-rm` / `pl-del`.
|
|
50
|
+
- `cover` — attach a cover to an already-uploaded track.
|
|
51
|
+
- works on an **empty (wiped) library** too — no iTunes needed to initialize it.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pipx install ipodsync # recommended (isolated), or: pip install ipodsync
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requires Python ≥ 3.9. Pure-Python — no compiler or native library needed.
|
|
60
|
+
|
|
61
|
+
> Note: uploading currently supports **MP3 only**. FLAC/AAC → ALAC transcoding
|
|
62
|
+
> (which will need `ffmpeg`) is on the roadmap and not implemented yet.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ipodsync status # ready / no access / not connected
|
|
68
|
+
ipodsync list
|
|
69
|
+
ipodsync add "Song.mp3" # + cover auto
|
|
70
|
+
ipodsync add "Song.mp3" --no-cover
|
|
71
|
+
ipodsync export ~/Music/ipod --by-album
|
|
72
|
+
ipodsync cover 123456789 --image cover.jpg
|
|
73
|
+
ipodsync rm 123456789 --delete-file
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The iPod is discovered under `/Volumes` (macOS) and `/media`, `/run/media`, `/mnt`
|
|
77
|
+
(Linux) by the presence of `iPod_Control/`. To point at it explicitly, set
|
|
78
|
+
`IPODSYNC_MOUNT=/path/to/mount`.
|
|
79
|
+
|
|
80
|
+
## How it works
|
|
81
|
+
|
|
82
|
+
- **Transport** — mass storage: files are written under `iPod_Control/`.
|
|
83
|
+
- **Database** — SQLite `iTunes Library.itlp/*.itdb` (not `iTunesCDB`, which the
|
|
84
|
+
device regenerates on its own). Only `Locations.itdb` is hash-protected (`.cbk`).
|
|
85
|
+
- **hashAB** — anti-tamper `.cbk` signature (white-box AES). **Pure-Python** port of
|
|
86
|
+
[dstaley/hashab](https://github.com/dstaley/hashab) (public domain), 100/100 test
|
|
87
|
+
vectors. No compiler or native lib — the package installs everywhere.
|
|
88
|
+
- **Cover art** — pure-Python writer for `ArtworkDB` + `F<fmt>_1.ithmb` (RGB565 LE,
|
|
89
|
+
formats 1010/1013/1015/1016), appended incrementally.
|
|
90
|
+
|
|
91
|
+
## Status / roadmap
|
|
92
|
+
|
|
93
|
+
| | Status |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Upload / remove / export / playlists (MP3) | ✅ confirmed on nano 7G |
|
|
96
|
+
| Cover art (pure-Python) | ✅ confirmed on nano 7G |
|
|
97
|
+
| Empty-library bootstrap | ✅ |
|
|
98
|
+
| **Pure-Python hashAB** (no native lib, installs everywhere) | ✅ 100/100 vectors |
|
|
99
|
+
| Cross-platform device discovery (macOS + Linux) | ✅ |
|
|
100
|
+
| FLAC/AAC → ALAC | ⬜ |
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git clone … && cd ipodsync
|
|
106
|
+
python -m venv .venv && . .venv/bin/activate
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
pytest
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT (see [LICENSE](LICENSE)). The vendored hashAB algorithm is public domain.
|
|
114
|
+
Not affiliated with Apple; iPod and iTunes are trademarks of Apple.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
ipodsync/__init__.py,sha256=REzF6I-MS8uPGE59UpRYrGQh79D388XzD2SY0PfZahE,728
|
|
2
|
+
ipodsync/__main__.py,sha256=Pf37VnDwFZ-NTChYi2r9XHtP1jxVecowifdRUeZ4CG4,114
|
|
3
|
+
ipodsync/_art_templates.py,sha256=R53aZ3UkypEaEawD7a1CO1rwc4fKn_L9duq5WsREcg0,4816
|
|
4
|
+
ipodsync/_hashab_gen.py,sha256=giyPzQlobsNUo4uf7mCfq1K2VM7pdZ3xzgPnKWsQw4E,162837
|
|
5
|
+
ipodsync/_hashab_gen2.py,sha256=9UcfJoJ9UbUO0eM_g9NvIaQzQUcoM8Q9yqzH22_cAtg,85793
|
|
6
|
+
ipodsync/_hashab_gen_rt.py,sha256=zvHYc1wNruCyMWoi7CX10HjL4RasXkoD76M8c-3FkNI,4829
|
|
7
|
+
ipodsync/_hashab_py.py,sha256=i6whCvzsHKGh3lo_WqmHmdyuGov5XM1xtc67CHMhzK4,11756
|
|
8
|
+
ipodsync/_hashab_tables.bin,sha256=PfbLKaUk-54FZjOq96VyZXrfX0j_8vIodKSbdPkJiUQ,443788
|
|
9
|
+
ipodsync/_hashab_tables.py,sha256=b5lVY59cGGsfZ70LDFdOyaAeX8SDcg3jjoNKifib9pk,1023
|
|
10
|
+
ipodsync/_lib_templates.py,sha256=tYy0e6VYr_acZ8e8qNDjVABhSGzjCrV2C2AR7CZGFuc,3167
|
|
11
|
+
ipodsync/artwork_writer.py,sha256=jfSON_D5L9G8jT_eGKCV7smn4kJTJFQehUouJ1M-cck,9986
|
|
12
|
+
ipodsync/cbk.py,sha256=GDqTKBagNvjdWzb0K24qPd1KcIPvS7JXbENuV7VCOho,1737
|
|
13
|
+
ipodsync/cli.py,sha256=DG8pG3go4hdfN7OKUpLeAveUmEhE72QdG6jdxhES0rM,11595
|
|
14
|
+
ipodsync/export.py,sha256=uhbPf4zZT_f1UeX-vD3SF149LNDis4wNvIB6CzynIZ8,3044
|
|
15
|
+
ipodsync/hashab.py,sha256=VDCopl-smX4dskISJoCW49MRbsxBOHCxI3XAJgZ8cSc,1555
|
|
16
|
+
ipodsync/importer.py,sha256=upYW2wG-x-9efgWUsI4c2_bX0_612yC6Nq7YUaK64vE,5122
|
|
17
|
+
ipodsync/library.py,sha256=Oat1MoKKzZ3HXMNYHd2hhRXTlORE5bNVpvec9i9WAVQ,16511
|
|
18
|
+
ipodsync/models.py,sha256=M6KlH_sKFLB0IOT6_emhO_N2qFl0F63stgt0MfnvJQw,855
|
|
19
|
+
ipodsync/sysinfo.py,sha256=eVqQouHUeQN1Ui3Pcxpjs2s_XC47eMI4TSTb29-9y_M,1486
|
|
20
|
+
ipodsync/transport.py,sha256=vNbomxeUUdruCfiaVA2XqWZNaWai7vEiLhSA8tli2kY,5361
|
|
21
|
+
ipodsync-0.1.0.dist-info/METADATA,sha256=KZJ1-pThmLNeixe3-_3CzCGv04HiBky12yFzSF7ArvI,4641
|
|
22
|
+
ipodsync-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
23
|
+
ipodsync-0.1.0.dist-info/entry_points.txt,sha256=vVHLPxHbzX23-SnQop2t_EYyOV4cQwAOeE2ecpiDfU8,47
|
|
24
|
+
ipodsync-0.1.0.dist-info/licenses/LICENSE,sha256=Fmqk7PBD9o10kvqvy5N1ojgf--SNLWkCovih-jM9HB4,1069
|
|
25
|
+
ipodsync-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Buldiei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|