playlists + lyrics: setlists with pages of their own, and the words timed to the voice

This commit is contained in:
KeelrLeek committed 2026-08-17 18:05:31 -05:00
1 parent c4ff6269a3
commit df3d15553a
49 files changed
+14419 -599

No files matched your search

+111 -8
View File
@@ -140,14 +140,85 @@ export const getSettings = () => invoke("get_settings");
export const setSettings = (patch) => invoke("set_settings", patch);
export const forgetCredentials = () => invoke("forget_credentials");
export const pickFolder = () => invoke("pick_folder");
// ---- playlists (see src-tauri/src/playlists.rs) ---------------------------
// Rows are OCCURRENCES with their own ids: the same recording may appear
// twice, and every mutation addresses item ids, never positions.
export const listPlaylists = () => invoke("list_playlists");
export const createPlaylist = (name) => invoke("create_playlist", { name });
export const createPlaylist = (name, kind = null) => invoke("create_playlist", { name, kind });
export const renamePlaylist = (id, name) => invoke("rename_playlist", { id, name });
export const setPlaylistDescription = (id, description) =>
invoke("set_playlist_description", { id, description: description ?? null });
export const setPlaylistPinned = (id, pinned) => invoke("set_playlist_pinned", { id, pinned });
export const duplicatePlaylist = (id, name) => invoke("duplicate_playlist", { id, name });
export const deletePlaylist = (id) => invoke("delete_playlist", { id });
export const playlistTracks = (id) => invoke("playlist_tracks", { id });
export const addToPlaylist = (playlistId, tracks) =>
invoke("add_to_playlist", { playlistId, tracks });
export const removeFromPlaylist = (playlistId, position) =>
invoke("remove_from_playlist", { playlistId, position });
export const playlistItems = (id) => invoke("playlist_items", { id });
export const playlistAddItems = (playlistId, items, at = null) =>
invoke("playlist_add_items", { playlistId, items, at });
export const playlistRemoveItems = (playlistId, itemIds) =>
invoke("playlist_remove_items", { playlistId, itemIds });
export const playlistSetOrder = (playlistId, orderedIds) =>
invoke("playlist_set_order", { playlistId, orderedIds });
// Repair one row: fields null = leave as stored; path non-null relinks the
// row to a playable file.
export const playlistRelinkItem = (playlistId, itemId, fields = {}) =>
invoke("playlist_relink_item", {
playlistId,
itemId,
title: fields.title ?? null,
artist: fields.artist ?? null,
album: fields.album ?? null,
durationSecs: fields.durationSecs ?? null,
path: fields.path ?? null,
artPath: fields.artPath ?? null,
});
// Cover as a data URL: custom master, else composed mosaic, else null.
export const playlistCover = (id, maxEdge = null) =>
invoke("playlist_cover", { id, maxEdge });
export const playlistCoverPick = (id) => invoke("playlist_cover_pick", { id });
export const playlistCoverClear = (id) => invoke("playlist_cover_clear", { id });
// Import: probe opens a file dialog and stages a reviewed draft; text stages
// a pasted tracklist; commit writes the accepted draft in one transaction.
export const playlistImportProbe = () => invoke("playlist_import_probe");
export const playlistImportFolder = () => invoke("playlist_import_folder");
export const playlistImportText = (text) => invoke("playlist_import_text", { text });
export const playlistImportCommit = (name, description, kind, entries) =>
invoke("playlist_import_commit", { name, description: description ?? null, kind, entries });
// Export via a save dialog; format ∈ m3u8 | xspf | csv | json. Resolves to
// the written path, or null when the dialog was cancelled.
export const playlistExport = (id, format) => invoke("playlist_export", { id, format });
// Local-library candidates for the unresolved-row repair panel.
export const libraryTrackSearch = (query) => invoke("library_track_search", { query });
// ---- lyrics (see src-tauri/src/lyrics.rs) ----------------------------------
export const lyricsGet = (args) =>
invoke("lyrics_get", {
title: args.title,
artist: args.artist,
album: args.album ?? null,
durationSecs: args.durationSecs ?? null,
path: args.path ?? null,
refresh: args.refresh ?? null,
});
export const lyricsSearch = (title, artist) => invoke("lyrics_search", { title, artist });
export const lyricsSelect = (title, artist, durationSecs, candidateId) =>
invoke("lyrics_select", { title, artist, durationSecs: durationSecs ?? null, candidateId });
export const lyricsSetOffset = (title, artist, durationSecs, offsetMs) =>
invoke("lyrics_set_offset", { title, artist, durationSecs: durationSecs ?? null, offsetMs });
export const lyricsSaveText = (title, artist, durationSecs, text) =>
invoke("lyrics_save_text", { title, artist, durationSecs: durationSecs ?? null, text });
export const lyricsClear = (title, artist, durationSecs) =>
invoke("lyrics_clear", { title, artist, durationSecs: durationSecs ?? null });
export const lyricsSetAutoSync = (title, artist, durationSecs, enabled) =>
invoke("lyrics_set_auto_sync", { title, artist, durationSecs: durationSecs ?? null, enabled });
// Offline vocal-reduction render (band-limited center cancel — honest DSP,
// not a learned separator). Resolves to { path, cached } of a playable WAV.
export const lyricsPrepareInstrumental = (path) =>
invoke("lyrics_prepare_instrumental", { path });
// Vocal-band activity envelope for the sync editor ({ frameMs, frames }).
export const lyricsVocalActivity = (path) => invoke("lyrics_vocal_activity", { path });
// Persist hand-synced lines as the track's own document (wins the waterfall).
export const lyricsSaveLines = (title, artist, durationSecs, lines) =>
invoke("lyrics_save_lines", { title, artist, durationSecs: durationSecs ?? null, lines });
export const catalogSearch = (query) => invoke("catalog_search", { query });
export const rememberFusedArtistPortrait = (artistId, portrait) =>
invoke("remember_fused_artist_portrait", { artistId, portrait });
@@ -232,6 +303,11 @@ export const debugStats = () => invoke("debug_stats");
// source folder is never written to.
export const importScan = (root) => invoke("import_scan", { root });
export const importSessionGet = () => invoke("import_session_get");
// The source folder relative to the library folder, and back again. Saving
// both is what lets a review started on one machine resume on another after
// the drive lands at a different letter.
export const importPortableRoot = (root) => invoke("import_portable_root", { root });
export const importResolveRoot = (saved, rel) => invoke("import_resolve_root", { saved, rel });
export const importSessionSet = (state) => invoke("import_session_set", { state });
export const importSessionClear = () => invoke("import_session_clear");
export const importGetDest = () => invoke("import_get_dest");
@@ -239,11 +315,29 @@ export const importSetDest = (dir, layout = null) => invoke("import_set_dest", {
export const importApply = (sourceRoot, albums) => invoke("import_apply", { sourceRoot, albums });
// On-demand per-record identification (Deezer try-list → MB fallback) and
// the artwork panel's cover report. Both read-only.
export const importIdentify = (artist, title) => invoke("import_identify", { artist, title });
// `title` is the record's own claim (tags); `folder` is the second witness,
// asked after it — a record whose tags are wrong still has its folder name.
// `shape` is everything ELSE the scan knows — track count, track titles, year,
// and where the files are so Rust can hash the record's own cover. That is what
// the discography pass compares when no phrase can find the record; the cover
// itself never crosses IPC (a master can be 8 MB).
export const importIdentify = (artist, title, folder = null, shape = {}) =>
invoke("import_identify", {
artist, title, folder,
tracks: shape.tracks ?? null,
trackTitles: shape.trackTitles ?? null,
year: shape.year ?? null,
sourceRoot: shape.sourceRoot ?? null,
rel: shape.rel ?? null,
// Bounds the cover hash to the record's own audio — a folded release
// gathers from these, an ordinary one from its folder alone.
parts: shape.parts ?? null,
});
export const importCoverReport = (artist, title, referenceUrl) =>
invoke("import_cover_report", { artist, title, referenceUrl });
// The record's own artwork, measured (embedded first, else folder images).
export const importLocalArt = (sourceRoot, rel) => invoke("import_local_art", { sourceRoot, rel });
export const importLocalArt = (sourceRoot, rel, parts) =>
invoke("import_local_art", { sourceRoot, rel, parts });
// Prune rows whose files are gone, then index anything new — the real rescan.
export const rescanLibrary = () => invoke("rescan_library");
// Download a found album copy into (Cache)\_audition\<rel>\ for the version
@@ -252,6 +346,15 @@ export const rescanLibrary = () => invoke("rescan_library");
export const importAudition = (username, rel, files, startIndex = 0) =>
invoke("import_audition", { username, rel, files, startIndex });
export const onImportAudition = (cb) => listen("import:audition", (e) => cb(e.payload));
// Which of a found copy's files the pen does not (fully) hold. Transfers die
// with the app and the arrived-count dies with them; the files on disk are
// what is left to ask.
export const importAuditionMissing = (rel, files) =>
invoke("import_audition_missing", { rel, files });
// Empty a record's pen when its peer is abandoned; returns the bytes freed.
// The pen is keyed by RECORD, so without this the dropped peer's files sit
// beside the replacement's until the session is cleared.
export const importAuditionClear = (rel) => invoke("import_audition_clear", { rel });
// Measured facts about one audio file (the version card's quality chips).
export const importProbeFile = (path) => invoke("import_probe_file", { path });
export const onImportScan = (cb) => listen("import:scan", (e) => cb(e.payload));