Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc5bcfa
fix(build): re-sign the compiled CLI ad hoc on macOS when Bun leaves …
Pred8or Oct 27, 2026
22dab37
fix(core): treat an iTunesCDB-only library as an existing iPod database
Pred8or Oct 27, 2026
a7754e4
fix(doctor): pass the injected identity assessor into the repair pipe…
Pred8or Oct 27, 2026
22cad5a
build(libgpod): vendor the open HashAB signer for iPod nano 6G/7G dat…
Pred8or Oct 27, 2026
3031e58
feat(libgpod): sign iPod nano 6G/7G databases with the vendored HashA…
Pred8or Oct 27, 2026
534cc74
fix(devices): describe nano 6G/7G as signed but not yet writable
Pred8or Oct 27, 2026
caf1d7c
feat(libgpod): give iPod nano 6G/7G SQLite libraries iTunes' schema l…
Pred8or Oct 27, 2026
bb0f779
feat(libgpod): write the iPod nano 6G/7G firmware categories to SQLite
Pred8or Oct 27, 2026
27f33d0
feat(libgpod): keep iPod nano 6G/7G play data and exact dates across …
Pred8or Oct 27, 2026
9c7d699
feat(devices): enable sync for the iPod nano 7th generation
Pred8or Oct 27, 2026
0bcf4fe
fix(transcode): enforce the device's H.264 profile and level for pass…
Pred8or Oct 27, 2026
5c0fc96
fix(cli): resolve the configured video quality after the device is open
Pred8or Oct 27, 2026
19f08f8
feat(core): add an iPod nano 7th generation video profile
Pred8or Oct 27, 2026
469dbbc
feat(cli): add --custom-bitrate and document customBitrate below the …
Pred8or Oct 27, 2026
83ff66f
feat(transcode): encode MP3 at a custom or reduced bitrate in LAME's …
Pred8or Oct 27, 2026
a9b4e9d
feat(libgpod-node): expose the iTunesDB sort fields
Pred8or Oct 27, 2026
f3e00d7
feat(sync): sync composer, grouping, track/disc totals and sort field…
Pred8or Oct 27, 2026
065f8bd
fix(sync): converge pending tag corrections with other updates in one…
Pred8or Oct 27, 2026
c9efcd9
fix(sync): converge tag corrections together with a transform apply o…
Pred8or Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/custom-bitrate-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"podkit": minor
"@podkit/core": patch
---

Add `--custom-bitrate <kbps>` to `podkit sync` and document `customBitrate` as the way below the `low` preset

The presets stop at `low` (~128 kbps). The existing `customBitrate` setting (global, per device, or `PODKIT_CUSTOM_BITRATE`) replaces the preset's bitrate with any value from 64 to 320 kbps and is the cap for every lossy target, including lossy reduction of MP3/AAC sources when `[bitrate].reduce` converts. It can now also be set for a single run with `--custom-bitrate`, and `sync --dry-run` shows it on the quality line (`Quality: low (custom bitrate 96 kbps)`).

For example, `customBitrate = 96` with `[bitrate] reduce = "always"` encodes lossless sources to AAC at 96 kbps and re-encodes MP3s more than 25% above that to AAC at 96 kbps, while MP3s at or near 96 kbps are copied. Without `reduce`, MP3 and AAC files are still copied untouched. Presets are unchanged.

MP3 output now follows a numeric target too: with a custom bitrate or a lossy reduction target, LAME encodes in average-bitrate (ABR) mode with that bitrate as its average instead of the preset's `-q:a` quality level, which has no bitrate bound. MP3s already encoded with a custom bitrate are not re-encoded; `--force-transcode` re-encodes those made from lossless sources.
15 changes: 15 additions & 0 deletions .changeset/extended-tag-fidelity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"podkit": minor
"@podkit/core": minor
"@podkit/libgpod-node": minor
---

Sync composer, grouping, track/disc totals and sort fields to iPods

The directory adapter now reads composer, grouping, total tracks, total discs and the sort fields (sort title, artist, album, album artist, composer) from file tags. They are written to the iPod database when a track is added, and a change to any of them at the source is detected as a metadata correction and updated in place without copying the file again. A tag removed at the source is cleared on the device. A field the source cannot report is left untouched rather than cleared; Subsonic sources do not report these fields yet.

Mass-storage change detection is unchanged: podkit does not compare these fields there. A multi-disc track added to a mass-storage device now gets the `Album (disc N)` folder its path template asks for, since the total disc count reaches it. The comment field still carries podkit's sync tag; file comments are not copied.

`@podkit/libgpod-node` adds `sortTitle`, `sortArtist`, `sortAlbum`, `sortAlbumArtist` and `sortComposer` to `Track`, `TrackInput` and `updateTrack`.

**One-time effect:** the first sync after upgrading updates the iPod database entry of every track whose files carry these tags, and clears these fields on the iPod for tracks whose files lack them, since a folder collection is authoritative for its tags. No audio is copied or re-encoded for it.
6 changes: 6 additions & 0 deletions .changeset/itunescdb-only-library.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@podkit/core': patch
'podkit': patch
---

Recognise an iPod library that only has the compressed `iTunesCDB`, as Apple software leaves it on an iPod nano 6th/7th generation. Such devices were reported as uninitialised, and `podkit device scan`/`sync` suggested `podkit device init`.
5 changes: 5 additions & 0 deletions .changeset/macos-compiled-binary-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'podkit': patch
---

Re-sign the compiled macOS binary ad hoc when Bun leaves an invalid signature behind, so the CLI no longer exits with status 137 on launch on Apple Silicon.
7 changes: 7 additions & 0 deletions .changeset/nano-7g-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@podkit/libgpod-node': patch
'@podkit/devices-ipod': minor
'podkit': minor
---

podkit now syncs the iPod nano 7th generation, confirmed on a device with firmware 1.0.4. Its libgpod build writes the hidden category containers and iTunes' schema level that the nano 7th generation needs to show a library, and keeps the plays, skips, ratings and bookmarks the device records across syncs. The nano 6th generation stays read-only: its library writes are covered only by synthetic tests, which its unsupported-device message now says.
5 changes: 5 additions & 0 deletions .changeset/nano-firmware-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@podkit/libgpod-node': patch
---

For the iPod nano 6th and 7th generation, libgpod now writes the hidden category containers (Music, Videos, …) and iTunes' database schema level that the nano 7th generation firmware needs to show a library, and keeps the Videos category's identity in the `iTunesCDB`. Such a write fails, instead of leaving a library the device cannot browse, when the device's SQLite post-process commands are missing or fail or when a database cannot be written completely.
7 changes: 7 additions & 0 deletions .changeset/nano-native-hashab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@podkit/libgpod-node': patch
'@podkit/devices-ipod': patch
'podkit': patch
---

podkit's libgpod build now signs iPod nano 6th and 7th generation databases itself, instead of needing an external `hashab` module. Signing alone does not make them writable: the nano 7th generation shows no music from a library written this way, although its signatures are valid, so both stay read-only with this change. Their support notes and unsupported-device messages now say so, instead of naming the missing module.
10 changes: 10 additions & 0 deletions .changeset/nano7-video-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"podkit": minor
"@podkit/core": minor
---

Add an iPod nano 7th generation video profile

Videos for a Nano 7 are now encoded for its 240x432 screen held sideways: fitted into 432x240 with the aspect ratio kept (a 16:9 film becomes 426x240), H.264 Baseline level 3.0, at most 30 fps, AAC-LC audio in M4V. Bitrates are the 320x240 tiers scaled by the extra pixels: 1000, 800, 550 and 400 kbps for `max`, `high`, `medium` and `low`.

The profile is chosen from podkit's own device identification, so it applies even though libgpod reports the Nano 7 as an unknown generation. Before, an unknown generation fell back to the iPod Classic profile (640x480 Main). The Nano 6 has no video playback and is unchanged.
8 changes: 8 additions & 0 deletions .changeset/video-level-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"podkit": patch
"@podkit/core": patch
---

Keep video within the device's H.264 profile and level

A video was copied to the iPod untouched when its container, codec, profile and resolution fit, without looking at its H.264 level, and the video sync path did not pass the probed profile, level, bitrate or frame rate to that check at all. A level above the device's (3.0 for the 320x240 iPods, 3.1 for the Classic) or an unknown level now means the file is transcoded, and the probed bitrate and frame rate limits apply too. When the profile is reported, only Baseline, and Main on the Classic, is copied; any other profile, such as High 10, is transcoded. The macOS VideoToolbox encoder is now given the level constraint as well as the profile.
9 changes: 9 additions & 0 deletions .changeset/video-quality-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"podkit": patch
---

Apply the configured video quality during sync

`podkit sync` resolved the video quality before the device was open, when its capabilities are still unknown, and an unknown device resolves to no video quality, so `quality`, `videoQuality` and their per-device and environment-variable forms were ignored and video was always encoded at `high` unless `--quality` or `--video-quality` was passed. The configured chain is now resolved once the device is open; `--video-quality` still overrides `--quality`, which overrides the device and then the global settings.

**One-time effect:** if you configured a video quality other than `high`, the first sync after upgrading re-transcodes the videos that were encoded at `high`, to the quality you configured.
8 changes: 4 additions & 4 deletions .github/workflows/build-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/**') }}

# Install build toolchain (only when building prebuilds from scratch)

Expand Down Expand Up @@ -250,7 +250,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-x64-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-x64-${{ hashFiles('tools/prebuild/**') }}

- name: Install Alpine build deps (full)
if: steps.prebuild-cache.outputs.cache-hit != 'true' && steps.static-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -448,7 +448,7 @@ jobs:
uses: actions/cache@v4
with:
path: static-deps
key: static-deps-linux-arm64-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-arm64-${{ hashFiles('tools/prebuild/**') }}

- name: Build prebuild in Alpine
if: steps.prebuild-cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -685,7 +685,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-${{ matrix.arch }}-gnu-${{ hashFiles('tools/prebuild/build-static-deps.sh', 'tools/prebuild/build-linux-glibc.sh') }}
key: static-deps-linux-${{ matrix.arch }}-gnu-${{ hashFiles('tools/prebuild/**') }}

# Full build toolchain. ubuntu:20.04 ships meson/ninja/cmake too old for
# recent glib/gdk-pixbuf/libxml2, so those come from pip, not apt.
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/prebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/build-static-deps.sh', 'tools/prebuild/build-linux-glibc.sh') }}
key: static-deps-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('tools/prebuild/**') }}

# ---- Cache MISS: install full build toolchain and build from source ----

Expand Down Expand Up @@ -129,6 +129,13 @@ jobs:
# Linux glibc verification happens inside tools/prebuild/build-linux-glibc.sh
# (the shared script). No additional step needed here.

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Test libgpod nano HashAB signing
run: bash tools/nano7/test-libgpod.sh

- name: Run tests
working-directory: packages/libgpod-node
run: bun run test:unit
Expand Down Expand Up @@ -164,7 +171,7 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.STATIC_DEPS_DIR }}
key: static-deps-linux-x64-musl-${{ hashFiles('tools/prebuild/build-static-deps.sh') }}
key: static-deps-linux-x64-musl-${{ hashFiles('tools/prebuild/**') }}

- name: Install Alpine build deps (full)
if: steps.cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -217,6 +224,9 @@ jobs:
echo "ERROR: Found unexpected dynamic dependencies"; exit 1
fi

- name: Test libgpod nano HashAB signing
run: bash tools/nano7/test-libgpod.sh

- name: Run tests
working-directory: packages/libgpod-node
run: bun run test:unit
Expand Down Expand Up @@ -259,6 +269,8 @@ jobs:
curl -fsSL https://bun.sh/install | bash
bun install

bash tools/nano7/test-libgpod.sh

cd packages/libgpod-node
npx prebuildify --napi --strip
mv prebuilds/linux-arm64 prebuilds/linux-arm64-musl
Expand Down
14 changes: 12 additions & 2 deletions devices/ipod.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ iPod_Control/
| Shuffle 1-2G | Yes | No | No | No | — | None |
| Nano 1-2G | Yes | No | Yes | No | — | 176x132 |
| Nano 3-5G | Yes | Yes | Yes | Yes | ipod-nano-3g | Varies |
| Nano 7G | Yes | Yes | Yes | Yes | ipod-nano-7g | 240x432 |
| Video 5G | Yes | Yes | Yes | Yes | ipod-video-5g | 320x240 |
| Video 5.5G | Yes | Yes | Yes | Yes | ipod-video-5g | 320x240 |
| Classic 6G | Yes | Yes | Yes | Yes | ipod-classic | 320x240 |
Expand All @@ -293,8 +294,7 @@ iPod_Control/
| Generation | Reason |
|------------|--------|
| Shuffle 3-4G | Buttonless; writing their iTunesSD playback database is unverified on hardware |
| Nano 6G | Writes an iTunesDB format libgpod cannot produce; read is untested |
| Nano 7G | Reads and archives cleanly (hardware-confirmed); writing needs the hashAB signature libgpod cannot produce without an external blob podkit doesn't ship |
| Nano 6G | Reads and archives (hardware-confirmed); its library writes are covered only by synthetic tests, not by hardware |
| Touch (all) | iOS protocol + cryptographic iTunesDB signing |
| iPhone (all) | iOS protocol + cryptographic iTunesDB signing |
| iPad | iOS protocol + cryptographic iTunesDB signing |
Expand All @@ -305,6 +305,11 @@ iPod_Control/
|---------|-----------|---------------|-------------------|-----------|-----|---------|
| ipod-video-5g | 320x240 | Baseline | 768 kbps | 128 kbps | 30 | Video 5G/5.5G, Nano 3-5G |
| ipod-classic | 640x480 | Main | 2500 kbps | 160 kbps | 30 | Classic 6G/6.5G/7G |
| ipod-nano-7g | 432x240 | Baseline 3.0 | 1000 kbps | 128 kbps | 30 | Nano 7G |

Nano 7G: 432x240 is the screen size, not the decoder's limit (see
`DEVICE_PROFILES` in `packages/podkit-core/src/video/types.ts`). Nano 6G has no
video playback.

## Quirks & Limitations

Expand Down Expand Up @@ -348,3 +353,8 @@ Fully implemented in podkit. Key implementation files:
- **Video handler:** `packages/podkit-core/src/sync/handlers/video-handler.ts`
- **Generation metadata:** `packages/podkit-core/src/ipod/generation.ts`
- **Model identification:** `packages/podkit-core/src/device/ipod-models.ts`

## Nano 6G/7G signing

The nano 6th and 7th generation sign `iTunesCDB` and `Locations.itdb.cbk` with hashAB, which podkit's libgpod builds compute ([ADR-030](../docs/adr/adr-030-native-nano-hashab.md); checks in `tools/nano7/`).
For them it also writes iTunes' category containers and schema level into the SQLite library and keeps the play data the device records (`tools/nano7/README.md`); the nano 7G syncs, the nano 6G stays read-only.
75 changes: 75 additions & 0 deletions docs/adr/adr-030-native-nano-hashab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: 'ADR-030: Link an Open HashAB Signer Into libgpod'
description: Build libgpod with the Unlicense dstaley/hashab C sources instead of loading an external hashab blob, so the iPod nano 6th/7th generation databases can be signed. Signing alone does not make those generations syncable.
sidebar:
order: 31
---

# ADR-030: Link an Open HashAB Signer Into libgpod

## Status

**Proposed** (2026-10-27)

## Context

The iPod nano 6th and 7th generation (SysInfoExtended `DBVersion` 5) sign
`iTunesCDB` and `iTunes Library.itlp/Locations.itdb.cbk` with HashAB.
libgpod 0.8.3 already writes both files, including the SQLite databases and
the artwork, but `itdb_hashAB.c` computes the signature only through a
`hashab` module loaded from `LIBGPOD_BLOB_DIR`, and fails the write without
it. podkit ships no such module.

[dstaley/hashab](https://github.com/dstaley/hashab) is an Unlicense C
implementation of the same `calcHashAB(target, sha1, uuid, rnd)` function,
with 100 reference vectors. It takes the eight-byte FireWire GUID of a nano,
not an iOS device identifier.

## Decision

- Vendor the signer unmodified in `tools/prebuild/vendor/hashab` at a pinned
commit, with its licence and provenance.
- `tools/prebuild/prepare-libgpod-nano.sh` applies
`tools/prebuild/libgpod-nano.patch` and copies the sources into
`src/nano_hashab/` before `autoreconf`, in both the macOS developer build
and the static prebuild dependencies. The patch replaces the module loader
with a direct call and builds the sources as an internal convenience
library, with `-fwrapv` as upstream does, and with their global symbols
renamed and hidden so that they neither clash with nor leak from libgpod.
- Before signing, the patch requires a 16-digit hexadecimal `FireWireGUID`
(an optional `0x` prefix is accepted) and otherwise fails the write with an
error. libgpod's CDB normalisation, SQLite generation and artwork writing
are unchanged.
- The static dependency cache records a fingerprint of the patch and the
vendored sources, and the CI cache keys hash all of `tools/prebuild/`, so a
change to either rebuilds libgpod.
- No runtime dependency, subprocess or N-API surface is added. Python is used
only by the developer checks in `tools/nano7/`.

## Verification

- The 100 upstream vectors and a third-party reference
`Locations.itdb`/`.cbk` pair reproduce byte for byte.
- `tools/nano7/test-libgpod.sh`, run in the prebuild workflow, lets the
patched libgpod create, reparse, resave and delete a synthetic track for a
nano 7th generation (from the repository's USB SysInfoExtended capture) and
a nano 6th generation (synthetic capabilities), and checks SQLite integrity
and both signatures. Malformed and iOS-sized GUIDs fail; a 5th generation
iPod still gets an unsigned `iTunesDB`.
- On a nano 7th generation (firmware 1.0.4), Apple-written `iTunesCDB` and
`Locations.itdb.cbk` verify with libgpod's normalisation (hashing scheme 3)
and the device GUID.

## Consequences

Signing is necessary but not sufficient. On a nano 7th generation (firmware
1.0.4), a library written by this libgpod has valid signatures, yet the
firmware shows no music. `nano_6g` and `nano_7g` therefore keep
`support.access = 'read-only'`; the nano 6th generation's writes are covered
only by synthetic tests. Enabling sync for either generation needs a complete
library write that is verified on the device.

The nano 7th generation became syncable once libgpod also writes the
firmware's category containers and iTunes' schema level and keeps the play
data the device records (`tools/nano7/README.md`); the nano 6th generation
stays read-only.
1 change: 1 addition & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This section contains Architecture Decision Records (ADRs) documenting significa
| [ADR-026](./adr-026-dual-libc-linux-distribution.md) | Dual-libc Linux Distribution (glibc + musl) | Accepted | 2026-08-04 |
| [ADR-027](./adr-027-lima-vm-substrate-consolidation.md) | Lima VM Substrate Consolidation (`@podkit/lima`) | Accepted | 2026-08-24 |
| [ADR-028](./adr-028-substrate-agnostic-device-harness.md) | Substrate-Agnostic Device Harness | Accepted | 2026-09-07 |
| [ADR-030](./adr-030-native-nano-hashab.md) | Link an Open HashAB Signer Into libgpod | Proposed | 2026-10-27 |

## What is an ADR?

Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/device/identity-support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ is the fallback on USB *transport* error
| iPod nano 1G–2G | **no (2G does not respond; 1G untested)** | **yes** | SCSI (host only) |
| iPod nano 3G | **yes** (first USB-capable) | untested | USB |
| iPod nano 4G–5G | **yes** | yes | USB |
| iPod nano 6G | yes (inquiry) | yes | Inquiry works but the generation is **read-only** (iTunesDB format podkit cannot write) |
| iPod nano 7G | yes (inquiry) | yes | Inquiry works; the generation is **read-only**, hardware-confirmed (hashAB signing unavailable — libgpod needs an external blob podkit doesn't ship — but reads and archives cleanly) |
| iPod nano 6G | yes (inquiry) | yes | Inquiry works but the generation is **read-only**, hardware-confirmed (reads and archives; its library writes are covered only by synthetic tests) |
| iPod nano 7G | yes (inquiry) | yes | USB |
| iPod shuffle 1G–2G | no | **yes** | SCSI (host only) |
| iPod shuffle 3G–4G | yes | yes | Inquiry works but sync is **refused** (bdhs iTunesSD write unverified; read-only tier) |
| iPod touch / iPhone / iPad | no | no | No disk mode at all — out of scope |
Expand Down
Loading