Add sync command #9

Open
r2d2-bot wants to merge 3 commits from 3-sync-command into 2-download-command
Collaborator

Closes #3

Adds sync, the second half of the download/sync split: reads saved playlist metadata from .jellyfin-meta/ (no Jellyfin call needed), sanitizes filenames for filesystem safety, and materializes a jellyfin/ subfolder on a target directory (e.g. an MP3 player mount point) containing the sanitized mp3s plus correct .m3u playlists.

  • lib/sync.ts: computeSongRelPaths, computeDesiredFiles, computeDesiredPlaylistFiles, diffDesiredVsExisting (copy/delete/prune), and syncToTarget (with an injectable confirmFn).
  • Deterministic, warn-and-disambiguate handling of sanitized-filename collisions for both songs and playlist .m3u files.
  • Guards: empty staging metadata aborts before touching the target; missing target directory aborts and creates nothing.
  • Stale-playlist confirmation gate: any .m3u on the target no longer part of the synced set triggers a single confirmation prompt (listing all stale playlists) before any copy/delete/write; declining aborts the whole sync with zero filesystem changes.
  • lib/m3uWriter.ts refactored to expose buildM3UContent(), shared by both download's writeM3UFile and sync.
  • index.ts: new sync command, SYNC_TARGET env-var-or-prompt resolution.

Targets 2-download-command (PR #8) per the stacked-PR rule in CONTRIBUTING.md, since #1 (PR #7) and #2 (PR #8) have not merged yet.

Verified with a throwaway (uncommitted) script against fake in-memory playlist/song data and scratch directories — no live Jellyfin server or real device involved. This surfaced and fixed one bug in the original plan draft: pruning now-empty directories used fs.rm(dir, { recursive: false }), which throws on any directory regardless of emptiness in Node and was being silently swallowed by a catch; switched to fs.rmdir(), which correctly removes empty directories.

Closes #3 Adds `sync`, the second half of the download/sync split: reads saved playlist metadata from `.jellyfin-meta/` (no Jellyfin call needed), sanitizes filenames for filesystem safety, and materializes a `jellyfin/` subfolder on a target directory (e.g. an MP3 player mount point) containing the sanitized mp3s plus correct `.m3u` playlists. - `lib/sync.ts`: `computeSongRelPaths`, `computeDesiredFiles`, `computeDesiredPlaylistFiles`, `diffDesiredVsExisting` (copy/delete/prune), and `syncToTarget` (with an injectable `confirmFn`). - Deterministic, warn-and-disambiguate handling of sanitized-filename collisions for both songs and playlist `.m3u` files. - Guards: empty staging metadata aborts before touching the target; missing target directory aborts and creates nothing. - Stale-playlist confirmation gate: any `.m3u` on the target no longer part of the synced set triggers a single confirmation prompt (listing all stale playlists) before any copy/delete/write; declining aborts the whole sync with zero filesystem changes. - `lib/m3uWriter.ts` refactored to expose `buildM3UContent()`, shared by both `download`'s `writeM3UFile` and `sync`. - `index.ts`: new `sync` command, `SYNC_TARGET` env-var-or-prompt resolution. Targets `2-download-command` (PR #8) per the stacked-PR rule in CONTRIBUTING.md, since #1 (PR #7) and #2 (PR #8) have not merged yet. Verified with a throwaway (uncommitted) script against fake in-memory playlist/song data and scratch directories — no live Jellyfin server or real device involved. This surfaced and fixed one bug in the original plan draft: pruning now-empty directories used `fs.rm(dir, { recursive: false })`, which throws on any directory regardless of emptiness in Node and was being silently swallowed by a catch; switched to `fs.rmdir()`, which correctly removes empty directories.
Reads saved playlist metadata from .jellyfin-meta/ (no Jellyfin call
needed), sanitizes filenames for filesystem safety, and syncs a
jellyfin/ subfolder on a target directory (e.g. an MP3 player mount)
to contain exactly the sanitized mp3s and .m3u files for the
currently downloaded playlists — copying missing files, deleting
stale ones, and pruning now-empty directories. Handles deterministic
sanitized-filename collision disambiguation for both songs and
playlists, guards against empty staging metadata and a missing
target directory, and gates any stale-playlist deletion behind an
interactive confirmation (with a full no-op abort on decline).

Refactors lib/m3uWriter.ts to expose buildM3UContent() so download
and sync share the same .m3u content-building logic.

Closes #3

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EncvBjUkvZfEutPhyYJHeB
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EncvBjUkvZfEutPhyYJHeB
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 3-sync-command:3-sync-command
git switch 3-sync-command

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch 2-download-command
git merge --no-ff 3-sync-command
git switch 3-sync-command
git rebase 2-download-command
git switch 2-download-command
git merge --ff-only 3-sync-command
git switch 3-sync-command
git rebase 2-download-command
git switch 2-download-command
git merge --no-ff 3-sync-command
git switch 2-download-command
git merge --squash 3-sync-command
git switch 2-download-command
git merge --ff-only 3-sync-command
git switch 2-download-command
git merge 3-sync-command
git push origin 2-download-command
Sign in to join this conversation.
No reviewers
No labels
m3u_files
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jshaver/jellyfin-to-m3u!9
No description provided.