Compare commits
10
Commits
4471db3aef
...
1128881e8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1128881e8d | ||
|
|
cad3d1339b | ||
|
|
453d5df0da | ||
|
|
84963e38bd | ||
|
|
deb3f3da7e | ||
|
|
60779c41c3 | ||
|
|
a4ada725a2 | ||
|
|
04114eabae | ||
|
|
162c68769f | ||
|
|
c9905fbcff |
@@ -22,9 +22,13 @@ jobs:
|
||||
steps:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
# Wails v3 resolves GTK4 + WebKitGTK 6.0 by default; webkit2gtk-4.1 +
|
||||
# gtk3 was v2's stack and is now only the `-tags gtk3` escape hatch.
|
||||
# These must match the PKGBUILD's depends=() — makepkg installs
|
||||
# nothing itself, so a mismatch fails at link time, not at check time.
|
||||
pacman -Syu --noconfirm --needed \
|
||||
base-devel git go nodejs pnpm curl sudo \
|
||||
webkit2gtk-4.1 gtk3 alsa-lib
|
||||
webkitgtk-6.0 gtk4 alsa-lib
|
||||
|
||||
- name: Create unprivileged build user
|
||||
run: |
|
||||
|
||||
+14
-19
@@ -171,7 +171,7 @@ jobs:
|
||||
working-directory: /src
|
||||
run: make ui-test
|
||||
|
||||
# frontend/wailsjs is generated by `wails`, not by `go generate`,
|
||||
# frontend/bindings is generated by `wails3`, not by `go generate`,
|
||||
# so the codegen pre-commit hook does not cover it.
|
||||
- name: Bindings are current
|
||||
working-directory: /src
|
||||
@@ -184,7 +184,9 @@ jobs:
|
||||
run: make skill-check
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# Job 2: the real app, under a virtual display. #
|
||||
# Job 2: the real app, headless. v3's `-tags server` needs no #
|
||||
# display, so the Xvfb this job used to wrap everything in is gone. #
|
||||
# `dbus-run-session` stays, for MPRIS. #
|
||||
# ---------------------------------------------------------------- #
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -220,7 +222,7 @@ jobs:
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
ca-certificates curl git jq build-essential pkg-config \
|
||||
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev \
|
||||
xvfb dbus dbus-x11 ffmpeg libasound2t64 \
|
||||
dbus dbus-x11 ffmpeg libasound2t64 \
|
||||
alsa-utils libasound2-plugins pulseaudio pulseaudio-utils
|
||||
|
||||
- name: Clone repo at this commit
|
||||
@@ -247,24 +249,17 @@ jobs:
|
||||
apt-get install -y -qq --no-install-recommends nodejs
|
||||
corepack enable
|
||||
|
||||
# scripts/seed-sandbox.sh drives the real AddLibrary binding
|
||||
# through playwright-cli, so the CLI has to be on PATH.
|
||||
- name: Playwright CLI
|
||||
run: npm install -g @playwright/cli
|
||||
|
||||
# @playwright/cli is gone with v2. seed-sandbox.sh drove the real
|
||||
# AddLibrary binding through a browser because `window.go` was the
|
||||
# only way in; v3 answers the same call over HTTP, so the seed is
|
||||
# curl now and needs no CLI, no second Chromium and no shared
|
||||
# PLAYWRIGHT_BROWSERS_PATH revision dance.
|
||||
- name: Browsers
|
||||
working-directory: /src/e2e
|
||||
run: |
|
||||
set -eu
|
||||
# PLAYWRIGHT_BROWSERS_PATH unifies the *location*, not the
|
||||
# *revisions*: @playwright/cli bundles its own playwright-core
|
||||
# pinned to a different Chromium build than @playwright/test,
|
||||
# so each installs its own into the shared directory. Drop
|
||||
# either line and the other fails with "Browser chromium is
|
||||
# not installed; expected executable at ...".
|
||||
pnpm install --frozen-lockfile
|
||||
npx playwright install --with-deps chromium webkit
|
||||
playwright-cli install-browser chromium
|
||||
|
||||
# oto/v3 talks to libasound directly, and a container has no
|
||||
# PulseAudio socket to fall back on — so it needs a default device
|
||||
@@ -287,10 +282,10 @@ jobs:
|
||||
#
|
||||
# PulseAudio's null sink is timer-scheduled and does pace — the
|
||||
# 0.76 s over is the buffer draining, not a rate error; 12 s of
|
||||
# audio takes 13.5 s. Verified under the private session bus and
|
||||
# Xvfb that dev-headless.sh runs the app in. It needs no system
|
||||
# D-Bus and no kernel module, which is why it is reachable from a
|
||||
# container at all.
|
||||
# audio takes 13.5 s. Verified under the private session bus
|
||||
# dev-headless.sh runs the app in. It needs no system D-Bus and
|
||||
# no kernel module, which is why it is reachable from a container
|
||||
# at all.
|
||||
- name: Real-time audio sink
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
+16
@@ -54,3 +54,19 @@ coverage/
|
||||
.cache/
|
||||
tmp/
|
||||
bin/
|
||||
|
||||
# Task's checksum cache, written by every `wails3 task` run.
|
||||
.task/
|
||||
|
||||
# Generated by build/linux/Taskfile.yml's generate:dotdesktop from
|
||||
# build/config.yml on every build, and consumed by the deb/rpm/AppImage
|
||||
# packaging tasks that depend on it. A derived file with one source.
|
||||
build/linux/yellowjacket.desktop
|
||||
|
||||
# `wails3 task common:update:build-assets` regenerates the mobile trees
|
||||
# whether or not anything asks for them. This is a desktop player and
|
||||
# cannot target iOS/Android, so their includes: entries are dropped from
|
||||
# Taskfile.yml and the trees themselves are not carried — ignored rather
|
||||
# than deleted-and-rediscovered on every asset refresh.
|
||||
build/ios/
|
||||
build/android/
|
||||
|
||||
@@ -21,12 +21,21 @@ here has disappeared.
|
||||
Fifteen things cost a cycle each the first time. They are here, not in a
|
||||
reference, because you need them *before* the failure, not after.
|
||||
|
||||
- **Time out every binding call.** A bound Go method called with wrong
|
||||
argument types makes the backend log `error parsing arguments` and
|
||||
**never fire the callback**, so the promise hangs forever. Use
|
||||
`window.__yjEvents.call(path, args, ms)` (browser) or `callBinding`
|
||||
(specs), never a bare `window.go.…`. When one hangs anyway,
|
||||
`make dev-logs` — `.dev/app.log` is the only place the reason appears.
|
||||
- **Call a binding through the bridge.** `window.go` does not exist
|
||||
under Wails v3 — the bindings are bundled modules, not a global — so
|
||||
use `window.__yjEvents.call(path, args, ms)` (browser) or
|
||||
`callBinding` (specs). Both post to the runtime's own endpoint by
|
||||
method name, so they work on any page, including one with no init
|
||||
script.
|
||||
|
||||
A bad call now *rejects*, and says why: a wrong type comes back as a
|
||||
TypeError naming the argument, a wrong count as
|
||||
`expects 4 arguments, got 3`, an unknown method as a ReferenceError.
|
||||
Under v2 the backend logged `error parsing arguments` and never fired
|
||||
the callback, so `.dev/app.log` was the only place the reason
|
||||
appeared and the timeout was the only thing that made the mistake
|
||||
visible. The timeout is still there, but now it means a genuinely
|
||||
hung request.
|
||||
- **Nothing is clickable on a fresh `YJ_HOME`.** `<first-run-wizard>`
|
||||
intercepts all pointer events until a library exists, and the click
|
||||
fails with a Playwright interception error that reads like a selector
|
||||
@@ -387,10 +396,10 @@ never get the shell back.
|
||||
when iterating on a single package:
|
||||
|
||||
```bash
|
||||
go test -tags webkit2_41 ./backend/player/ # the app build
|
||||
go test -tags webkit2_41 -run TestName ./backend/player/
|
||||
go test -tags "webkit2_41 indexbuild" ./backend/explore/... ./cmd/... # dump importer
|
||||
go test -tags "webkit2_41 dev" ./backend/testctl/... # control surface
|
||||
go test ./backend/player/ # the app build
|
||||
go test -run TestName ./backend/player/
|
||||
go test -tags indexbuild ./backend/explore/... ./cmd/... # dump importer
|
||||
go test -tags dev ./backend/testctl/... # control surface
|
||||
```
|
||||
|
||||
Forgetting the tag gives a build error that looks like a missing
|
||||
|
||||
@@ -74,9 +74,14 @@ behind `YJ_TESTCTL=1`, which `scripts/dev-headless.sh` sets and
|
||||
- **`snapshot` writes a file, it does not print the tree.** The
|
||||
command prints a path under `outputDir`; read that. Only the tail
|
||||
is echoed.
|
||||
- **Three separate browser caches.** `playwright-cli`, `@playwright/test`
|
||||
- **Two separate browser caches.** `@playwright/test`
|
||||
(`make e2e-setup`) and the Vitest provider (`make ui-setup`) each
|
||||
download their own Chromium. One working is no guarantee for the next.
|
||||
download their own Chromium. One working is no guarantee for the
|
||||
other. There used to be a third: `playwright-cli` was a *required*
|
||||
dependency because `scripts/seed-sandbox.sh` drove `AddLibrary`
|
||||
through a real page, `window.go` being v2's only way in. v3 answers
|
||||
the same call over HTTP, so the seed is `curl` now and the CLI is
|
||||
only an exploratory convenience.
|
||||
- **`getByRole('button', { name })` matches substrings.** "Play" also
|
||||
matches "Add queue to playlist"; transport controls need
|
||||
`exact: true`.
|
||||
|
||||
@@ -35,7 +35,7 @@ Then:
|
||||
|
||||
```bash
|
||||
make generate # sqlc + templ
|
||||
go test -tags webkit2_41 ./backend/database/ # migration + column-order tests
|
||||
go test ./backend/database/ # migration + column-order tests
|
||||
make test
|
||||
```
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# The component and store tier (`make ui-test`)
|
||||
|
||||
313 tests in a real Chromium in ~2 s with no Wails, no backend, no
|
||||
seeded library and no virtual display. This is the cheapest coverage
|
||||
available and where the bulk of UI regression belongs.
|
||||
757 tests in a real Chromium with no Wails, no backend, no seeded
|
||||
library and no virtual display. This is the cheapest coverage available
|
||||
and where the bulk of UI regression belongs.
|
||||
|
||||
```bash
|
||||
make ui-setup # once: the Vitest provider's own Chromium
|
||||
@@ -15,12 +15,20 @@ make ui-test UI_ARGS='store/queue' # filter
|
||||
|
||||
## How it works
|
||||
|
||||
`frontend/wailsjs/` is a pure passthrough — every binding is
|
||||
`window.go[svc][Type][Method](args)`, every runtime call is
|
||||
`window.runtime.X(...)`. So `frontend/test/support/wails-fake.ts`
|
||||
replaces **those two globals and nothing else**, and the tests then
|
||||
exercise the *real* generated bindings and the *real* store code. No
|
||||
module mocking, and no second description of the Wails layer.
|
||||
Wails v3 routes every runtime call — bindings, event emits, window,
|
||||
dialogs, clipboard — through one IPC transport, and `setTransport()` is
|
||||
a public seam for replacing it. So
|
||||
`frontend/test/support/wails-fake.ts` replaces **that and nothing
|
||||
else**, and the tests then exercise the *real* generated bindings, the
|
||||
*real* runtime and the *real* store code. No module mocking, and no
|
||||
second description of the Wails layer.
|
||||
|
||||
A binding call carries a *method ID* (an FNV-1a hash of the Go method's
|
||||
fully-qualified name), not a name, so the fake derives the ID → path
|
||||
map from the generated tree at setup: each package's `index.ts`
|
||||
re-exports its service under the Go type's real name, which is the one
|
||||
place that casing survives. A path that never maps records as `#<id>`
|
||||
and fails the assertion naming it.
|
||||
|
||||
```ts
|
||||
emit(Events.QueueChanged, payload); // push a backend event
|
||||
@@ -31,11 +39,19 @@ lastArgs('queue.Queue.SetQueue');
|
||||
const el = await fixture('now-playing'); // mount; shadow()/text() query it
|
||||
```
|
||||
|
||||
The dispatcher mirrors wails' own `desktop/events.js`, including
|
||||
`maxCallbacks` expiry and the fact that a frontend `EventsEmit`
|
||||
notifies local listeners *before* Go.
|
||||
Delivery is not mirrored — `emit()` goes through the runtime's own
|
||||
`window._wails.dispatchWailsEvent`, which is the entry point the
|
||||
backend's push uses, so listener expiry and ordering are the runtime's
|
||||
real code. What *is* mirrored is one line of Go: how
|
||||
`EventManager.Emit` packs variadic data into an event's single `data`
|
||||
field (none is null, one is the value, more is the slice).
|
||||
|
||||
## Four things that will cost you time
|
||||
A frontend `Events.Emit` no longer notifies local listeners before Go —
|
||||
v3 calls the backend, which sends the event back out to every window.
|
||||
The page still sees its own emit, one round trip later rather than
|
||||
synchronously.
|
||||
|
||||
## Five things that will cost you time
|
||||
|
||||
- **Store singletons are constructed at module import**, before any test
|
||||
can stub. `test/setup.ts` therefore carries import-time defaults for
|
||||
@@ -54,6 +70,13 @@ notifies local listeners *before* Go.
|
||||
- **`@lit-labs/virtualizer` never produces two identical frames**, so
|
||||
`toMatchScreenshot` on `<queue-panel>` fails with "could not capture a
|
||||
stable screenshot" rather than a diff. Assert on its rows instead.
|
||||
- **A v3 binding settles several microtasks after a v2 one did** — it
|
||||
goes through `Call()`, an async `runtimeCallWithID`, the transport and
|
||||
a `CancellablePromise`, where v2's `window.go` proxy resolved one
|
||||
promise. `fixture()` drains microtasks between two renders so a
|
||||
component that loads in `firstUpdated` is loaded when it returns.
|
||||
Microtasks and not a timer, deliberately: a timer hangs forever under
|
||||
the suites that install fake ones.
|
||||
|
||||
Visual baselines are font-hinting and compositing sensitive, which is
|
||||
why they are opt-in: they only mean anything on the machine that
|
||||
@@ -61,14 +84,15 @@ recorded them.
|
||||
|
||||
## Bindings
|
||||
|
||||
`frontend/wailsjs/` is generated by `wails`, **not** by `go generate`,
|
||||
`frontend/bindings/` is generated by `wails3`, **not** by `go generate`,
|
||||
so the pre-commit codegen check does not cover it — a renamed Go bound
|
||||
method first shows up at runtime, as a call that never settles.
|
||||
|
||||
```bash
|
||||
make bindings-check # ~1.5 s, also a pre-commit hook
|
||||
make bindings-check # ~3.5 s warm, also a pre-commit hook
|
||||
make bindings # regenerate for real
|
||||
```
|
||||
|
||||
The generator rewrites `wailsjs/runtime/*` as mode 755 every run; that
|
||||
is churn, not drift, and the check ignores it.
|
||||
No build tags are passed: the generator is a static analyser that sees
|
||||
only the configuration it is told about, and the one that matters is
|
||||
the one users run, which is the default tag set.
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# 009 — Wails v3 migration
|
||||
|
||||
**Status:** Phases 0–3 complete. The Go side is entirely on v3: nothing
|
||||
in the tree imports `wails/v2`, all three lint and test configurations
|
||||
are green, and `go build .` produces a running binary. **Phase 4
|
||||
(bindings) is next, and until it lands the app builds but the frontend
|
||||
cannot talk to it** — `frontend/wailsjs/` is v2's tree and nothing
|
||||
regenerates it.
|
||||
**Status:** Phases 0–7 complete. CI needed nothing (it had ridden along
|
||||
with Phases 1–6); **packaging needed everything** — both recipes still
|
||||
called v2's CLI, installed from v2's output path, and built from a
|
||||
scaffold's `yjref` metadata. Locally green: `make lint` and `make test`
|
||||
(all three build configurations), `tsc --noEmit`, `make build-prod`,
|
||||
757 Vitest tests across all 58 files, `make e2e` 92/92 on chromium,
|
||||
`make bindings-check`, `make skill-check`, `make commit-check`,
|
||||
`make css-check`, and `make perf` runs and reports real binding counts
|
||||
again. **WebKit is unverified** — it is CI-only by design — so the
|
||||
merge waits on a green CI run.
|
||||
**Branch:** `wails-v3`, off `main` at `edb13a6`.
|
||||
**Created:** 2026-08-13
|
||||
**Phase 0 run:** 2026-08-13 against **v3.0.0-beta.8**
|
||||
@@ -293,6 +297,14 @@ Taskfile tree — a genuinely larger and more visible build surface.
|
||||
`make build-prod` still strips and UPX-compresses; `make skill-check`
|
||||
passes; `grep -r webkit2_41` returns nothing.
|
||||
|
||||
> **Corrected after the fact.** `make build-prod` strips and trims
|
||||
> (`-trimpath -ldflags="-w -s"`, 28.8 MB against the dev build's
|
||||
> 38 MB) but **does not UPX-compress**: that was v2's `wails build
|
||||
> -upx` flag, and v3's Taskfile has no equivalent. Neither `make
|
||||
> build-dev` nor `make build-prod` was actually run when Phase 1 was
|
||||
> recorded — both were broken until the shim below. Whether to
|
||||
> reintroduce UPX is a packaging decision for Phase 7, not a port.
|
||||
|
||||
**Est.** Half a session. Low risk, high churn.
|
||||
|
||||
### Phase 1 — what actually landed
|
||||
@@ -568,6 +580,99 @@ is still a pre-commit hook and a CI step (`ci.yml:176`); the 12
|
||||
|
||||
**Est.** One session, mostly codemod-and-verify.
|
||||
|
||||
### Phase 4 — what actually landed
|
||||
|
||||
`frontend/wailsjs/` is deleted; `frontend/bindings/` is committed in
|
||||
its place. **272 methods across 12 services**, and the acceptance
|
||||
criteria hold: no `SetContext` binding survives and there is no
|
||||
`context` model, which is Phase 2c's `ServiceStartup` port showing up
|
||||
where it was predicted to.
|
||||
|
||||
**The alias absorbs the prefix, so the codemod was one line per site.**
|
||||
`@go/*` points at `bindings/yellowjacket/backend/*` rather than at
|
||||
`bindings/`, so `@go/library/Library` became `@go/library/library.js`
|
||||
— the same shape, lowercased, plus the extension the generated tree
|
||||
uses internally. The models cluster was the only structural change:
|
||||
v2's single `models.ts` of namespaces became one module per package, so
|
||||
`import type { library } from '@go/models'` is
|
||||
`import type * as library from '@go/library/models.js'` and every
|
||||
`library.Track` usage is untouched. **Two sites the plan's inventory
|
||||
missed**, both because they are outside `src/`: `frontend/index.ts`'s
|
||||
three imports, found by the vite build rather than by `tsc` (the alias
|
||||
resolves for the compiler and not for the bundler when the path's case
|
||||
is wrong on a case-sensitive filesystem).
|
||||
|
||||
**The 102 type errors were not migration damage. They were v2's
|
||||
generator being caught lying**, and that is worth stating because the
|
||||
temptation is to suppress them. A Go `nil` slice marshals to JSON
|
||||
`null` and always has; v2 typed it `T[]`. A Go named string type is a
|
||||
closed set; v2 typed it `string`. v3 types them `T[] | null` and as a
|
||||
real TS `enum`, and there is no generator flag to turn either off —
|
||||
correctly, since both are true.
|
||||
|
||||
So the fix is one seam rather than 78 patches: **`utils/binding.ts`
|
||||
states the app's actual contract at the only place it is true.** `list`
|
||||
yields `[]` for a nil slice, `dict`/`dictByName` yield `{}` for a nil
|
||||
map and drop null-valued keys (which loses nothing —
|
||||
`noUncheckedIndexedAccess` already makes every read `V | undefined`, so
|
||||
an absent key and a nil value are indistinguishable downstream), and
|
||||
`compact` is the same thing for a map arriving as a *field*. All three
|
||||
also return a plain `Promise`: v3 bindings return a
|
||||
`CancellablePromise` and nothing in this app cancels one, so letting it
|
||||
inward would put a Wails type in every store signature for a capability
|
||||
none of them use.
|
||||
|
||||
Where a nullable slice is a model *field* rather than a return value
|
||||
there is no boundary to put it at, and those are `?? []` at the point
|
||||
of use — `score.candidates`, `shelf.albums`, `page.shelves`,
|
||||
`view.items`.
|
||||
|
||||
Four smaller consequences, all of them v3 being stricter:
|
||||
|
||||
- **`createFrom` is gone.** v3 emits interfaces (`-i`), so the three
|
||||
`explore.ShelfPage.createFrom({…})` sites are object literals and
|
||||
`new tracklist.Column()` is one too.
|
||||
- **An enum needs a value import.** `import type * as download` cannot
|
||||
reach `Format`, so `download-clients.ts` imports it separately — and
|
||||
its local format list is now `Format[]` rather than `string[]`, which
|
||||
is a small win v2 could not have given.
|
||||
- **Four test fixtures widen an enum field back to its value union**
|
||||
(`` state?: `${Job['state']}` ``) rather than casting, so the
|
||||
fixtures stay literal-checked.
|
||||
- `job-store.ts` still hand-mirrors `JobState`/`JobKind` as string
|
||||
unions beside the generated enums. Left alone deliberately; folding
|
||||
them together is a redesign, not a port.
|
||||
|
||||
**`bindings-check` got simpler and slower.** The `chmod` dance and
|
||||
`core.fileMode=false` diff are gone with v2's generator (which wrote
|
||||
three runtime files 755); a check for added or removed *files* is new,
|
||||
because a renamed service is now a renamed module rather than a changed
|
||||
line. It runs in ~3.5 s warm against v2's ~1.5 s, ~20 s on a cold build
|
||||
cache — v3's generator is a static analyser over the whole package
|
||||
graph.
|
||||
|
||||
**On step 6 (the tag set): the answer is "no flag", and that is the
|
||||
deliberate answer.** The generator sees only the configuration it is
|
||||
told about, and the one that matters is the one users run — which,
|
||||
since Phase 1, is the *default* tag set. Neither of this repo's other
|
||||
two configurations (`indexbuild`, `dev`) adds a bound service, so
|
||||
generating under them would only widen the API past what ships. Written
|
||||
into `scripts/bindings-check.sh` so it is not re-derived. Note this is
|
||||
*not* what Wails' own Taskfile does (`-tags server,production`), for
|
||||
the good reason that its shipped artifact is the Docker image.
|
||||
|
||||
**One error is left, and it is Phase 5's.**
|
||||
`test/harness.test.ts:69` imports `EventsEmit` from the shim, which
|
||||
does not export one — because nothing in `src/` emits from the
|
||||
frontend, and adding an export to production code to satisfy a test
|
||||
would be the wrong way round. The test underneath it is a bigger
|
||||
problem than its import: it asserts that a frontend emit notifies
|
||||
in-page listeners before reaching Go, which v2 did and **v3 does
|
||||
not** — `Events.Emit` in `@wailsio/runtime` calls the backend and
|
||||
touches no local listener. That is exactly the "re-derive the fake,
|
||||
don't port it" risk the plan flagged, arriving early. `make ui-test`
|
||||
is broken regardless: the fake still fakes `window.go`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — The Vitest fake (`make ui-test`, 480 tests)
|
||||
@@ -599,6 +704,93 @@ that needs changing is evidence the fake is wrong, not the test.
|
||||
**Est.** One session. This is where the official estimate stops
|
||||
applying.
|
||||
|
||||
### Phase 5 — what actually landed
|
||||
|
||||
**757 tests pass across all 63 files, and one test file changed.** The
|
||||
plan's design survived and got smaller, because v3 has a seam v2 did
|
||||
not.
|
||||
|
||||
**`setTransport()` is the whole fake.** v3 routes *every* runtime call
|
||||
— bindings, event emits, window, dialogs, clipboard, screens — through
|
||||
one IPC transport, and replacing it is public, documented API. So the
|
||||
fake covers strictly more than v2's two globals did while being
|
||||
shorter, and the tests still exercise the real generated bindings, the
|
||||
real runtime and the real store code.
|
||||
|
||||
**The dispatcher is deleted rather than re-derived.** The plan said to
|
||||
re-derive `Listener`/`notify()` against v3 instead of porting them; the
|
||||
better answer is that neither is needed. `emit()` goes through
|
||||
`window._wails.dispatchWailsEvent`, the exact entry point the backend's
|
||||
push uses, so delivery, `maxCallbacks` expiry and the post-dispatch
|
||||
filter are the runtime's own code; registration and unregistration are
|
||||
`Events.OnMultiple` / `Off` / `OffAll`. What *is* mirrored is one line
|
||||
of Go — how `EventManager.Emit` packs variadic data into an event's
|
||||
single `data` field (none is null, one is the value, more is the
|
||||
slice), which is invisible when wrong and shows up as a store reading
|
||||
`undefined` off its payload.
|
||||
|
||||
One thing stayed non-public: the listener registry, for
|
||||
`listenerNames()`. `listener.js` has no entry in the package's exports
|
||||
map, so `vitest.config.mts` aliases it. It buys the one question the
|
||||
public surface cannot answer — did importing a store subscribe it —
|
||||
and if Wails moves the file the import throws at setup, which is loud.
|
||||
|
||||
**A binding carries an ID, not a name, and the map has to be
|
||||
complete.** `$Call.ByID(2822423495)` is FNV-1a over
|
||||
`yellowjacket/backend/home.Service.GetShelves`, so the fake computes the
|
||||
same hash — deriving the FQN from the generated tree rather than
|
||||
writing it down. The Go type's casing survives in exactly one place,
|
||||
each package's `index.ts` (`export { Library }`); the filename cannot
|
||||
tell you `frontendutil.ts` is `FrontendUtil`. Building the map lazily
|
||||
as paths are mentioned does not work: 21 assertions read `calls()` with
|
||||
no argument and compare the whole list of paths, including methods no
|
||||
test stubs. An unmapped ID records as `#<id>`, which fails the
|
||||
assertion naming it.
|
||||
|
||||
**Two things had to change that are not the fake**, and both are
|
||||
findings rather than accommodations:
|
||||
|
||||
- **`fixture()` drains microtasks between two renders.** A v3 binding
|
||||
settles several hops later than v2's — `Call()`, an async
|
||||
`runtimeCallWithID`, the transport, a `CancellablePromise`, against
|
||||
v2's one resolved promise — and the tests were already written as
|
||||
though `fixture()` meant "mounted *and loaded*". Fixing it there
|
||||
rather than in each test is what kept this to one test-file edit.
|
||||
Microtasks and **not** `setTimeout`: the first attempt used a timer,
|
||||
which hung `transport.test.ts` for 45 s because it installs fake ones.
|
||||
- **`tracklist-store` keeps its defaults on an empty answer.**
|
||||
`GetTrackListColumns` substitutes `tracklist.DefaultColumns` only when
|
||||
the whole config section is missing — a section that exists with no
|
||||
columns returns nothing, and a track list with no columns is not what
|
||||
that means. Until v3 this was accidental: the binding was typed
|
||||
`Column[]`, an absent answer arrived as `undefined`, and `.map` threw
|
||||
into the `catch` that restores the defaults. Phase 4's `list()` turned
|
||||
that into an honest empty list and the accident stopped working.
|
||||
|
||||
**The one test file edited was `harness.test.ts`, and it was asserting
|
||||
something no longer true.** v2's `EventsEmit` notified in-page listeners
|
||||
*before* Go, so a frontend emit was observable synchronously. v3's
|
||||
`Events.Emit` does not touch the local registry at all: it calls the
|
||||
backend, and `EventProcessor.Emit` sends the event back out to every
|
||||
window, including the emitting one. The page still sees its own emit,
|
||||
one round trip later. The test says that now, and the fake reproduces
|
||||
it with a microtask. That is the "re-derive, don't port" risk paying
|
||||
off — ported blindly, this would have looked like a store bug.
|
||||
|
||||
**`make ui-test` still cannot complete in one run on this machine, and
|
||||
that is not this migration.** A single browser session dies partway
|
||||
through the 58 files it queues, with "Cannot connect to the iframe"
|
||||
after ~5 s. It reproduces **unchanged at `c9905fb`**, the commit before
|
||||
Phase 4 — checked in a worktree, not assumed — so it is a resource
|
||||
limit here (6 GB available, 9 GB already in swap), not a regression.
|
||||
Run in batches of six it is 757 passed, 0 failed. If CI is green on the
|
||||
single run, nothing needs doing; if it is not, that is a pre-existing
|
||||
problem to file separately rather than something Phase 5 introduced.
|
||||
|
||||
**Not verified: `make ui-visual`.** Screenshot baselines only mean
|
||||
anything on the machine that recorded them, and nothing here changes
|
||||
what a component renders.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — E2E harness and testctl
|
||||
@@ -666,6 +858,118 @@ spec edits.
|
||||
|
||||
**Est.** One to two sessions. The largest and riskiest phase.
|
||||
|
||||
### Phase 6 — what actually landed
|
||||
|
||||
**`make e2e` is green on chromium: 92 passed, 0 failed.** Three of the
|
||||
four things this phase replaced came out better than what they
|
||||
replaced, and the fourth — `window.go` enumeration — turned out not to
|
||||
be needed at all.
|
||||
|
||||
**6d decided itself.** The plan asked whether `-tags server` was worth
|
||||
adopting; it was not optional. `dev-headless.sh` ran a `-tags dev`
|
||||
binary whose `app_dev.go` parsed `-devserver` / `-assetdir` straight
|
||||
out of `os.Args`, and that file went with v2 — so the harness had **no
|
||||
server at all**, not a worse one. `-tags dev,server` is a first-class
|
||||
mode, it needs no display, and testctl mounts on it unchanged. **Xvfb
|
||||
is gone** from the script and from CI, which retires the "Xvfb is not
|
||||
optional" note the script had carried since plan 005. `dbus-run-session`
|
||||
stays, for MPRIS, exactly as before.
|
||||
|
||||
**6a hooks two places, and the outbound one is the good surprise.**
|
||||
Inbound is `window._wails.dispatchWailsEvent` — the entry point the
|
||||
backend's own push uses. It is wrapped by *pre-creating* the object the
|
||||
runtime keeps (`window._wails = window._wails || {}`) and putting an
|
||||
accessor on the one property, which is simpler than v2's
|
||||
accessor-on-`window`, where the whole object was replaced.
|
||||
|
||||
Outbound is **`fetch`**. v3 routes every runtime call — bindings, event
|
||||
emits, window, dialogs, clipboard — through one POST to
|
||||
`/wails/runtime`. There is no global to wrap the way v2's
|
||||
`window.runtime` could be, and it does not matter: one hook sees calls
|
||||
from any module, needs no walk of an object graph, and cannot miss a
|
||||
call made before the harness looked, which is what v2's "runs twice"
|
||||
dance in `measure.mjs` existed to work around.
|
||||
|
||||
**`__yjEvents.call` is now HTTP, and that is what unblocked everything
|
||||
else.** It posts by *method name*, so it depends on nothing in the
|
||||
app's bundle and works on a page with no init script. That is what lets
|
||||
`seed-sandbox.sh` **drop `playwright-cli` entirely** — it drove
|
||||
`AddLibrary` through a real browser only because `window.go` was v2's
|
||||
one way in — taking with it a global npm install, a second Chromium,
|
||||
and the `PLAYWRIGHT_BROWSERS_PATH` revision dance in CI. A seed is
|
||||
`curl` now and still produced by running the app.
|
||||
|
||||
**6b: option (1), and it costs less than feared.**
|
||||
`e2e/support/method-ids.mjs` derives `methodID → pkg.Type.Method` from
|
||||
`frontend/bindings/` by reading the id literal beside the function that
|
||||
sends it — no hashing, nothing to drift. Plain `.mjs` rather than `.ts`
|
||||
because `measure.mjs` runs under bare `node`, and one derivation is
|
||||
better than two that can disagree. `harness.spec`'s enumeration was not
|
||||
worth replacing in kind: "is this the real app" is now asked of the
|
||||
runtime (`_wails.clientId`, `dispatchWailsEvent`) and of the backend
|
||||
(a real method answers, an invented one is refused), which is a better
|
||||
question than `Object.keys`.
|
||||
|
||||
**6c: nothing to do.** testctl's mount on the asset handler works in
|
||||
both modes and is shared with `/artist-images/`; `ServiceOptions{Route}`
|
||||
would be churn. `Deps.Context` stays a function, because
|
||||
`testctl.Register` still runs in `NewYellowJacketApp`, before any
|
||||
context exists.
|
||||
|
||||
**Four bugs, and the migration is how each surfaced.**
|
||||
|
||||
- **The cross-service wiring never ran headless.** Phase 2 hung it off
|
||||
`Common.ApplicationStarted`, which is the right *moment* and the
|
||||
wrong *mechanism*: server mode emits no application events at all
|
||||
(`setupCommonEvents` is an explicit no-op under `-tags server`). So
|
||||
the desktop build wired itself and the harness build did not — "No
|
||||
player set, cannot load track", the queue with no `TrackLoader`, a
|
||||
track that changed the queue and then silently did nothing. It is a
|
||||
service registered last now (`backend/startup.go`), which gets the
|
||||
ordering from the mechanism rather than from an event: services start
|
||||
in registration order, on the main goroutine, in every mode.
|
||||
- **Six specs called `SetQueue` with three of its four arguments.** v2
|
||||
accepted the call and filled the gap with a zero value; v3 answers
|
||||
`expects 4 arguments, got 3`. `NO_QUEUE_SOURCE` says explicitly what
|
||||
was being supplied silently.
|
||||
- **`requested-badge`'s cleanup was a no-op.** It read `window.go`
|
||||
inside a type assertion and `return`ed on `if (!svc)` — the silent
|
||||
cleanup its own comment was written to prevent, one migration later,
|
||||
which is why the spec failed against its own leftovers. It posts to
|
||||
the runtime endpoint now, which needs no bridge and no global.
|
||||
- **`SearchIndex.Search` trusted a startup latch.** `IsReady()` is set
|
||||
once, so rows staged by a spec afterwards were unsearchable, and
|
||||
three specs passed only when an earlier one happened to flip it —
|
||||
order-dependent, and reproducibly red in isolation. `shelves.go` had
|
||||
already fixed exactly this and left `hasCatalogRows` behind; the
|
||||
search path uses it as the fallback, with the latch still the fast
|
||||
path.
|
||||
|
||||
**Two spec edits, both deletions of assertions about v2.**
|
||||
`harness.spec` checked `Object.keys(window.go)` and that a bad call
|
||||
*hung* — the second being a test that the harness's own invented
|
||||
deadline fired, since v2 gave it nothing else. And `album-actions`
|
||||
asserted a `.tracklist-legend` that **`dcc40b1` deleted on `main`**:
|
||||
that spec has been failing since, verified in a worktree, and what
|
||||
replaced it (the dimming and its `aria-disabled`) is covered in
|
||||
`frontend/test/components/album-actions.test.ts`.
|
||||
|
||||
**One thing to know before trusting a local run.** `dev-headless.sh`
|
||||
does not set `YJ_CORE_INDEX_URL`, so a local `make e2e` fetches the
|
||||
real 1.1 M-row explore artifact and then behaves differently from CI
|
||||
(which points it at a dead address at the job level) — slower, and with
|
||||
testctl's snapshot/restore copying an enormous table set. Run it the
|
||||
way CI does:
|
||||
|
||||
```
|
||||
YJ_CORE_INDEX_URL="http://127.0.0.1:1/none.tar.zst" make dev-headless SEED=default
|
||||
```
|
||||
|
||||
**Not verified: WebKit.** Playwright's Linux WebKit links Ubuntu
|
||||
libraries Arch does not provide, so it remains CI-only — which is
|
||||
precisely why `ci.yml`'s `if: ${{ !cancelled() }}` on that step still
|
||||
matters.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — CI and packaging
|
||||
@@ -693,6 +997,83 @@ spec edits.
|
||||
|
||||
**Est.** Half a session.
|
||||
|
||||
### Phase 7 — what actually landed
|
||||
|
||||
Most of the bullets above had already ridden along with Phases 1–6:
|
||||
`ci.yml`'s apt lists were `libwebkitgtk-6.0-dev libgtk-4-dev` on both
|
||||
jobs, Xvfb and `@playwright/cli` were gone from it, the PKGBUILD's
|
||||
`depends=()` was `webkitgtk-6.0 gtk4`, and `nfpm.yaml` shipped the GTK4
|
||||
dependency set. **What had not been checked is whether any of the
|
||||
packaging recipes still work**, and neither did.
|
||||
|
||||
**Both of them were still calling v2's CLI.** `go tool wails3 build
|
||||
-clean -trimpath -ldflags …` fails outright — `flag provided but not
|
||||
defined: -clean`. v3's `build` takes `-tags`, `-obfuscated` and
|
||||
`-garbleargs` and nothing else, because the build is a Taskfile tree
|
||||
now and `-trimpath`/`-w -s` live in the production task's own flags.
|
||||
Both recipes also installed from `build/bin/`, which is v2's output
|
||||
path; v3 writes to `bin/`, and `build/` is *tracked build assets*.
|
||||
Neither had been run since Phase 1 — the same gap the corrected note
|
||||
under Phase 1 records about `make build-dev`.
|
||||
|
||||
**The version stamp needed a seam, and it is this repo's one edit to
|
||||
the scaffold Taskfiles.** `wails3 build` has no `-ldflags`, and
|
||||
`build:native` computes `BUILD_FLAGS` in its own `vars:`, so a CLI
|
||||
variable cannot override it. `LDFLAGS_EXTRA` is appended *inside* the
|
||||
production `-ldflags` string in `build/linux/Taskfile.yml` and
|
||||
`build/darwin/Taskfile.yml` (identical on both, so the Homebrew formula
|
||||
has one invocation rather than two), empty by default so `make
|
||||
build-dev` and `make build-prod` are unchanged. Verified end to end,
|
||||
not by reading the template: `wails3 task build LDFLAGS_EXTRA="-X
|
||||
'main.version=v9.9.9' …"` produces a binary that logs `version: v9.9.9`
|
||||
on startup.
|
||||
|
||||
**The tasks invoke `wails3` by bare name**, which is why the Makefile
|
||||
has `scripts/toolbin` — and neither packaging recipe had it, so both
|
||||
would have died at the first sub-task even with correct flags. Both
|
||||
prepend it now.
|
||||
|
||||
**Bundling is a separate step from building in v3**, which the formula
|
||||
did not know: `task build` produces a bare binary on *both* platforms,
|
||||
and the macOS `.app` is `task package`. The formula's `Dir["build/bin/
|
||||
*.app"]` would have found nothing and `odie`'d.
|
||||
|
||||
**The build assets were a scaffold's, not this app's** — and this is
|
||||
the find that mattered most. `build/darwin/Info.plist` named
|
||||
`CFBundleExecutable` **`yjref`** (the scratchpad app Phase 1 scaffolded
|
||||
from) and `com.example.yjref`; `nfpm.yaml` packaged `./bin/yjref` to
|
||||
`/usr/local/bin/yjref`; the `.desktop` template said "A yjref
|
||||
application"; the Windows manifest said `com.example.yjref`. A macOS
|
||||
`.app` built from that plist would not have launched. They are
|
||||
generated from `build/config.yml`, so the fix is
|
||||
`wails3 task common:update:build-assets` and filling that file's `info`
|
||||
block — which had also never been filled from `wails.json`, contrary to
|
||||
Phase 1 step 3. Two consequences recorded in place: nfpm's `homepage`
|
||||
and `license` are **not** derived from `config.yml` and survive the
|
||||
refresh (they were `wails.io` / `MIT`), and the refresh regenerates
|
||||
`build/ios/` and `build/android/`, which are gitignored rather than
|
||||
deleted-and-rediscovered on every run.
|
||||
|
||||
**Documentation.** `make skill-check` passes; `.pi/` needed no changes
|
||||
(Phase 6 had already retired `window.go` and Xvfb from it). `README.md`
|
||||
still told a contributor to `go install wails/v2/cmd/wails` and
|
||||
`apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev`. CLAUDE.md gained
|
||||
a **Packaging** section for the four Taskfile facts above, and its
|
||||
lifecycle, bindings, harness, events and CI sections were rewritten
|
||||
onto v3 — including the point of the whole migration, that
|
||||
`TestNoDirectRuntimeEmits`'s justification is now the *weaker* one.
|
||||
|
||||
**Locally green after the change:** `make lint` and `make test` (all
|
||||
three build configurations), `tsc --noEmit`, `make css-check`,
|
||||
`make bindings-check`, `make skill-check`, `make commit-check`,
|
||||
`make build-prod`, 757 Vitest tests across all 58 files (batched — the
|
||||
single-run failure still reproduces and is still this machine's
|
||||
resource limit, unchanged from Phase 5), and `make e2e` **92/92 on
|
||||
chromium**.
|
||||
|
||||
**Still unverified: WebKit**, and it is CI-only by design. That is the
|
||||
one thing standing between this branch and a merge.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — What v3 unlocks (explicitly out of scope)
|
||||
@@ -723,15 +1104,17 @@ Listed so nobody smuggles them into the port and calls it a migration.
|
||||
|---|---|---|
|
||||
| v3 can't drive the headless harness | ~~fatal~~ | **Retired.** `-tags server` verified: calls + events, no display |
|
||||
| Arch/Ubuntu need different webkit tags | ~~high~~ | **Retired.** Both ship webkitgtk-6.0; default builds in the CI container |
|
||||
| No `window.go` → e2e/perf lose binding enumeration | **high** | *New, confirmed.* Decide 6b option (1)/(2) |
|
||||
| 93 `@go` sites need editing after all | **high** | *Confirmed.* Bindings nest by import path; codemod required |
|
||||
| Binding generation analyses the wrong build config | **high** | *New.* Pin tags in Phase 4 step 6 |
|
||||
| No `window.go` → e2e/perf lose binding enumeration | ~~high~~ | **Retired.** 6b option (1): `e2e/support/method-ids.mjs` derives id → name from `frontend/bindings/`. The fetch hook made enumeration unnecessary for *wrapping*; only labelling needed the map |
|
||||
| 93 `@go` sites need editing after all | ~~high~~ | **Retired.** Codemod done in Phase 4; the alias absorbed the prefix, so it was a specifier rewrite |
|
||||
| Binding generation analyses the wrong build config | ~~high~~ | **Retired.** Default tags *are* the shipped configuration since Phase 1; recorded in `scripts/bindings-check.sh` |
|
||||
| Beta churn mid-migration | high | Pin `beta.8`. `beta.3`→`beta.8` in one app's lifetime |
|
||||
| E2E rewrite silently weakens coverage | high | Acceptance = `make e2e` green, **zero spec edits** |
|
||||
| v3 event ordering differs from v2's | medium | Re-derive the fake; don't port it |
|
||||
| E2E rewrite silently weakens coverage | ~~high~~ | **Retired.** 92/92 on chromium. Two spec edits, both deleting assertions about v2 behaviour that is gone; one deletion of a spec already failing on `main` |
|
||||
| `make ui-test` cannot complete in one browser session here | low | *New.* Pre-existing — reproduces at `c9905fb`. A resource limit on this machine; batched runs are green. Watch CI |
|
||||
| v3 event ordering differs from v2's | ~~high~~ | **Retired.** Confirmed and handled in Phase 5: v3's frontend `Events.Emit` round-trips through Go instead of notifying locally first. One test asserted the old behaviour and now asserts the new |
|
||||
| `ServiceShutdown()` signature trap | medium | Silent no-call; grep after Phase 2c |
|
||||
| An app-level event that a headless mode never emits | — | *Found in Phase 6, not predicted.* Server mode emits **no** application events; the cross-service wiring is a service now. Anything else keyed on `Common.*` is suspect |
|
||||
| Quit-during-writes veto breaks | medium | Data-safety path; test deliberately in 2d |
|
||||
| Regression no tier covers | medium | `make perf` before/after on the same seed |
|
||||
| Regression no tier covers | medium | `make perf` before/after on the same seed. Note the fixture seed leaves the bulk-library rows blank; a real comparison wants `make sandbox-seed-bulk` |
|
||||
| GTK4 changes rendering vs GTK3 | low | Unmeasured; visual check on first run |
|
||||
|
||||
---
|
||||
|
||||
+214
-97
@@ -7,31 +7,34 @@
|
||||
* other events arrive from Go whenever they arrive. An assertion that
|
||||
* sleeps and hopes is flaky; an assertion that awaits the event is not.
|
||||
*
|
||||
* Three things it provides on `window.__yjEvents`:
|
||||
* Four things it provides on `window.__yjEvents`:
|
||||
*
|
||||
* record every backend -> frontend event, in order, with payloads
|
||||
* wait a promise that settles on a matching event (or rejects
|
||||
* with the list of events that *did* arrive, which is the
|
||||
* single most useful failure message this harness can give)
|
||||
* call a bound Go method that is guaranteed to settle: a binding
|
||||
* invoked with wrong argument types makes the backend log
|
||||
* "error parsing arguments" and never fire the callback, so
|
||||
* the in-page promise hangs forever. Timing out here fixes
|
||||
* that once instead of in every eval.
|
||||
* call a bound Go method, by name, over the runtime's own HTTP
|
||||
* endpoint — no dependence on the app's bundle
|
||||
* bindings every binding call the *app* made, which is what turns
|
||||
* "did that refetch the library" from an inference into a
|
||||
* fact (e2e/perf/measure.mjs labels and reads these)
|
||||
*
|
||||
* WHERE IT HOOKS. Not EventsOn. Every backend event enters the page
|
||||
* at exactly one place — wails' ipc_websocket.js does
|
||||
* WHERE IT HOOKS. Two places, and neither is `EventsOn`.
|
||||
*
|
||||
* case "n": window.wails.EventsNotify(message)
|
||||
* Inbound, `window._wails.dispatchWailsEvent`: v3's runtime assigns it
|
||||
* at module scope and it is the single point every backend event enters
|
||||
* the page through, so wrapping it captures all 46 whether or not the
|
||||
* app subscribes to them. The runtime does
|
||||
* `window._wails = window._wails || {}`, so this script creates that
|
||||
* object first and puts an accessor on the *property*, wrapping at
|
||||
* assignment time — v2 needed the accessor on `window` itself, because
|
||||
* there the whole object was replaced.
|
||||
*
|
||||
* and EventsNotify fans out to listeners from there. Wrapping that
|
||||
* single choke point captures all 46 events whether or not the app
|
||||
* subscribes to them, and needs one wrap rather than 46.
|
||||
*
|
||||
* `window.wails` does not exist yet when this script runs, so we install
|
||||
* an accessor on `window` and wrap at assignment time (wails' main.js
|
||||
* does a plain `window.wails = {...}`), then collapse the accessor back
|
||||
* to a data property so nothing downstream can tell.
|
||||
* Outbound, `fetch`: v3 routes every runtime call — binding calls, event
|
||||
* emits, window and dialog calls — through one POST to /wails/runtime.
|
||||
* There is no global to wrap the way v2's `window.runtime` could be, and
|
||||
* this is better anyway: it sees calls from any module, needs no walk of
|
||||
* an object graph, and cannot miss one made before the harness looked.
|
||||
*
|
||||
* INSTALL EXACTLY ONCE. Listeners registered by one `eval` survive into
|
||||
* the next, so a recorder that re-registers double-counts. Tests call
|
||||
@@ -44,8 +47,27 @@
|
||||
|
||||
const LIMIT = 2000;
|
||||
|
||||
// Every bound service in this app lives under this Go module path,
|
||||
// so specs name a binding the short way — 'queue.Queue.GetState' —
|
||||
// and this is what makes that the same thing the backend calls
|
||||
// 'yellowjacket/backend/queue.Queue.GetState'.
|
||||
const FQN_PREFIX = "yellowjacket/backend/";
|
||||
|
||||
// The runtime's own object and method ids (objectNames in
|
||||
// @wailsio/runtime): 0 is Call, 3 is Events, and method 0 on each is
|
||||
// CallBinding and Emit respectively.
|
||||
const OBJECT_CALL = 0;
|
||||
const OBJECT_EVENTS = 3;
|
||||
|
||||
// Captured before the wrap below, and used for the harness's own
|
||||
// calls: `__yjEvents.call` is this file talking to the backend, not
|
||||
// the app, and counting it would make "did that action refetch the
|
||||
// library" answer for the question as well as the app.
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
|
||||
let seq = 0;
|
||||
const log = [];
|
||||
const bindings = [];
|
||||
const waiters = new Set();
|
||||
|
||||
const summarize = () => {
|
||||
@@ -56,6 +78,25 @@
|
||||
return counts;
|
||||
};
|
||||
|
||||
/*
|
||||
* `data` is recorded as the argument list Go emitted, which is the
|
||||
* shape every spec reads (`ev.data[0]`).
|
||||
*
|
||||
* v3's EventManager.Emit packs a variadic call into one field: no
|
||||
* arguments is null, one is the value itself, more than one is the
|
||||
* slice. Unpacking that back into a list is exact except for a
|
||||
* single argument that is itself an array, which is indistinguishable
|
||||
* from several arguments — an ambiguity v3 introduced and no
|
||||
* assertion here depends on, since nothing in backend/events emits
|
||||
* more than one value.
|
||||
*/
|
||||
const argsOf = (data) => {
|
||||
if (data === null || data === undefined) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(data) ? data : [data];
|
||||
};
|
||||
|
||||
const record = (name, data, dir) => {
|
||||
const entry = { seq: ++seq, name, data, dir, t: Date.now() };
|
||||
log.push(entry);
|
||||
@@ -89,7 +130,7 @@
|
||||
};
|
||||
|
||||
const api = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
|
||||
/** Every recorded event, oldest first. */
|
||||
get log() {
|
||||
@@ -101,10 +142,30 @@
|
||||
return seq;
|
||||
},
|
||||
|
||||
/** Drop the buffer. Does NOT touch the recorder or waiters. */
|
||||
/**
|
||||
* Every binding call the app made, oldest first. Each is
|
||||
* { methodID, methodName, start, ms, bytes } — the id is what the
|
||||
* generated bindings send, and turning it back into a name is
|
||||
* e2e/perf/measure.mjs's job, which derives the map from
|
||||
* frontend/bindings/.
|
||||
*/
|
||||
get bindings() {
|
||||
return bindings.slice();
|
||||
},
|
||||
|
||||
/**
|
||||
* Read the size of every binding response. Off by default: it
|
||||
* costs a clone-and-read of each body, which only a measurement
|
||||
* wants to pay. With it off, `bytes` is the Content-Length when
|
||||
* the server sent one and -1 otherwise.
|
||||
*/
|
||||
measureBytes: false,
|
||||
|
||||
/** Drop the buffers. Does NOT touch the recorder or waiters. */
|
||||
reset() {
|
||||
const n = log.length;
|
||||
log.length = 0;
|
||||
bindings.length = 0;
|
||||
return n;
|
||||
},
|
||||
|
||||
@@ -177,13 +238,11 @@
|
||||
async ready(timeoutMs) {
|
||||
const deadline = Date.now() + (timeoutMs || 15000);
|
||||
for (;;) {
|
||||
if (window.go?.queue?.Queue?.GetState) {
|
||||
try {
|
||||
await api.call("queue.Queue.GetState", [], 2000);
|
||||
return true;
|
||||
} catch {
|
||||
/* backend not up yet */
|
||||
}
|
||||
try {
|
||||
await api.call("queue.Queue.GetState", [], 2000);
|
||||
return true;
|
||||
} catch {
|
||||
/* backend not up yet */
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error("__yjEvents.ready timed out");
|
||||
@@ -193,38 +252,66 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Call a bound Go method by dotted path, with a timeout.
|
||||
* Call a bound Go method by dotted path.
|
||||
*
|
||||
* await __yjEvents.call('player.Player.SetVolume', [42])
|
||||
*
|
||||
* A binding called with the wrong argument types never fires its
|
||||
* callback — the reason appears only in .dev/app.log. Without a
|
||||
* timeout the caller waits forever; with one it gets told where
|
||||
* to look.
|
||||
* This posts to the runtime's own endpoint rather than reaching
|
||||
* into the page for a binding function, because v3 has no
|
||||
* `window.go` and the generated bindings are ordinary bundled
|
||||
* modules an initScript cannot import. It calls *by name*, which
|
||||
* the backend resolves the same way it resolves the id the
|
||||
* bundle sends.
|
||||
*
|
||||
* v3 rejects a bad call rather than silently never firing its
|
||||
* callback the way v2 did — wrong argument types come back as a
|
||||
* TypeError naming the argument, an unknown method as a
|
||||
* ReferenceError. The timeout below is therefore a backstop for
|
||||
* a genuinely hung request, not the mechanism that makes a
|
||||
* mistake visible.
|
||||
*/
|
||||
call(path, args, timeoutMs) {
|
||||
const parts = String(path).split(".");
|
||||
let fn = window.go;
|
||||
for (const p of parts) {
|
||||
fn = fn?.[p];
|
||||
}
|
||||
if (typeof fn !== "function") {
|
||||
return Promise.reject(
|
||||
new Error(`__yjEvents.call: no such binding: ${path}`),
|
||||
);
|
||||
}
|
||||
const request = nativeFetch("/wails/runtime", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-wails-client-id": window._wails?.clientId ?? "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
object: OBJECT_CALL,
|
||||
method: 0,
|
||||
args: {
|
||||
"call-id": `yj-${Math.random().toString(36).slice(2)}`,
|
||||
methodName: FQN_PREFIX + String(path),
|
||||
args: args || [],
|
||||
},
|
||||
}),
|
||||
}).then(async (res) => {
|
||||
const type = res.headers.get("Content-Type") || "";
|
||||
const json = type.includes("application/json");
|
||||
|
||||
if (!res.ok) {
|
||||
const body = json ? await res.json() : { message: await res.text() };
|
||||
throw new Error(
|
||||
`__yjEvents.call(${path}) failed: ` +
|
||||
`${body.kind || "Error"}: ${body.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return json ? res.json() : res.text();
|
||||
});
|
||||
|
||||
return Promise.race([
|
||||
Promise.resolve(fn(...(args || []))),
|
||||
request,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
`__yjEvents.call(${path}) did not settle in ` +
|
||||
`${timeoutMs || 10000}ms — almost always wrong ` +
|
||||
`argument types; check .dev/app.log for ` +
|
||||
`"error parsing arguments"`,
|
||||
`${timeoutMs || 10000}ms — the runtime endpoint ` +
|
||||
`hung, which is not how a bad argument fails; ` +
|
||||
`check .dev/app.log`,
|
||||
),
|
||||
),
|
||||
timeoutMs || 10000,
|
||||
@@ -241,62 +328,92 @@
|
||||
writable: false,
|
||||
});
|
||||
|
||||
// Wrap `obj[method]` once, routing every invocation through `tap`.
|
||||
const wrap = (obj, method, tap) => {
|
||||
const original = obj[method];
|
||||
if (typeof original !== "function" || original.__yjWrapped) {
|
||||
return;
|
||||
}
|
||||
const wrapped = function (...args) {
|
||||
try {
|
||||
tap(args);
|
||||
} catch {
|
||||
/* a broken recorder must never break the app */
|
||||
}
|
||||
return original.apply(this, args);
|
||||
};
|
||||
wrapped.__yjWrapped = true;
|
||||
obj[method] = wrapped;
|
||||
};
|
||||
// ── Inbound ──────────────────────────────────────────────────────
|
||||
//
|
||||
// The runtime keeps whatever `window._wails` already is, so creating
|
||||
// it here and defining an accessor on the one property we care about
|
||||
// means the wrap happens the moment the runtime module is evaluated.
|
||||
window._wails = window._wails || {};
|
||||
|
||||
// Install an accessor that wraps on first assignment, then collapses
|
||||
// back into an ordinary property.
|
||||
const hookOnAssign = (name, onAssign) => {
|
||||
let value;
|
||||
Object.defineProperty(window, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => value,
|
||||
set: (v) => {
|
||||
value = v;
|
||||
let dispatch;
|
||||
|
||||
Object.defineProperty(window._wails, "dispatchWailsEvent", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => dispatch,
|
||||
set: (fn) => {
|
||||
dispatch = function (event) {
|
||||
try {
|
||||
onAssign(v);
|
||||
record(event?.name, argsOf(event?.data), "in");
|
||||
} catch {
|
||||
/* ditto */
|
||||
/* a broken recorder must never break the app */
|
||||
}
|
||||
Object.defineProperty(window, name, {
|
||||
value: v,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
});
|
||||
},
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── Outbound ─────────────────────────────────────────────────────
|
||||
//
|
||||
// One POST per runtime call. Only two of the thirteen object ids
|
||||
// are interesting here; the rest (window, dialogs, clipboard) pass
|
||||
// through untouched and unrecorded.
|
||||
window.fetch = function (input, init) {
|
||||
let call = null;
|
||||
|
||||
try {
|
||||
// The runtime passes a **URL object**, not a string — it
|
||||
// builds `new URL(runtimeURL())` — and a URL has no `.url`,
|
||||
// only a Request does. Reading the wrong one matched
|
||||
// nothing and recorded no calls at all, which looks
|
||||
// identical to an app that made none.
|
||||
const url =
|
||||
input && typeof input === "object" && "url" in input
|
||||
? input.url
|
||||
: String(input ?? "");
|
||||
|
||||
if (
|
||||
url.includes("/wails/runtime") &&
|
||||
init?.method === "POST" &&
|
||||
typeof init.body === "string"
|
||||
) {
|
||||
const body = JSON.parse(init.body);
|
||||
|
||||
if (body.object === OBJECT_EVENTS && body.method === 0) {
|
||||
record(body.args?.name, argsOf(body.args?.data), "out");
|
||||
} else if (body.object === OBJECT_CALL && body.method === 0) {
|
||||
call = {
|
||||
methodID: body.args?.methodID ?? null,
|
||||
methodName: body.args?.methodName ?? null,
|
||||
start: performance.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ditto */
|
||||
}
|
||||
|
||||
const response = nativeFetch(input, init);
|
||||
|
||||
if (!call) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return response.then(async (res) => {
|
||||
try {
|
||||
call.ms = performance.now() - call.start;
|
||||
call.bytes = api.measureBytes
|
||||
? (await res.clone().text()).length
|
||||
: Number(res.headers.get("Content-Length") ?? -1);
|
||||
bindings.push(call);
|
||||
if (bindings.length > LIMIT) {
|
||||
bindings.splice(0, bindings.length - LIMIT);
|
||||
}
|
||||
} catch {
|
||||
/* ditto */
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
// Inbound: every backend -> frontend event.
|
||||
hookOnAssign("wails", (w) => {
|
||||
wrap(w, "EventsNotify", ([message]) => {
|
||||
const parsed = JSON.parse(message);
|
||||
record(parsed.name, parsed.data, "in");
|
||||
});
|
||||
});
|
||||
|
||||
// Outbound: events the frontend emits, so a flow that round-trips
|
||||
// through Go is legible from one buffer.
|
||||
hookOnAssign("runtime", (r) => {
|
||||
wrap(r, "EventsEmit", (args) => {
|
||||
record(args[0], args.slice(1), "out");
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -32,14 +32,14 @@ make sandbox-seed-bulk # Same, from the bulk library (minutes; it is a real scan
|
||||
make perf LABEL=<n> # Measure a running app; writes .dev/perf/<n>.json
|
||||
make perf-compare BEFORE=<a> AFTER=<b> # Print the before/after table
|
||||
make build-dev # Debug build with symbols
|
||||
make build-prod # Production build (stripped, UPX-compressed)
|
||||
make build-prod # Production build (stripped and trimmed; no UPX)
|
||||
make generate # Run code generators (sqlc + templ via go generate)
|
||||
make e2e # Playwright smoke suite against a running dev-headless app
|
||||
make e2e-setup # Install the e2e runner + its browser (once)
|
||||
make ui-test # Vitest component/store suite in a real browser (no app)
|
||||
make ui-visual # Same, including toMatchScreenshot comparisons
|
||||
make ui-setup # Install the Vitest provider's own Chromium (once)
|
||||
make bindings-check # Fail if frontend/wailsjs is stale vs the Go bindings
|
||||
make bindings-check # Fail if frontend/bindings is stale vs the Go bindings
|
||||
make skill-check # Fail if .pi/ documents a make target that doesn't exist
|
||||
make commit-check # Fail if a commit subject is not a Conventional Commit
|
||||
make lint # golangci-lint v2 (strict), all three build configurations
|
||||
@@ -50,12 +50,15 @@ make setup # Install go tools, frontend deps, git hooks (lefthook)
|
||||
|
||||
### Running tests
|
||||
|
||||
All Go test commands require the `-tags webkit2_41` build tag:
|
||||
Go test commands need no build tag for the app configuration. The
|
||||
`webkit2_41` tag every command here used to carry is gone with wails
|
||||
v2: v3 builds against GTK4 + WebKitGTK 6.0 by default, which both Arch
|
||||
and ubuntu:24.04 ship.
|
||||
|
||||
```bash
|
||||
go test -tags webkit2_41 ./... # All tests
|
||||
go test -tags webkit2_41 ./backend/player/ # Single package
|
||||
go test -tags webkit2_41 -run TestName ./backend/player/ # Single test
|
||||
go test ./... # All tests
|
||||
go test ./backend/player/ # Single package
|
||||
go test -run TestName ./backend/player/ # Single test
|
||||
```
|
||||
|
||||
The central index builder is behind a second tag and is **not** covered
|
||||
@@ -63,14 +66,14 @@ by the command above — `make test` runs both passes, but a manual run
|
||||
needs it spelled out:
|
||||
|
||||
```bash
|
||||
go test -tags "webkit2_41 indexbuild" ./backend/explore/... ./cmd/...
|
||||
go test -tags indexbuild ./backend/explore/... ./cmd/...
|
||||
```
|
||||
|
||||
`backend/testctl` is behind a third tag and needs its own pass too
|
||||
(`make test` runs all three):
|
||||
|
||||
```bash
|
||||
go test -tags "webkit2_41 dev" ./backend/testctl/...
|
||||
go test -tags dev ./backend/testctl/...
|
||||
```
|
||||
|
||||
Audio playback integration tests require `YELLOWJACKET_INTEGRATION=1`.
|
||||
@@ -84,9 +87,12 @@ through `internal/testfixtures`, selecting files by *case*
|
||||
path, and skip themselves when it has not been generated.
|
||||
|
||||
The app itself can be run without a blocking window — `make
|
||||
dev-headless` — and driven with `playwright-cli` against the dev server
|
||||
on `:34115`, which is the real app with real bindings on `window.go`,
|
||||
bridged to the same Go backend a desktop window would use.
|
||||
dev-headless` — and driven with `playwright-cli` against it on
|
||||
`:34115`. That is wails v3's first-class `-tags server` mode: the real
|
||||
app, the real bindings, the same Go backend a desktop window would use,
|
||||
served over HTTP with **no display at all**. The Xvfb this used to
|
||||
require is gone, from the script and from CI; `dbus-run-session` stays,
|
||||
for MPRIS.
|
||||
|
||||
**The operational half of all this lives in the
|
||||
`yellowjacket-dev` skill** (`.pi/skills/yellowjacket-dev/`): which tier
|
||||
@@ -103,8 +109,16 @@ phase 3:
|
||||
backend event on `window.__yjEvents`. Half this app is push-driven,
|
||||
so assertions **await an event, not a timeout**:
|
||||
`await window.__yjEvents.wait('LibraryScanComplete', {timeoutMs: 60000})`.
|
||||
It also provides `ready()` and `call('queue.Queue.GetState', [])`,
|
||||
which times out instead of hanging.
|
||||
It also provides `ready()` and `call('queue.Queue.GetState', [])`.
|
||||
Both hook v3's own seams rather than an internal: inbound is
|
||||
`window._wails.dispatchWailsEvent`, the entry point the backend's push
|
||||
uses, and outbound is **`fetch`** — v3 routes every runtime call
|
||||
through one POST to `/wails/runtime`, so one hook sees calls from any
|
||||
module and cannot miss one made before the harness looked. `call()`
|
||||
posts by method name, so it depends on nothing in the app's bundle and
|
||||
works on a page with no init script — which is why `seed-sandbox.sh`
|
||||
is `curl` now and needs no browser. It no longer races a timeout
|
||||
either: v3 rejects bad arguments and unknown methods cleanly.
|
||||
- **The dev-only control surface**, `backend/testctl`, mounted at
|
||||
`/__test/` on the same port: `health`, `db/snapshot`, `db/restore`,
|
||||
`emit` (force any backend event, which renders push-driven views
|
||||
@@ -130,18 +144,29 @@ meaningless against the seed's one empty playlist, so it builds ten
|
||||
It wraps every bound Go method, so "did that refetch the library" is a
|
||||
fact rather than an inference. It is not a spec and does not run in CI.
|
||||
|
||||
**The cheapest tier needs none of that.** `make ui-test` runs 672
|
||||
Vitest tests in a real Chromium in ~2 s with no Wails, no backend, no
|
||||
seeded library and no virtual display, because `frontend/wailsjs/` is a
|
||||
pure passthrough to `window.go` / `window.runtime` and
|
||||
`frontend/test/support/wails-fake.ts` replaces just those two globals —
|
||||
so the tests exercise the real generated bindings and the real store
|
||||
code.
|
||||
**The cheapest tier needs none of that.** `make ui-test` runs 757
|
||||
Vitest tests in a real Chromium with no Wails, no backend, no seeded
|
||||
library and no virtual display, because **v3 routes every runtime call
|
||||
— bindings, event emits, window, dialogs, clipboard — through one IPC
|
||||
transport**, and `frontend/test/support/wails-fake.ts` replaces it via
|
||||
`setTransport()`, which is public documented API. So the fake covers
|
||||
strictly more than v2's two globals did while being shorter, and the
|
||||
tests exercise the real generated bindings, the real runtime and the
|
||||
real store code. A binding carries an **ID**, not a name
|
||||
(`$Call.ByID(2822423495)` is FNV-1a over
|
||||
`yellowjacket/backend/home.Service.GetShelves`), so the fake derives
|
||||
that map from the generated tree rather than writing it down.
|
||||
|
||||
**`frontend/wailsjs/` is generated by `wails`, not `go generate`**, so
|
||||
**`frontend/bindings/` is generated by `wails3`, not `go generate`**, so
|
||||
the pre-commit codegen check does not cover it. `make bindings-check`
|
||||
(~1.5 s, also a pre-commit hook) regenerates it and fails on a dirty
|
||||
tree; `make bindings` regenerates it for real.
|
||||
(~3.5 s warm, ~20 s on a cold build cache, also a pre-commit hook)
|
||||
regenerates it and fails on a dirty tree; `make bindings` regenerates it
|
||||
for real. It is slower than v2's because v3's generator is a **static
|
||||
analyser** over the whole package graph rather than runtime reflection
|
||||
— which is also why the tag set it runs under matters, and why it is
|
||||
the *default* one: that is the configuration users run, and neither
|
||||
`indexbuild` nor `dev` adds a bound service. See
|
||||
`scripts/bindings-check.sh`.
|
||||
|
||||
**Seeds are produced by running the app**, never by hand-writing a
|
||||
`config.toml` and DB rows — the same discipline `sql/schemas/` gets,
|
||||
@@ -151,7 +176,46 @@ See `.planning/plans/completed/005-agent-development-harness.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Wails app lifecycle** (`main.go` → `backend/app.go`): `YellowJacketApp` is the root struct bound to Wails. Its methods are callable from the frontend. Lifecycle hooks: `OnStartup` (init audio), `OnDomReady` (start library scan), `OnBeforeClose` (save window state), `OnShutdown` (persist player/queue state).
|
||||
**Wails app lifecycle** (`main.go` → `backend/app.go`): `main.go` is
|
||||
`application.New(opts)` → `app.Window.NewWithOptions(…)` → `app.Run()`.
|
||||
Each bound service takes its context from `ServiceStartup(ctx,
|
||||
application.ServiceOptions{})` — v3 calls it on every service, in
|
||||
registration order, on the main goroutine — and gives it back in
|
||||
`ServiceShutdown()`. That context is **cancelled on app shutdown**,
|
||||
which `SetContext` never was.
|
||||
|
||||
Three things about it are load-bearing.
|
||||
|
||||
**`ServiceShutdown()` takes no context.** A method with a
|
||||
`context.Context` parameter does not satisfy the interface and is
|
||||
**silently never called** — no error, no warning.
|
||||
|
||||
**The cross-service wiring is a service, not an event.** v3 has no
|
||||
`OnStartup`/`OnDomReady` option, and the obvious replacement —
|
||||
`app.Event.OnApplicationEvent(events.Common.ApplicationStarted, …)` —
|
||||
is the right *moment* and the wrong *mechanism*: **server mode emits no
|
||||
application events at all** (`setupCommonEvents` is an explicit no-op
|
||||
under `-tags server`), so the desktop build wired itself and the
|
||||
headless harness did not. `backend/startup.go` is registered last
|
||||
instead, which takes the ordering from the mechanism rather than from
|
||||
an event and therefore holds in every mode. Anything else keyed on
|
||||
`Common.*` is suspect for the same reason.
|
||||
|
||||
**The quit veto is asynchronous now.** v2's `MessageDialog` blocked and
|
||||
returned the button; v3's `Show()` returns immediately and the answer
|
||||
arrives on a `Button.OnClick` callback, so `ShouldQuit` cannot ask and
|
||||
answer in one call — it vetoes, shows the dialog, and calls
|
||||
`app.Quit()` from the callback. `quitConfirmed` is what stops that
|
||||
second `Quit()` asking again; `quitAsking` stops a second close attempt
|
||||
stacking dialogs. Window state moved off that path entirely, onto a
|
||||
`Common.WindowClosing` hook, because the size has to be read while the
|
||||
window still exists and `OnShutdown` has neither a context nor a
|
||||
window.
|
||||
|
||||
`internalServiceMethods` auto-excludes `ServiceStartup`,
|
||||
`ServiceShutdown`, `ServiceName` and `ServeHTTP` from bindings, so this
|
||||
shape **removed** 12 spurious bindings and the bogus `context` model
|
||||
rather than renaming them.
|
||||
|
||||
**Backend packages** (under `backend/`):
|
||||
- `player` — Audio playback via beep. `BufferedStreamer` provides a ring buffer for smooth seeking.
|
||||
@@ -441,7 +505,11 @@ that invents its own flat layout agrees with the bug.
|
||||
keeping only `explore.ArtistImageKeepNames()` and refusing an empty
|
||||
keep set for the reason the covers sweep refuses an empty live set.
|
||||
|
||||
**Frontend** (`frontend/`): Lit 3.2 web components + Web Awesome UI library + HTMX. State management via singleton reactive stores in `src/store/`. Wails bindings auto-generated in `frontend/wailsjs/` — don't edit by hand.
|
||||
**Frontend** (`frontend/`): Lit 3.2 web components + Web Awesome UI library + HTMX. State management via singleton reactive stores in `src/store/`. Wails bindings auto-generated as TypeScript in `frontend/bindings/`, nested by Go import path — don't edit by hand. The `@go` alias absorbs the constant prefix, so a call site imports `@go/library/library.js`.
|
||||
|
||||
**One seam states what the generated types get wrong, rather than 78 patches.** v3's generator is honest where v2's lied: a Go `nil` slice marshals to JSON `null` and always has (v2 typed it `T[]`), and a Go named string type is a closed set (v2 typed it `string`). There is no flag to turn either off, correctly. So `utils/binding.ts` states the app's actual contract at the only place it is true — `list` yields `[]` for a nil slice, `dict`/`dictByName` yield `{}` for a nil map and drop null-valued keys (which loses nothing: `noUncheckedIndexedAccess` already makes every read `V | undefined`), and `compact` is the same for a map arriving as a *field*. Where a nullable slice is a model field there is no boundary to put it at, and those are `?? []` at the point of use.
|
||||
|
||||
All three also return a **plain `Promise`**: v3 bindings return a `CancellablePromise` and nothing in this app cancels one, so letting it inward would put a Wails type in every store signature for a capability none of them use.
|
||||
|
||||
**A view is a chunk, and three components are not.** `index.ts` holds a
|
||||
loader table (`VIEW_LOADERS`, `DETAIL_LOADERS`) and `await`s a view's
|
||||
@@ -1424,11 +1492,26 @@ on every change, or `scrollToIndex` from calling `scrollIntoView()` on
|
||||
something invisible.
|
||||
|
||||
Emit through **`events.Emit(ctx, name, data...)`**, never
|
||||
`runtime.EventsEmit` — wails `log.Fatalf`s (unrecoverably) on any
|
||||
context that does not carry its runtime, which includes every
|
||||
`context.Background()`, so a direct call cannot run under test and can
|
||||
kill the app from a background worker. `TestNoDirectRuntimeEmits` fails
|
||||
the build on a direct call anywhere outside `backend/events`.
|
||||
`app.Event.Emit`. `TestNoDirectRuntimeEmits` fails the build on a
|
||||
direct call anywhere outside `backend/events`.
|
||||
|
||||
**Its justification changed with v3 and is now the weaker one.** Under
|
||||
v2 this was a safety rule: `runtime.EventsEmit` `log.Fatalf`'d —
|
||||
unrecoverably, taking the process down — on any context not carrying
|
||||
the runtime, which includes every `context.Background()`, so a
|
||||
background worker could kill the app by emitting. That is gone. v3's
|
||||
emit takes **no context at all**, and `application.Get()` with no app
|
||||
running returns `nil` rather than dying, so `Deliver` is
|
||||
`if app == nil { return ErrNoRuntime }` where it used to probe the
|
||||
**v2-private context key** `ctx.Value("events")`. What remains worth
|
||||
pinning is narrower and still real: one emit path is what lets
|
||||
`emitStatus` drop an unchanged payload for every caller at once.
|
||||
|
||||
**`events.Emit` keeps its `ctx` anyway, and it is now purely a test
|
||||
seam.** Delivery does not go through it; `events.WithSink(ctx, rec)`
|
||||
does, which is how a service is asserted on in-process
|
||||
(`backend/queue/emit_test.go` is the model). Dropping the parameter
|
||||
would have churned 45 call sites and every test for no gain.
|
||||
|
||||
That wrapper is what makes services testable in-process: install a
|
||||
recorder with `events.WithSink(ctx, rec)` and assert on the payload the
|
||||
@@ -1486,8 +1569,10 @@ Two jobs, both in an `ubuntu:24.04` container:
|
||||
`make lint` and `make test` (three build configurations each),
|
||||
`tsc --noEmit`, `make ui-test`, `make bindings-check`,
|
||||
`make skill-check`.
|
||||
- **`e2e`** — under Xvfb and a private D-Bus: fixtures, a seed built by
|
||||
running the app, `make dev-headless`, then the Playwright suite
|
||||
- **`e2e`** — under a private D-Bus and **no display at all**, since
|
||||
v3's `-tags server` is a real headless mode: fixtures, a seed built by
|
||||
running the app (`curl` against the runtime endpoint — no browser),
|
||||
`make dev-headless`, then the Playwright suite
|
||||
against **both** Chromium and WebKit. Playwright's Linux WebKit links
|
||||
Ubuntu 24.04 libraries that Arch does not provide, so CI is the only
|
||||
place it can run, and it is the closest available approximation of
|
||||
@@ -1518,8 +1603,51 @@ And
|
||||
`YJ_CORE_INDEX_URL` points at a dead address so no run fetches the real
|
||||
explore artifact, matching what `scripts/seed-sandbox.sh` already does.
|
||||
|
||||
**`make lint`'s tag sets must stay identical to `make test`'s.**
|
||||
Without `webkit2_41` wails resolves `webkit2gtk-4.0`, which Arch still
|
||||
ships and Ubuntu 24.04 does not — so a mismatch lints a configuration
|
||||
that only builds on one developer's distro, and says nothing about what
|
||||
ships.
|
||||
**`make lint`'s tag sets must stay identical to `make test`'s**, or
|
||||
lint is checking configurations nothing builds. There are three, and
|
||||
the app's is now the *default* tag set: v3 resolves GTK4 +
|
||||
WebKitGTK 6.0, which Arch and ubuntu:24.04 both ship, so the
|
||||
`webkit2_41` tag that used to be mandatory everywhere is gone. A
|
||||
machine without `webkitgtk-6.0` can still build with `-tags gtk3`, but
|
||||
that is an escape hatch, not what CI or a release builds.
|
||||
|
||||
## Packaging
|
||||
|
||||
**The Makefile is the front door and `Taskfile.yml` is an
|
||||
implementation detail behind it.** `make dev`, `make build-dev`,
|
||||
`make build-prod`, `make bindings` and `make e2e` all keep their names;
|
||||
what changed underneath is that a build is now a Taskfile tree
|
||||
(`Taskfile.yml` → `build/<platform>/Taskfile.yml`) rather than one
|
||||
`wails build` invocation, and `wails3` is still a **vendored Go tool**
|
||||
(`go tool wails3`), never a global install.
|
||||
|
||||
Four things about that tree bite anything outside the Makefile, and all
|
||||
four bit the packaging recipes:
|
||||
|
||||
- **The tasks invoke `wails3` by bare name**, in 54 places across the
|
||||
scaffold files. `scripts/toolbin/wails3` puts that name on PATH
|
||||
pointing back at the vendored tool; without it a build dies at its
|
||||
first sub-task with `wails3: command not found`. The Makefile
|
||||
prepends it, and so must `packaging/arch/PKGBUILD` and the Homebrew
|
||||
formula.
|
||||
- **`wails3 build` has no `-ldflags`, `-trimpath` or `-clean`** — those
|
||||
were v2's. `-trimpath` and `-w -s` are already in the production
|
||||
task's own flags; the version stamp goes through **`LDFLAGS_EXTRA`**,
|
||||
this repo's one edit to the scaffold Taskfiles (linux and darwin
|
||||
alike), passed as `wails3 task build LDFLAGS_EXTRA="-X 'main.version=…'"`.
|
||||
- **The output is `bin/`, not v2's `build/bin/`.** `build/` is *tracked
|
||||
build assets* now.
|
||||
- **Bundling is a separate step from building.** `task build` produces
|
||||
a bare binary on every platform; the macOS `.app` is `task package`.
|
||||
|
||||
**`build/`'s platform metadata is generated from `build/config.yml`.**
|
||||
`wails3 task common:update:build-assets` rewrites `Info.plist`, the
|
||||
`.desktop` template, `nfpm.yaml` and the Windows manifest from that
|
||||
one file — so a hand edit to any of them is lost on the next refresh,
|
||||
and the two fields it does *not* own (nfpm's `homepage` and `license`)
|
||||
say so in place. That refresh also regenerates `build/ios/` and
|
||||
`build/android/`, which this repo does not carry: they are gitignored
|
||||
rather than deleted-and-rediscovered, and their `includes:` entries
|
||||
are dropped from `Taskfile.yml`. `build/config.yml`'s `version` is the
|
||||
*metadata* version and is not what the app reports — `main.version` is
|
||||
stamped at link time from the packaging recipe's git-derived version.
|
||||
|
||||
@@ -7,17 +7,25 @@ LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)'
|
||||
# point elsewhere (or unset it there to share the real user dirs).
|
||||
DEV_YJ_HOME ?= $(HOME)/.local/share/yellowjacket-dev
|
||||
|
||||
# `wails3 dev` and `wails3 task` run the scaffold's Taskfile tree, which
|
||||
# invokes `wails3` by bare name. The CLI is a vendored Go tool, so the
|
||||
# name only exists on PATH via this shim -- see scripts/toolbin/wails3.
|
||||
# Without it every supervisor target dies with
|
||||
# "/bin/sh: wails3: command not found" at its first sub-task.
|
||||
TOOLBIN := $(CURDIR)/scripts/toolbin
|
||||
|
||||
dev: setup generate clean
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi; : "$${YJ_HOME:=$(DEV_YJ_HOME)}"; export YJ_HOME; go tool wails3 dev -config ./build/config.yml
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi; : "$${YJ_HOME:=$(DEV_YJ_HOME)}"; export YJ_HOME; PATH="$(TOOLBIN):$$PATH" go tool wails3 dev -config ./build/config.yml
|
||||
|
||||
dev-debug: setup generate clean
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi; : "$${YJ_HOME:=$(DEV_YJ_HOME)}"; export YJ_HOME; YJ_LOG_LEVEL=debug go tool wails3 dev -config ./build/config.yml
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi; : "$${YJ_HOME:=$(DEV_YJ_HOME)}"; export YJ_HOME; YJ_LOG_LEVEL=debug PATH="$(TOOLBIN):$$PATH" go tool wails3 dev -config ./build/config.yml
|
||||
|
||||
# ── Headless harness (plan 005) ──────────────────────────────────────
|
||||
# The same dev server `make dev` runs, minus the blocking GTK window:
|
||||
# Xvfb gives it the display it insists on, and the script returns once
|
||||
# :34115 answers. This is the only entry point an agent can use, since
|
||||
# every other one blocks the terminal forever.
|
||||
# The same app `make dev` runs, minus the window: v3's `-tags server`
|
||||
# is a first-class headless mode that needs no display at all, so the
|
||||
# Xvfb this used to require is gone. The script returns once :34115
|
||||
# answers. This is the only entry point an agent can use, since every
|
||||
# other one blocks the terminal forever.
|
||||
dev-headless: ## Start the app headless in the background (SEED=<name> to seed)
|
||||
@./scripts/dev-headless.sh $(if $(SEED),--seed $(SEED),) $(HEADLESS_ARGS)
|
||||
|
||||
@@ -100,14 +108,9 @@ ui-visual-update: ## Re-record the screenshot baselines
|
||||
ui-setup: ## Install the Vitest browser provider's own Chromium (once)
|
||||
@cd frontend && pnpm install && npx playwright install chromium
|
||||
|
||||
# Bindings are generated by `wails`, NOT by `go generate`, so the
|
||||
# Bindings are generated by `wails3`, NOT by `go generate`, so the
|
||||
# pre-commit codegen check does not cover them: a renamed Go struct
|
||||
# field currently surfaces at runtime, in a window.
|
||||
#
|
||||
# NOTE (plan 009 phase 4): this still checks v2's frontend/wailsjs,
|
||||
# which nothing regenerates now that the v2 CLI is gone. It is
|
||||
# excluded from the pre-commit hook until the frontend moves to
|
||||
# frontend/bindings.
|
||||
# field would otherwise surface at runtime, inside a window.
|
||||
bindings-check: ## Fail if the generated bindings are stale
|
||||
@./scripts/bindings-check.sh
|
||||
|
||||
@@ -130,14 +133,13 @@ commit-check: ## Fail if a commit subject is not a Conventional Commit
|
||||
@./scripts/commit-check.sh $(if $(RANGE),--range $(RANGE))
|
||||
|
||||
# v3 generates TypeScript into frontend/bindings/, nested by Go import
|
||||
# path, rather than v2's frontend/wailsjs/. The 93 frontend import
|
||||
# sites still point at the old tree, so this writes the new one beside
|
||||
# it until plan 009 phase 4 moves them; the chmod dance that used to
|
||||
# follow is gone with v2's generator, which wrote its runtime files 755.
|
||||
# path, rather than v2's frontend/wailsjs/. The `@go` alias absorbs the
|
||||
# constant prefix, so a call site imports '@go/library/library.js'.
|
||||
#
|
||||
# The tag set is pinned deliberately: the generator is a static
|
||||
# analyser, so it only sees the configuration it is told about, and the
|
||||
# one that matters is the one users run.
|
||||
# No -f flag: the tag set is the default one, deliberately, because the
|
||||
# generator is a static analyser that sees only the configuration it is
|
||||
# told about and the one that matters is the one users run. See
|
||||
# scripts/bindings-check.sh for why the other two do not apply.
|
||||
bindings: ## Regenerate frontend/bindings from the bound Go services
|
||||
go tool wails3 generate bindings -clean=true -ts -i
|
||||
|
||||
@@ -168,7 +170,7 @@ fresh-install: setup generate clean
|
||||
case "$$(findmnt -no FSTYPE -T "$$YJ_HOME" 2>/dev/null)" in \
|
||||
tmpfs|ramfs) echo "==> WARNING: $$YJ_HOME is RAM-backed; the search index import needs ~6GB of real disk. Set FRESH_HOME_BASE to a disk-backed path." ;; \
|
||||
esac; \
|
||||
go tool wails3 dev -config ./build/config.yml
|
||||
PATH="$(TOOLBIN):$$PATH" go tool wails3 dev -config ./build/config.yml
|
||||
|
||||
# Named, persistent sandboxes: `make sandbox foo` runs dev against
|
||||
# $(FRESH_HOME_BASE)/yellowjacket-sandbox-foo, creating it on first use
|
||||
@@ -228,7 +230,7 @@ sandbox-%: setup generate clean
|
||||
case "$$(findmnt -no FSTYPE -T "$$YJ_HOME" 2>/dev/null)" in \
|
||||
tmpfs|ramfs) echo "==> WARNING: $$YJ_HOME is RAM-backed; the search index import needs ~6GB of real disk. Set FRESH_HOME_BASE to a disk-backed path." ;; \
|
||||
esac; \
|
||||
go tool wails3 dev -config ./build/config.yml
|
||||
PATH="$(TOOLBIN):$$PATH" go tool wails3 dev -config ./build/config.yml
|
||||
|
||||
sandboxes: ## List existing named sandboxes
|
||||
@ls -d "$(SANDBOX_DIR)"-* 2>/dev/null \
|
||||
@@ -238,10 +240,10 @@ sandboxes: ## List existing named sandboxes
|
||||
.PHONY: sandbox sandbox-rm sandboxes
|
||||
|
||||
build-dev: generate
|
||||
go tool wails3 task build DEV=true
|
||||
PATH="$(TOOLBIN):$$PATH" go tool wails3 task build DEV=true
|
||||
|
||||
build-prod: generate
|
||||
go tool wails3 task build
|
||||
PATH="$(TOOLBIN):$$PATH" go tool wails3 task build
|
||||
|
||||
build-frontend:
|
||||
cd frontend && pnpm install && pnpm build
|
||||
|
||||
@@ -78,16 +78,25 @@ YellowJacket is built with [Go](https://go.dev/) and a
|
||||
| Go | 1.25+ |
|
||||
| Node.js | 22+ |
|
||||
| pnpm | 10+ |
|
||||
| Wails CLI | v2 (`go install github.com/wailsapp/wails/v2/cmd/wails@latest`) |
|
||||
| Wails CLI | v3 — vendored, no install needed (`go tool wails3`) |
|
||||
|
||||
On Linux, install the system libraries Wails needs:
|
||||
The Wails v3 CLI resolves from the `tool` block in `go.mod`, so there is nothing
|
||||
to install globally; `make setup` fetches it with the rest of the tooling.
|
||||
|
||||
On Linux, install the system libraries Wails needs. v3 builds against GTK4 +
|
||||
WebKitGTK 6.0 by default:
|
||||
|
||||
```bash
|
||||
sudo apt-get install libasound2-dev libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
sudo apt-get install libasound2-dev libgtk-4-dev libwebkitgtk-6.0-dev # Debian/Ubuntu
|
||||
sudo pacman -S alsa-lib gtk4 webkitgtk-6.0 # Arch
|
||||
```
|
||||
|
||||
macOS and Windows need no extra system packages. Run `wails doctor` to check your
|
||||
environment.
|
||||
A machine without `webkitgtk-6.0` can still build with `-tags gtk3` against the
|
||||
older WebKit2GTK 4.1 stack, but that is an escape hatch, not what CI or a
|
||||
release builds.
|
||||
|
||||
macOS and Windows need no extra system packages. Run `go tool wails3 doctor` to
|
||||
check your environment.
|
||||
|
||||
**Build**
|
||||
|
||||
|
||||
@@ -247,6 +247,15 @@ func NewYellowJacketApp(
|
||||
)
|
||||
}
|
||||
|
||||
// Last, deliberately: services start in registration order, so this
|
||||
// runs once every service above has taken its context. See
|
||||
// startup.go for why the wiring is a service rather than an
|
||||
// application-event hook.
|
||||
yjApp.Services = append(
|
||||
yjApp.Services,
|
||||
application.NewService(&startupService{app: yjApp}),
|
||||
)
|
||||
|
||||
return yjApp, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1414,7 +1414,18 @@ func (si *SearchIndex) ExactMatches(query string, perCategory int) []SearchIndex
|
||||
// by relevance (popularity-blended). Returns nil when the index
|
||||
// hasn't finished its initial build.
|
||||
func (si *SearchIndex) Search(ctx context.Context, query string, limit int) []SearchIndexResult {
|
||||
if !si.IsReady() {
|
||||
// IsReady is latched once at startup, so it is right in the app and
|
||||
// wrong for anything that fills the table afterwards — including the
|
||||
// e2e suite staging a catalog, which is how search gets tested in
|
||||
// CI, where the artifact URL points at a dead address on purpose.
|
||||
// shelves.go learned this already; the search path did not, and the
|
||||
// symptom was three specs that passed only when an earlier one
|
||||
// happened to flip the flag first.
|
||||
//
|
||||
// The latch stays as the fast path — once it is true nothing can
|
||||
// make it false — and the probe is one indexed `SELECT 1 … LIMIT 1`
|
||||
// on the only path that could otherwise answer "no catalog" wrongly.
|
||||
if !si.IsReady() && !si.hasCatalogRows(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// startupService is the cross-service wiring, wearing a service's
|
||||
// clothes so the runtime starts it like everything else.
|
||||
//
|
||||
// The wiring belongs to no single service — it is the hooks, adapters
|
||||
// and callbacks that make one package drive another — so v2 put it in
|
||||
// OnStartup and the first v3 port hung it off
|
||||
// events.Common.ApplicationStarted, which fires after every service's
|
||||
// own ServiceStartup and is therefore the right *moment*.
|
||||
//
|
||||
// It is the wrong *mechanism*, because server mode emits no
|
||||
// application events at all: v3's setupCommonEvents is an explicit
|
||||
// no-op under `-tags server` ("server mode has no platform-specific
|
||||
// events to map"). So the desktop build wired itself and the headless
|
||||
// build did not, which showed up as "No player set, cannot load track"
|
||||
// — the queue had no TrackLoader, so a track played from the UI
|
||||
// changed the queue and then silently did nothing.
|
||||
//
|
||||
// Registering last is what preserves the ordering the wiring depends
|
||||
// on: services start in registration order, on the main goroutine,
|
||||
// before the platform run loop (application.Run's startup closure), so
|
||||
// every service this touches has taken its context by the time this
|
||||
// runs. A service is also the honest description of what this is —
|
||||
// something with a lifecycle the app owns — and it costs no bindings,
|
||||
// since ServiceStartup and ServiceShutdown are excluded from them.
|
||||
type startupService struct {
|
||||
app *YellowJacketApp
|
||||
}
|
||||
|
||||
// ServiceStartup runs the app-level wiring.
|
||||
//
|
||||
// OnDomReady no longer means the DOM is ready — nothing in v3 offers
|
||||
// that — and it does not need to: what it does is start the soft
|
||||
// rescan and report a startup failure, neither of which wants a
|
||||
// frontend. The frontend drives its own state synchronisation by
|
||||
// calling EmitCurrentState once its stores are listening, which is
|
||||
// what makes the rename harmless.
|
||||
func (s *startupService) ServiceStartup(
|
||||
ctx context.Context,
|
||||
_ application.ServiceOptions,
|
||||
) error {
|
||||
s.app.OnStartup(ctx)
|
||||
s.app.OnDomReady(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
+7
-2
@@ -4,14 +4,19 @@
|
||||
version: '3'
|
||||
|
||||
# This information is used to generate the build assets.
|
||||
# The version here is the one baked into the platform *metadata* assets
|
||||
# (Info.plist, the .desktop file, nfpm). It is not what the app reports:
|
||||
# main.version is stamped at link time from the packaging recipes, which
|
||||
# derive it from the git tag. Keep it in step with packaging/arch/PKGBUILD's
|
||||
# pkgver and the Homebrew formula's version when cutting a release.
|
||||
info:
|
||||
companyName: "yellowjacket" # The name of the company
|
||||
productName: "yellowjacket" # The name of the application
|
||||
productIdentifier: "app.yellowjacket" # The unique product identifier
|
||||
description: "A cross-platform desktop music player" # The application description
|
||||
description: "Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag" # The application description
|
||||
copyright: "(c) 2026, yellowjacket" # Copyright text
|
||||
comments: "yj@yellowjacket.app" # Comments
|
||||
version: "0.0.1" # The application version
|
||||
version: "1.3.0" # The application version
|
||||
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
|
||||
# # Should match the name of your .icon file without the extension
|
||||
# # If not set and Assets.car exists, defaults to "appicon"
|
||||
|
||||
+32
-31
@@ -1,34 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>yjref</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.example.yjref</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>appicon</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2026, My Company</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>yellowjacket</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>yj@yellowjacket.app</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>appicon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>app.yellowjacket</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>yellowjacket</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.3.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0.0</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>(c) 2026, yellowjacket</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+27
-26
@@ -1,29 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>yjref</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.example.yjref</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>appicon</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2026, My Company</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>yellowjacket</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>yj@yellowjacket.app</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>appicon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>app.yellowjacket</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>yellowjacket</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.3.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>(c) 2026, yellowjacket</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -43,7 +43,10 @@ tasks:
|
||||
cmds:
|
||||
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"'
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||
# LDFLAGS_EXTRA — see build/linux/Taskfile.yml for why this exists.
|
||||
# Kept identical on both platforms so the Homebrew formula, which
|
||||
# builds on either, has one invocation rather than two.
|
||||
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s{{if .LDFLAGS_EXTRA}} {{.LDFLAGS_EXTRA}}{{end}}"{{end}}'
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||
env:
|
||||
|
||||
Binary file not shown.
@@ -59,7 +59,15 @@ tasks:
|
||||
cmds:
|
||||
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||
# LDFLAGS_EXTRA is this repo's one edit to the scaffold: `wails3
|
||||
# build` has no -ldflags flag of its own, and the packaging recipes
|
||||
# (packaging/arch/PKGBUILD, the Homebrew formula) need to stamp
|
||||
# main.version / main.commit. Pass it as a task variable:
|
||||
# wails3 task build LDFLAGS_EXTRA="-X 'main.version=v1.3.0'"
|
||||
# It is appended inside the production -ldflags string, so the
|
||||
# default -w -s still applies. Empty by default, which is what
|
||||
# keeps `make build-dev` / `make build-prod` unchanged.
|
||||
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s{{if .LDFLAGS_EXTRA}} {{.LDFLAGS_EXTRA}}{{end}}"{{end}}'
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||
env:
|
||||
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=My Product
|
||||
Comment=A yjref application
|
||||
Name=yellowjacket
|
||||
Comment=Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag
|
||||
# The Exec line includes %u to pass the URL to the application
|
||||
Exec=/usr/local/bin/yjref %u
|
||||
Exec=/usr/local/bin/yellowjacket %u
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=yjref
|
||||
Icon=yellowjacket
|
||||
Categories=Utility;
|
||||
StartupWMClass=yjref
|
||||
StartupWMClass=yellowjacket
|
||||
|
||||
|
||||
|
||||
+14
-11
@@ -3,26 +3,29 @@
|
||||
#
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
|
||||
name: "yjref"
|
||||
name: "yellowjacket"
|
||||
arch: ${GOARCH}
|
||||
platform: "linux"
|
||||
version: "0.1.0"
|
||||
version: "1.3.0"
|
||||
section: "default"
|
||||
priority: "extra"
|
||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||
description: "A yjref application"
|
||||
vendor: "My Company"
|
||||
homepage: "https://wails.io"
|
||||
license: "MIT"
|
||||
description: "Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag"
|
||||
vendor: "yellowjacket"
|
||||
# homepage and license are NOT derived from build/config.yml, so
|
||||
# `wails3 task common:update:build-assets` leaves them alone — everything
|
||||
# above them is regenerated from that file and hand edits there are lost.
|
||||
homepage: "https://git.ljones.me/yonlu/yellowjacket"
|
||||
license: "custom" # see the repository; packaging/arch/PKGBUILD says the same
|
||||
release: "1"
|
||||
|
||||
contents:
|
||||
- src: "./bin/yjref"
|
||||
dst: "/usr/local/bin/yjref"
|
||||
- src: "./bin/yellowjacket"
|
||||
dst: "/usr/local/bin/yellowjacket"
|
||||
- src: "./build/appicon.png"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/yjref.png"
|
||||
- src: "./build/linux/yjref.desktop"
|
||||
dst: "/usr/share/applications/yjref.desktop"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/yellowjacket.png"
|
||||
- src: "./build/linux/yellowjacket.desktop"
|
||||
dst: "/usr/share/applications/yellowjacket.desktop"
|
||||
|
||||
# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
|
||||
depends:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "0.1.0"
|
||||
"file_version": "1.3.0"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "0.1.0",
|
||||
"CompanyName": "My Company",
|
||||
"FileDescription": "A yjref application",
|
||||
"LegalCopyright": "© 2026, My Company",
|
||||
"ProductName": "My Product",
|
||||
"Comments": "This is a comment"
|
||||
"ProductVersion": "1.3.0",
|
||||
"CompanyName": "yellowjacket",
|
||||
"FileDescription": "Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag",
|
||||
"LegalCopyright": "(c) 2026, yellowjacket",
|
||||
"ProductName": "yellowjacket",
|
||||
"Comments": "yj@yellowjacket.app"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "yjref"
|
||||
!define INFO_PROJECTNAME "yellowjacket"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "My Company"
|
||||
!define INFO_COMPANYNAME "yellowjacket"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "My Product"
|
||||
!define INFO_PRODUCTNAME "yellowjacket"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "0.1.0"
|
||||
!define INFO_PRODUCTVERSION "1.3.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "© 2026, My Company"
|
||||
!define INFO_COPYRIGHT "(c) 2026, yellowjacket"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.example.yjref" version="0.1.0" processorArchitecture="*"/>
|
||||
<assemblyIdentity type="win32" name="app.yellowjacket" version="1.3.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
|
||||
+74
-62
@@ -56,6 +56,8 @@ import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
import { methodIDs } from '../support/method-ids.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO = resolve(HERE, '../..');
|
||||
const OUT_DIR = resolve(REPO, '.dev/perf');
|
||||
@@ -63,6 +65,16 @@ const BRIDGE = resolve(REPO, '.playwright/init-events.js');
|
||||
|
||||
const BASE_URL = process.env.YJ_URL ?? 'http://localhost:34115';
|
||||
|
||||
/**
|
||||
* methodID -> 'pkg.Type.Method', derived from frontend/bindings/.
|
||||
*
|
||||
* A binding call carries only the id, so this is what turns a
|
||||
* measurement's "which bindings did that provoke" back into names. It
|
||||
* is derived rather than written down for the reason plan 009 phase 6b
|
||||
* gives: a hand-maintained list goes stale silently.
|
||||
*/
|
||||
const METHOD_NAMES = Object.fromEntries(methodIDs());
|
||||
|
||||
// The browse script visited for the heap measurement. Deliberately the
|
||||
// views the audit named as retaining: explore (two unbounded caches),
|
||||
// artists and genres (per-frame work), settings (the 3 s ticker).
|
||||
@@ -123,64 +135,41 @@ function parseArgs(argv) {
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Wrap every bound Go method so a measurement can say which bindings a
|
||||
* user action provoked and how much they returned.
|
||||
* Say which bindings a user action provoked, and how much they
|
||||
* returned.
|
||||
*
|
||||
* Post-hoc wrapping is safe because `frontend/wailsjs` looks its target
|
||||
* up at call time (`window['go']['library']['Library']['GetAllTracks']()`),
|
||||
* so a store holding an imported wrapper still lands here.
|
||||
* This used to walk `window.go` and wrap every bound method in place,
|
||||
* which worked because v2's generated stubs looked their target up at
|
||||
* call time. v3 has no such object — the bindings are bundled modules
|
||||
* — so `.playwright/init-events.js` records every call off the single
|
||||
* POST v3 routes them all through, and this reads that log. It is
|
||||
* strictly better: it needs no walk, sees calls from any module, and
|
||||
* cannot miss one made before a wrapper was installed, which is what
|
||||
* the old "runs twice" dance was working around.
|
||||
*
|
||||
* `bytes` is the response size, so `measureBytes` is turned on here —
|
||||
* only a measurement wants to pay for a clone-and-read of every body.
|
||||
*/
|
||||
const INSTRUMENT = `() => {
|
||||
// Runs twice: once as an initScript (before window.go exists, which
|
||||
// is the only moment early enough to catch the long-task observer's
|
||||
// first entries) and once after the bridge reports ready. So the
|
||||
// state is created at most once and the *walk* happens every time —
|
||||
// getting that backwards silently measures zero binding calls.
|
||||
const first = !window.__yjPerf;
|
||||
const INSTRUMENT = `(names) => {
|
||||
if (window.__yjPerf) return;
|
||||
|
||||
if (first) {
|
||||
const calls = [];
|
||||
window.__yjPerf = {
|
||||
calls,
|
||||
reset: () => { calls.length = 0; },
|
||||
since: (t) => calls.filter((c) => c.start >= t),
|
||||
longtasks: [],
|
||||
};
|
||||
}
|
||||
window.__yjEvents.measureBytes = true;
|
||||
|
||||
const calls = window.__yjPerf.calls;
|
||||
|
||||
const wrap = (obj, key, path) => {
|
||||
const fn = obj[key];
|
||||
if (typeof fn !== 'function' || fn.__yjPerfWrapped) return;
|
||||
const wrapped = function (...args) {
|
||||
const start = performance.now();
|
||||
let out;
|
||||
try { out = fn.apply(this, args); } catch (e) { throw e; }
|
||||
return Promise.resolve(out).then((v) => {
|
||||
let bytes = 0;
|
||||
try { bytes = JSON.stringify(v ?? null).length; } catch { bytes = -1; }
|
||||
calls.push({ path, start, ms: performance.now() - start, bytes });
|
||||
return v;
|
||||
});
|
||||
};
|
||||
wrapped.__yjPerfWrapped = true;
|
||||
obj[key] = wrapped;
|
||||
window.__yjPerf = {
|
||||
get calls() {
|
||||
return window.__yjEvents.bindings.map((c) => ({
|
||||
path: names[c.methodID] || ('#' + c.methodID),
|
||||
methodID: c.methodID,
|
||||
start: c.start,
|
||||
ms: c.ms,
|
||||
bytes: c.bytes,
|
||||
}));
|
||||
},
|
||||
reset: () => { window.__yjEvents.reset(); },
|
||||
since: (t) => window.__yjPerf.calls.filter((c) => c.start >= t),
|
||||
longtasks: [],
|
||||
};
|
||||
|
||||
const walk = (obj, prefix, depth) => {
|
||||
if (!obj || depth > 4) return;
|
||||
for (const key of Object.keys(obj)) {
|
||||
const v = obj[key];
|
||||
if (typeof v === 'function') wrap(obj, key, prefix + key);
|
||||
else if (v && typeof v === 'object') walk(v, prefix + key + '.', depth + 1);
|
||||
}
|
||||
};
|
||||
|
||||
walk(window.go, '', 0);
|
||||
|
||||
if (!first) return;
|
||||
|
||||
// Long tasks are the honest form of "the app stalls": a 25 MB JSON
|
||||
// parse on the main thread shows up here and nowhere else.
|
||||
try {
|
||||
@@ -352,7 +341,15 @@ async function measureTrackChange(page) {
|
||||
const paths = (tracks ?? []).slice(0, 4).map((t) => t.FilePath);
|
||||
if (paths.length < 2) return { error: 'library too small to measure' };
|
||||
|
||||
await ev.call('queue.Queue.SetQueue', [paths, 0, false], 15000);
|
||||
await ev.call(
|
||||
'queue.Queue.SetQueue',
|
||||
// The fourth argument is the queue's source; these are
|
||||
// ad-hoc tracks, so it is the empty one. v3 rejects a
|
||||
// call with the wrong argument count where v2 filled the
|
||||
// gap with a zero value.
|
||||
[paths, 0, false, { type: '', id: 0, label: '' }],
|
||||
15000,
|
||||
);
|
||||
await ev.call('queue.Queue.PlayIndex', [0], 15000);
|
||||
|
||||
// Settle mid-track before starting to record. Starting playback
|
||||
@@ -1046,14 +1043,15 @@ const SCROLL_SETTLE_MS = 260;
|
||||
async function measureScroll(page) {
|
||||
// -- M3: the track list, with the Art column staged on. --
|
||||
const priorColumns = await page.evaluate(async () => {
|
||||
const prior = await window.go.config.Config.GetTrackListColumns();
|
||||
const ev = window.__yjEvents;
|
||||
const prior = await ev.call('config.Config.GetTrackListColumns', [], 15000);
|
||||
|
||||
await window.go.config.Config.SetTrackListColumns(
|
||||
await ev.call('config.Config.SetTrackListColumns', [
|
||||
[{ id: 'albumArt' }, { id: 'trackName' },
|
||||
{ id: 'artistName' }, { id: 'trackLength' }],
|
||||
);
|
||||
], 15000);
|
||||
|
||||
return prior.map((c) => ({ id: c.id }));
|
||||
return (prior ?? []).map((c) => ({ id: c.id }));
|
||||
});
|
||||
|
||||
const scrollView = async (view, tag) => {
|
||||
@@ -1141,7 +1139,9 @@ async function measureScroll(page) {
|
||||
const artists = await scrollView('artists', 'artists-view');
|
||||
|
||||
await page.evaluate(
|
||||
(cols) => window.go.config.Config.SetTrackListColumns(cols),
|
||||
(cols) => window.__yjEvents.call(
|
||||
'config.Config.SetTrackListColumns', [cols], 15000,
|
||||
),
|
||||
priorColumns,
|
||||
);
|
||||
|
||||
@@ -1605,7 +1605,15 @@ async function measurePlayerBarPass(page) {
|
||||
|
||||
if (paths.length < 2) return { error: 'library too small to measure' };
|
||||
|
||||
await ev.call('queue.Queue.SetQueue', [paths, 0, false], 15000);
|
||||
await ev.call(
|
||||
'queue.Queue.SetQueue',
|
||||
// The fourth argument is the queue's source; these are
|
||||
// ad-hoc tracks, so it is the empty one. v3 rejects a
|
||||
// call with the wrong argument count where v2 filled the
|
||||
// gap with a zero value.
|
||||
[paths, 0, false, { type: '', id: 0, label: '' }],
|
||||
15000,
|
||||
);
|
||||
await ev.call('queue.Queue.PlayIndex', [0], 15000);
|
||||
await ev.call('player.Player.Pause', [], 5000).catch(() => {});
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
@@ -1804,7 +1812,9 @@ async function run(label) {
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
await context.addInitScript({ path: BRIDGE });
|
||||
await context.addInitScript(`(${INSTRUMENT})()`);
|
||||
await context.addInitScript(
|
||||
`(${INSTRUMENT})(${JSON.stringify(METHOD_NAMES)})`,
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
const client = await context.newCDPSession(page);
|
||||
@@ -1813,8 +1823,10 @@ async function run(label) {
|
||||
const t0 = Date.now();
|
||||
await page.goto(BASE_URL, { waitUntil: 'load' });
|
||||
await page.evaluate(() => window.__yjEvents.ready(30000));
|
||||
// Wrapping runs before `window.go` exists; re-run now that it does.
|
||||
await page.evaluate(`(${INSTRUMENT})()`);
|
||||
// No second instrumentation pass. The old one existed because
|
||||
// wrapping had to happen after `window.go` appeared, yet the long
|
||||
// task observer had to start before it; the bridge now records every
|
||||
// binding call from the initScript onward, so one pass does both.
|
||||
|
||||
const report = {
|
||||
label,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* These specs drive the *real* application: the Wails dev server on
|
||||
* :34115 serves the real frontend with real bindings on `window.go`,
|
||||
* These specs drive the *real* application: Wails v3's server mode on
|
||||
* :34115 serves the real frontend with the real generated bindings,
|
||||
* bridged to the same Go backend a desktop window would use. Nothing
|
||||
* here is mocked.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,14 @@ import type { Page } from '@playwright/test';
|
||||
* none, so Play was wired, labelled correctly, clicked cleanly and
|
||||
* queued **nothing**. Every component test still passed.
|
||||
*/
|
||||
// There is no test for the tracklist legend, and there should not be:
|
||||
// `dcc40b1` inverted the mark — rows *not* in the library are dimmed in
|
||||
// place and nothing marks the ones that are — and deleted
|
||||
// `.tracklist-legend` with it. The spec asserting it survived that
|
||||
// commit and has been failing on main ever since. What replaced it
|
||||
// (the dimming, and the `aria-disabled` that carries it to anyone not
|
||||
// seeing the page) is covered at the component tier, in
|
||||
// frontend/test/components/album-actions.test.ts.
|
||||
test.describe('playing an album from its page', () => {
|
||||
test.beforeEach(async ({ app }) => {
|
||||
await openFirstAlbum(app);
|
||||
@@ -55,16 +63,6 @@ test.describe('playing an album from its page', () => {
|
||||
await expect.poll(() => queueLength(app)).toBe(before * 2);
|
||||
});
|
||||
|
||||
test('the ticks against the tracks have a legend', async ({ app }) => {
|
||||
// `H-13` calls them unexplained. They were never *unlabelled* — the
|
||||
// indicator has carried a title and an aria-label reading
|
||||
// "Track “X” is in your library" all along — but a sighted user
|
||||
// scanning the page got a column of green circles and no key.
|
||||
await expect(
|
||||
app.locator('explore-album-details').locator('.tracklist-legend'),
|
||||
).toContainText('in your library');
|
||||
});
|
||||
|
||||
test('the ticks are badges, not keyboard stops', async ({ app }) => {
|
||||
// Every one of them was a <button> whose click handler was a
|
||||
// stopPropagation() and a comment saying to wire up the download
|
||||
|
||||
+25
-10
@@ -15,12 +15,18 @@ import {
|
||||
*/
|
||||
test.describe('harness', () => {
|
||||
test('the app is the real app, not a mock', async ({ app }) => {
|
||||
// All 11 bound services land on window.go through the dev server.
|
||||
const services = await app.evaluate(() => Object.keys(window.go));
|
||||
// v2 landed every bound service on `window.go`, so "is this real"
|
||||
// could be asked of an object. v3 has no such global — the
|
||||
// bindings are ordinary bundled modules — so the question is asked
|
||||
// of the runtime instead, which is a better question anyway: the
|
||||
// real runtime is loaded, and it answers for real methods and
|
||||
// refuses invented ones.
|
||||
const runtime = await app.evaluate(() => ({
|
||||
dispatch: typeof window._wails?.dispatchWailsEvent,
|
||||
client: typeof window._wails?.clientId,
|
||||
}));
|
||||
|
||||
expect(services).toEqual(
|
||||
expect.arrayContaining(['queue', 'player', 'library', 'explore']),
|
||||
);
|
||||
expect(runtime).toEqual({ dispatch: 'function', client: 'string' });
|
||||
|
||||
const state = await callBinding<{ tracks: unknown[] }>(
|
||||
app,
|
||||
@@ -28,6 +34,12 @@ test.describe('harness', () => {
|
||||
);
|
||||
|
||||
expect(state).toHaveProperty('tracks');
|
||||
|
||||
const unknown = await app
|
||||
.evaluate(() => window.__yjEvents.call('queue.Queue.Nope', [], 2_000))
|
||||
.catch((err: Error) => err.message);
|
||||
|
||||
expect(unknown).toContain('unknown bound method');
|
||||
});
|
||||
|
||||
test('backend events are recorded, in order, with payloads', async ({
|
||||
@@ -58,10 +70,12 @@ test.describe('harness', () => {
|
||||
});
|
||||
|
||||
test('a binding called with wrong types fails fast', async ({ app }) => {
|
||||
// player.UserVolume is an int. Passing a float makes the backend
|
||||
// log "error parsing arguments" and never fire the callback; without
|
||||
// a timeout the promise never settles and the spec hangs until the
|
||||
// suite gives up.
|
||||
// player.UserVolume is an int. Under v2 a float made the backend
|
||||
// log "error parsing arguments" and never fire the callback, so
|
||||
// this asserted that the harness's own timeout fired — the failure
|
||||
// was visible only because the harness invented a deadline. v3
|
||||
// answers 422 with a TypeError naming the argument, so the
|
||||
// assertion is now on the backend's own words.
|
||||
const failure = await app.evaluate(async () => {
|
||||
try {
|
||||
await window.__yjEvents.call(
|
||||
@@ -76,7 +90,8 @@ test.describe('harness', () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(failure).toContain('did not settle');
|
||||
expect(failure).toContain('TypeError');
|
||||
expect(failure).toContain('player.UserVolume');
|
||||
});
|
||||
|
||||
test('the control surface is mounted and seeded', async ({ testctl }) => {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { test, expect, resetEvents, callBinding } from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
resetEvents,
|
||||
callBinding,
|
||||
bindingCalls,
|
||||
NO_QUEUE_SOURCE,
|
||||
} from '../support/fixtures.js';
|
||||
|
||||
/**
|
||||
* Finishing a track is cheap, and does not disturb the user.
|
||||
@@ -19,23 +28,17 @@ import { test, expect, resetEvents, callBinding } from '../support/fixtures.js';
|
||||
/** Long enough for a fixture track (2–6 s) to finish by itself. */
|
||||
const FINISH_TIMEOUT = 60_000;
|
||||
|
||||
/** Instrument the library bindings so "was anything refetched" is a fact. */
|
||||
const COUNT_LIBRARY_CALLS = `(() => {
|
||||
const w = window;
|
||||
if (w.__yjCalls) { w.__yjCalls.length = 0; return; }
|
||||
w.__yjCalls = [];
|
||||
const lib = w.go.library.Library;
|
||||
for (const key of Object.keys(lib)) {
|
||||
const fn = lib[key];
|
||||
if (typeof fn !== 'function' || fn.__counted) continue;
|
||||
const wrapped = function (...args) {
|
||||
w.__yjCalls.push(key);
|
||||
return fn.apply(this, args);
|
||||
};
|
||||
wrapped.__counted = true;
|
||||
lib[key] = wrapped;
|
||||
}
|
||||
})()`;
|
||||
/**
|
||||
* "Was anything refetched" is a fact, not an inference.
|
||||
*
|
||||
* This used to wrap every method on `window.go.library.Library` in
|
||||
* place. v3 has no such object, and does not need one: the harness
|
||||
* bridge records every binding call off the single POST they all go
|
||||
* through, so the question is answered by reading that log rather than
|
||||
* by instrumenting a target first. `resetEvents` clears it.
|
||||
*/
|
||||
const libraryCalls = async (app: Page): Promise<string[]> =>
|
||||
(await bindingCalls(app)).filter((c) => c.startsWith('library.Library.'));
|
||||
|
||||
/**
|
||||
* Select rows by dispatching on the row rather than clicking it.
|
||||
@@ -82,8 +85,7 @@ test.describe('a finished track', () => {
|
||||
|
||||
expect(paths.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
||||
await app.evaluate(COUNT_LIBRARY_CALLS);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
await resetEvents(app);
|
||||
|
||||
await callBinding(app, 'queue.Queue.PlayIndex', [0]);
|
||||
@@ -111,12 +113,10 @@ test.describe('a finished track', () => {
|
||||
'a play emitted the retag event, which invalidates every cache',
|
||||
).toBe(0);
|
||||
|
||||
const refetched = await app.evaluate(
|
||||
() => (window as unknown as { __yjCalls: string[] }).__yjCalls,
|
||||
);
|
||||
const refetched = await libraryCalls(app);
|
||||
|
||||
expect(
|
||||
refetched.filter((c) => c.startsWith('GetAll')),
|
||||
refetched.filter((c) => c.startsWith('library.Library.GetAll')),
|
||||
'a play refetched a collection',
|
||||
).toEqual([]);
|
||||
|
||||
@@ -136,7 +136,7 @@ test.describe('a finished track', () => {
|
||||
|
||||
const paths = await app.evaluate(firstPaths, 2);
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
await resetEvents(app);
|
||||
await callBinding(app, 'queue.Queue.PlayIndex', [0]);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resetEvents,
|
||||
waitForEvent,
|
||||
LONG_TRACK,
|
||||
NO_QUEUE_SOURCE,
|
||||
} from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
@@ -161,6 +162,7 @@ test.describe('a finished queue keeps its context', () => {
|
||||
[rows[0].file_path],
|
||||
0,
|
||||
false,
|
||||
NO_QUEUE_SOURCE,
|
||||
]);
|
||||
await waitForEvent(app, 'QueueChanged');
|
||||
await callBinding(app, 'queue.Queue.Play');
|
||||
@@ -199,7 +201,7 @@ test.describe('a track that will not play says so', () => {
|
||||
try {
|
||||
await callBinding(app, 'queue.Queue.Clear');
|
||||
await resetEvents(app);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
await waitForEvent(app, 'QueueChanged');
|
||||
await callBinding(app, 'queue.Queue.Play');
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { test, expect, callBinding } from '../support/fixtures.js';
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
NO_QUEUE_SOURCE,
|
||||
} from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
@@ -35,7 +40,7 @@ async function queueFourAndOpen(app: Page): Promise<string[]> {
|
||||
return (tracks as { FilePath: string }[]).slice(0, 4).map((t) => t.FilePath);
|
||||
});
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
|
||||
// A closed panel renders no list at all, so there is no row to focus.
|
||||
await app.locator('#queue-button').click();
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { test, expect, callBinding, waitForEvent } from '../support/fixtures.js';
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
waitForEvent,
|
||||
NO_QUEUE_SOURCE,
|
||||
} from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
@@ -64,7 +70,7 @@ async function playTheLongOne(app: Page): Promise<void> {
|
||||
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
await waitForEvent(app, 'TrackChanged');
|
||||
|
||||
// The scroll cycle is armed 1500 ms after the geometry is measured,
|
||||
|
||||
@@ -199,39 +199,49 @@ function addRequest(
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop any request for this album, through the raw binding.
|
||||
* Drop any request for this album, over the runtime endpoint.
|
||||
*
|
||||
* Deliberately not `callBinding`: that goes through `window.__yjEvents`,
|
||||
* which only exists on a page the `app` fixture created — a bare
|
||||
* `browser.newPage()` has no init script, so the bridge is undefined and
|
||||
* the cleanup throws where nobody is looking. The first version of this
|
||||
* did exactly that and left the request behind, which failed the *next*
|
||||
* run of this same spec.
|
||||
* Deliberately not `callBinding`: that goes through
|
||||
* `window.__yjEvents`, which only exists on a page the `app` fixture
|
||||
* created — a bare `browser.newPage()` has no init script, so the
|
||||
* bridge is undefined and the cleanup throws where nobody is looking.
|
||||
* The first version of this did exactly that and left the request
|
||||
* behind, which failed the *next* run of this same spec.
|
||||
*
|
||||
* v2's answer was `window.go`, which every page had. v3 has no such
|
||||
* global, and the version of this that kept reading it did not throw —
|
||||
* it returned early on `if (!svc)`, which is the same silent cleanup
|
||||
* with a different cause. A POST to `/wails/runtime` needs neither:
|
||||
* it is the same request the bundle makes, and any page can make it.
|
||||
*/
|
||||
async function clearRequest(page: import('@playwright/test').Page) {
|
||||
await page.evaluate(async (mbid) => {
|
||||
const go = (
|
||||
window as unknown as {
|
||||
go?: {
|
||||
download?: {
|
||||
Service?: {
|
||||
ListRequests(): Promise<{ id: number; mbid: string }[]>;
|
||||
RemoveRequest(id: number): Promise<void>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
).go;
|
||||
const call = async (method: string, args: unknown[]) => {
|
||||
const res = await fetch('/wails/runtime', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
object: 0,
|
||||
method: 0,
|
||||
args: {
|
||||
'call-id': `clear-${method}`,
|
||||
methodName: `yellowjacket/backend/${method}`,
|
||||
args,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const svc = go?.download?.Service;
|
||||
if (!res.ok) throw new Error(`${method}: ${await res.text()}`);
|
||||
|
||||
if (!svc) return;
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const rows = (await svc.ListRequests()) ?? [];
|
||||
const rows: { id: number; mbid: string }[] =
|
||||
(await call('download.Service.ListRequests', [])) ?? [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.mbid?.toLowerCase() === mbid.toLowerCase()) {
|
||||
await svc.RemoveRequest(row.id);
|
||||
await call('download.Service.RemoveRequest', [row.id]);
|
||||
}
|
||||
}
|
||||
}, MBID);
|
||||
|
||||
+49
-6
@@ -3,12 +3,26 @@ import { dirname, resolve } from 'node:path';
|
||||
|
||||
import { test as base, expect, type Page } from '@playwright/test';
|
||||
|
||||
import { nameOf } from './method-ids.mjs';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/** The same bridge `playwright-cli` loads, so an exploratory session and
|
||||
* a committed spec see an identical page. */
|
||||
const INIT_SCRIPT = resolve(here, '../../.playwright/init-events.js');
|
||||
|
||||
/**
|
||||
* The fourth argument to `queue.Queue.SetQueue`, for a queue with no
|
||||
* single source — the whole library, or a handful of ad-hoc tracks,
|
||||
* which is what every spec here builds.
|
||||
*
|
||||
* It is passed explicitly because v3 rejects a call with the wrong
|
||||
* argument count (`expects 4 arguments, got 3`) where v2 accepted one
|
||||
* and filled the gap with a zero value. The specs had been three-arg
|
||||
* since the parameter was added; nothing said so.
|
||||
*/
|
||||
export const NO_QUEUE_SOURCE = { type: '', id: 0, label: '' };
|
||||
|
||||
/**
|
||||
* The 90-second fixture track (`cmd/gentestdata`, case `edge-lengths`).
|
||||
*
|
||||
@@ -61,12 +75,13 @@ export async function eventNames(
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a bound Go method with a timeout.
|
||||
* Call a bound Go method by name, over the runtime's own endpoint.
|
||||
*
|
||||
* Wrong argument types make the backend log "error parsing arguments"
|
||||
* and never fire the callback, so an unguarded call hangs until the
|
||||
* whole spec times out with no clue why. This fails in seconds and
|
||||
* says where to look.
|
||||
* v3 rejects a bad call rather than never firing its callback the way
|
||||
* v2 did: a wrong argument type comes back as a TypeError naming the
|
||||
* argument, a wrong count as `expects 4 arguments, got 3`, an unknown
|
||||
* method as a ReferenceError. The timeout is a backstop for a hung
|
||||
* request, not the mechanism that makes a mistake visible.
|
||||
*/
|
||||
export async function callBinding<T = unknown>(
|
||||
page: Page,
|
||||
@@ -81,6 +96,21 @@ export async function callBinding<T = unknown>(
|
||||
) as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The binding calls the *app* made, newest last, as `pkg.Type.Method`.
|
||||
*
|
||||
* This is what replaces v2's trick of wrapping `window.go` in place:
|
||||
* `.playwright/init-events.js` records every call off the one POST v3
|
||||
* routes them all through, and `method-ids.ts` names them from the
|
||||
* generated tree. It sees calls from any module and cannot miss one
|
||||
* made before a wrapper was installed.
|
||||
*/
|
||||
export async function bindingCalls(page: Page): Promise<string[]> {
|
||||
const calls = await page.evaluate(() => window.__yjEvents.bindings);
|
||||
|
||||
return calls.map(nameOf);
|
||||
}
|
||||
|
||||
/** Thin client for the dev-only /__test/ surface (backend/testctl). */
|
||||
export class TestCtl {
|
||||
constructor(private readonly baseURL: string) {}
|
||||
@@ -166,7 +196,20 @@ declare global {
|
||||
): Promise<YjEvent>;
|
||||
ready(timeoutMs?: number): Promise<boolean>;
|
||||
call(path: string, args?: unknown[], timeoutMs?: number): Promise<any>;
|
||||
/** Every binding call the app itself made; see init-events.js. */
|
||||
bindings: {
|
||||
methodID: number | null;
|
||||
methodName: string | null;
|
||||
start: number;
|
||||
ms: number;
|
||||
bytes: number;
|
||||
}[];
|
||||
measureBytes: boolean;
|
||||
};
|
||||
/** The v3 runtime's own namespace, installed by @wailsio/runtime. */
|
||||
_wails?: {
|
||||
dispatchWailsEvent?: (event: unknown) => void;
|
||||
clientId?: string;
|
||||
};
|
||||
go: Record<string, Record<string, Record<string, (...a: any[]) => any>>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
|
||||
/**
|
||||
* Turns the method ids a binding call carries back into the names a
|
||||
* spec (or a measurement) wants to read.
|
||||
*
|
||||
* Plain JavaScript, not TypeScript, because `e2e/perf/measure.mjs` runs
|
||||
* under bare `node` and both it and the specs need exactly this map —
|
||||
* and one derivation with a `.mjs` extension is better than two that
|
||||
* can disagree.
|
||||
*
|
||||
* v2 put every bound method on `window.go`, so a harness could walk
|
||||
* that object to wrap or enumerate them. v3 has no such surface: the
|
||||
* generated bindings are ordinary bundled modules calling
|
||||
* `$Call.ByID(<fnv hash of the fully-qualified name>)`, and what
|
||||
* reaches the wire — and therefore what `.playwright/init-events.js`
|
||||
* can record — is the number.
|
||||
*
|
||||
* Plan 009 phase 6b named two ways to get the list back. This is the
|
||||
* preferred one: derive it from `frontend/bindings/`, which is a real
|
||||
* generated tree and is already gated by `make bindings-check`. The
|
||||
* alternative — a hand-maintained list — goes stale silently, which is
|
||||
* the failure mode that check exists to prevent.
|
||||
*
|
||||
* Nothing here hashes anything. The generated source carries the id as
|
||||
* a literal beside the function that sends it, so this reads the two
|
||||
* together rather than recomputing one from the other and hoping the
|
||||
* hash still matches.
|
||||
*/
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const BINDINGS_ROOT = resolve(here, '../../frontend/bindings/yellowjacket');
|
||||
|
||||
/** `export function Name(…) { return $Call.ByID(123, …) }` */
|
||||
const BOUND_METHOD = /export function (\w+)\([\s\S]*?\$Call\.ByID\((\d+)/g;
|
||||
|
||||
/** `import * as Library from "./library.js";` — the only place the Go
|
||||
* type's casing survives; the file is `library.ts`, and
|
||||
* `frontendutil.ts` cannot tell you it is `FrontendUtil`. */
|
||||
const SERVICE_EXPORT = /import \* as (\w+) from "\.\/([\w.]+)\.js"/g;
|
||||
|
||||
function* walk(dir) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const path = join(dir, entry);
|
||||
|
||||
if (statSync(path).isDirectory()) yield* walk(path);
|
||||
else if (entry.endsWith('.ts')) yield path;
|
||||
}
|
||||
}
|
||||
|
||||
let cached;
|
||||
|
||||
/**
|
||||
* methodIDs maps a binding's method id to `pkg.Type.Method` — the same
|
||||
* short path `callBinding` takes, so a spec never has to know that the
|
||||
* backend calls it `yellowjacket/backend/queue.Queue.GetState`.
|
||||
*/
|
||||
export function methodIDs() {
|
||||
if (cached) return cached;
|
||||
|
||||
const byID = new Map();
|
||||
|
||||
for (const file of walk(BINDINGS_ROOT)) {
|
||||
if (!file.endsWith('index.ts')) continue;
|
||||
|
||||
const dir = dirname(file);
|
||||
const pkg = dir.split('/').pop() ?? '';
|
||||
const index = readFileSync(file, 'utf8');
|
||||
|
||||
for (const [, typeName, base] of index.matchAll(SERVICE_EXPORT)) {
|
||||
let source;
|
||||
|
||||
try {
|
||||
source = readFileSync(join(dir, `${base}.ts`), 'utf8');
|
||||
} catch {
|
||||
continue; // a models-only re-export, which binds nothing
|
||||
}
|
||||
|
||||
for (const [, method, id] of source.matchAll(BOUND_METHOD)) {
|
||||
byID.set(Number(id), `${pkg}.${typeName}.${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (byID.size === 0) {
|
||||
throw new Error(
|
||||
`method-ids: no bound methods under ${BINDINGS_ROOT}; ` +
|
||||
`run 'make bindings'`,
|
||||
);
|
||||
}
|
||||
|
||||
cached = byID;
|
||||
|
||||
return byID;
|
||||
}
|
||||
|
||||
/** Names one recorded binding call, or `#<id>` if the map has no entry
|
||||
* — which fails the assertion naming it rather than passing quietly. */
|
||||
export function nameOf(call) {
|
||||
if (call.methodName) {
|
||||
return call.methodName.replace(/^yellowjacket\/backend\//, '');
|
||||
}
|
||||
|
||||
return call.methodID === null
|
||||
? '#unknown'
|
||||
: (methodIDs().get(call.methodID) ?? `#${call.methodID}`);
|
||||
}
|
||||
+8
-2
@@ -8,7 +8,13 @@
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"allowImportingTsExtensions": true
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["specs/**/*.ts", "support/**/*.ts", "playwright.config.ts"]
|
||||
"include": [
|
||||
"specs/**/*.ts",
|
||||
"support/**/*.ts",
|
||||
"support/**/*.mjs",
|
||||
"playwright.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//@ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
Object.freeze($Create.Events);
|
||||
@@ -0,0 +1,2 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
Executable → Regular
+3
-2
@@ -1,5 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {home} from '../models';
|
||||
|
||||
export function GetShelves():Promise<Array<home.Shelf>>;
|
||||
export {
|
||||
Duration
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,38 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* A Duration represents the elapsed time between two instants
|
||||
* as an int64 nanosecond count. The representation limits the
|
||||
* largest representable duration to approximately 290 years.
|
||||
*/
|
||||
export enum Duration {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = 0,
|
||||
|
||||
minDuration = -9223372036854775808,
|
||||
maxDuration = 9223372036854775807,
|
||||
|
||||
/**
|
||||
* Common durations. There is no definition for units of Day or larger
|
||||
* to avoid confusion across daylight savings time zone transitions.
|
||||
*
|
||||
* To count the number of units in a [Duration], divide:
|
||||
*
|
||||
* second := time.Second
|
||||
* fmt.Print(int64(second/time.Millisecond)) // prints 1000
|
||||
*
|
||||
* To convert an integer number of units to a Duration, multiply:
|
||||
*
|
||||
* seconds := 10
|
||||
* fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
|
||||
*/
|
||||
Nanosecond = 1,
|
||||
Microsecond = 1000,
|
||||
Millisecond = 1000000,
|
||||
Second = 1000000000,
|
||||
Minute = 60000000000,
|
||||
Hour = 3600000000000,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export type {
|
||||
AlignmentView,
|
||||
ApplyResultView,
|
||||
CandidateView,
|
||||
FailureView,
|
||||
LocalTrackView,
|
||||
PendingItem,
|
||||
ScoreBreakdownView,
|
||||
ScoreView,
|
||||
SearchHitView
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,187 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* AlignmentView mirrors autotag.TrackAlignment. LocalIndex of -1
|
||||
* means "candidate has this track, folder doesn't" (status=missing).
|
||||
*/
|
||||
export interface AlignmentView {
|
||||
"localIndex": number;
|
||||
"localTitle": string;
|
||||
"localLengthMillis": number;
|
||||
"candidatePosition": number;
|
||||
"candidateDiscNumber": number;
|
||||
"candidateTitle": string;
|
||||
"candidateMbid": string;
|
||||
"candidateLength": number;
|
||||
"titleScore": number;
|
||||
"lengthDeltaMs": number;
|
||||
"trackNumberOk": boolean;
|
||||
"status": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApplyResultView mirrors autotag.ApplyResult for the frontend.
|
||||
*/
|
||||
export interface ApplyResultView {
|
||||
"groupKey": string;
|
||||
"succeeded": number;
|
||||
"failed": number;
|
||||
"failures": FailureView[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CandidateView mirrors autotag.Candidate + its alignments. The
|
||||
* CoverArtURL is populated eagerly only for the top-ranked
|
||||
* candidate (eager network fetch); the frontend calls
|
||||
* GetCandidateCoverArt to lazy-load art for the others when the
|
||||
* user selects them. Date is this release's own date (re-issue
|
||||
* year for remasters); OriginalDate is the release-group's
|
||||
* first-release-date (the original album year).
|
||||
*/
|
||||
export interface CandidateView {
|
||||
"releaseMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"title": string;
|
||||
"artistCredit": string;
|
||||
"date": string;
|
||||
"originalDate": string;
|
||||
"country": string;
|
||||
"status": string;
|
||||
"primaryType": string;
|
||||
"trackCount": number;
|
||||
"score": number;
|
||||
"breakdown": ScoreBreakdownView;
|
||||
"source": string;
|
||||
"provenance": string;
|
||||
"coverArtUrl": string;
|
||||
"alignments": AlignmentView[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* FailureView carries one failed track for the frontend's toast.
|
||||
*/
|
||||
export interface FailureView {
|
||||
"filePath": string;
|
||||
"error": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalTrackView mirrors autotag.LocalTrack.
|
||||
*/
|
||||
export interface LocalTrackView {
|
||||
"audioFileId": number;
|
||||
"filePath": string;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"trackNumber": number;
|
||||
"discNumber": number;
|
||||
"lengthMillis": number;
|
||||
"recordingMbid": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* PendingItem is a projection of tagging_items that's safe to hand
|
||||
* to the frontend. Score is dereferenced to 0 when NULL so TS sees
|
||||
* a plain number.
|
||||
*/
|
||||
export interface PendingItem {
|
||||
"groupKey": string;
|
||||
"libraryId": number;
|
||||
"libraryName": string;
|
||||
|
||||
/**
|
||||
* FolderSubPath is the album folder's path relative to its
|
||||
* library root (e.g. "Beatles/Abbey Road"). Empty when the
|
||||
* path can't be derived (no audio files yet, missing library
|
||||
* row, etc.). Populated by ListPendingFolders and
|
||||
* GetPendingFolder; not present on rows returned by other
|
||||
* list/get queries that haven't been extended.
|
||||
*/
|
||||
"folderSubPath": string;
|
||||
"trackCount": number;
|
||||
"albumName": string;
|
||||
"albumArtist": string;
|
||||
"discNumber": number;
|
||||
"bestMatchReleaseMbid": string;
|
||||
"score": number;
|
||||
"status": string;
|
||||
|
||||
/**
|
||||
* Synthetic marks a group SplitMixedFolder carved out of a
|
||||
* bigger folder by matching tags rather than a directory — the
|
||||
* review UI labels these distinctly since several may share the
|
||||
* same FolderSubPath.
|
||||
*/
|
||||
"synthetic": boolean;
|
||||
|
||||
/**
|
||||
* LikelyMixedBag is a cheap SQL-side approximation of autotag.
|
||||
* IsMixedBag, computed for the whole library in one pass by
|
||||
* ListPendingFolders (see ListLikelyMixedBagGroupKeys) rather
|
||||
* than hydrating every group's tracks in Go. It's a badge hint,
|
||||
* not a guarantee — ScoreView.MixedBag (computed from the real
|
||||
* track list when a folder is opened) is the authoritative check
|
||||
* that gates the SplitMixedFolder action itself.
|
||||
*/
|
||||
"likelyMixedBag": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScoreBreakdownView mirrors autotag.ScoreBreakdown for display.
|
||||
* Each field is in [0, 1].
|
||||
*/
|
||||
export interface ScoreBreakdownView {
|
||||
"titleAvg": number;
|
||||
"lengthAvg": number;
|
||||
"artistFit": number;
|
||||
"albumFit": number;
|
||||
"trackCountFit": number;
|
||||
"releaseMeta": number;
|
||||
"evidence": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScoreView is the Wails-friendly projection of autotag.GroupScore.
|
||||
*/
|
||||
export interface ScoreView {
|
||||
"groupKey": string;
|
||||
"localTracks": LocalTrackView[] | null;
|
||||
"candidates": CandidateView[] | null;
|
||||
|
||||
/**
|
||||
* Recommendation is the qualitative confidence tier for the
|
||||
* ranked list: "none", "low", "medium", or "strong". Unlike the
|
||||
* raw score it accounts for ambiguity (a rival release group
|
||||
* scoring nearly as high) and alignment defects.
|
||||
*/
|
||||
"recommendation": string;
|
||||
|
||||
/**
|
||||
* MixedBag is true when this group's tracks look like an
|
||||
* unrelated pile rather than one release (autotag.IsMixedBag) —
|
||||
* the review UI offers SplitMixedFolder when set. Always false
|
||||
* for a group that's already Synthetic; a split group doesn't
|
||||
* get split again.
|
||||
*/
|
||||
"mixedBag": boolean;
|
||||
|
||||
/**
|
||||
* Synthetic mirrors PendingItem.Synthetic for the currently
|
||||
* open group.
|
||||
*/
|
||||
"synthetic": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchHitView is one in-app MusicBrainz search result surfaced to
|
||||
* the review UI. Kind is "releasegroup" or "recording" so the
|
||||
* frontend knows which resolve path SelectSearchCandidate must take.
|
||||
*/
|
||||
export interface SearchHitView {
|
||||
"mbid": string;
|
||||
"kind": string;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"detail": string;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service is the Wails-bound surface for the review UI. All
|
||||
* exported methods become TypeScript stubs that the frontend calls.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as jobs$0 from "../jobs/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AckLibraryWarning records that the user has seen the first-
|
||||
* time-apply irreversibility warning for this library.
|
||||
*/
|
||||
export function AckLibraryWarning(libraryID: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(1523853598, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply runs the apply pipeline for the given group + chosen
|
||||
* release MBID. Uses the cached candidate list from the last
|
||||
* GetCandidates / GetCandidatesForPasteURL call so the user
|
||||
* always applies the exact release they saw in the UI.
|
||||
* Empty releaseMBID picks the top-scored candidate.
|
||||
*/
|
||||
export function Apply(groupKey: string, releaseMBID: string): $CancellablePromise<$models.ApplyResultView | null> {
|
||||
return $Call.ByID(2015906074, groupKey, releaseMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ApplyAsync dispatches an Apply for the given group in a
|
||||
* background goroutine and returns immediately. Progress is
|
||||
* reported to the frontend via three Wails events:
|
||||
*
|
||||
* - AutotagApplyStarted {groupKey, total}
|
||||
* - AutotagApplyProgress {groupKey, current, total, succeeded, failed}
|
||||
* - AutotagApplyFinished {groupKey, succeeded, failed, error}
|
||||
*
|
||||
* The candidate list and locals are snapshot synchronously before
|
||||
* returning so a concurrent GetCandidates can't shuffle the
|
||||
* selected release out from under the running job. Returns
|
||||
* ErrApplyInFlight when a job for the same groupKey is already
|
||||
* running; otherwise nil. Job-level errors land in the Finished
|
||||
* event payload, not the return value.
|
||||
*/
|
||||
export function ApplyAsync(groupKey: string, releaseMBID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2246879134, groupKey, releaseMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ClearCompletedEntries marks every confirmed item in the given
|
||||
* library (or all libraries when libraryID = 0) as cleared. The
|
||||
* rows stay in the table — so a re-scan of the same folder
|
||||
* won't bounce them back into 'pending' — but they no longer
|
||||
* appear in the review queue. Returns the number of rows
|
||||
* affected so the frontend can show a quick toast.
|
||||
*/
|
||||
export function ClearCompletedEntries(libraryID: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(1606786204, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCandidateCoverArt is the lazy-load entrypoint for cover art
|
||||
* when the user picks a non-top candidate. Goes through the
|
||||
* CAA-only chain (disk cache → network) — never the local
|
||||
* library-by-name index, since that index conflates embedded ID3
|
||||
* art with externally-fetched art and the embedded path produces
|
||||
* stale or wrong-version covers for autotag review.
|
||||
*/
|
||||
export function GetCandidateCoverArt(releaseMBID: string, releaseGroupMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1410264131, releaseMBID, releaseGroupMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCandidates runs the scorer for a group and returns the full
|
||||
* candidate list (ordered best-first) along with the local tracks.
|
||||
* Caches the scored candidate list so Apply can operate on the
|
||||
* exact release the user saw — even if a concurrent rescore would
|
||||
* have shuffled the ranking. Also writes the top candidate's
|
||||
* score back to the tagging_items row so the sidebar's match-%
|
||||
* pill and the score-desc list sort reflect the freshly-computed
|
||||
* ranking instead of the scanner's pre-review estimate.
|
||||
*/
|
||||
export function GetCandidates(groupKey: string): $CancellablePromise<$models.ScoreView | null> {
|
||||
return $Call.ByID(873001524, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCandidatesForPasteURL resolves a MusicBrainz release-or-
|
||||
* release-group URL into a ScoreView by fetching the pasted entity
|
||||
* from MB, running track alignment against the local group, and
|
||||
* prepending a fully-scored candidate to the existing list. The
|
||||
* pasted candidate's provenance is "paste" so the UI can label it.
|
||||
*/
|
||||
export function GetCandidatesForPasteURL(groupKey: string, mbReleaseURL: string): $CancellablePromise<$models.ScoreView | null> {
|
||||
return $Call.ByID(2470223543, groupKey, mbReleaseURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetLocalCoverArt returns a data-URI for the artwork associated
|
||||
* with the group's local files, so the review UI can show "what the
|
||||
* folder already looks like" beside the fetched candidate art.
|
||||
* It first checks each file for embedded ID3/Vorbis pictures, then
|
||||
* falls back to a sidecar cover image (cover.jpg, folder.png, …)
|
||||
* sitting in the album directory. Untagged folders carry one or the
|
||||
* other far more often than they have a DB release-group cover, so we
|
||||
* read the files directly. Returns "" when nothing is found.
|
||||
*/
|
||||
export function GetLocalCoverArt(groupKey: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(2603982821, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetNextPending returns the next pending tagging item after the
|
||||
* internal cursor, or nil when the queue is empty. Advances the
|
||||
* cursor on success.
|
||||
*/
|
||||
export function GetNextPending(): $CancellablePromise<$models.PendingItem | null> {
|
||||
return $Call.ByID(1751929138);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPendingFolder returns the tagging item for a specific group
|
||||
* key — used by the sidebar after the user clicks an entry, so
|
||||
* the header can render even when the cursor-based queue iteration
|
||||
* hasn't visited that key yet.
|
||||
*/
|
||||
export function GetPendingFolder(groupKey: string): $CancellablePromise<$models.PendingItem | null> {
|
||||
return $Call.ByID(3455545217, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* LeaveAsIs is the "local tags are correct, don't rescore"
|
||||
* acknowledgement — bumps status to 'confirmed' without touching
|
||||
* any files.
|
||||
*/
|
||||
export function LeaveAsIs(groupKey: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3897348801, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* ListPendingFolders returns every tagging item in the given
|
||||
* library (or all libraries when libraryID = 0) that has not been
|
||||
* explicitly cleared. Includes items in all review states —
|
||||
* pending, skipped, and confirmed — so the sidebar can group
|
||||
* them into sections (the user wanted skipped/completed to stay
|
||||
* visible at the bottom rather than vanish). Sorted by score
|
||||
* descending so the UI surfaces the most confident matches first
|
||||
* (NULL scores fall to the bottom); the frontend re-groups by
|
||||
* status for display.
|
||||
*/
|
||||
export function ListPendingFolders(libraryID: number): $CancellablePromise<$models.PendingItem[] | null> {
|
||||
return $Call.ByID(617511590, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* RetagGroup flips a group back to 'pending' so the user can
|
||||
* re-review after an apply or skip. Drops the durably-cached
|
||||
* candidate list too, so the next open recomputes against fresh
|
||||
* MusicBrainz data rather than reusing the stale stored result.
|
||||
*/
|
||||
export function RetagGroup(groupKey: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2552507548, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchCandidates runs an in-app MusicBrainz search — the "suggest a
|
||||
* new candidate" escape hatch when the automatic cascade misses.
|
||||
* kind is "recording" (title + artist, for singletons) or anything
|
||||
* else (album + artist release-group search). Returns lightweight
|
||||
* hits; SelectSearchCandidate resolves the picked one into a scored
|
||||
* candidate.
|
||||
*/
|
||||
export function SearchCandidates(kind: string, query: string, artist: string): $CancellablePromise<$models.SearchHitView[] | null> {
|
||||
return $Call.ByID(4062823000, kind, query, artist);
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectSearchCandidate resolves a picked search hit into a fully-
|
||||
* scored candidate and splices it to the top of the group's candidate
|
||||
* list — the same shape as the paste-URL path, so Apply works
|
||||
* unchanged. A "recording" hit is resolved to a representative
|
||||
* release first.
|
||||
*/
|
||||
export function SelectSearchCandidate(groupKey: string, kind: string, mbid: string): $CancellablePromise<$models.ScoreView | null> {
|
||||
return $Call.ByID(2540832563, groupKey, kind, mbid);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetJobRegistry wires the background job registry so an apply reports
|
||||
* progress and offers a cancel like every other long-running operation.
|
||||
*
|
||||
* Before this, apply was a bare goroutine whose progress lived in a
|
||||
* component field that navigation discarded, with no cancel and no
|
||||
* record of where it stopped (errors.C3). Everything routed through the
|
||||
* registry gets progress, cancel and the global indicator for free; the
|
||||
* three subsystems that lacked them were the three that were not
|
||||
* registered.
|
||||
*/
|
||||
export function SetJobRegistry(reg: jobs$0.Registry | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(3755409662, reg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip marks the current group as skipped — it stays in the
|
||||
* queue but renders in the "Skipped" section at the bottom of
|
||||
* the sidebar, separate from pending items, so the user can
|
||||
* revisit it later if they change their mind.
|
||||
*/
|
||||
export function Skip(groupKey: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1248497081, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* SplitMixedFolder is the "this folder is a pile of unrelated
|
||||
* tracks" escape hatch: it partitions the group's local tracks via
|
||||
* autotag.SplitPlan — tag-matched sub-albums (see
|
||||
* autotag.ClusterByAlbumArtist) plus a one-track cluster for every
|
||||
* track that didn't share an (album, album-artist) pair with
|
||||
* anything else — and carves each piece out into its own synthetic
|
||||
* tagging group, reassigning just those audio_files rows (no files
|
||||
* move on disk). Every track leaves the original group; nothing is
|
||||
* left behind to be scored as "extra tracks" of whichever piece
|
||||
* happens to match first. The synthetic groups are scored with
|
||||
* relaxed missing-track handling (rank.go, recommend.go), since
|
||||
* they're expected to be an incomplete subset of whatever release
|
||||
* they belong to.
|
||||
*
|
||||
* Returns the resulting PendingItems — the leftover original group
|
||||
* first (if anything remains in it), then the new synthetic groups
|
||||
* — so the frontend can splice them into the sidebar without a full
|
||||
* reload. Errors with errNothingToSplit when the folder is already
|
||||
* one coherent unit (SplitPlan produces a single cluster covering
|
||||
* every track); callers should treat that as "nothing to show", not
|
||||
* a failure.
|
||||
*/
|
||||
export function SplitMixedFolder(groupKey: string): $CancellablePromise<$models.PendingItem[] | null> {
|
||||
return $Call.ByID(3640008069, groupKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* StartAutotagQueue resets the cursor for a new review session
|
||||
* and kicks off the background prefetch worker so sidebar pills
|
||||
* populate while the user reviews the first folder. Cancels any
|
||||
* previous prefetch (library filter may have changed). Pass
|
||||
* libraryID=0 to cover all libraries.
|
||||
*/
|
||||
export function StartAutotagQueue(libraryID: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(568310002, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* StartBackgroundPrefetch kicks off (or restarts) the prefetch
|
||||
* worker over all libraries. Called from app startup hooks so
|
||||
* unscored pending items get their match-% backfilled while the
|
||||
* user does other things — by the time they navigate to the
|
||||
* autotag page, the sidebar pills are already populated.
|
||||
* Idempotent: items with an existing score are skipped, so it's
|
||||
* safe to call after every library scan and on every app launch.
|
||||
*/
|
||||
export function StartBackgroundPrefetch(): $CancellablePromise<void> {
|
||||
return $Call.ByID(428378477);
|
||||
}
|
||||
|
||||
/**
|
||||
* WritesInFlight reports whether an apply is currently rewriting tags
|
||||
* on disk. Quitting mid-apply leaves a folder half-retagged, so the app
|
||||
* asks before closing (errors.p4).
|
||||
*/
|
||||
export function WritesInFlight(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(1561223347);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Config represents the application configuration.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as download$0 from "../download/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as tracklist$0 from "../tracklist/models.js";
|
||||
|
||||
/**
|
||||
* GetDefaultPage returns the view the app opens to on launch.
|
||||
*/
|
||||
export function GetDefaultPage(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3209119019);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDownloadPreferences returns the configured auto-download
|
||||
* guardrails.
|
||||
*/
|
||||
export function GetDownloadPreferences(): $CancellablePromise<download$0.AutoDownloadPrefs> {
|
||||
return $Call.ByID(4104118319);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFavoritesIconStyle returns the configured icon style.
|
||||
*/
|
||||
export function GetFavoritesIconStyle(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3222927230);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFavoritesPlaylistID returns the configured default playlist ID.
|
||||
*/
|
||||
export function GetFavoritesPlaylistID(): $CancellablePromise<number> {
|
||||
return $Call.ByID(4116868495);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetLibraryDirectory returns the currently configured library directory path.
|
||||
*/
|
||||
export function GetLibraryDirectory(): $CancellablePromise<string> {
|
||||
return $Call.ByID(2123565657);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPinDefaultPlaylist returns whether the default playlist
|
||||
* is pinned to the top of the playlist view.
|
||||
*/
|
||||
export function GetPinDefaultPlaylist(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3818283301);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetQueueFallback returns what plays, if anything, once the queue
|
||||
* runs out.
|
||||
*/
|
||||
export function GetQueueFallback(): $CancellablePromise<string> {
|
||||
return $Call.ByID(1504773860);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetScanConcurrency returns the configured scan concurrency mode.
|
||||
*/
|
||||
export function GetScanConcurrency(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3233312385);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetShortcuts returns the current shortcut bindings map.
|
||||
*/
|
||||
export function GetShortcuts(): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(1616717634);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeAccentColor returns the configured accent colour.
|
||||
*/
|
||||
export function GetThemeAccentColor(): $CancellablePromise<string> {
|
||||
return $Call.ByID(4264374057);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeBackgroundShade returns the configured background shade.
|
||||
*/
|
||||
export function GetThemeBackgroundShade(): $CancellablePromise<string> {
|
||||
return $Call.ByID(578772653);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTrackListColumns returns the configured track-list columns.
|
||||
*/
|
||||
export function GetTrackListColumns(): $CancellablePromise<tracklist$0.Column[] | null> {
|
||||
return $Call.ByID(3426289065);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load reads and parses the config file from disk.
|
||||
*/
|
||||
export function Load(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2408891113);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetShortcuts resets all shortcuts to defaults.
|
||||
*/
|
||||
export function ResetShortcuts(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3197898083);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save writes the config to disk. Refuses to write if the config
|
||||
* was never successfully loaded — prevents overwriting user config
|
||||
* with defaults during abnormal startup/shutdown sequences.
|
||||
*/
|
||||
export function Save(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1988945736);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetDefaultPage validates and saves a new launch page.
|
||||
*/
|
||||
export function SetDefaultPage(page: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2714957423, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetDownloadPreferences saves new auto-download guardrails. This only
|
||||
* persists them; the download package cannot depend on config (config
|
||||
* already depends on download for UserConfig), so making the change
|
||||
* live without a restart is the caller's job — the frontend settings
|
||||
* save calls this and download.Service.SetPreferences in the same
|
||||
* action, and app.go's initDownloadRuntime applies the saved value to
|
||||
* the running Manager at startup.
|
||||
*/
|
||||
export function SetDownloadPreferences(prefs: download$0.AutoDownloadPrefs): $CancellablePromise<void> {
|
||||
return $Call.ByID(2655203755, prefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetFavoritesIconStyle validates and saves a new icon style.
|
||||
*/
|
||||
export function SetFavoritesIconStyle(style: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(812590378, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetFavoritesPlaylistID saves a new default playlist ID.
|
||||
*/
|
||||
export function SetFavoritesPlaylistID(id: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2117079163, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetLibraryDirectory validates and saves a new library directory,
|
||||
* then emits the LibraryConfigChanged event so listeners (e.g. the
|
||||
* Library scanner) can react.
|
||||
*/
|
||||
export function SetLibraryDirectory(dir: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1352505021, dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetPinDefaultPlaylist saves whether the default playlist
|
||||
* should be pinned to the top of the playlist view.
|
||||
*/
|
||||
export function SetPinDefaultPlaylist(pin: boolean): $CancellablePromise<void> {
|
||||
return $Call.ByID(372446849, pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetQueueFallback validates and saves a new queue-fallback mode.
|
||||
*/
|
||||
export function SetQueueFallback(mode: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2824429312, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetScanConcurrency validates and saves a new scan concurrency
|
||||
* mode. The change takes effect on the next scan.
|
||||
*/
|
||||
export function SetScanConcurrency(mode: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(742893445, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetShortcut saves a single shortcut binding.
|
||||
*/
|
||||
export function SetShortcut(action: string, key: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(275130705, action, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetShortcuts saves the entire shortcut bindings map.
|
||||
*/
|
||||
export function SetShortcuts(bindings: { [_ in string]?: string } | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(4073898118, bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetThemeAccentColor validates and saves a new accent colour.
|
||||
*/
|
||||
export function SetThemeAccentColor(color: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3536665861, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetThemeBackgroundShade validates and saves a new background shade.
|
||||
*/
|
||||
export function SetThemeBackgroundShade(shade: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2709290065, shade);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetTrackListColumns validates and saves a new column layout.
|
||||
*/
|
||||
export function SetTrackListColumns(columns: tracklist$0.Column[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(4226159685, columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate returns errors if there is a breaking issue with the config.
|
||||
*/
|
||||
export function Validate(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3087982155);
|
||||
}
|
||||
Executable → Regular
+4
-4
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetShelves() {
|
||||
return window['go']['home']['Service']['GetShelves']();
|
||||
}
|
||||
import * as Config from "./config.js";
|
||||
export {
|
||||
Config
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export type {
|
||||
Library
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export interface Library {
|
||||
"ID": number;
|
||||
"Name": string;
|
||||
"Path": string;
|
||||
"CreatedAt": string;
|
||||
"AutotagWarningAcked": number;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
Entity,
|
||||
Format,
|
||||
Kind,
|
||||
Protocol,
|
||||
RequestScope,
|
||||
RequestState,
|
||||
State
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
AutoDownloadPrefs,
|
||||
Candidate,
|
||||
CandidateFile,
|
||||
Caps,
|
||||
Config,
|
||||
Descriptor,
|
||||
DownloadItem,
|
||||
DownloadView,
|
||||
ExpectedTrack,
|
||||
Field,
|
||||
MatchScore,
|
||||
QualityScore,
|
||||
Reconciler,
|
||||
Request,
|
||||
RequestInput,
|
||||
SearchRequest,
|
||||
StartResult,
|
||||
Summary
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,732 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* AutoDownloadPrefs gates and scores what AutoPickable may choose
|
||||
* without asking. Zero values are permissive: no size window and no
|
||||
* format restriction.
|
||||
*/
|
||||
export interface AutoDownloadPrefs {
|
||||
/**
|
||||
* MinSizeMB and MaxSizeMB bound what auto-pick will grab. Zero
|
||||
* means no bound on that side. A candidate outside the window is
|
||||
* filtered out of auto-pick entirely, not merely scored down — a
|
||||
* tiny "sampler" torrent or a boxset ten times the expected size is
|
||||
* usually the wrong thing entirely, not a worse copy of the right
|
||||
* thing.
|
||||
*/
|
||||
"minSizeMb": number;
|
||||
"maxSizeMb": number;
|
||||
|
||||
/**
|
||||
* PreferredSizeMB nudges the score toward a target size within the
|
||||
* min/max window (a lossless rip and a heavily-padded lossless rip
|
||||
* can both pass the window). Zero disables the nudge; sizeFit then
|
||||
* returns a neutral value that does not affect ranking.
|
||||
*/
|
||||
"preferredSizeMb": number;
|
||||
|
||||
/**
|
||||
* AllowedFormats restricts auto-pick to candidates whose audio
|
||||
* files are all in one of these formats. Empty means no
|
||||
* restriction.
|
||||
*/
|
||||
"allowedFormats": Format[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidate is one acquirable thing a provider found: a Soulseek user's
|
||||
* folder, a torrent, a YouTube playlist. Providers fill the descriptive
|
||||
* fields; the ranker fills Match, Quality and Score.
|
||||
*/
|
||||
export interface Candidate {
|
||||
/**
|
||||
* ID is unique within the provider that produced it, and is what
|
||||
* gets handed back to Grab.
|
||||
*/
|
||||
"id": string;
|
||||
"providerId": number;
|
||||
"kind": Kind;
|
||||
|
||||
/**
|
||||
* Protocol determines which transport can fetch this.
|
||||
*/
|
||||
"protocol": Protocol;
|
||||
|
||||
/**
|
||||
* Descriptive.
|
||||
*/
|
||||
"title": string;
|
||||
"artist"?: string;
|
||||
|
||||
/**
|
||||
* peer username, indexer name, channel
|
||||
*/
|
||||
"origin"?: string;
|
||||
"files": CandidateFile[] | null;
|
||||
"totalSize": number;
|
||||
|
||||
/**
|
||||
* Health is the provider's own availability signal, normalized to
|
||||
* 0..1: seeder count for torrents, free upload slots and queue
|
||||
* length for Soulseek. 0.5 when the provider has no signal.
|
||||
*/
|
||||
"health": number;
|
||||
|
||||
/**
|
||||
* Scores, filled by the ranker.
|
||||
*/
|
||||
"match": MatchScore;
|
||||
"quality": QualityScore;
|
||||
"score": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CandidateFile is one file inside a candidate. Soulseek and torrent
|
||||
* results give paths and sizes but no tags, so Format and duration are
|
||||
* inferred from the path and size where possible.
|
||||
*/
|
||||
export interface CandidateFile {
|
||||
"path": string;
|
||||
"size": number;
|
||||
"format": Format;
|
||||
|
||||
/**
|
||||
* kbps, 0 when unknown
|
||||
*/
|
||||
"bitrate"?: number;
|
||||
"isAudio": boolean;
|
||||
|
||||
/**
|
||||
* expected track position
|
||||
*/
|
||||
"matchedTo"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caps declares which roles a provider fills and which optional
|
||||
* behaviours it supports. The frontend renders controls from this
|
||||
* rather than switching on Kind, so a provider that gains resume
|
||||
* support later needs no frontend change.
|
||||
*/
|
||||
export interface Caps {
|
||||
/**
|
||||
* Roles.
|
||||
*/
|
||||
"canSearch": boolean;
|
||||
"canTransport": boolean;
|
||||
"canDelegate": boolean;
|
||||
|
||||
/**
|
||||
* CanList marks a provider that keeps a persistent wanted list of
|
||||
* its own, which the reconciler mirrors this app's list into.
|
||||
*/
|
||||
"canList": boolean;
|
||||
|
||||
/**
|
||||
* Optional behaviours.
|
||||
*/
|
||||
"canResume": boolean;
|
||||
"canCancel": boolean;
|
||||
"reportsSize": boolean;
|
||||
|
||||
/**
|
||||
* Protocols this provider can transport. Empty for providers that
|
||||
* only fetch their own search results.
|
||||
*/
|
||||
"transports": Protocol[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Config is a provider's stored settings. Secret values are not held
|
||||
* here — they live in the secrets store keyed by provider ID, so a
|
||||
* config blob can be logged or shown in the UI without redaction.
|
||||
*/
|
||||
export interface Config {
|
||||
"id": number;
|
||||
"kind": Kind;
|
||||
"name": string;
|
||||
"enabled": boolean;
|
||||
"priority": number;
|
||||
"settings": { [_ in string]?: string } | null;
|
||||
|
||||
/**
|
||||
* SetSecrets names which of the descriptor's secret fields already
|
||||
* have a stored value, without exposing it. Populated only when a
|
||||
* Config is built for the frontend (see Service.withSecretFlags);
|
||||
* empty when read from or written to the store.
|
||||
*/
|
||||
"setSecrets"?: { [_ in string]?: boolean } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor is the static, instance-independent description of a
|
||||
* provider kind: what it is called, what it can do, and which settings
|
||||
* it needs. The settings page renders its form from this, so a new
|
||||
* provider gets a config UI without any frontend work.
|
||||
*/
|
||||
export interface Descriptor {
|
||||
"kind": Kind;
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* Summary is one line explaining what connecting this gets you.
|
||||
*/
|
||||
"summary": string;
|
||||
|
||||
/**
|
||||
* Caps are the kind's inherent capabilities, before configuration.
|
||||
*/
|
||||
"caps": Caps;
|
||||
|
||||
/**
|
||||
* Fields are the settings the user must supply.
|
||||
*/
|
||||
"fields": Field[] | null;
|
||||
|
||||
/**
|
||||
* RequiresExternal names the software the user must run themselves
|
||||
* (a slskd daemon, a Lidarr instance), or is empty for providers
|
||||
* that need nothing but a binary on PATH.
|
||||
*/
|
||||
"requiresExternal"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DownloadItem is one grab attempt, as stored.
|
||||
*
|
||||
* deliberate: distinguishes it from download.Download (the attempt) and
|
||||
* download.Request (the durable record) at every call site, which a bare
|
||||
* "Item" would not.
|
||||
*/
|
||||
export interface DownloadItem {
|
||||
"id": string;
|
||||
"downloadId": string;
|
||||
"providerId": number;
|
||||
"transportId"?: number;
|
||||
"externalId"?: string;
|
||||
"candidate": Candidate;
|
||||
"state": State;
|
||||
"bytesDone": number;
|
||||
"bytesTotal": number;
|
||||
"error"?: string;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DownloadView is one row of the downloads list.
|
||||
*
|
||||
* would stop reading as "one row of the Downloads list" the moment this
|
||||
* package also has a Requests list — see RequestInput/Request nearby.
|
||||
*/
|
||||
export interface DownloadView {
|
||||
"id": string;
|
||||
|
||||
/**
|
||||
* Anchors. Any may be empty; all empty means free-text.
|
||||
*/
|
||||
"releaseMbid"?: string;
|
||||
"releaseGroupMbid"?: string;
|
||||
|
||||
/**
|
||||
* RecordingMBID anchors a single-track download. Its Expected holds
|
||||
* exactly that one track, which is what lets a track download be
|
||||
* scored — and therefore auto-picked — on the same footing as an
|
||||
* album.
|
||||
*/
|
||||
"recordingMbid"?: string;
|
||||
|
||||
/**
|
||||
* RequestID links back to the durable Request row this download was
|
||||
* raised for or attached to, or 0 for a free-text download with
|
||||
* nothing stable to attach to. The reconciler and manual anchored
|
||||
* downloads both write the outcome back through it.
|
||||
*/
|
||||
"requestId"?: number;
|
||||
|
||||
/**
|
||||
* Source records where the download came from, for the downloads
|
||||
* list. Empty means "manual".
|
||||
*/
|
||||
"source"?: string;
|
||||
|
||||
/**
|
||||
* Display and query text. Artist/Album are what searches are built
|
||||
* from; Query overrides them when the user typed something raw.
|
||||
*/
|
||||
"artist": string;
|
||||
"album": string;
|
||||
"query"?: string;
|
||||
|
||||
/**
|
||||
* Expected is the tracklist the anchor resolves to, used for
|
||||
* completeness scoring and for the autotag match at import. Empty
|
||||
* for free-text downloads.
|
||||
*/
|
||||
"expected"?: ExpectedTrack[] | null;
|
||||
|
||||
/**
|
||||
* LibraryID is the library imported files belong to.
|
||||
*/
|
||||
"libraryId": number;
|
||||
"createdAt": string;
|
||||
"state": State;
|
||||
"error"?: string;
|
||||
"items": DownloadItem[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity says what a request's MBID names, and is the only type
|
||||
* distinction the durable request list makes.
|
||||
*/
|
||||
export enum Entity {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Request entity types.
|
||||
*
|
||||
* EntityArtist is a subscription rather than a thing to fetch: it
|
||||
* is never satisfied, and each reconcile expands the artist's
|
||||
* discography into child requests.
|
||||
*/
|
||||
EntityArtist = "artist",
|
||||
|
||||
/**
|
||||
* EntityReleaseGroup is an album in the abstract — any release of
|
||||
* it satisfies the request, which is what a user means by "I want
|
||||
* this album".
|
||||
*/
|
||||
EntityReleaseGroup = "release-group",
|
||||
|
||||
/**
|
||||
* EntityRelease is one specific edition, used when the user picked
|
||||
* a particular pressing.
|
||||
*/
|
||||
EntityRelease = "release",
|
||||
|
||||
/**
|
||||
* EntityRecording is a single track.
|
||||
*/
|
||||
EntityRecording = "recording",
|
||||
};
|
||||
|
||||
/**
|
||||
* ExpectedTrack is one track of the release the user asked for.
|
||||
*/
|
||||
export interface ExpectedTrack {
|
||||
"position": number;
|
||||
"discNumber": number;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"lengthMillis": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Field describes one provider setting for the settings form.
|
||||
*/
|
||||
export interface Field {
|
||||
"key": string;
|
||||
"label": string;
|
||||
"placeholder"?: string;
|
||||
"help"?: string;
|
||||
|
||||
/**
|
||||
* Secret marks a value stored in the secrets store rather than the
|
||||
* provider config row, and rendered as a password input.
|
||||
*/
|
||||
"secret": boolean;
|
||||
|
||||
/**
|
||||
* Path marks a value that names a local filesystem directory, so
|
||||
* the settings form can offer a native folder picker beside the
|
||||
* text input rather than making the user type or paste it.
|
||||
*/
|
||||
"path": boolean;
|
||||
"required": boolean;
|
||||
"default"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format is a normalized audio container/codec name.
|
||||
*/
|
||||
export enum Format {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Audio formats, ordered by the quality ranking in formatRank.
|
||||
*/
|
||||
FormatUnknown = "",
|
||||
FormatFLAC = "flac",
|
||||
FormatALAC = "alac",
|
||||
FormatWAV = "wav",
|
||||
FormatMP3 = "mp3",
|
||||
FormatAAC = "aac",
|
||||
FormatOGG = "ogg",
|
||||
FormatOpus = "opus",
|
||||
FormatWMA = "wma",
|
||||
};
|
||||
|
||||
/**
|
||||
* Kind identifies a provider implementation. It is stored in the
|
||||
* database and used to look up the constructor in the registry, so
|
||||
* values are stable strings and never renamed.
|
||||
*/
|
||||
export enum Kind {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Provider kinds.
|
||||
*/
|
||||
KindSlskd = "slskd",
|
||||
KindYtDlp = "yt-dlp",
|
||||
KindLidarr = "lidarr",
|
||||
KindProwlarr = "prowlarr",
|
||||
KindQBittorrent = "qbittorrent",
|
||||
KindSABnzbd = "sabnzbd",
|
||||
|
||||
/**
|
||||
* KindFake is an in-memory provider used by tests. It is never
|
||||
* offered in the UI.
|
||||
*/
|
||||
KindFake = "fake",
|
||||
};
|
||||
|
||||
/**
|
||||
* MatchScore answers "is this the release the user asked for?" It is
|
||||
* deliberately separate from QualityScore: a perfect match at 128kbps
|
||||
* and a mediocre match in FLAC are different failures, and collapsing
|
||||
* them into one number makes the ranking impossible to explain.
|
||||
*/
|
||||
export interface MatchScore {
|
||||
/**
|
||||
* Overall is 0..1.
|
||||
*/
|
||||
"overall": number;
|
||||
|
||||
/**
|
||||
* filenames vs expected titles
|
||||
*/
|
||||
"titleFit": number;
|
||||
|
||||
/**
|
||||
* path/origin vs expected artist
|
||||
*/
|
||||
"artistFit": number;
|
||||
|
||||
/**
|
||||
* folder name vs album title
|
||||
*/
|
||||
"albumFit": number;
|
||||
|
||||
/**
|
||||
* audio files vs expected count
|
||||
*/
|
||||
"completeness": number;
|
||||
|
||||
/**
|
||||
* Anchored records whether an MBID drove this score. Unanchored
|
||||
* matches are capped, because there is nothing to be right about.
|
||||
*/
|
||||
"anchored": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protocol is how a candidate's bytes are moved. Search-only providers
|
||||
* report it so the pipeline can pick a compatible transport; providers
|
||||
* that transport their own results use ProtocolDirect.
|
||||
*/
|
||||
export enum Protocol {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Transport protocols.
|
||||
*
|
||||
* ProtocolDirect means the finding provider also does the fetch.
|
||||
*/
|
||||
ProtocolDirect = "direct",
|
||||
ProtocolTorrent = "torrent",
|
||||
ProtocolUsenet = "usenet",
|
||||
};
|
||||
|
||||
/**
|
||||
* QualityScore answers "is this a good copy?".
|
||||
*/
|
||||
export interface QualityScore {
|
||||
/**
|
||||
* Overall is 0..1.
|
||||
*/
|
||||
"overall": number;
|
||||
|
||||
/**
|
||||
* FLAC > V0 > 320 > lower
|
||||
*/
|
||||
"formatRank": number;
|
||||
"bitrate": number;
|
||||
|
||||
/**
|
||||
* seeders, free slots
|
||||
*/
|
||||
"health": number;
|
||||
|
||||
/**
|
||||
* user's per-provider preference
|
||||
*/
|
||||
"priority": number;
|
||||
|
||||
/**
|
||||
* closeness to the preferred download size
|
||||
*/
|
||||
"sizeFit": number;
|
||||
|
||||
/**
|
||||
* Mixed marks a candidate whose files are not all the same format,
|
||||
* which usually means a hand-assembled folder rather than a rip.
|
||||
*/
|
||||
"mixed": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciler works the request list.
|
||||
*/
|
||||
export interface Reconciler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request is one row of the durable request list.
|
||||
*/
|
||||
export interface Request {
|
||||
"id": number;
|
||||
"mbid": string;
|
||||
"entity": Entity;
|
||||
"libraryId": number;
|
||||
|
||||
/**
|
||||
* Artist and Title are display cache only. Matching always uses
|
||||
* the MBID.
|
||||
*/
|
||||
"artist": string;
|
||||
"title": string;
|
||||
"scope": RequestScope;
|
||||
|
||||
/**
|
||||
* Secondary includes compilations, live albums and remixes in an
|
||||
* artist request's expansion.
|
||||
*/
|
||||
"secondary": boolean;
|
||||
"state": RequestState;
|
||||
|
||||
/**
|
||||
* ParentID is set on requests the reconciler derived from an artist
|
||||
* subscription. A request the user pinned directly has none, so
|
||||
* removing the artist leaves it alone.
|
||||
*/
|
||||
"parentId"?: number;
|
||||
"attempts": number;
|
||||
"lastError"?: string;
|
||||
"lastTriedAt"?: string;
|
||||
"nextTryAt"?: string;
|
||||
|
||||
/**
|
||||
* ExternalIDs maps provider row ID (as a string, because JSON
|
||||
* object keys are strings) to that provider's own identifier for
|
||||
* this request. Only set for providers that keep a persistent
|
||||
* list of their own.
|
||||
*/
|
||||
"externalIds"?: { [_ in string]?: string } | null;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestInput is what the frontend submits to request something. It
|
||||
* is one MBID and the type of thing it names, because that is
|
||||
* genuinely all a durable request is.
|
||||
*/
|
||||
export interface RequestInput {
|
||||
"mbid": string;
|
||||
"entity": string;
|
||||
"libraryId": number;
|
||||
|
||||
/**
|
||||
* Artist and Title are display text only, and optional: the
|
||||
* reconciler fills them in from the catalog when the caller has
|
||||
* nothing but an MBID.
|
||||
*/
|
||||
"artist": string;
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* Scope and Secondary apply to artist requests.
|
||||
*/
|
||||
"scope": string;
|
||||
"secondary": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* RequestScope applies to artist requests only.
|
||||
*/
|
||||
export enum RequestScope {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Artist request scopes.
|
||||
*
|
||||
* ScopeFuture takes only releases first published after the artist
|
||||
* was added. Default, because subscribing to an artist should not
|
||||
* silently queue their entire back catalogue.
|
||||
*/
|
||||
ScopeFuture = "future",
|
||||
|
||||
/**
|
||||
* ScopeAll backfills the whole discography as well.
|
||||
*/
|
||||
ScopeAll = "all",
|
||||
};
|
||||
|
||||
/**
|
||||
* RequestState is where a request sits. There is deliberately no
|
||||
* "failed": an attempt can fail, a request cannot. A request that has
|
||||
* tried and not found anything is still wanted, with attempts and
|
||||
* last_error recording why it is taking a while.
|
||||
*/
|
||||
export enum RequestState {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Request states.
|
||||
*
|
||||
* RequestStateWanted is the active state: due for another attempt
|
||||
* when its backoff elapses.
|
||||
*/
|
||||
RequestStateWanted = "wanted",
|
||||
|
||||
/**
|
||||
* RequestStateSatisfied means the library owns it. How it got
|
||||
* there — downloaded here, ripped, bought elsewhere — does not
|
||||
* matter.
|
||||
*/
|
||||
RequestStateSatisfied = "satisfied",
|
||||
|
||||
/**
|
||||
* RequestStatePaused is the user saying "keep this on the list but
|
||||
* stop trying".
|
||||
*/
|
||||
RequestStatePaused = "paused",
|
||||
};
|
||||
|
||||
/**
|
||||
* SearchRequest is what the frontend submits to start a download.
|
||||
*/
|
||||
export interface SearchRequest {
|
||||
"libraryId": number;
|
||||
"releaseMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"artist": string;
|
||||
"album": string;
|
||||
"query": string;
|
||||
"expected": ExpectedTrack[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartResult is what the picker needs after a search.
|
||||
*/
|
||||
export interface StartResult {
|
||||
"downloadId": string;
|
||||
"candidates": Candidate[] | null;
|
||||
|
||||
/**
|
||||
* AutoPicked reports that the pipeline already chose and is
|
||||
* downloading, so the picker should show progress rather than a
|
||||
* list of choices.
|
||||
*/
|
||||
"autoPicked": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* State is the lifecycle position of a download item.
|
||||
*/
|
||||
export enum State {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Download item states. Searching through Importing are live;
|
||||
* Complete, Cancelled and Failed are terminal.
|
||||
*/
|
||||
StateSearching = "searching",
|
||||
StateFound = "found",
|
||||
StateQueued = "queued",
|
||||
StateGrabbing = "grabbing",
|
||||
StateVerifying = "verifying",
|
||||
StateTagging = "tagging",
|
||||
StateImporting = "importing",
|
||||
StateComplete = "complete",
|
||||
StateCancelled = "cancelled",
|
||||
StateFailed = "failed",
|
||||
};
|
||||
|
||||
/**
|
||||
* Summary reports what a pass did, for logging and for the UI.
|
||||
*/
|
||||
export interface Summary {
|
||||
/**
|
||||
* Expanded is how many child requests artist subscriptions produced.
|
||||
*/
|
||||
"expanded": number;
|
||||
|
||||
/**
|
||||
* Satisfied is how many requests the library turned out to own.
|
||||
*/
|
||||
"satisfied": number;
|
||||
|
||||
/**
|
||||
* Attempted is how many requests were searched for.
|
||||
*/
|
||||
"attempted": number;
|
||||
|
||||
/**
|
||||
* Started is how many of those found a clear enough winner to
|
||||
* download unattended.
|
||||
*/
|
||||
"started": number;
|
||||
|
||||
/**
|
||||
* Synced is how many requests were pushed to an external list.
|
||||
*/
|
||||
"synced": number;
|
||||
|
||||
/**
|
||||
* Waiting is how many requests are on the list and still being
|
||||
* looked for. A pass that did nothing is the normal case, and the
|
||||
* UI can only say so honestly if it knows the list was not empty.
|
||||
*/
|
||||
"waiting": number;
|
||||
|
||||
/**
|
||||
* NoProviders reports that nothing could be searched because no
|
||||
* download client is enabled — the one "nothing happened" the user
|
||||
* can actually fix.
|
||||
*/
|
||||
"noProviders": boolean;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service is the frontend-facing surface of the download subsystem.
|
||||
* Its methods are bound into Wails and called from TypeScript, so
|
||||
* signatures use plain types and return errors the UI can render.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AddProvider creates a provider and stores any secret settings
|
||||
* separately. Secrets arrive in the same map as ordinary settings
|
||||
* because that is what the form submits; they are split out here and
|
||||
* never written to the provider row.
|
||||
*/
|
||||
export function AddProvider(kind: string, name: string, settings: { [_ in string]?: string } | null): $CancellablePromise<number> {
|
||||
return $Call.ByID(2479360532, kind, name, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* AddRequest puts something on the request list and asks for a
|
||||
* reconcile pass, so the user sees something happen rather than
|
||||
* waiting six hours for the next scheduled one.
|
||||
*/
|
||||
export function AddRequest(req: $models.RequestInput): $CancellablePromise<number> {
|
||||
return $Call.ByID(2621441882, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel aborts a live download.
|
||||
*/
|
||||
export function Cancel(downloadID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1726338100, downloadID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidates returns the ranked candidates of a live download, so the
|
||||
* picker can be reopened without searching again.
|
||||
*/
|
||||
export function Candidates(downloadID: string): $CancellablePromise<$models.Candidate[] | null> {
|
||||
return $Call.ByID(2436454548, downloadID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ClearFinished removes terminal downloads from the list.
|
||||
*/
|
||||
export function ClearFinished(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3757713449);
|
||||
}
|
||||
|
||||
/**
|
||||
* ClearSatisfiedRequests drops everything already owned.
|
||||
*/
|
||||
export function ClearSatisfiedRequests(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2681765363);
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteProvider removes a provider and its credentials.
|
||||
*/
|
||||
export function DeleteProvider(id: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(495153424, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ImportExternalRequests adopts a provider's own list — "import the
|
||||
* artists Lidarr is already monitoring".
|
||||
*/
|
||||
export function ImportExternalRequests(providerID: number, libraryID: number): $CancellablePromise<number> {
|
||||
return $Call.ByID(4055326022, providerID, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* IsRequested answers the Explore pages' question — should this album
|
||||
* show "want" or "wanted?" — without making them load the whole list.
|
||||
*/
|
||||
export function IsRequested(mbid: string, libraryID: number): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3695628158, mbid, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ListDownloads returns recent downloads, newest first.
|
||||
*/
|
||||
export function ListDownloads(limit: number): $CancellablePromise<$models.DownloadView[] | null> {
|
||||
return $Call.ByID(2505337995, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* ListProviders returns the user's configured download clients.
|
||||
*/
|
||||
export function ListProviders(): $CancellablePromise<$models.Config[] | null> {
|
||||
return $Call.ByID(913508028);
|
||||
}
|
||||
|
||||
/**
|
||||
* ListRequests returns the whole durable request list.
|
||||
*/
|
||||
export function ListRequests(): $CancellablePromise<$models.Request[] | null> {
|
||||
return $Call.ByID(3593452892);
|
||||
}
|
||||
|
||||
/**
|
||||
* PauseRequest stops attempts without forgetting the request.
|
||||
*/
|
||||
export function PauseRequest(id: number, paused: boolean): $CancellablePromise<void> {
|
||||
return $Call.ByID(2652842089, id, paused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick starts the transfer for the candidate the user chose.
|
||||
*/
|
||||
export function Pick(downloadID: string, candidateID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(40308223, downloadID, candidateID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ProviderKinds returns every provider type that can be added, with the
|
||||
* settings each one needs. The settings page renders its forms from
|
||||
* this, so a new adapter needs no frontend change.
|
||||
*/
|
||||
export function ProviderKinds(): $CancellablePromise<$models.Descriptor[] | null> {
|
||||
return $Call.ByID(2902376358);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReconcileRequests runs a pass now and reports what it did. This
|
||||
* backs the "check now" button, so it runs synchronously: the user
|
||||
* pressed it and is waiting for an answer.
|
||||
*/
|
||||
export function ReconcileRequests(): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(593439868);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveRequest takes something off the list. Removing an artist takes
|
||||
* its derived albums with it, by cascade; an album the user pinned
|
||||
* themselves has no parent and survives.
|
||||
*/
|
||||
export function RemoveRequest(id: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2423963479, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetPreferences pushes the auto-download guardrails straight into the
|
||||
* running Manager, without persisting them. Persistence is
|
||||
* config.Config's job (GetDownloadPreferences/SetDownloadPreferences);
|
||||
* this package cannot depend on config, since config already depends on
|
||||
* download for UserConfig. The frontend settings save is expected to
|
||||
* call the config setter and this method in the same action, the way
|
||||
* UpdateProvider already achieves "live without a restart" by touching
|
||||
* storage and the running Manager together.
|
||||
*/
|
||||
export function SetPreferences(prefs: $models.AutoDownloadPrefs): $CancellablePromise<void> {
|
||||
return $Call.ByID(3789717356, prefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetReconciler wires the request-list loop. Optional: without it the
|
||||
* request list still stores and lists requests, it just never acts on
|
||||
* them.
|
||||
*/
|
||||
export function SetReconciler(r: $models.Reconciler | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(2390832784, r);
|
||||
}
|
||||
|
||||
/**
|
||||
* StartDownload searches for a release and either auto-picks a clear
|
||||
* winner or returns ranked candidates for the user to choose from.
|
||||
*
|
||||
* When the search carries a MusicBrainz anchor, it also resolves or
|
||||
* creates the durable Request the anchor names and attaches it, via
|
||||
* ensureRequest — so a manual download that fails or finds nothing
|
||||
* right now leaves a durable record behind instead of just vanishing,
|
||||
* and the reconciler picks it up on its normal schedule exactly as if
|
||||
* the user had explicitly added it to the request list.
|
||||
*/
|
||||
export function StartDownload(req: $models.SearchRequest): $CancellablePromise<$models.StartResult> {
|
||||
return $Call.ByID(1191306198, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* TestProvider backs the "test connection" button. It builds the
|
||||
* provider from its stored config and asks it to check itself, so the
|
||||
* result reflects exactly what a real search would use.
|
||||
*/
|
||||
export function TestProvider(id: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2688601475, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateProvider saves changes to a provider. A secret field left
|
||||
* blank keeps its stored value rather than clearing it — the form does
|
||||
* not echo secrets back, so an empty box means "unchanged", not
|
||||
* "delete".
|
||||
*/
|
||||
export function UpdateProvider(id: number, name: string, enabled: boolean, priority: number, settings: { [_ in string]?: string } | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(42359606, id, name, enabled, priority, settings);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
ShelfKind
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
AlbumCompleteFunc,
|
||||
IndexStatus,
|
||||
LBSimilarArtist,
|
||||
LBTopRecording,
|
||||
LBTopReleaseGroup,
|
||||
LyricsResult,
|
||||
MBArtist,
|
||||
MBRecording,
|
||||
MBRelease,
|
||||
MBReleaseGroup,
|
||||
MBSearchResult,
|
||||
MBTrack,
|
||||
MusicBrainzClient,
|
||||
RateLimiter,
|
||||
Shelf,
|
||||
ShelfPage,
|
||||
ThumbnailRequest,
|
||||
TierStatus,
|
||||
TopResult,
|
||||
TrackLyrics,
|
||||
TrackThumbnailRequest
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,469 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* AlbumCompleteFunc answers "is the local album with this id complete",
|
||||
* i.e. do its files declare a track total the library actually has.
|
||||
*/
|
||||
export type AlbumCompleteFunc = any;
|
||||
|
||||
/**
|
||||
* IndexStatus is the full index build status, exposed to the frontend.
|
||||
*/
|
||||
export interface IndexStatus {
|
||||
"building": boolean;
|
||||
"ready": boolean;
|
||||
|
||||
/**
|
||||
* RFC3339 timestamp of last complete build
|
||||
*/
|
||||
"lastBuilt"?: string;
|
||||
"tiers": TierStatus[] | null;
|
||||
"artists": number;
|
||||
"recordings": number;
|
||||
"releaseGroups": number;
|
||||
"totalRows": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LBSimilarArtist represents a similar artist from the
|
||||
* ListenBrainz labs API.
|
||||
*/
|
||||
export interface LBSimilarArtist {
|
||||
"artistMbid": string;
|
||||
"name": string;
|
||||
"score": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LBTopRecording represents a popular recording from the
|
||||
* ListenBrainz popularity API.
|
||||
*
|
||||
* JSON tags use camelCase for Wails→frontend serialization.
|
||||
* The API response uses snake_case, so we unmarshal into
|
||||
* lbTopRecordingWire first, then convert.
|
||||
*/
|
||||
export interface LBTopRecording {
|
||||
"recordingMbid": string;
|
||||
"artistName": string;
|
||||
"trackName": string;
|
||||
"totalListenCount": number;
|
||||
"caaReleaseMbid": string;
|
||||
|
||||
/**
|
||||
* ReleaseGroupMBID is resolved from CAAReleaseMBID so a top-track
|
||||
* row can link to its album page with the track highlighted.
|
||||
*/
|
||||
"releaseGroupMbid"?: string;
|
||||
"releaseName": string;
|
||||
|
||||
/**
|
||||
* milliseconds (from LB API)
|
||||
*/
|
||||
"length": number;
|
||||
"inLibrary": boolean;
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LBTopReleaseGroup represents a popular release group from the
|
||||
* ListenBrainz popularity API.
|
||||
*/
|
||||
export interface LBTopReleaseGroup {
|
||||
"releaseGroupMbid": string;
|
||||
"title": string;
|
||||
"artistName": string;
|
||||
"type": string;
|
||||
"date": string;
|
||||
"totalListenCount": number;
|
||||
"caaReleaseMbid": string;
|
||||
"inLibrary": boolean;
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LyricsResult is a single lyric-search hit, mapped from the DB layer
|
||||
* into the camelCase shape the frontend consumes.
|
||||
*/
|
||||
export interface LyricsResult {
|
||||
"recordingId": number;
|
||||
"filePath": string;
|
||||
"lengthMs": number;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"album": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBArtist is a Wails-friendly projection of a MusicBrainz artist.
|
||||
*/
|
||||
export interface MBArtist {
|
||||
"mbid": string;
|
||||
"name": string;
|
||||
"sortName": string;
|
||||
"englishName"?: string;
|
||||
"type": string;
|
||||
"country": string;
|
||||
"disambiguation": string;
|
||||
"score": number;
|
||||
|
||||
/**
|
||||
* raw LB listen count (0 if unknown)
|
||||
*/
|
||||
"popularity": number;
|
||||
"listenerCount": number;
|
||||
|
||||
/**
|
||||
* true if the user owns music by this artist
|
||||
*/
|
||||
"inLibrary": boolean;
|
||||
|
||||
/**
|
||||
* local artist row ID for navigation
|
||||
*/
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBRecording is a Wails-friendly projection of a MusicBrainz
|
||||
* recording.
|
||||
*/
|
||||
export interface MBRecording {
|
||||
"mbid": string;
|
||||
"title": string;
|
||||
"length": number;
|
||||
"artistCredit": string;
|
||||
|
||||
/**
|
||||
* for linking the artist to its detail page
|
||||
*/
|
||||
"artistMbid"?: string;
|
||||
"score": number;
|
||||
|
||||
/**
|
||||
* raw LB listen count (0 if unknown)
|
||||
*/
|
||||
"popularity": number;
|
||||
"listenerCount": number;
|
||||
|
||||
/**
|
||||
* parent release, for album navigation
|
||||
*/
|
||||
"caaReleaseMbid"?: string;
|
||||
|
||||
/**
|
||||
* ReleaseGroupMBID is resolved from CAAReleaseMBID so a track can
|
||||
* link to its album page with the track highlighted, matching how
|
||||
* tracks behave everywhere else.
|
||||
*/
|
||||
"releaseGroupMbid"?: string;
|
||||
|
||||
/**
|
||||
* album title
|
||||
*/
|
||||
"releaseName"?: string;
|
||||
|
||||
/**
|
||||
* true if the user owns this recording
|
||||
*/
|
||||
"inLibrary": boolean;
|
||||
|
||||
/**
|
||||
* local recording row ID
|
||||
*/
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBRelease is a Wails-friendly projection of a MusicBrainz release.
|
||||
*/
|
||||
export interface MBRelease {
|
||||
"mbid": string;
|
||||
"title": string;
|
||||
"date": string;
|
||||
"country": string;
|
||||
"status": string;
|
||||
"artistCredit"?: string;
|
||||
"tracks"?: MBTrack[] | null;
|
||||
|
||||
/**
|
||||
* ReleaseGroupMBID is the parent release group's MBID. Empty unless
|
||||
* the lookup requested the "release-groups" include (LookupRelease
|
||||
* does); used to resolve a release-level MBID (what many taggers
|
||||
* write) back to the release-group MBID everything else on the
|
||||
* album page is keyed by.
|
||||
*/
|
||||
"releaseGroupMbid"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBReleaseGroup is a Wails-friendly projection of a MusicBrainz
|
||||
* release group.
|
||||
*/
|
||||
export interface MBReleaseGroup {
|
||||
"mbid": string;
|
||||
"title": string;
|
||||
"primaryType": string;
|
||||
"secondaryTypes"?: string[] | null;
|
||||
"firstReleaseDate": string;
|
||||
"artistCredit": string;
|
||||
|
||||
/**
|
||||
* for linking the artist to its detail page
|
||||
*/
|
||||
"artistMbid"?: string;
|
||||
|
||||
/**
|
||||
* raw LB listen count (0 if unknown)
|
||||
*/
|
||||
"popularity": number;
|
||||
"listenerCount": number;
|
||||
|
||||
/**
|
||||
* true if the user owns this album
|
||||
*/
|
||||
"inLibrary": boolean;
|
||||
|
||||
/**
|
||||
* local release_group row ID
|
||||
*/
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBSearchResult aggregates the three searchable entity types
|
||||
* returned by the MusicBrainz search API.
|
||||
*/
|
||||
export interface MBSearchResult {
|
||||
"artists"?: MBArtist[] | null;
|
||||
"releaseGroups"?: MBReleaseGroup[] | null;
|
||||
"recordings"?: MBRecording[] | null;
|
||||
"topResults"?: TopResult[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* MBTrack is a Wails-friendly projection of a MusicBrainz track.
|
||||
*/
|
||||
export interface MBTrack {
|
||||
"position": number;
|
||||
"discNumber": number;
|
||||
"title": string;
|
||||
"length": number;
|
||||
"mbid": string;
|
||||
"inLibrary": boolean;
|
||||
"localId"?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MusicBrainzClient wraps the musicbrainzws2 library with a local
|
||||
* response cache. Every API call checks the cache first and stores
|
||||
* successful responses for future hits.
|
||||
*
|
||||
* A proactive rate limiter gates all outgoing requests at 1 req/sec
|
||||
* to avoid triggering MusicBrainz 429 responses. The underlying
|
||||
* musicbrainzws2.Client still retries on 429 as a safety net, but
|
||||
* the limiter should prevent most rate-limit hits.
|
||||
*/
|
||||
export interface MusicBrainzClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* RateLimiter enforces a maximum request rate using a token bucket.
|
||||
* MusicBrainz requires ≤1 request per second and rejects ALL
|
||||
* requests (not just excess) when the rate is exceeded, so callers
|
||||
* block proactively via Wait rather than retrying reactively.
|
||||
*
|
||||
* A limiter may carry a second, slower **background lane** (see
|
||||
* WithBackgroundLane). A caller marked by WithBackgroundPriority is
|
||||
* paced by that lane *and* yields to interactive callers: while any
|
||||
* interactive Wait is outstanding, background waits do not take a
|
||||
* token at all. This is what keeps a multi-thousand-request backfill
|
||||
* from putting the album page the user is looking at right now behind
|
||||
* hours of queued work.
|
||||
*
|
||||
* RateLimiter is safe for concurrent use.
|
||||
*/
|
||||
export interface RateLimiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shelf is one horizontal row on the Explore page.
|
||||
*
|
||||
* A shelf carries albums or artists, never both: they route to
|
||||
* different pages and render as different cards, and a row that is
|
||||
* sometimes one and sometimes the other is two components pretending to
|
||||
* be one.
|
||||
*/
|
||||
export interface Shelf {
|
||||
"id": string;
|
||||
"kind": ShelfKind;
|
||||
|
||||
/**
|
||||
* Title is the row heading.
|
||||
*/
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* Subtitle says why these are here. As on Home it is not
|
||||
* decoration: without it a shelf is indistinguishable from a
|
||||
* random grid.
|
||||
*/
|
||||
"subtitle": string;
|
||||
"albums"?: MBReleaseGroup[] | null;
|
||||
"artists"?: MBArtist[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShelfKind identifies what a shelf is built from, so the frontend can
|
||||
* pick an icon and a spec can assert on a shelf without matching
|
||||
* display copy.
|
||||
*/
|
||||
export enum ShelfKind {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Shelf kinds.
|
||||
*/
|
||||
ShelfPopularAlbums = "popular-albums",
|
||||
ShelfPopularArtists = "popular-artists",
|
||||
ShelfMoreFromOwned = "more-from-owned",
|
||||
};
|
||||
|
||||
/**
|
||||
* ShelfPage is what Explore renders before a query.
|
||||
*
|
||||
* State exists because "no shelves" has three different causes here and
|
||||
* the page must not present them identically — a blank panel is the bug
|
||||
* this whole feature is fixing, and a blank panel that says nothing
|
||||
* about why is the same bug with more code behind it.
|
||||
*/
|
||||
export interface ShelfPage {
|
||||
"shelves": Shelf[] | null;
|
||||
|
||||
/**
|
||||
* State is one of:
|
||||
*
|
||||
* "ready" — the catalog is here and the shelves are below.
|
||||
* "building" — it is being fetched or built right now.
|
||||
* "no-index" — there is no catalog. Search still works over
|
||||
* whatever is in the index, which may be nothing;
|
||||
* the page says so and points at Settings.
|
||||
*/
|
||||
"state": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ThumbnailRequest is a single item in a batch thumbnail request.
|
||||
*/
|
||||
export interface ThumbnailRequest {
|
||||
"mbid": string;
|
||||
"albumName": string;
|
||||
"artistName": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TierStatus represents the state of a single index tier.
|
||||
*/
|
||||
export interface TierStatus {
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* "pending", "running", "complete", "error", "skipped"
|
||||
*/
|
||||
"state": string;
|
||||
"total": number;
|
||||
"completed": number;
|
||||
"error"?: string;
|
||||
|
||||
/**
|
||||
* Detail is a human-readable progress line for stages whose raw
|
||||
* completed/total numbers say little on their own — the listens
|
||||
* stream reports "42.3 / 205.1 GB · 18 MB/s · ~3h20m left" here.
|
||||
*/
|
||||
"detail"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TopResult represents a single top-result card shown above the
|
||||
* categorized search lists. Computed by intent scoring after all
|
||||
* reranking is complete.
|
||||
*/
|
||||
export interface TopResult {
|
||||
/**
|
||||
* "artist", "release_group", "recording"
|
||||
*/
|
||||
"entityType": string;
|
||||
"mbid": string;
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* for tracks/albums
|
||||
*/
|
||||
"artistCredit"?: string;
|
||||
|
||||
/**
|
||||
* for linking the artist subtitle
|
||||
*/
|
||||
"artistMbid"?: string;
|
||||
"intentScore": number;
|
||||
|
||||
/**
|
||||
* Artist-specific
|
||||
* "Group", "Person"
|
||||
*/
|
||||
"artistType"?: string;
|
||||
"country"?: string;
|
||||
|
||||
/**
|
||||
* Album-specific
|
||||
*/
|
||||
"primaryType"?: string;
|
||||
"year"?: string;
|
||||
|
||||
/**
|
||||
* Track-specific. ReleaseGroupMBID is resolved (from CAAReleaseMBID)
|
||||
* so a track click can open its album page with the track highlighted,
|
||||
* matching how tracks behave everywhere else. ReleaseName is the album
|
||||
* title used for the album page header.
|
||||
*/
|
||||
"length"?: number;
|
||||
"caaReleaseMbid"?: string;
|
||||
"releaseGroupMbid"?: string;
|
||||
"releaseName"?: string;
|
||||
|
||||
/**
|
||||
* Library status — populated from index cross-reference columns.
|
||||
*/
|
||||
"inLibrary": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackLyrics is the stored or freshly-fetched lyrics for one track.
|
||||
* Source is "embedded" (from the file's tags / library DB), "lrclib"
|
||||
* (fetched on demand), or "" when none are available.
|
||||
*/
|
||||
export interface TrackLyrics {
|
||||
"plain": string;
|
||||
"synced": string;
|
||||
"instrumental": boolean;
|
||||
"source": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackThumbnailRequest is a single item in a batch track thumbnail
|
||||
* request. Either ReleaseMBID or ReleaseGroupMBID may be empty;
|
||||
* the proxy tries whichever is present.
|
||||
*/
|
||||
export interface TrackThumbnailRequest {
|
||||
/**
|
||||
* stable key used in the returned map
|
||||
*/
|
||||
"key": string;
|
||||
"releaseMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"albumName": string;
|
||||
"artistName": string;
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service is the Wails-bound service for the explore feature.
|
||||
* It owns the lifecycle of all explore-related components: the
|
||||
* MusicBrainz client, ListenBrainz client, rate limiter, and
|
||||
* response cache. Its exported methods form the binding surface
|
||||
* that the frontend calls via generated TypeScript stubs.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as jobs$0 from "../jobs/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AdoptPausedIndexBuild re-registers a build paused in a previous
|
||||
* session so it appears in the jobs panel, still paused.
|
||||
*/
|
||||
export function AdoptPausedIndexBuild(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3754928399);
|
||||
}
|
||||
|
||||
/**
|
||||
* BackfillLibraryDiscographies enriches owned artists that have not had
|
||||
* their discography fetched yet, in the background. Idempotent and
|
||||
* bounded — the query only returns unenriched artists and each is marked
|
||||
* discog_fetched on success, so this is cheap (an empty query) once every
|
||||
* owned artist is covered and safe to call on every scan and launch.
|
||||
*/
|
||||
export function BackfillLibraryDiscographies(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3678857155);
|
||||
}
|
||||
|
||||
/**
|
||||
* BackfillLibraryLyrics fetches lyrics from LRCLIB for library tracks
|
||||
* that don't have them, in the background. Idempotent and bounded —
|
||||
* each recording is tried once (a miss is cached), and a run stops
|
||||
* after a fixed number of passes, resuming on the next launch.
|
||||
*/
|
||||
export function BackfillLibraryLyrics(): $CancellablePromise<void> {
|
||||
return $Call.ByID(4204427482);
|
||||
}
|
||||
|
||||
/**
|
||||
* BackfillReleaseGroupMBIDs resolves release groups whose scan only
|
||||
* found a release-level MBID (MUSICBRAINZ_ALBUMID — many taggers write
|
||||
* this instead of, or in addition to, MUSICBRAINZ_RELEASEGROUPID) into
|
||||
* the release-group MBID everything else on the album page is keyed
|
||||
* by. Bounded and resumable, in the background: a scan can't afford a
|
||||
* live MusicBrainz call, so `library.updateMBIDs` stashes the release
|
||||
* MBID in `pending_release_mbid` instead, and this is what resolves it
|
||||
* — the same "defer the network call out of the scan path" shape as
|
||||
* BackfillLibraryDiscographies.
|
||||
*/
|
||||
export function BackfillReleaseGroupMBIDs(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2594807082);
|
||||
}
|
||||
|
||||
/**
|
||||
* BrowseReleaseGroups fetches release groups for a given artist MBID.
|
||||
* Checks the local index first for instant results, then fetches from
|
||||
* MusicBrainz for complete data (secondary types, precise dates).
|
||||
* Also adds results to the search index (Tier 5: organic growth).
|
||||
*/
|
||||
export function BrowseReleaseGroups(artistMBID: string): $CancellablePromise<$models.MBReleaseGroup[] | null> {
|
||||
return $Call.ByID(404562912, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* BrowseReleases fetches releases for a given release group MBID.
|
||||
*
|
||||
* Local-first, non-blocking: a warm response cache is served instantly;
|
||||
* on a miss the request does NOT block on a live MusicBrainz browse
|
||||
* (which pulls every version's full tracklist and can take seconds).
|
||||
* Instead it kicks off a background fetch and returns empty — the
|
||||
* AlbumReleasesReady event signals the caller to re-fetch once the cache
|
||||
* is warm.
|
||||
*/
|
||||
export function BrowseReleases(releaseGroupMBID: string): $CancellablePromise<$models.MBRelease[] | null> {
|
||||
return $Call.ByID(2551207897, releaseGroupMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* CAALimiter returns the shared Cover Art Archive rate limiter.
|
||||
* Consumers must respect it for any fresh CAA HTTP GETs.
|
||||
*/
|
||||
export function CAALimiter(): $CancellablePromise<$models.RateLimiter | null> {
|
||||
return $Call.ByID(1239092428);
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckLibraryMBIDs returns which of the given MBIDs exist in the
|
||||
* local music library. Returns a map of MBID → entity type
|
||||
* ("artist", "release_group", "recording").
|
||||
*
|
||||
* It has no frontend caller — `downloadcatalog.go` is the one consumer,
|
||||
* asking about a single MBID at a time.
|
||||
*/
|
||||
export function CheckLibraryMBIDs(mbids: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(3168338597, mbids);
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreCatalogImported reports whether a prebuilt catalog artifact has
|
||||
* been merged into this index.
|
||||
*/
|
||||
export function CoreCatalogImported(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(186364153);
|
||||
}
|
||||
|
||||
/**
|
||||
* CoverArtGroupURL returns the Cover Art Archive URL for a release
|
||||
* group's front cover at the default 250px size. This is the
|
||||
* correct endpoint for search results, which return release group
|
||||
* MBIDs rather than individual release MBIDs.
|
||||
*/
|
||||
export function CoverArtGroupURL(releaseGroupMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(2102228641, releaseGroupMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* CoverArtURL returns the Cover Art Archive URL for a release's
|
||||
* front cover at the default 250px size.
|
||||
*/
|
||||
export function CoverArtURL(releaseMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(3231496554, releaseMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GenerateMix returns the next batch of tracks for a dynamic-mix queue
|
||||
* fallback, built by expanding the seed's artists to their similar
|
||||
* artists (weighted by how often each appears in the seed, sharpened
|
||||
* by shared genre tags) and restricting candidates to what is actually
|
||||
* in the library — a queue can only play files that exist.
|
||||
*
|
||||
* continuing extends the current mix session — regenerating from its
|
||||
* original seed rather than seedPaths — instead of starting a fresh
|
||||
* one. Pass false whenever the queue that just exhausted was not
|
||||
* itself a mix batch (a real selection just ran out); pass true when
|
||||
* it was (the mix keeps going indefinitely). label names the batch
|
||||
* after its most-represented seed artist, for the "Playing from" UI.
|
||||
*/
|
||||
export function GenerateMix(seedPaths: string[] | null, continuing: boolean): $CancellablePromise<[string[] | null, string]> {
|
||||
return $Call.ByID(4293623744, seedPaths, continuing);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistImageCached returns a base64 data URL for the artist's
|
||||
* photo ONLY if it's already on disk — no MB/Wikidata resolution
|
||||
* or Wikimedia fetch. Returns "" if not cached.
|
||||
*/
|
||||
export function GetArtistImageCached(artistMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(670428351, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistImageCachedPath returns the asset-handler URL path for
|
||||
* the artist's cached medium thumbnail, e.g.
|
||||
* "/artist-images/b1/b10bbbfc-.../primary_md.jpg". No base64, no
|
||||
* network calls — just a disk existence check. Returns "" if no
|
||||
* image is cached.
|
||||
*/
|
||||
export function GetArtistImageCachedPath(artistMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(589683480, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistImageURL returns a base64 data URL for the artist's
|
||||
* photo. Cached on disk — first call resolves via MB/Wikidata and
|
||||
* fetches from Wikimedia Commons, subsequent calls are instant.
|
||||
* Returns "" if no image is available.
|
||||
*/
|
||||
export function GetArtistImageURL(artistMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1780431074, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistImagesCachedPaths resolves artist portraits for many MBIDs
|
||||
* in one call, returning MBID → asset-handler path for the medium
|
||||
* thumbnail. Disk existence checks only: no MusicBrainz, no Wikidata,
|
||||
* no Wikimedia, no network of any kind. MBIDs with no cached portrait
|
||||
* are omitted rather than returned empty.
|
||||
*
|
||||
* It exists because the resolving entry point (GetArtistImageURL) was
|
||||
* being used where a cache check belonged: a page rendering a dozen
|
||||
* search results paid a full MB → Wikidata → Wikipedia → Wikimedia
|
||||
* resolution for every artist whose portrait was already on disk. Its
|
||||
* predecessor could not serve that — it keyed on artist *name* through
|
||||
* the library's own MBID map, so it only ever answered for artists the
|
||||
* user already owned, which on a catalog search is nearly none of them.
|
||||
*
|
||||
* Paths rather than base64: a portrait is ~128 kB as a data URL, and
|
||||
* the asset handler serves the same bytes without crossing the IPC
|
||||
* boundary or being retained by a JS string.
|
||||
*/
|
||||
export function GetArtistImagesCachedPaths(mbids: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(2104788604, mbids);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistMBID returns the MusicBrainz ID for a local library
|
||||
* artist by name, or "" if not found or no MBID tagged.
|
||||
*/
|
||||
export function GetArtistMBID(artistName: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(944736774, artistName);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetArtistPlayCount returns the total LB listen count for an artist.
|
||||
* Returns 0 if unknown.
|
||||
*/
|
||||
export function GetArtistPlayCount(artistMBID: string): $CancellablePromise<number> {
|
||||
return $Call.ByID(143535277, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCandidateThumbnail returns CAA-only cover art for an autotag
|
||||
* candidate, skipping the library-by-name index so embedded ID3
|
||||
* art on the user's existing files doesn't pollute the candidate
|
||||
* preview. Disk cache → network on RG → network on release.
|
||||
*/
|
||||
export function GetCandidateThumbnail(releaseMBID: string, releaseGroupMBID: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1946932424, releaseMBID, releaseGroupMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExploreShelves builds the page Explore shows before a query.
|
||||
*
|
||||
* One call rather than one per shelf, for `home`'s reason: the shelves
|
||||
* share nothing expensive, but the page has nothing useful to render
|
||||
* until it knows which rows exist, and rows that pop in one at a time
|
||||
* reflow under the cursor.
|
||||
*/
|
||||
export function GetExploreShelves(): $CancellablePromise<$models.ShelfPage> {
|
||||
return $Call.ByID(144329614);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetIndexStatus returns the current search index build status.
|
||||
*/
|
||||
export function GetIndexStatus(): $CancellablePromise<$models.IndexStatus> {
|
||||
return $Call.ByID(3481063523);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetLibrarySimilarArtists returns similar artists to the given
|
||||
* MBID that are also in the user's local library. Uses the
|
||||
* pre-computed similar_artist_map table (populated during Tier 4
|
||||
* index build) joined with the artists table. No API calls.
|
||||
*
|
||||
* The artists table allows multiple rows with the same MBID
|
||||
* (different artist credits like "A feat. B" that resolve to the
|
||||
* same MB artist), so we use EXISTS instead of JOIN to avoid
|
||||
* duplicating similar_artist_map rows.
|
||||
*/
|
||||
export function GetLibrarySimilarArtists(artistMBID: string): $CancellablePromise<$models.LBSimilarArtist[] | null> {
|
||||
return $Call.ByID(104313157, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThumbnail returns a base64 data URL for the release group's
|
||||
* cover art. Checks local library art first (by album+artist
|
||||
* name), then disk cache, then Cover Art Archive.
|
||||
* Returns "" if no cover art is available.
|
||||
*/
|
||||
export function GetThumbnail(releaseGroupMBID: string, albumName: string, artistName: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(4046192225, releaseGroupMBID, albumName, artistName);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThumbnails fetches multiple thumbnails in one call and returns
|
||||
* a map of MBID → base64 data URL. Entries with no art are omitted.
|
||||
* GetThumbnails returns ONLY cached/local art instantly — no network
|
||||
* fetches. For items missing from the cache, the frontend should
|
||||
* call GetThumbnail() individually so results stream in rather than
|
||||
* blocking on a batch.
|
||||
*/
|
||||
export function GetThumbnails(requests: $models.ThumbnailRequest[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(3124819542, requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTrackLyrics returns lyrics for a recording. If the library
|
||||
* already has them (from embedded tags) they're returned as-is;
|
||||
* otherwise it fetches from LRCLIB, persists them (updating the FTS
|
||||
* index), and returns them. Never returns an error to the frontend —
|
||||
* a miss just yields an empty result.
|
||||
*/
|
||||
export function GetTrackLyrics(recordingID: number): $CancellablePromise<$models.TrackLyrics> {
|
||||
return $Call.ByID(1131284622, recordingID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTrackThumbnail returns cover art for a track. Accepts both
|
||||
* the track's CAA release MBID and the resolved parent release
|
||||
* group MBID (either may be empty). Tries the RG first to reuse
|
||||
* discography cache; falls back to the release-level CAA endpoint
|
||||
* when the RG isn't known — useful when the track's preferred CAA
|
||||
* release doesn't belong to any RG currently in the index.
|
||||
*/
|
||||
export function GetTrackThumbnail(releaseMBID: string, releaseGroupMBID: string, albumName: string, artistName: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(4191599666, releaseMBID, releaseGroupMBID, albumName, artistName);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTrackThumbnails returns ONLY cached/local art for track
|
||||
* requests, keyed by the caller-provided Key so callers can map
|
||||
* results back to rows in their UI.
|
||||
*/
|
||||
export function GetTrackThumbnails(requests: $models.TrackThumbnailRequest[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(2383043155, requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexBaselineSeries returns the incremental listens series the index's
|
||||
* popularity is caught up to. A change across a refresh means new data
|
||||
* was folded in.
|
||||
*/
|
||||
export function IndexBaselineSeries(): $CancellablePromise<number> {
|
||||
return $Call.ByID(3317566669);
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexImportComplete reports whether the dump import has finished all
|
||||
* of its stages. Distinct from IsIndexReady, which only means the index
|
||||
* holds enough rows to answer queries — a partially imported index is
|
||||
* ready but not complete. Used by the headless builder to decide
|
||||
* whether another run is needed.
|
||||
*/
|
||||
export function IndexImportComplete(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3872948181);
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexLastImported returns when the dump import last completed, or the
|
||||
* zero time if it never has.
|
||||
*/
|
||||
export function IndexLastImported(): $CancellablePromise<string> {
|
||||
return $Call.ByID(1889359471);
|
||||
}
|
||||
|
||||
/**
|
||||
* InvalidateIndexDiscographies clears the discography build
|
||||
* timestamp so the next index build re-runs Tiers 2-4. Call
|
||||
* after a library rescan that may have populated new MBIDs.
|
||||
*/
|
||||
export function InvalidateIndexDiscographies(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2573191521);
|
||||
}
|
||||
|
||||
/**
|
||||
* InvalidateLibrarySync clears the "ready" markers guarding the gated
|
||||
* library-sync steps so they re-run on the next launch. Call after a
|
||||
* mutation that changes owned content outside a scan (e.g. removing a
|
||||
* library), which would otherwise leave stale in_library flags and
|
||||
* orphaned lyric-index rows.
|
||||
*/
|
||||
export function InvalidateLibrarySync(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2446309672);
|
||||
}
|
||||
|
||||
/**
|
||||
* IsIndexReady returns true once the index has been populated.
|
||||
*/
|
||||
export function IsIndexReady(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(1366461294);
|
||||
}
|
||||
|
||||
/**
|
||||
* LookupArtist fetches a single MusicBrainz artist by MBID.
|
||||
* Checks the local index first — has name, type, country,
|
||||
* disambiguation, sort_name for indexed artists. Falls back to
|
||||
* MB API for unknown artists and backfills the index for next time.
|
||||
*/
|
||||
export function LookupArtist(mbid: string): $CancellablePromise<$models.MBArtist | null> {
|
||||
return $Call.ByID(3986196952, mbid);
|
||||
}
|
||||
|
||||
/**
|
||||
* LookupReleaseGroup fetches a single MusicBrainz release group by MBID.
|
||||
*/
|
||||
export function LookupReleaseGroup(mbid: string): $CancellablePromise<$models.MBReleaseGroup | null> {
|
||||
return $Call.ByID(2946174711, mbid);
|
||||
}
|
||||
|
||||
/**
|
||||
* MusicBrainz returns the shared cached MB client so other services
|
||||
* (e.g. autotag) can reuse it without spinning up a second limiter.
|
||||
*/
|
||||
export function MusicBrainz(): $CancellablePromise<$models.MusicBrainzClient | null> {
|
||||
return $Call.ByID(3453528034);
|
||||
}
|
||||
|
||||
/**
|
||||
* PopulateLocalCrossReferences updates the local_*_id columns on
|
||||
* explore_index after a library scan.
|
||||
*/
|
||||
export function PopulateLocalCrossReferences(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1058376024);
|
||||
}
|
||||
|
||||
/**
|
||||
* PopulateLocalCrossReferencesIfNeeded runs the library→index sync only
|
||||
* when it has not run since the last library change. Use it on the
|
||||
* unchanged-library launch path so the write-heavy re-sync is skipped in
|
||||
* steady state; the scan-completion path calls the unconditional form.
|
||||
*/
|
||||
export function PopulateLocalCrossReferencesIfNeeded(): $CancellablePromise<void> {
|
||||
return $Call.ByID(814782146);
|
||||
}
|
||||
|
||||
/**
|
||||
* PrefetchReleases warms the local response cache for a set of release
|
||||
* groups in the background so opening any of them is instant. Called from
|
||||
* the artist page once its top-releases / discography render — album
|
||||
* navigation almost always originates there. Already-cached groups are
|
||||
* skipped; a cap bounds how many live fetches a single artist view can
|
||||
* trigger so the MusicBrainz rate limiter isn't flooded.
|
||||
* A release group the user owns *completely* is skipped outright: since
|
||||
* tag-derived completeness landed, such an album opens with no catalog
|
||||
* call at all — identity from its MBID, tracklist from its own files —
|
||||
* so warming the most expensive request in the app on its behalf buys
|
||||
* nothing. The skip is not merely an optimisation; those slots go to
|
||||
* albums that will actually need the browse.
|
||||
*/
|
||||
export function PrefetchReleases(releaseGroupMBIDs: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(306872972, releaseGroupMBIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* PrepareIndexRebuild clears the completion marker so the next build
|
||||
* re-imports from the newest published dump.
|
||||
*/
|
||||
export function PrepareIndexRebuild(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2772457219);
|
||||
}
|
||||
|
||||
/**
|
||||
* RebuildLyricsIndex rebuilds the FTS lyrics index from the current
|
||||
* library. Cheap; safe to call after every scan.
|
||||
*/
|
||||
export function RebuildLyricsIndex(): $CancellablePromise<void> {
|
||||
return $Call.ByID(300335776);
|
||||
}
|
||||
|
||||
/**
|
||||
* RebuildLyricsIndexIfNeeded rebuilds the lyrics FTS only when it has not
|
||||
* been built since the last library change. The backfill keeps the index
|
||||
* in sync incrementally thereafter, so on an unchanged library the full
|
||||
* rebuild is redundant; the scan-completion path calls the unconditional
|
||||
* form.
|
||||
*/
|
||||
export function RebuildLyricsIndexIfNeeded(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1447291626);
|
||||
}
|
||||
|
||||
/**
|
||||
* RecordSearchClick records that the user clicked a search result.
|
||||
* Called from the frontend when any search result is clicked.
|
||||
*/
|
||||
export function RecordSearchClick(query: string, mbid: string, entityType: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(824485756, query, mbid, entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* RefreshIndexNow folds newly published incremental listens dumps into
|
||||
* the index synchronously. Pass 0 to bypass the cadence gate.
|
||||
*/
|
||||
export function RefreshIndexNow(minInterval: time$0.Duration): $CancellablePromise<void> {
|
||||
return $Call.ByID(4165220556, minInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* RefreshListenCounts folds any newly-published incremental listen dumps
|
||||
* into the index's popularity numbers, in the background. No-op when
|
||||
* offline, when a full build is running, when there is no baseline
|
||||
* import, or when the last refresh was within the weekly cadence. Fully
|
||||
* local — downloads the small daily dumps but makes no ListenBrainz API
|
||||
* calls.
|
||||
*/
|
||||
export function RefreshListenCounts(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3813092323);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolveReleaseGroupMBIDs takes a list of CAA release MBIDs (from
|
||||
* recording metadata) and returns a map of release MBID → release
|
||||
* group MBID. The frontend uses this to fetch track cover art via
|
||||
* the parent release group, reusing whatever cache exists for the
|
||||
* album already.
|
||||
*/
|
||||
export function ResolveReleaseGroupMBIDs(caaReleaseMBIDs: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
|
||||
return $Call.ByID(325093206, caaReleaseMBIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchLocal queries only the local FTS5 index and returns fully
|
||||
* ranked results instantly with no network calls. This is the
|
||||
* primary interactive search path: now that the index is populated
|
||||
* from the MetaBrainz dumps it covers essentially every popular
|
||||
* entity, so the frontend drives search entirely from here. The
|
||||
* old MusicBrainz network pipeline (Search) is retained for a future
|
||||
* opt-in "search online" affordance but is no longer called on the
|
||||
* hot path.
|
||||
*
|
||||
* Returns nil if the index has no hits for the query, so the caller
|
||||
* can fall back to whatever owned-library matches it already has.
|
||||
*/
|
||||
export function SearchLocal(query: string): $CancellablePromise<$models.MBSearchResult | null> {
|
||||
return $Call.ByID(189423736, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchLyrics finds library tracks whose lyrics contain the given
|
||||
* fragment, ranked by relevance. Pure local FTS — no network.
|
||||
*/
|
||||
export function SearchLyrics(query: string): $CancellablePromise<$models.LyricsResult[] | null> {
|
||||
return $Call.ByID(4153164053, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetAlbumComplete injects the completeness check. It is injected
|
||||
* rather than imported because `library` and `explore` do not depend on
|
||||
* each other in either direction today, and one prefetch heuristic is
|
||||
* not a reason to introduce that edge — the alternative, re-deriving
|
||||
* "complete" from SQL here, would be a second definition of it.
|
||||
*/
|
||||
export function SetAlbumComplete(fn: $models.AlbumCompleteFunc): $CancellablePromise<void> {
|
||||
return $Call.ByID(942474493, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetJobRegistry wires the background job registry into the search
|
||||
* index so its build reports progress and controls to the frontend.
|
||||
*/
|
||||
export function SetJobRegistry(reg: jobs$0.Registry | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(4291900709, reg);
|
||||
}
|
||||
|
||||
/**
|
||||
* SimilarArtists returns artists similar to the given artist MBID.
|
||||
*/
|
||||
export function SimilarArtists(artistMBID: string): $CancellablePromise<$models.LBSimilarArtist[] | null> {
|
||||
return $Call.ByID(2048211426, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* StartIndexBuild kicks off the background search index build.
|
||||
* Call this after the library scan completes so the indexer doesn't
|
||||
* starve the scan for DB access.
|
||||
*/
|
||||
export function StartIndexBuild(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1444396415);
|
||||
}
|
||||
|
||||
/**
|
||||
* StopIndexBuild cancels the background search index build.
|
||||
* Call before a full rescan to free the DB for the scan.
|
||||
*/
|
||||
export function StopIndexBuild(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3056245285);
|
||||
}
|
||||
|
||||
/**
|
||||
* TopRecordingsForArtist returns the most-listened recordings for an
|
||||
* artist. Serves instantly from the local index when available; when the
|
||||
* artist isn't indexed yet it returns empty immediately and fetches the
|
||||
* discography in the background, emitting ArtistDiscographyReady so the
|
||||
* caller can re-fetch — the request never blocks on a live fetch.
|
||||
*/
|
||||
export function TopRecordingsForArtist(artistMBID: string): $CancellablePromise<$models.LBTopRecording[] | null> {
|
||||
return $Call.ByID(2077579960, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* TopReleaseGroupsForArtist returns the most-listened release groups for
|
||||
* an artist. Same non-blocking contract as TopRecordingsForArtist: index
|
||||
* hit is instant, a miss kicks off a background discography fetch and
|
||||
* returns empty, and ArtistDiscographyReady signals when to re-fetch.
|
||||
*/
|
||||
export function TopReleaseGroupsForArtist(artistMBID: string): $CancellablePromise<$models.LBTopReleaseGroup[] | null> {
|
||||
return $Call.ByID(602197643, artistMBID);
|
||||
}
|
||||
|
||||
/**
|
||||
* WaitForIndexIdle blocks until no index build or artist indexing
|
||||
* goroutine is running. Does not cancel a running build.
|
||||
*/
|
||||
export function WaitForIndexIdle(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3200929511);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* FrontendUtil provides frontend-bound Go functions.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* DirectoryPicker opens a directory selection dialog.
|
||||
*
|
||||
* v3 has no separate directory dialog: it is the file dialog told to
|
||||
* choose directories and not files.
|
||||
*/
|
||||
export function DirectoryPicker(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3245034282);
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageFilePicker opens a file selection dialog filtered to image
|
||||
* files (JPEG, PNG). Returns the selected file path, or empty
|
||||
* string if the user cancelled.
|
||||
*/
|
||||
export function ImageFilePicker(): $CancellablePromise<string> {
|
||||
return $Call.ByID(3408786006);
|
||||
}
|
||||
|
||||
/**
|
||||
* PlaylistFilePicker opens a file selection dialog filtered
|
||||
* to M3U/M3U8 playlist files. Multiple files may be selected.
|
||||
*/
|
||||
export function PlaylistFilePicker(): $CancellablePromise<string[] | null> {
|
||||
return $Call.ByID(2858498591);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReadFile reads a file from disk and returns its contents.
|
||||
* Used by the frontend to read cover art image files selected
|
||||
* via ImageFilePicker.
|
||||
*/
|
||||
export function ReadFile(path: string): $CancellablePromise<string | null> {
|
||||
return $Call.ByID(3350710867, path);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as FrontendUtil from "./frontendutil.js";
|
||||
export {
|
||||
FrontendUtil
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
Kind
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
Shelf
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,53 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as library$0 from "../library/models.js";
|
||||
|
||||
/**
|
||||
* Kind identifies what a shelf is built from, so the frontend can pick
|
||||
* an icon and the e2e suite can assert on a shelf without matching
|
||||
* display copy.
|
||||
*/
|
||||
export enum Kind {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Shelf kinds.
|
||||
*/
|
||||
KindRecentlyPlayed = "recently-played",
|
||||
KindRecentlyAdded = "recently-added",
|
||||
KindMostPlayed = "most-played",
|
||||
KindUnplayed = "unplayed",
|
||||
KindStale = "stale",
|
||||
KindArtist = "artist",
|
||||
KindGenre = "genre",
|
||||
KindRandom = "random",
|
||||
};
|
||||
|
||||
/**
|
||||
* Shelf is one horizontal row on the home page.
|
||||
*/
|
||||
export interface Shelf {
|
||||
/**
|
||||
* ID is stable within a response, for list keying.
|
||||
*/
|
||||
"id": string;
|
||||
"kind": Kind;
|
||||
|
||||
/**
|
||||
* Title is the row heading.
|
||||
*/
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* Subtitle says why these albums are here. It is not decoration:
|
||||
* without it a shelf is indistinguishable from a random grid.
|
||||
*/
|
||||
"subtitle": string;
|
||||
"albums": library$0.Album[] | null;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service builds the home page's shelves.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* GetShelves returns the home page's rows, in display order.
|
||||
*
|
||||
* It is a single call rather than one per shelf because the shelves
|
||||
* share an album lookup and because the page has nothing useful to
|
||||
* render until it knows which rows exist — a page that pops rows in one
|
||||
* at a time reflows under the user's cursor.
|
||||
*/
|
||||
export function GetShelves(): $CancellablePromise<$models.Shelf[] | null> {
|
||||
return $Call.ByID(2822423495);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export {
|
||||
Kind,
|
||||
Level,
|
||||
State
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
Caps,
|
||||
Job,
|
||||
LogEntry,
|
||||
Registry,
|
||||
Stage,
|
||||
Stat
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,159 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Caps describes which controls a job supports. The frontend renders
|
||||
* buttons from these rather than switching on Kind, so a job that gains
|
||||
* pause support later needs no frontend change.
|
||||
*/
|
||||
export interface Caps {
|
||||
"pausable": boolean;
|
||||
"cancellable": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Job is the frontend-facing snapshot of a single background job.
|
||||
*/
|
||||
export interface Job {
|
||||
"id": string;
|
||||
"kind": Kind;
|
||||
"title": string;
|
||||
"subtitle"?: string;
|
||||
"state": State;
|
||||
"phase"?: string;
|
||||
|
||||
/**
|
||||
* Current/Total drive the progress bar. Total == 0 means the job
|
||||
* is indeterminate and the frontend should render a spinner.
|
||||
*/
|
||||
"current": number;
|
||||
"total": number;
|
||||
"caps": Caps;
|
||||
"stages": Stage[] | null;
|
||||
"stats": Stat[] | null;
|
||||
"error"?: string;
|
||||
|
||||
/**
|
||||
* unix milliseconds
|
||||
*/
|
||||
"startedAt": number;
|
||||
"updatedAt": number;
|
||||
"endedAt"?: number;
|
||||
"logCount": number;
|
||||
"warnCount": number;
|
||||
"errorCount": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind identifies the subsystem that owns a job. The frontend uses it
|
||||
* to pick an icon and to route "view details" to the right panel.
|
||||
*/
|
||||
export enum Kind {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Job kinds.
|
||||
*/
|
||||
KindLibraryScan = "library-scan",
|
||||
KindIndexBuild = "index-build",
|
||||
KindDownload = "download",
|
||||
KindAutotagApply = "autotag-apply",
|
||||
|
||||
/**
|
||||
* KindCatalogEnrich is background catalog work for content the user
|
||||
* already owns — the discography backfills. It is distinct from
|
||||
* KindIndexBuild because the two differ in what cancelling costs:
|
||||
* an index build discards hours of downloading and the frontend
|
||||
* confirms before stopping one, where a backfill is resumable per
|
||||
* artist and stopping it is free.
|
||||
*/
|
||||
KindCatalogEnrich = "catalog-enrich",
|
||||
};
|
||||
|
||||
/**
|
||||
* Level is the severity of a job log entry.
|
||||
*/
|
||||
export enum Level {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Log levels.
|
||||
*/
|
||||
LevelInfo = "info",
|
||||
LevelWarn = "warn",
|
||||
LevelError = "error",
|
||||
};
|
||||
|
||||
/**
|
||||
* LogEntry is one line of a job's output log.
|
||||
*/
|
||||
export interface LogEntry {
|
||||
/**
|
||||
* unix milliseconds
|
||||
*/
|
||||
"time": number;
|
||||
"level": Level;
|
||||
"message": string;
|
||||
"detail"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry owns every known job and pushes coalesced snapshots to the
|
||||
* frontend. It is safe for concurrent use.
|
||||
*/
|
||||
export interface Registry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage is one named sub-step of a multi-stage job, such as an index
|
||||
* build tier. Jobs with a single linear phase leave Stages empty.
|
||||
*/
|
||||
export interface Stage {
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* pending, running, complete, error, skipped
|
||||
*/
|
||||
"state": string;
|
||||
"current": number;
|
||||
"total": number;
|
||||
"error"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat is a display-only key/value pair shown in the job detail panel
|
||||
* (e.g. "Added" / "1,204").
|
||||
*/
|
||||
export interface Stat {
|
||||
"label": string;
|
||||
"value": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State is the lifecycle position of a job.
|
||||
*/
|
||||
export enum State {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Job states. Queued, Running, Paused and Pausing are live; Complete,
|
||||
* Cancelled and Error are terminal.
|
||||
*/
|
||||
StateQueued = "queued",
|
||||
StateRunning = "running",
|
||||
StatePausing = "pausing",
|
||||
StatePaused = "paused",
|
||||
StateCancelling = "cancelling",
|
||||
StateComplete = "complete",
|
||||
StateCancelled = "cancelled",
|
||||
StateError = "error",
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service is the Wails-bound facade over the registry. It deliberately
|
||||
* exposes only the read and control surface — producers get a *Handle
|
||||
* through the registry instead, so the frontend cannot invent jobs.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* CancelJob abandons a job.
|
||||
*/
|
||||
export function CancelJob(id: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(680141523, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ClearFinishedJobs removes every terminal job from the list.
|
||||
*/
|
||||
export function ClearFinishedJobs(): $CancellablePromise<void> {
|
||||
return $Call.ByID(4219744941);
|
||||
}
|
||||
|
||||
/**
|
||||
* DismissJob removes a single finished job from the list.
|
||||
*/
|
||||
export function DismissJob(id: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1904307161, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetJobLog returns the retained log tail for one job.
|
||||
*/
|
||||
export function GetJobLog(id: string): $CancellablePromise<$models.LogEntry[] | null> {
|
||||
return $Call.ByID(441057229, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetJobs returns every known job — active first by registration order,
|
||||
* including recently finished ones so the panel can show outcomes.
|
||||
*/
|
||||
export function GetJobs(): $CancellablePromise<$models.Job[] | null> {
|
||||
return $Call.ByID(3979035366);
|
||||
}
|
||||
|
||||
/**
|
||||
* PauseJob asks the owning subsystem to pause a job. Returns
|
||||
* immediately; the job reports "paused" once it actually stops.
|
||||
*/
|
||||
export function PauseJob(id: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(4094353111, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResumeJob continues a paused job.
|
||||
*/
|
||||
export function ResumeJob(id: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3937701508, id);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Library from "./library.js";
|
||||
export {
|
||||
Library
|
||||
};
|
||||
|
||||
export type {
|
||||
Album,
|
||||
AlbumCompleteness,
|
||||
Artist,
|
||||
GenreWithCount,
|
||||
Info,
|
||||
RemovalHooks,
|
||||
RemovalImpact,
|
||||
RemovalResult,
|
||||
RemovalSummary,
|
||||
RescanHooks,
|
||||
ScanHooks,
|
||||
ScanMetrics,
|
||||
ScanWarning,
|
||||
Track,
|
||||
TrackMBIDs
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,454 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Library manages scanning and querying the music collection.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as sqlcgen$0 from "../database/sql/sqlcgen/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as jobs$0 from "../jobs/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AcquirePipelineLock acquires the pipeline mutex for a tag write
|
||||
* operation. The caller must call ReleasePipelineLock when done.
|
||||
* If a scan is currently in progress, AcquirePipelineLock blocks
|
||||
* until it completes (and vice versa).
|
||||
*/
|
||||
export function AcquirePipelineLock(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2056761494);
|
||||
}
|
||||
|
||||
/**
|
||||
* AddLibrary creates a new library from a directory path, emits a
|
||||
* LibraryAdded event, and starts an asynchronous scan.
|
||||
*/
|
||||
export function AddLibrary(path: string): $CancellablePromise<sqlcgen$0.Library | null> {
|
||||
return $Call.ByID(4258719739, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelAllScans cancels the current scan and clears the entire
|
||||
* queue so no further libraries are scanned.
|
||||
*/
|
||||
export function CancelAllScans(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2826471660);
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelCurrentScan cancels only the currently scanning library.
|
||||
* The next queued library (if any) starts automatically when the
|
||||
* current scan's goroutine completes.
|
||||
*/
|
||||
export function CancelCurrentScan(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2190880081);
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelScan cancels an in-progress scan. Returns immediately;
|
||||
* scan goroutines stop at their next checkpoint.
|
||||
*
|
||||
* Deprecated: Use CancelCurrentScan or CancelAllScans for
|
||||
* queue-aware cancellation.
|
||||
*/
|
||||
export function CancelScan(): $CancellablePromise<void> {
|
||||
return $Call.ByID(4160132270);
|
||||
}
|
||||
|
||||
/**
|
||||
* FullRescan clears the queue and player, wipes all library data
|
||||
* (database records and cover art files), and performs a fresh
|
||||
* scan of every library in the database. The returned ScanMetrics
|
||||
* reflects the last library scanned; clear-phase durations are
|
||||
* folded into its totals.
|
||||
*/
|
||||
export function FullRescan(): $CancellablePromise<$models.ScanMetrics | null> {
|
||||
return $Call.ByID(378891236);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAlbumCompleteness answers "do I have all of this album" from the
|
||||
* tags read at scan time, with no network.
|
||||
*
|
||||
* The album page used to ask MusicBrainz, because the only track total
|
||||
* it had was the length of whatever tracklist it was already showing —
|
||||
* which for a library copy is a tautology. The denominator in a file's
|
||||
* "5/12" is a real answer and it is already on disk; this is where it
|
||||
* gets read.
|
||||
*
|
||||
* Complete is deliberately >= rather than ==: bonus and hidden tracks
|
||||
* routinely put a folder over its declared total, and that is a
|
||||
* complete album, not a broken one.
|
||||
*/
|
||||
export function GetAlbumCompleteness(albumID: number): $CancellablePromise<$models.AlbumCompleteness> {
|
||||
return $Call.ByID(2895787150, albumID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAlbumTracks returns all tracks for a given album (release group), ordered by disc and track number.
|
||||
*/
|
||||
export function GetAlbumTracks(albumID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(300451334, albumID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAlbumTracksByLibrary returns tracks for the given album,
|
||||
* scoped to the given library.
|
||||
*/
|
||||
export function GetAlbumTracksByLibrary(albumID: number, libraryID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(3304485554, albumID, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAlbumsByArtist returns all albums where the given artist is the album artist.
|
||||
*/
|
||||
export function GetAlbumsByArtist(artistID: number): $CancellablePromise<$models.Album[] | null> {
|
||||
return $Call.ByID(1456840721, artistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAlbumsByArtistByLibrary returns albums for the given artist
|
||||
* that have tracks in the given library.
|
||||
*/
|
||||
export function GetAlbumsByArtistByLibrary(artistID: number, libraryID: number): $CancellablePromise<$models.Album[] | null> {
|
||||
return $Call.ByID(2809291, artistID, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllAlbums returns all albums with cover art and artist info for the cover grid.
|
||||
*/
|
||||
export function GetAllAlbums(): $CancellablePromise<$models.Album[] | null> {
|
||||
return $Call.ByID(2015458954);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllAlbumsByLibrary returns albums that have tracks in the given library.
|
||||
*/
|
||||
export function GetAllAlbumsByLibrary(libraryID: number): $CancellablePromise<$models.Album[] | null> {
|
||||
return $Call.ByID(4023050470, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllArtists returns artists that are credited as album artists, ordered by name.
|
||||
*/
|
||||
export function GetAllArtists(): $CancellablePromise<$models.Artist[] | null> {
|
||||
return $Call.ByID(2529088294);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllArtistsByLibrary returns artists that have albums with tracks
|
||||
* in the given library.
|
||||
*/
|
||||
export function GetAllArtistsByLibrary(libraryID: number): $CancellablePromise<$models.Artist[] | null> {
|
||||
return $Call.ByID(1170594642, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllGenresWithCounts returns all genres with their track counts.
|
||||
*/
|
||||
export function GetAllGenresWithCounts(): $CancellablePromise<$models.GenreWithCount[] | null> {
|
||||
return $Call.ByID(602231298);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllGenresWithCountsByLibrary returns genres with track counts
|
||||
* scoped to the given library.
|
||||
*/
|
||||
export function GetAllGenresWithCountsByLibrary(libraryID: number): $CancellablePromise<$models.GenreWithCount[] | null> {
|
||||
return $Call.ByID(772684334, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllLibrariesWithTrackCounts returns all libraries with their
|
||||
* audio file counts. Typically 1-5 libraries so the loop is trivial.
|
||||
*/
|
||||
export function GetAllLibrariesWithTrackCounts(): $CancellablePromise<$models.Info[] | null> {
|
||||
return $Call.ByID(3420301148);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllTracks returns an array of track structs of every file in the library.
|
||||
*/
|
||||
export function GetAllTracks(): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(2991050010);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllTracksByLibrary returns tracks scoped to a specific library.
|
||||
*/
|
||||
export function GetAllTracksByLibrary(libraryID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(3882999766, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFilePathsByAlbums returns the file paths of every track in the
|
||||
* given albums, grouped by album id.
|
||||
*
|
||||
* "Play this artist", "play these albums" and the album drag cache each
|
||||
* resolved paths with one binding call per album, sequentially, and each
|
||||
* asked for whole track rows to read one field off them (perf.m2). This
|
||||
* is that question asked once. The result is grouped rather than
|
||||
* flattened because the caller owns the ordering — an album list is
|
||||
* sorted by name, not by id — and because the drag cache stores it per
|
||||
* album.
|
||||
*
|
||||
* A library id of 0 means "every library", matching the caller's
|
||||
* selected-library filter being unset.
|
||||
*/
|
||||
export function GetFilePathsByAlbums(albumIDs: number[] | null, libraryID: number): $CancellablePromise<{ [_ in `${number}`]?: string[] | null } | null> {
|
||||
return $Call.ByID(1692993358, albumIDs, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFilePathsByGenres returns the file paths of every track tagged with
|
||||
* the given genres, grouped by genre name. See GetFilePathsByAlbums —
|
||||
* same finding, same shape, and the caller still owns the de-duplication
|
||||
* across genres because it owns the order.
|
||||
*/
|
||||
export function GetFilePathsByGenres(genreNames: string[] | null, libraryID: number): $CancellablePromise<{ [_ in string]?: string[] | null } | null> {
|
||||
return $Call.ByID(1180707302, genreNames, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFilePathsByRecordingMBIDs returns the file paths of every track
|
||||
* whose recording MBID is in mbids, grouped by MBID.
|
||||
*
|
||||
* This is the catalog side of GetFilePathsByAlbums. An Explore album
|
||||
* page knows what the user owns as a set of recording MBIDs and nothing
|
||||
* else: that is exactly how the backend decides a track's InLibrary
|
||||
* flag (markReleasesInLibrary → CheckMBIDs), and MBTrack.LocalID is a
|
||||
* declared field that nothing writes, so there is no id to ask by.
|
||||
*
|
||||
* Grouped rather than flattened for the same two reasons as its
|
||||
* siblings — the caller owns the order (the tracklist's, not the
|
||||
* database's), and one recording can have more than one file, which is
|
||||
* what this app's duplicate detection exists for.
|
||||
*
|
||||
* A library id of 0 means "every library".
|
||||
*/
|
||||
export function GetFilePathsByRecordingMBIDs(mbids: string[] | null, libraryID: number): $CancellablePromise<{ [_ in string]?: string[] | null } | null> {
|
||||
return $Call.ByID(2789061644, mbids, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetRemovalImpact returns pre-removal counts for the confirmation
|
||||
* dialog. All queries are read-only.
|
||||
*/
|
||||
export function GetRemovalImpact(libraryID: number): $CancellablePromise<$models.RemovalImpact | null> {
|
||||
return $Call.ByID(2715588999, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetScanQueueLength returns the number of libraries waiting in the
|
||||
* scan queue (excludes the currently scanning library).
|
||||
*/
|
||||
export function GetScanQueueLength(): $CancellablePromise<number> {
|
||||
return $Call.ByID(2739382973);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTrackMBIDs returns the MusicBrainz IDs for the track at the
|
||||
* given file path. Returns empty strings for entities without MBIDs.
|
||||
*/
|
||||
export function GetTrackMBIDs(filePath: string): $CancellablePromise<$models.TrackMBIDs> {
|
||||
return $Call.ByID(56752473, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTracksByGenre returns all tracks tagged with the given genre.
|
||||
*/
|
||||
export function GetTracksByGenre(genreName: string): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(1674220245, genreName);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTracksByGenreByLibrary returns tracks tagged with the given
|
||||
* genre, scoped to the given library.
|
||||
*/
|
||||
export function GetTracksByGenreByLibrary(genreName: string, libraryID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(4240564783, genreName, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* IsScanActive returns whether a scan is currently running.
|
||||
*/
|
||||
export function IsScanActive(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(1365062070);
|
||||
}
|
||||
|
||||
/**
|
||||
* IsScanPaused returns whether the scan is currently paused.
|
||||
*/
|
||||
export function IsScanPaused(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2930130958);
|
||||
}
|
||||
|
||||
/**
|
||||
* LibraryPath resolves a library's root directory by id.
|
||||
*/
|
||||
export function LibraryPath(id: number): $CancellablePromise<string> {
|
||||
return $Call.ByID(1798172787, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* PauseScan pauses an in-progress scan. Workers block at their
|
||||
* next pause checkpoint until ResumeScan is called.
|
||||
*/
|
||||
export function PauseScan(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3522392788);
|
||||
}
|
||||
|
||||
/**
|
||||
* QueuedLibraryNames returns the display names of libraries waiting
|
||||
* in the scan queue, in FIFO order.
|
||||
*/
|
||||
export function QueuedLibraryNames(): $CancellablePromise<string[] | null> {
|
||||
return $Call.ByID(2036951097);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReleasePipelineLock releases the pipeline mutex after a tag write.
|
||||
*/
|
||||
export function ReleasePipelineLock(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2053843147);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveFromLibrary deletes the database rows for the given file paths
|
||||
* and records each path as excluded, so the next scan does not import
|
||||
* it again. **It does not touch the files on disk** — that is the
|
||||
* promise the confirmation dialog makes, and the reason this operation
|
||||
* is safe to put one keystroke from a focused row.
|
||||
*
|
||||
* The exclusion is not an enhancement. Without it the next scan finds
|
||||
* the file, sees no row, and imports it again — a button that undoes
|
||||
* itself, which is worse than no button.
|
||||
*/
|
||||
export function RemoveFromLibrary(filePaths: string[] | null): $CancellablePromise<$models.RemovalResult | null> {
|
||||
return $Call.ByID(825509452, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveLibrary atomically removes a library and all its data,
|
||||
* performing orphan cleanup, phantom metadata conversion, FTS5
|
||||
* rebuild, and queue compaction. Returns a summary of what was removed.
|
||||
*/
|
||||
export function RemoveLibrary(id: number): $CancellablePromise<$models.RemovalSummary | null> {
|
||||
return $Call.ByID(4110443412, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* RenameLibrary validates and updates a library's display name.
|
||||
*/
|
||||
export function RenameLibrary(id: number, newName: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3949052986, id, newName);
|
||||
}
|
||||
|
||||
/**
|
||||
* RestorePausedScans adopts scans that were paused when the app last
|
||||
* shut down back into the job registry, still paused. Call during
|
||||
* startup before SoftScanAllLibraries so the soft scan does not restart
|
||||
* a library the user deliberately paused.
|
||||
*/
|
||||
export function RestorePausedScans(): $CancellablePromise<void> {
|
||||
return $Call.ByID(4243712755);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResumeScan unblocks a paused scan.
|
||||
*/
|
||||
export function ResumeScan(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3079702539);
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanAllLibraries queries all libraries from the database and queues
|
||||
* each one for scanning. Existing dedup logic ensures no duplicates.
|
||||
*/
|
||||
export function ScanAllLibraries(): $CancellablePromise<void> {
|
||||
return $Call.ByID(266098306);
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanLibrary queues a scan for the library with the given database ID.
|
||||
* If no scan is active the library is scanned immediately; otherwise it
|
||||
* is appended to the queue. Duplicate requests (same library already
|
||||
* scanning or already queued) are silently ignored.
|
||||
*/
|
||||
export function ScanLibrary(id: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2994459743, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchTracks performs an FTS5 full-text search and returns
|
||||
* matching tracks with full metadata.
|
||||
*/
|
||||
export function SearchTracks(query: string): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(3848515709, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchTracksByLibrary performs an FTS5 search scoped to a specific
|
||||
* library and returns matching tracks with full metadata.
|
||||
*/
|
||||
export function SearchTracksByLibrary(query: string, libraryID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(152384327, query, libraryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetJobRegistry wires the background job registry so scans report
|
||||
* progress, logs, and pause/cancel controls to the frontend.
|
||||
*/
|
||||
export function SetJobRegistry(reg: jobs$0.Registry | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(4271773525, reg);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetRemovalHooks provides optional hooks for cross-cutting
|
||||
* orchestration during RemoveLibrary.
|
||||
*/
|
||||
export function SetRemovalHooks(h: $models.RemovalHooks): $CancellablePromise<void> {
|
||||
return $Call.ByID(3190933207, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetRescanHooks provides optional hooks for cross-cutting
|
||||
* orchestration during FullRescan.
|
||||
*/
|
||||
export function SetRescanHooks(h: $models.RescanHooks): $CancellablePromise<void> {
|
||||
return $Call.ByID(1944064333, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetScanHooks provides optional hooks for cross-cutting
|
||||
* orchestration after each library scan.
|
||||
*/
|
||||
export function SetScanHooks(h: $models.ScanHooks): $CancellablePromise<void> {
|
||||
return $Call.ByID(520513414, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* SoftScanAllLibraries performs a lightweight launch-time scan.
|
||||
* First it claims any orphaned tracks (library_id=0) left over from
|
||||
* the pre-multi-library schema. Then for each library it compares
|
||||
* the number of audio files on disk against the track count in the
|
||||
* database. Only libraries where the counts differ (files added or
|
||||
* removed since last scan) are queued for a full scan. Libraries
|
||||
* that are unchanged are silently skipped — no progress bar, no
|
||||
* scan events.
|
||||
*/
|
||||
export function SoftScanAllLibraries(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1401473402);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* Album represents an album for the cover grid display.
|
||||
*
|
||||
* Year is the album's preferred display year — the release-group's
|
||||
* original-release-date (MusicBrainz first-release-date) when known,
|
||||
* falling back to the file-tag year. ReleaseYear is the file-tag
|
||||
* year of the specific release in the library; for a 2010 remaster
|
||||
* of a 1973 album, Year=1973 and ReleaseYear=2010.
|
||||
*/
|
||||
export interface Album {
|
||||
"ID": number;
|
||||
"Name": string;
|
||||
"ArtistName": string;
|
||||
"ArtistMBID": string;
|
||||
"MBID": string;
|
||||
"CoverArtPath": string;
|
||||
"CoverArtSmall": string;
|
||||
"CoverArtMedium": string;
|
||||
"CoverArtLarge": string;
|
||||
"Year": number;
|
||||
"ReleaseYear": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AlbumCompleteness says how much of an album is present, as the files
|
||||
* themselves claim.
|
||||
*
|
||||
* Known is the part that matters: a tag that never declared a total is
|
||||
* not the same as a total that is unmet, and rendering the two alike
|
||||
* would put an "incomplete" mark on most of an untagged library. When
|
||||
* Known is false, Expected means nothing and the caller must say
|
||||
* nothing.
|
||||
*/
|
||||
export interface AlbumCompleteness {
|
||||
"owned": number;
|
||||
"expected": number;
|
||||
"known": boolean;
|
||||
"complete": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Artist represents an artist in the library.
|
||||
*/
|
||||
export interface Artist {
|
||||
"ID": number;
|
||||
"Name": string;
|
||||
"MBID": string;
|
||||
"ImageSmall": string;
|
||||
"ImageMedium": string;
|
||||
"ImageLarge": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GenreWithCount holds a genre name and its associated track count.
|
||||
*/
|
||||
export interface GenreWithCount {
|
||||
"Name": string;
|
||||
"TrackCount": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Info contains library metadata enriched with track count
|
||||
* for the frontend settings UI.
|
||||
*/
|
||||
export interface Info {
|
||||
"id": number;
|
||||
"name": string;
|
||||
"path": string;
|
||||
"trackCount": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovalHooks contains callbacks invoked during library removal.
|
||||
* These break circular dependencies between the library, player,
|
||||
* and queue packages.
|
||||
*/
|
||||
export interface RemovalHooks {
|
||||
/**
|
||||
* StopPlayback stops the currently-playing track.
|
||||
*/
|
||||
"StopPlayback": any;
|
||||
|
||||
/**
|
||||
* CompactQueue reloads queue state after cascade deletes.
|
||||
*/
|
||||
"CompactQueue": any;
|
||||
|
||||
/**
|
||||
* PostRemove runs after the removal commits, for cross-cutting
|
||||
* invalidation (e.g. clearing library-sync "ready" markers).
|
||||
*/
|
||||
"PostRemove": any;
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovalImpact contains pre-removal counts for the confirmation dialog.
|
||||
*/
|
||||
export interface RemovalImpact {
|
||||
"trackCount": number;
|
||||
"playlistsAffected": number;
|
||||
"queueItemCount": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovalResult reports what one RemoveFromLibrary call did.
|
||||
*/
|
||||
export interface RemovalResult {
|
||||
/**
|
||||
* TracksRemoved is how many audio_files rows were deleted. It can
|
||||
* be lower than len(filePaths) if a path was already gone.
|
||||
*/
|
||||
"tracksRemoved": number;
|
||||
|
||||
/**
|
||||
* PathsExcluded is how many paths the scanner will now skip.
|
||||
*/
|
||||
"pathsExcluded": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovalSummary contains post-removal counts for the toast notification.
|
||||
*/
|
||||
export interface RemovalSummary {
|
||||
"tracksDeleted": number;
|
||||
"artistsRemoved": number;
|
||||
"albumsRemoved": number;
|
||||
"genresRemoved": number;
|
||||
"playlistsAffected": number;
|
||||
"queueItemsRemoved": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RescanHooks holds optional callbacks that run before and after
|
||||
* the library-clear-and-scan phase of a full rescan. The app
|
||||
* layer sets these to coordinate cross-cutting concerns (e.g.
|
||||
* clearing the queue, restoring playlists) without the library
|
||||
* needing to know about those packages.
|
||||
*/
|
||||
export interface RescanHooks {
|
||||
/**
|
||||
* PreClear runs before library data is wiped
|
||||
* (e.g. clear queue and stop playback).
|
||||
*/
|
||||
"PreClear": any;
|
||||
|
||||
/**
|
||||
* PostScan runs after the scan completes
|
||||
* (e.g. restore playlists from M3U8 files).
|
||||
*/
|
||||
"PostScan": any;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanHooks contains callbacks invoked after a library scan
|
||||
* completes. The app layer wires these so the library package
|
||||
* does not depend on the playlist package directly.
|
||||
*/
|
||||
export interface ScanHooks {
|
||||
/**
|
||||
* RepopulatePlaylists re-imports tracks for playlists that
|
||||
* lost their playlist_tracks rows (e.g., from a pre-fix
|
||||
* FullRescan). Runs before ResolvePhantoms.
|
||||
*/
|
||||
"RepopulatePlaylists": any;
|
||||
|
||||
/**
|
||||
* ResolvePhantoms re-links phantom playlist tracks whose
|
||||
* files now exist in the library after scanning.
|
||||
*/
|
||||
"ResolvePhantoms": any;
|
||||
|
||||
/**
|
||||
* OnAllScansComplete runs after ALL queued scans finish
|
||||
* (queue drained).
|
||||
*/
|
||||
"OnAllScansComplete": any;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanMetrics holds timing and count data collected during a library scan.
|
||||
* Worker-pool fields are protected by a mutex; DB-writer fields are
|
||||
* single-threaded and use plain addition.
|
||||
*/
|
||||
export interface ScanMetrics {
|
||||
/**
|
||||
* Top-level phases (wall-clock).
|
||||
*/
|
||||
"total": time$0.Duration;
|
||||
"loadExisting": time$0.Duration;
|
||||
"walkDuration": time$0.Duration;
|
||||
"extractionWallClock": time$0.Duration;
|
||||
"dbWritesWallClock": time$0.Duration;
|
||||
"orphanCleanup": time$0.Duration;
|
||||
"postScanVariants": time$0.Duration;
|
||||
|
||||
/**
|
||||
* Per-format extraction (cumulative across workers).
|
||||
*/
|
||||
"formatExtraction": { [_ in string]?: number } | null;
|
||||
"formatCount": { [_ in string]?: number } | null;
|
||||
|
||||
/**
|
||||
* Sub-operation cumulative times (across workers).
|
||||
*/
|
||||
"tagExtraction": time$0.Duration;
|
||||
"durationExtraction": time$0.Duration;
|
||||
|
||||
/**
|
||||
* DB sub-operations (cumulative, single-threaded DB writer).
|
||||
*/
|
||||
"batchCommits": time$0.Duration;
|
||||
"coverArtSave": time$0.Duration;
|
||||
|
||||
/**
|
||||
* Thumbnail generation (async worker pool).
|
||||
*/
|
||||
"thumbnailWallClock": time$0.Duration;
|
||||
"thumbnailGeneration": time$0.Duration;
|
||||
"thumbnailSmall": time$0.Duration;
|
||||
"thumbnailMedium": time$0.Duration;
|
||||
"thumbnailLarge": time$0.Duration;
|
||||
|
||||
/**
|
||||
* Full-rescan-specific phases.
|
||||
*/
|
||||
"clearQueue": time$0.Duration;
|
||||
"clearDatabase": time$0.Duration;
|
||||
"clearCoverFiles": time$0.Duration;
|
||||
|
||||
/**
|
||||
* File counts.
|
||||
*/
|
||||
"added": number;
|
||||
"updated": number;
|
||||
"skipped": number;
|
||||
"removed": number;
|
||||
|
||||
/**
|
||||
* Cancelled is true when the scan was stopped via CancelScan.
|
||||
*/
|
||||
"cancelled": boolean;
|
||||
|
||||
/**
|
||||
* Library identification.
|
||||
* library that was scanned
|
||||
*/
|
||||
"libraryId": number;
|
||||
|
||||
/**
|
||||
* display name of scanned library
|
||||
*/
|
||||
"libraryName": string;
|
||||
|
||||
/**
|
||||
* Non-fatal issues encountered during scanning.
|
||||
*/
|
||||
"warnings": ScanWarning[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanWarning represents a non-fatal issue encountered during scanning.
|
||||
*/
|
||||
export interface ScanWarning {
|
||||
"filePath": string;
|
||||
"phase": string;
|
||||
"err": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track represents a playable audio file in the library.
|
||||
*/
|
||||
export interface Track {
|
||||
"TrackName": string;
|
||||
"ArtistName": string;
|
||||
"TrackLength": string;
|
||||
"FilePath": string;
|
||||
"TrackNumber": number;
|
||||
"DiscNumber": number;
|
||||
"Album": string;
|
||||
"Genre": string[] | null;
|
||||
"Year": number;
|
||||
"Composer": string;
|
||||
"FileType": string;
|
||||
"SampleRate": number;
|
||||
"BitDepth": number;
|
||||
"Channels": number;
|
||||
"Bitrate": number;
|
||||
"FileSize": number;
|
||||
"PlayCount": number;
|
||||
"LastPlayed": string;
|
||||
"RecordingMBID": string;
|
||||
"ArtistMBID": string;
|
||||
"ReleaseGroupMBID": string;
|
||||
"CoverArtPath": string;
|
||||
"CoverArtSmall": string;
|
||||
"CoverArtMedium": string;
|
||||
"CoverArtLarge": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackMBIDs holds MusicBrainz identifiers for a track, resolved
|
||||
* from the recording, release group, and artist tables.
|
||||
*/
|
||||
export interface TrackMBIDs {
|
||||
"recordingMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"artistMbid": string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export type {
|
||||
Handler
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,7 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Handler manages the OS media control integration.
|
||||
*/
|
||||
export type Handler = any;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Player from "./player.js";
|
||||
export {
|
||||
Player
|
||||
};
|
||||
|
||||
export {
|
||||
State,
|
||||
UserVolume
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
TrackInfo
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,61 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* State represents the current playback state.
|
||||
*/
|
||||
export enum State {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Playback state values.
|
||||
*/
|
||||
Playing = "playing",
|
||||
Paused = "paused",
|
||||
Stopped = "stopped",
|
||||
};
|
||||
|
||||
/**
|
||||
* TrackInfo contains metadata and playback state for the currently
|
||||
* loaded track. It is emitted as the payload of the TrackChanged
|
||||
* event and serialized as camelCase JSON to match the frontend
|
||||
* TrackInfo interface in player-store.ts.
|
||||
*/
|
||||
export interface TrackInfo {
|
||||
"fileName": string;
|
||||
"filePath": string;
|
||||
"state": State;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"album": string;
|
||||
"coverArt": string;
|
||||
"coverArtSmall": string;
|
||||
"coverArtMedium": string;
|
||||
"coverArtLarge": string;
|
||||
"trackLength": number;
|
||||
"seekPosition": number;
|
||||
"trackChangeId": number;
|
||||
"artistMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"recordingMbid": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserVolume represents volume on a user-facing scale (0-100).
|
||||
*/
|
||||
export enum UserVolume {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = 0,
|
||||
|
||||
/**
|
||||
* User volume range bounds.
|
||||
*/
|
||||
MinUserVol = 0,
|
||||
MaxUserVol = 100,
|
||||
DefaultUserVol = 50,
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Player handles audio playback and state management.
|
||||
*
|
||||
* Lock ordering: always acquire p.mu BEFORE speaker.Lock().
|
||||
* The beep playback-finished callback dispatches to a new goroutine
|
||||
* so it never holds p.mu while the speaker lock is held.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as mediacontrols$0 from "../mediacontrols/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* ChangeVolume adjusts the volume by a relative amount.
|
||||
*/
|
||||
export function ChangeVolume(deltaVolume: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(742861629, deltaVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* CurrentPosition returns the playback position as a percentage
|
||||
* (0-100).
|
||||
*/
|
||||
export function CurrentPosition(): $CancellablePromise<number> {
|
||||
return $Call.ByID(2723620683);
|
||||
}
|
||||
|
||||
/**
|
||||
* CurrentPositionSeconds returns the current playback position in
|
||||
* display seconds.
|
||||
*/
|
||||
export function CurrentPositionSeconds(): $CancellablePromise<number> {
|
||||
return $Call.ByID(1423359008);
|
||||
}
|
||||
|
||||
/**
|
||||
* EmitCurrentState pushes the current player state to the frontend.
|
||||
* This is intended to be called after the frontend is ready to
|
||||
* receive events, separately from RestoreState which does the heavy
|
||||
* lifting during OnStartup.
|
||||
*/
|
||||
export function EmitCurrentState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2143346946);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCurrentTrackInfo returns information about the currently
|
||||
* loaded track.
|
||||
*/
|
||||
export function GetCurrentTrackInfo(): $CancellablePromise<$models.TrackInfo> {
|
||||
return $Call.ByID(2615351053);
|
||||
}
|
||||
|
||||
/**
|
||||
* InitSpeaker initializes the audio output device. This is
|
||||
* separated from NewPlayer so the player struct can be created
|
||||
* before wails.Run (for binding registration) while deferring
|
||||
* hardware initialization to OnStartup.
|
||||
*/
|
||||
export function InitSpeaker(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3317123670);
|
||||
}
|
||||
|
||||
/**
|
||||
* IsPlaying reports whether the player is currently playing audio.
|
||||
*/
|
||||
export function IsPlaying(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(1219303535);
|
||||
}
|
||||
|
||||
/**
|
||||
* LoadFile opens and decodes an audio file for playback.
|
||||
*/
|
||||
export function LoadFile(filePath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2214941041, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* MuteToggle toggles the mute state.
|
||||
*/
|
||||
export function MuteToggle(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2840614678);
|
||||
}
|
||||
|
||||
/**
|
||||
* Muted reports whether playback is currently silenced.
|
||||
*/
|
||||
export function Muted(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2954670878);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause pauses the current playback.
|
||||
*/
|
||||
export function Pause(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1402758839);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play starts or resumes audio playback.
|
||||
*/
|
||||
export function Play(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3327176373);
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreState loads the persisted player state from the database.
|
||||
*/
|
||||
export function RestoreState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(318202560);
|
||||
}
|
||||
|
||||
/**
|
||||
* SaveState persists the current player state to the database and
|
||||
* waits for the write. This is called during shutdown to capture the
|
||||
* final state, which is the one case that cannot be deferred.
|
||||
*/
|
||||
export function SaveState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3845072215);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek jumps to a specific position in seconds.
|
||||
*/
|
||||
export function Seek(targetSeconds: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(829056707, targetSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetMediaControls provides an OS media controls handler. When set,
|
||||
* the player pushes metadata, playback state, volume, and seek
|
||||
* notifications to the OS media overlay.
|
||||
*/
|
||||
export function SetMediaControls(h: mediacontrols$0.Handler): $CancellablePromise<void> {
|
||||
return $Call.ByID(1896286877, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetPlaybackFinishedHandler sets a callback invoked when a track
|
||||
* finishes naturally. This allows the queue to drive auto-advance
|
||||
* without circular imports.
|
||||
*/
|
||||
export function SetPlaybackFinishedHandler(handler: any): $CancellablePromise<void> {
|
||||
return $Call.ByID(2023377546, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetVolume sets the playback volume (0-100), emits a
|
||||
* VolumeChanged event, and persists the new level.
|
||||
*/
|
||||
export function SetVolume(desiredVolume: $models.UserVolume): $CancellablePromise<void> {
|
||||
return $Call.ByID(1375836663, desiredVolume);
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackLengthInSeconds returns the duration of the current track.
|
||||
*/
|
||||
export function TrackLengthInSeconds(): $CancellablePromise<number> {
|
||||
return $Call.ByID(3749851426);
|
||||
}
|
||||
|
||||
/**
|
||||
* UnloadTrack tears down the current track, releasing the file and
|
||||
* streamer chain. The player returns to the initial "no track
|
||||
* loaded" state and emits events so the frontend clears its
|
||||
* current-track display.
|
||||
*/
|
||||
export function UnloadTrack(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3127425467);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export type {
|
||||
CandidateTrack,
|
||||
DuplicateCheckResult,
|
||||
DuplicateTrackInfo,
|
||||
FavoritesConfigProvider,
|
||||
PhantomMatch,
|
||||
PhantomSearchResult,
|
||||
Summary,
|
||||
Track,
|
||||
WithTracks
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,103 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* CandidateTrack represents a potential library match for a
|
||||
* phantom track.
|
||||
*/
|
||||
export interface CandidateTrack {
|
||||
"FilePath": string;
|
||||
"Title": string;
|
||||
"Artist": string;
|
||||
"Album": string;
|
||||
"Duration": string;
|
||||
"Score": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DuplicateCheckResult contains the outcome of checking for
|
||||
* duplicate tracks in a playlist.
|
||||
*/
|
||||
export interface DuplicateCheckResult {
|
||||
"Duplicates": DuplicateTrackInfo[] | null;
|
||||
"Unique": string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* DuplicateTrackInfo holds metadata for a track that already
|
||||
* exists in a playlist.
|
||||
*/
|
||||
export interface DuplicateTrackInfo {
|
||||
"FilePath": string;
|
||||
"Title": string;
|
||||
"Artist": string;
|
||||
"Album": string;
|
||||
"Duration": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FavoritesConfigProvider is a narrow interface for reading and
|
||||
* writing the default-playlist configuration.
|
||||
*/
|
||||
export type FavoritesConfigProvider = any;
|
||||
|
||||
/**
|
||||
* PhantomMatch represents a high-confidence pairing of a phantom
|
||||
* track to a library track.
|
||||
*/
|
||||
export interface PhantomMatch {
|
||||
"PhantomPath": string;
|
||||
"PhantomTitle": string;
|
||||
"Candidate": CandidateTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* PhantomSearchResult contains auto-matched pairs and remaining
|
||||
* unmatched phantom paths for a batch search operation.
|
||||
*/
|
||||
export interface PhantomSearchResult {
|
||||
"AutoMatched": PhantomMatch[] | null;
|
||||
"Unmatched": string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary is a lightweight representation of a playlist for the
|
||||
* picker UI.
|
||||
*/
|
||||
export interface Summary {
|
||||
"ID": number;
|
||||
"Name": string;
|
||||
"CreatedAt": string;
|
||||
"UpdatedAt": string;
|
||||
"IsSmart": boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track represents a track within a playlist, including its
|
||||
* metadata.
|
||||
*/
|
||||
export interface Track {
|
||||
"ID": number;
|
||||
"Position": number;
|
||||
"FilePath": string;
|
||||
"Title": string;
|
||||
"Artist": string;
|
||||
"Album": string;
|
||||
"CoverArtPath": string;
|
||||
"CoverArtSmall": string;
|
||||
"CoverArtMedium": string;
|
||||
"CoverArtLarge": string;
|
||||
"Duration": string;
|
||||
"Phantom": boolean;
|
||||
"ArtistMBID": string;
|
||||
"ReleaseGroupMBID": string;
|
||||
"RecordingMBID": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WithTracks contains a playlist summary and all its tracks.
|
||||
*/
|
||||
export interface WithTracks {
|
||||
"Summary": Summary;
|
||||
"Tracks": Track[] | null;
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service manages playlist operations.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as library$0 from "../library/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AddToDefaultPlaylist adds multiple tracks to the default
|
||||
* playlist, skipping any that are already present.
|
||||
*/
|
||||
export function AddToDefaultPlaylist(filePaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(1493850977, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* AddTracksToPlaylist adds one or more tracks to an existing
|
||||
* playlist.
|
||||
*/
|
||||
export function AddTracksToPlaylist(playlistID: number, filePaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(2843242868, playlistID, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* CreatePlaylist creates a new empty playlist with the given name.
|
||||
*/
|
||||
export function CreatePlaylist(name: string): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(4080218878, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* CreatePlaylistWithTracks creates a new playlist and populates
|
||||
* it with tracks.
|
||||
*/
|
||||
export function CreatePlaylistWithTracks(name: string, filePaths: string[] | null): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(4079964334, name, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateSmartPlaylist creates a new smart playlist with the given
|
||||
* name and JSON rule set. The rules are validated before storage.
|
||||
*/
|
||||
export function CreateSmartPlaylist(name: string, rulesJSON: string): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(2020825359, name, rulesJSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* DeletePlaylist deletes a playlist and its M3U8 file.
|
||||
* If the deleted playlist was the default, a new default
|
||||
* playlist is automatically created.
|
||||
*/
|
||||
export function DeletePlaylist(playlistID: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(1998239209, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* EnsureDefaultPlaylist verifies the configured default playlist
|
||||
* exists in the database. If the playlist is missing or no ID
|
||||
* has been configured yet, a new playlist named "Favorites" is
|
||||
* created and the config is updated.
|
||||
*/
|
||||
export function EnsureDefaultPlaylist(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3280319649);
|
||||
}
|
||||
|
||||
/**
|
||||
* EvaluateSmartPlaylist loads the rule set for a smart playlist
|
||||
* from the database and evaluates it against the track library,
|
||||
* returning the matching tracks.
|
||||
*/
|
||||
export function EvaluateSmartPlaylist(playlistID: number): $CancellablePromise<library$0.Track[] | null> {
|
||||
return $Call.ByID(2192251014, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* FindDuplicateTracksInPlaylist checks which of the given file
|
||||
* paths already exist in the specified playlist. Returns metadata
|
||||
* for each duplicate and a list of non-duplicate file paths.
|
||||
*/
|
||||
export function FindDuplicateTracksInPlaylist(playlistID: number, filePaths: string[] | null): $CancellablePromise<$models.DuplicateCheckResult> {
|
||||
return $Call.ByID(57159613, playlistID, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* FindPhantomMatches searches the library for matches for the
|
||||
* given phantom file paths. High-confidence matches are returned
|
||||
* as auto-matched pairs; the rest remain in the unmatched list.
|
||||
*/
|
||||
export function FindPhantomMatches(playlistID: number, phantomPaths: string[] | null): $CancellablePromise<$models.PhantomSearchResult> {
|
||||
return $Call.ByID(2112903901, playlistID, phantomPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllPlaylists returns all playlists ordered by most recently
|
||||
* updated.
|
||||
*/
|
||||
export function GetAllPlaylists(): $CancellablePromise<$models.Summary[] | null> {
|
||||
return $Call.ByID(1751004770);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllPlaylistsWithTracks returns all playlists with their
|
||||
* tracks in a single call, merging phantom tracks from M3U8 files.
|
||||
*/
|
||||
export function GetAllPlaylistsWithTracks(): $CancellablePromise<$models.WithTracks[] | null> {
|
||||
return $Call.ByID(1602859466);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultPlaylistInfo returns the ID and name of the default
|
||||
* playlist for display in the frontend.
|
||||
*/
|
||||
export function GetDefaultPlaylistInfo(): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(845855111);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultPlaylistTrackPaths returns the file paths of all
|
||||
* tracks in the default playlist.
|
||||
*/
|
||||
export function GetDefaultPlaylistTrackPaths(): $CancellablePromise<string[] | null> {
|
||||
return $Call.ByID(135237414);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPhantomCandidates returns scored candidate matches for a
|
||||
* single phantom track.
|
||||
*/
|
||||
export function GetPhantomCandidates(playlistID: number, phantomPath: string): $CancellablePromise<$models.CandidateTrack[] | null> {
|
||||
return $Call.ByID(904105323, playlistID, phantomPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPlaylistTracks returns all tracks in a playlist with full
|
||||
* metadata, merging phantom tracks from the M3U8 file.
|
||||
*/
|
||||
export function GetPlaylistTracks(playlistID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(3997481994, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSmartPlaylistRules returns the raw JSON rule string for an
|
||||
* existing smart playlist. This is used when the user opens the
|
||||
* rule editor for an existing smart playlist.
|
||||
*/
|
||||
export function GetSmartPlaylistRules(playlistID: number): $CancellablePromise<string> {
|
||||
return $Call.ByID(997403286, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSmartPlaylistTracks returns the persisted snapshot of a smart
|
||||
* playlist as regular playlist tracks (with resolved cover art and
|
||||
* phantom entries), identical to a normal playlist. If the playlist
|
||||
* has never been materialized — e.g. it predates snapshot support —
|
||||
* it is evaluated and stored on first access.
|
||||
*/
|
||||
export function GetSmartPlaylistTracks(playlistID: number): $CancellablePromise<$models.Track[] | null> {
|
||||
return $Call.ByID(2517005223, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* ImportPlaylist imports a playlist from an external M3U/M3U8
|
||||
* file. It creates a new playlist in the DB, resolves tracks
|
||||
* against the library, and saves an M3U8 file.
|
||||
*/
|
||||
export function ImportPlaylist(filePath: string): $CancellablePromise<$models.Summary> {
|
||||
return $Call.ByID(3295216919, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* ImportPlaylists imports multiple playlists from external M3U/M3U8
|
||||
* files. Each file is imported sequentially using ImportPlaylist.
|
||||
* Errors from individual imports are collected; partial success is
|
||||
* possible. Returns the summaries of successfully imported playlists
|
||||
* and the first error encountered (if any).
|
||||
*/
|
||||
export function ImportPlaylists(filePaths: string[] | null): $CancellablePromise<$models.Summary[] | null> {
|
||||
return $Call.ByID(2505276524, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* MaterializeUnmaterializedSmartPlaylists evaluates and snapshots any
|
||||
* smart playlist that has never been materialized (smart_snapshot_at
|
||||
* IS NULL) — e.g. playlists created before creation-time
|
||||
* materialization existed. It runs once at startup and is idempotent:
|
||||
* once every smart playlist has a snapshot it becomes a no-op. Errors
|
||||
* on individual playlists are logged and skipped so one bad rule set
|
||||
* doesn't block the rest.
|
||||
*/
|
||||
export function MaterializeUnmaterializedSmartPlaylists(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3018316369);
|
||||
}
|
||||
|
||||
/**
|
||||
* PreviewSmartPlaylist evaluates a rule set from raw JSON without
|
||||
* requiring a saved playlist. This powers live preview in the rule
|
||||
* editor — the frontend sends rules as they are being edited and
|
||||
* receives matching tracks immediately.
|
||||
*/
|
||||
export function PreviewSmartPlaylist(rulesJSON: string): $CancellablePromise<library$0.Track[] | null> {
|
||||
return $Call.ByID(3006325439, rulesJSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* RefreshSmartPlaylist re-evaluates a smart playlist's rules against
|
||||
* the current library and replaces its persisted membership in
|
||||
* playlist_tracks with the result. This is the only path that
|
||||
* re-evaluates a smart playlist — opening one otherwise reads the
|
||||
* stored snapshot. Triggered on rule save and by the manual Refresh
|
||||
* button.
|
||||
*/
|
||||
export function RefreshSmartPlaylist(playlistID: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2225480718, playlistID);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveFromDefaultPlaylist removes multiple tracks from the
|
||||
* default playlist.
|
||||
*/
|
||||
export function RemoveFromDefaultPlaylist(filePaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(619411935, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovePhantomTracks removes phantom entries from a playlist's
|
||||
* M3U8 file. Since phantom tracks have no DB rows, only the
|
||||
* M3U8 file is modified.
|
||||
*/
|
||||
export function RemovePhantomTracks(playlistID: number, phantomPaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(823798913, playlistID, phantomPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveTracksFromPlaylist removes multiple tracks from a playlist
|
||||
* by their playlist_track IDs.
|
||||
*/
|
||||
export function RemoveTracksFromPlaylist(playlistID: number, trackIDs: number[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(2519855654, playlistID, trackIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* RenamePlaylist renames a playlist and updates its M3U8 file.
|
||||
*/
|
||||
export function RenamePlaylist(playlistID: number, newName: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1835045264, playlistID, newName);
|
||||
}
|
||||
|
||||
/**
|
||||
* RepopulateFromM3U re-imports tracks for playlists that have zero
|
||||
* playlist_tracks rows but still have a corresponding M3U8 file.
|
||||
* This recovers from a FullRescan that deleted playlist tracks
|
||||
* before the ON DELETE SET NULL fix was in place. Each M3U8 entry
|
||||
* is resolved against the audio_files table; unresolved entries
|
||||
* become phantom tracks with metadata preserved from the M3U8.
|
||||
*/
|
||||
export function RepopulateFromM3U(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3399290718);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolvePhantomTracks replaces phantom entries in a playlist
|
||||
* with real library tracks. The matches map keys are phantom
|
||||
* absolute paths and values are resolved absolute paths.
|
||||
*/
|
||||
export function ResolvePhantomTracks(playlistID: number, matches: { [_ in string]?: string } | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(1239473037, playlistID, matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolvePhantomTracksAfterScan re-links phantom playlist tracks
|
||||
* whose files now exist in the library. It iterates each playlist
|
||||
* that has phantoms, reads its M3U8 file, resolves each entry
|
||||
* against the current audio_files table using multi-root path
|
||||
* resolution, and updates matching phantom playlist_tracks. This
|
||||
* handles both pre-existing phantoms (created before migration 7,
|
||||
* with NULL phantom_file_path) and new ones.
|
||||
*/
|
||||
export function ResolvePhantomTracksAfterScan(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3836161100);
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreAllPlaylists restores playlist tracks from M3U8 files.
|
||||
* This is called after a full library rescan to repopulate
|
||||
* playlist_tracks from the surviving M3U8 files.
|
||||
*/
|
||||
export function RestoreAllPlaylists(): $CancellablePromise<void> {
|
||||
return $Call.ByID(453794624);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchLibrary searches the entire library by a free-text query
|
||||
* for manual phantom resolution.
|
||||
*/
|
||||
export function SearchLibrary(query: string): $CancellablePromise<$models.CandidateTrack[] | null> {
|
||||
return $Call.ByID(3912116995, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetFavoritesConfig sets the provider used to read and write
|
||||
* the default-playlist configuration.
|
||||
*/
|
||||
export function SetFavoritesConfig(provider: $models.FavoritesConfigProvider): $CancellablePromise<void> {
|
||||
return $Call.ByID(2117418507, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* ToggleDefaultPlaylistTrack adds or removes a single track
|
||||
* from the default playlist. Returns true if the track is now
|
||||
* in the playlist (was added), false if it was removed.
|
||||
*/
|
||||
export function ToggleDefaultPlaylistTrack(filePath: string): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(450278306, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateSmartPlaylistRules updates the rule set for an existing
|
||||
* smart playlist. Returns an error if the playlist does not exist
|
||||
* or is not a smart playlist.
|
||||
*/
|
||||
export function UpdateSmartPlaylistRules(playlistID: number, rulesJSON: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2746792411, playlistID, rulesJSON);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Queue from "./queue.js";
|
||||
export {
|
||||
Queue
|
||||
};
|
||||
|
||||
export {
|
||||
RepeatMode
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
FallbackSource,
|
||||
Source,
|
||||
State,
|
||||
Track,
|
||||
TrackLoader
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,72 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* FallbackSource resolves what should auto-play, if anything, once the
|
||||
* queue is exhausted. Implemented outside this package (see app.go) so
|
||||
* the queue does not need to know about config, playlists or
|
||||
* similarity data.
|
||||
*/
|
||||
export type FallbackSource = any;
|
||||
|
||||
/**
|
||||
* RepeatMode represents the queue repeat behavior.
|
||||
*/
|
||||
export enum RepeatMode {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Repeat mode values.
|
||||
*/
|
||||
RepeatOff = "off",
|
||||
RepeatAll = "all",
|
||||
RepeatOne = "one",
|
||||
};
|
||||
|
||||
/**
|
||||
* Source describes the collection a queue was built from — an album, a
|
||||
* playlist, a genre, an artist — so the frontend can offer to navigate
|
||||
* back to it. An empty Type means the queue has no single source (the
|
||||
* whole library, or one ad-hoc track).
|
||||
*/
|
||||
export interface Source {
|
||||
"type": string;
|
||||
"id": number;
|
||||
"label": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State is the full state emitted to the frontend.
|
||||
*/
|
||||
export interface State {
|
||||
"tracks": Track[] | null;
|
||||
"currentIndex": number;
|
||||
"shuffleMode": boolean;
|
||||
"repeatMode": RepeatMode;
|
||||
"source": Source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track represents a track in the queue with its metadata.
|
||||
*/
|
||||
export interface Track {
|
||||
"id": number;
|
||||
"audioFileId": number;
|
||||
"filePath": string;
|
||||
"position": number;
|
||||
"title": string;
|
||||
"artist": string;
|
||||
"album": string;
|
||||
"coverArtPath": string;
|
||||
"artistMbid": string;
|
||||
"releaseGroupMbid": string;
|
||||
"recordingMbid": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackLoader is the interface the queue uses to tell the player to load a file.
|
||||
*/
|
||||
export type TrackLoader = any;
|
||||
@@ -0,0 +1,219 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Queue manages an ordered list of tracks for playback.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* AddTrack appends a track to the end of the queue.
|
||||
* If the queue was empty, it loads the added track in a paused state.
|
||||
*/
|
||||
export function AddTrack(filePath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3566864127, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* AddTracks appends multiple tracks to the end of the queue.
|
||||
* If the queue was empty, it loads the first added track in a paused state.
|
||||
*/
|
||||
export function AddTracks(filePaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(980962916, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear removes all tracks from the queue, stops playback, and
|
||||
* resets the queue state. It persists the cleared state and
|
||||
* notifies the frontend.
|
||||
*/
|
||||
export function Clear(): $CancellablePromise<void> {
|
||||
return $Call.ByID(4166243766);
|
||||
}
|
||||
|
||||
/**
|
||||
* CompactAfterLibraryRemoval reloads queue state from the database
|
||||
* after a library removal has cascade-deleted queue_tracks rows.
|
||||
* It resets currentIndex to 0 (or -1 if empty), clears shuffleOrder,
|
||||
* unloads the current track if it was removed, and emits QueueChanged.
|
||||
*/
|
||||
export function CompactAfterLibraryRemoval(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3620379645);
|
||||
}
|
||||
|
||||
/**
|
||||
* CycleRepeat cycles through repeat modes: off -> all -> one -> off.
|
||||
*/
|
||||
export function CycleRepeat(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3510519482);
|
||||
}
|
||||
|
||||
/**
|
||||
* EmitCurrentState emits the current queue state to the frontend.
|
||||
* This is called after the frontend DOM is ready.
|
||||
*/
|
||||
export function EmitCurrentState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1245022280);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetState returns the current queue state for the frontend.
|
||||
*/
|
||||
export function GetState(): $CancellablePromise<$models.State> {
|
||||
return $Call.ByID(3472178852);
|
||||
}
|
||||
|
||||
/**
|
||||
* InsertNext inserts a track right after the currently playing track.
|
||||
* If the queue was empty, it loads the inserted track in a paused state.
|
||||
*/
|
||||
export function InsertNext(filePath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1498463573, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* InsertNextTracks inserts multiple tracks as a contiguous block after the current track.
|
||||
* If the queue was empty, it loads the first inserted track in a paused state.
|
||||
*/
|
||||
export function InsertNextTracks(filePaths: string[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(2824705517, filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* InsertTracksAt inserts multiple tracks at the given index.
|
||||
* If the queue was empty, it loads the first inserted track in a paused state.
|
||||
*/
|
||||
export function InsertTracksAt(filePaths: string[] | null, index: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2550945669, filePaths, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* MoveQueueTracks moves tracks at the given indices to a new position
|
||||
* as a contiguous block. The toIndex is the target position in the
|
||||
* original (pre-move) array.
|
||||
*/
|
||||
export function MoveQueueTracks(fromIndices: number[] | null, toIndex: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(2149753655, fromIndices, toIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Next advances to the next track. If the player was paused, the next
|
||||
* track is loaded but not played. In RepeatOne mode, the current track
|
||||
* is replayed instead of advancing.
|
||||
*/
|
||||
export function Next(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1968784044);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnPlaybackFinished is called when a track finishes playing naturally.
|
||||
* This drives the auto-advance behavior and records the play.
|
||||
*/
|
||||
export function OnPlaybackFinished(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2184869763);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play handles a play request by either resuming the current track or
|
||||
* starting playback from the beginning of the queue. When a track is
|
||||
* already active (currentIndex != -1) the player is told to resume;
|
||||
* otherwise playback starts from the first track (or a random one when
|
||||
* shuffle is enabled).
|
||||
*/
|
||||
export function Play(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3197379127);
|
||||
}
|
||||
|
||||
/**
|
||||
* PlayIndex jumps to and plays the track at the given index.
|
||||
*/
|
||||
export function PlayIndex(index: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(200440797, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous goes to the previous track (or restarts current if >3s in).
|
||||
* If the player was paused, the track is loaded but not played.
|
||||
* In RepeatOne mode, the current track is replayed instead of navigating.
|
||||
*/
|
||||
export function Previous(): $CancellablePromise<void> {
|
||||
return $Call.ByID(708701476);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveTrack removes a track at the given position from the queue.
|
||||
*/
|
||||
export function RemoveTrack(position: number): $CancellablePromise<void> {
|
||||
return $Call.ByID(6811080, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemoveTracks removes multiple tracks at the given positions from the queue.
|
||||
* Positions are deduplicated, validated, and removed in descending order so
|
||||
* that indices remain stable during removal.
|
||||
*/
|
||||
export function RemoveTracks(positions: number[] | null): $CancellablePromise<void> {
|
||||
return $Call.ByID(1587232097, positions);
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreState loads the queue state from the database.
|
||||
*/
|
||||
export function RestoreState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1652105446);
|
||||
}
|
||||
|
||||
/**
|
||||
* SaveState persists the queue state to the database. Unlike every
|
||||
* other write here it waits for the writer: its callers are shutdown
|
||||
* and the tests, both of which need the row to exist on return.
|
||||
*/
|
||||
export function SaveState(): $CancellablePromise<void> {
|
||||
return $Call.ByID(2913531465);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetFallbackSource provides the queue with what to auto-play, if
|
||||
* anything, once it runs out. A nil source (the default) leaves
|
||||
* today's behavior: the queue just goes idle.
|
||||
*/
|
||||
export function SetFallbackSource(fs: $models.FallbackSource): $CancellablePromise<void> {
|
||||
return $Call.ByID(1344903240, fs);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetPlayer provides the queue with a reference to the player for auto-advance.
|
||||
*/
|
||||
export function SetPlayer(player: $models.TrackLoader): $CancellablePromise<void> {
|
||||
return $Call.ByID(2806250342, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* SetQueue replaces the entire queue with new tracks and starts playing.
|
||||
* When shuffleStart is true and shuffle mode is active, a random first
|
||||
* track is chosen instead of the one at startIndex. This is intended for
|
||||
* "Play All" type actions where no specific track was selected.
|
||||
* It uses a two-phase approach: the first batch of tracks (up to
|
||||
* initialBatchSize) is resolved immediately so playback begins and the
|
||||
* queue panel is populated without delay. The remaining tracks are then
|
||||
* resolved in the background. A generation counter ensures stale
|
||||
* background work is discarded if SetQueue is called again.
|
||||
*/
|
||||
export function SetQueue(filePaths: string[] | null, startIndex: number, shuffleStart: boolean, source: $models.Source): $CancellablePromise<void> {
|
||||
return $Call.ByID(1990450094, filePaths, startIndex, shuffleStart, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* ToggleShuffle toggles shuffle mode on/off.
|
||||
*/
|
||||
export function ToggleShuffle(): $CancellablePromise<void> {
|
||||
return $Call.ByID(612647562);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as TagWriter from "./tagwriter.js";
|
||||
export {
|
||||
TagWriter
|
||||
};
|
||||
|
||||
export type {
|
||||
BatchFailure,
|
||||
BatchResult,
|
||||
TagChanges
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,28 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* BatchFailure records a single track that failed during a batch write.
|
||||
*/
|
||||
export interface BatchFailure {
|
||||
"filePath": string;
|
||||
"error": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* BatchResult summarises the outcome of a batch tag write.
|
||||
*/
|
||||
export interface BatchResult {
|
||||
"total": number;
|
||||
"succeeded": number;
|
||||
"failed": number;
|
||||
"cancelled": boolean;
|
||||
"failures": BatchFailure[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TagChanges is a diff map of field name → new value. Only changed
|
||||
* fields are present. Callers specify changed fields; unchanged
|
||||
* fields are left as-is in the file.
|
||||
*/
|
||||
export type TagChanges = { [_ in string]?: any } | null;
|
||||
@@ -0,0 +1,80 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* TagWriter orchestrates the complete tag writing pipeline:
|
||||
* file write → DB sync → event emission.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* BatchWriteTrackTags applies the same TagChanges to every file in
|
||||
* filePaths. It processes tracks sequentially, emits a
|
||||
* BatchWriteProgress event after each track, and continues past
|
||||
* individual failures. Returns a BatchResult summarising outcomes.
|
||||
*/
|
||||
export function BatchWriteTrackTags(filePaths: string[] | null, changes: $models.TagChanges): $CancellablePromise<$models.BatchResult> {
|
||||
return $Call.ByID(2772510056, filePaths, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelBatchWrite signals the in-progress batch write to stop after
|
||||
* the current track completes.
|
||||
*/
|
||||
export function CancelBatchWrite(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3075727604);
|
||||
}
|
||||
|
||||
/**
|
||||
* WriteTrackTags is the single entry point for writing metadata to
|
||||
* a track's audio file and synchronising all changes to the
|
||||
* database. It accepts a track ID (audio_file.id) and a diff map
|
||||
* of changed fields.
|
||||
*
|
||||
* The pipeline:
|
||||
* 1. Look up track from DB.
|
||||
* 2. Detect audio format.
|
||||
* 3. Acquire pipeline lock (mutual exclusion with scan).
|
||||
* 4. Stop player if this file is currently playing.
|
||||
* 5. Write tags to file (format-specific writer).
|
||||
* 6. Sync database (entity relink, FTS5, orphan cleanup).
|
||||
* 7. Emit TrackMetadataChanged event.
|
||||
*/
|
||||
export function WriteTrackTags(trackID: number, changes: $models.TagChanges): $CancellablePromise<void> {
|
||||
return $Call.ByID(1881385878, trackID, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* WriteTrackTagsByPath resolves a file path to its audio_file.id and
|
||||
* delegates to WriteTrackTags. This is the frontend-facing entry
|
||||
* point since the frontend identifies tracks by FilePath.
|
||||
*/
|
||||
export function WriteTrackTagsByPath(filePath: string, changes: $models.TagChanges): $CancellablePromise<void> {
|
||||
return $Call.ByID(3526432184, filePath, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* WriteUntrackedFileTags writes tags to a file that is not in the
|
||||
* library, skipping every step of the full pipeline that assumes it is:
|
||||
* no audio_file lookup, no database sync, no event.
|
||||
*
|
||||
* This exists for the download import path, which tags files while they
|
||||
* are still in the staging directory. Tagging before the move is what
|
||||
* makes the import atomic from the library's point of view — the
|
||||
* scanner only ever sees a finished, correctly tagged file, instead of
|
||||
* ingesting a mislabelled one and being corrected afterwards.
|
||||
*
|
||||
* Callers are responsible for ensuring the file is not in a library
|
||||
* path; using this on a tracked file would leave the database stale.
|
||||
*/
|
||||
export function WriteUntrackedFileTags(filePath: string, changes: $models.TagChanges): $CancellablePromise<void> {
|
||||
return $Call.ByID(3625602876, filePath, changes);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
ColumnID
|
||||
} from "./models.js";
|
||||
|
||||
export type {
|
||||
Column
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,41 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Column represents a visible column in the track list.
|
||||
*/
|
||||
export interface Column {
|
||||
"id": ColumnID;
|
||||
}
|
||||
|
||||
/**
|
||||
* ColumnID identifies a displayable column in the track list.
|
||||
*/
|
||||
export enum ColumnID {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Valid column identifiers.
|
||||
*/
|
||||
ColTrackName = "trackName",
|
||||
ColArtistName = "artistName",
|
||||
ColTrackLength = "trackLength",
|
||||
ColAlbum = "album",
|
||||
ColGenre = "genre",
|
||||
ColYear = "year",
|
||||
ColComposer = "composer",
|
||||
ColTrackNumber = "trackNumber",
|
||||
ColDiscNumber = "discNumber",
|
||||
ColFilePath = "filePath",
|
||||
ColFileType = "fileType",
|
||||
ColSampleRate = "sampleRate",
|
||||
ColBitDepth = "bitDepth",
|
||||
ColChannels = "channels",
|
||||
ColBitrate = "bitrate",
|
||||
ColFileSize = "fileSize",
|
||||
ColPlayCount = "playCount",
|
||||
ColAlbumArt = "albumArt",
|
||||
};
|
||||
+3
-3
@@ -39,9 +39,9 @@ import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
|
||||
import { registerBundledIcons } from './src/icons';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
import { searchStore } from '@store/search-store';
|
||||
import * as Player from '@go/player/Player';
|
||||
import * as Queue from '@go/queue/Queue';
|
||||
import { GetDefaultPage } from '@go/config/Config';
|
||||
import * as Player from '@go/player/player.js';
|
||||
import * as Queue from '@go/queue/queue.js';
|
||||
import { GetDefaultPage } from '@go/config/config.js';
|
||||
// Importing the theme store triggers initialization: it fetches the saved
|
||||
// theme from the backend and applies CSS custom properties to :root.
|
||||
import '@store/theme-store';
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@awesome.me/webawesome": "^3.2.1",
|
||||
"@lit-labs/signals": "^0.2.0",
|
||||
"@lit-labs/virtualizer": "^2.1.1",
|
||||
"@wailsio/runtime": "3.0.0-beta.8",
|
||||
"lit": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+8
@@ -17,6 +17,9 @@ importers:
|
||||
'@lit-labs/virtualizer':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
'@wailsio/runtime':
|
||||
specifier: 3.0.0-beta.8
|
||||
version: 3.0.0-beta.8
|
||||
lit:
|
||||
specifier: ^3.2.1
|
||||
version: 3.3.2
|
||||
@@ -543,6 +546,9 @@ packages:
|
||||
'@vscode/web-custom-data@0.4.13':
|
||||
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
|
||||
|
||||
'@wailsio/runtime@3.0.0-beta.8':
|
||||
resolution: {integrity: sha512-c9PZJcOR9z1a6cxtBS2q5cygNajlxDE8Oxv/vvcTFYRbZSoOGBszq4uzlkPTOd0Bot1xkM81jnpaBgKWRpBh2g==}
|
||||
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
@@ -1734,6 +1740,8 @@ snapshots:
|
||||
|
||||
'@vscode/web-custom-data@0.4.13': {}
|
||||
|
||||
'@wailsio/runtime@3.0.0-beta.8': {}
|
||||
|
||||
ajv@8.18.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
property,
|
||||
state,
|
||||
} from 'lit/decorators.js';
|
||||
import { library } from '@go/models';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import {
|
||||
GetArtistImageURL,
|
||||
GetArtistImageCachedPath,
|
||||
GetArtistMBID,
|
||||
} from '@go/explore/Service';
|
||||
} from '@go/explore/service.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@components/cover-grid/cover-grid.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
GetAlbumsByArtist,
|
||||
GetAlbumsByArtistByLibrary,
|
||||
GetFilePathsByAlbums,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
@@ -36,6 +36,7 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
import { dict, list } from '@utils/binding';
|
||||
|
||||
/** Pixels to change card width per scroll tick. */
|
||||
const ZOOM_STEP = 16;
|
||||
@@ -1055,18 +1056,21 @@ export class ArtistsView
|
||||
const libId =
|
||||
this.libraryCtrl.selectedLibraryId;
|
||||
|
||||
const albums = libId !== null
|
||||
? await GetAlbumsByArtistByLibrary(
|
||||
artist.ID,
|
||||
libId,
|
||||
)
|
||||
: await GetAlbumsByArtist(artist.ID);
|
||||
const albums = await list(
|
||||
libId !== null
|
||||
? GetAlbumsByArtistByLibrary(
|
||||
artist.ID,
|
||||
libId,
|
||||
)
|
||||
: GetAlbumsByArtist(artist.ID),
|
||||
);
|
||||
|
||||
const byAlbum =
|
||||
await GetFilePathsByAlbums(
|
||||
const byAlbum = await dict(
|
||||
GetFilePathsByAlbums(
|
||||
albums.map((a) => a.ID),
|
||||
libId ?? 0,
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
const allPaths: string[] = [];
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
ClearCompletedEntries,
|
||||
SearchCandidates,
|
||||
SelectSearchCandidate,
|
||||
} from '@go/autotagservice/Service';
|
||||
import type { autotagservice } from '@go/models';
|
||||
} from '@go/autotagservice/service.js';
|
||||
import type * as autotagservice from '@go/autotagservice/models.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { inlineDiff, normalizeStrict, isCosmeticDiff } from '../../utils/text-diff';
|
||||
@@ -29,6 +29,7 @@ import { nameDialogsIn } from '../../utils/name-dialog';
|
||||
import { ViewLifecycleMixin } from '../../utils/view-lifecycle';
|
||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
type PendingItem = autotagservice.PendingItem;
|
||||
type ScoreView = autotagservice.ScoreView;
|
||||
@@ -1483,8 +1484,10 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
this.searchError = '';
|
||||
this.searchRan = true;
|
||||
try {
|
||||
this.searchResults = await SearchCandidates(
|
||||
this.searchKind, query, this.searchArtist.trim(),
|
||||
this.searchResults = await list(
|
||||
SearchCandidates(
|
||||
this.searchKind, query, this.searchArtist.trim(),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('autotag: candidate search failed', err);
|
||||
@@ -1755,12 +1758,12 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
}
|
||||
|
||||
private topScore(): number {
|
||||
if (!this.score || this.score.candidates.length === 0) return 0;
|
||||
return this.score.candidates[0]?.score ?? 0;
|
||||
if (!this.score || (this.score.candidates ?? []).length === 0) return 0;
|
||||
return (this.score.candidates ?? [])[0]?.score ?? 0;
|
||||
}
|
||||
|
||||
private async onApply(): Promise<void> {
|
||||
if (!this.current || !this.score || this.score.candidates.length === 0) return;
|
||||
if (!this.current || !this.score || (this.score.candidates ?? []).length === 0) return;
|
||||
if (!this.hasLibraryWarningBeenAcked(this.current.libraryId)) {
|
||||
await this.confirmWarningThenApply();
|
||||
|
||||
@@ -1773,7 +1776,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
private async executeApply(): Promise<void> {
|
||||
if (!this.current || !this.score) return;
|
||||
|
||||
const cand = this.score.candidates[this.selectedCandidateIdx];
|
||||
const cand = (this.score.candidates ?? [])[this.selectedCandidateIdx];
|
||||
if (!cand) {
|
||||
this.errorMessage = 'No candidate selected.';
|
||||
return;
|
||||
@@ -1781,7 +1784,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
|
||||
const groupKey = this.current.groupKey;
|
||||
const mbid = cand.releaseMbid || cand.releaseGroupMbid;
|
||||
const total = this.score.localTracks.length;
|
||||
const total = (this.score.localTracks ?? []).length;
|
||||
|
||||
// Pre-mark the folder as running so the sidebar icon flips
|
||||
// to the progress ring immediately — the Started event will
|
||||
@@ -1955,16 +1958,16 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
*/
|
||||
private currentCandidate(): CandidateView | null {
|
||||
if (!this.score) return null;
|
||||
return this.score.candidates[this.selectedCandidateIdx] ?? null;
|
||||
return (this.score.candidates ?? [])[this.selectedCandidateIdx] ?? null;
|
||||
}
|
||||
|
||||
private async selectCandidateByIdx(idx: number): Promise<void> {
|
||||
if (!this.score || idx < 0 || idx >= this.score.candidates.length) return;
|
||||
if (!this.score || idx < 0 || idx >= (this.score.candidates ?? []).length) return;
|
||||
this.selectedCandidateIdx = idx;
|
||||
// Lazy-load cover art for non-top candidates. The backend
|
||||
// populates art for the top one eagerly; everything else
|
||||
// arrives empty until the user picks it.
|
||||
const cand = this.score.candidates[idx]!;
|
||||
const cand = (this.score.candidates ?? [])[idx]!;
|
||||
if (!cand.coverArtUrl) {
|
||||
try {
|
||||
const url = await GetCandidateCoverArt(
|
||||
@@ -2252,7 +2255,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
if (!cluster || cluster.candidates.length <= 1) return nothing;
|
||||
|
||||
const editions = cluster.candidates
|
||||
.map((c) => ({ cand: c, idx: this.score?.candidates.indexOf(c) ?? -1 }))
|
||||
.map((c) => ({ cand: c, idx: (this.score?.candidates ?? []).indexOf(c) ?? -1 }))
|
||||
.filter((e) => e.idx >= 0);
|
||||
|
||||
return html`
|
||||
@@ -2316,11 +2319,11 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
// the score forgives, the UI mutes.
|
||||
const SUBTLE_LENGTH_MAX_MS = 5000;
|
||||
|
||||
for (const a of cand.alignments) {
|
||||
for (const a of (cand.alignments ?? [])) {
|
||||
if (a.status === 'matched' || a.status === 'mismatched') {
|
||||
paired++;
|
||||
const local = a.localIndex >= 0
|
||||
? this.score?.localTracks[a.localIndex] ?? null
|
||||
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||
: null;
|
||||
if (local) {
|
||||
if (local.title !== a.candidateTitle) {
|
||||
@@ -2374,7 +2377,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
missingTitles.push(a.candidateTitle || '(untitled)');
|
||||
} else if (a.status === 'unmatched') {
|
||||
const local = a.localIndex >= 0
|
||||
? this.score?.localTracks[a.localIndex] ?? null
|
||||
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||
: null;
|
||||
extraTitles.push(a.localTitle || local?.title || '(untitled)');
|
||||
}
|
||||
@@ -2659,7 +2662,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
}
|
||||
|
||||
private renderLowConfidenceBanner(clusters: VersionCluster[]) {
|
||||
const top = this.score?.candidates[0];
|
||||
const top = (this.score?.candidates ?? [])[0];
|
||||
if (!top) return nothing;
|
||||
if (top.score >= LOW_CONFIDENCE_THRESHOLD) return nothing;
|
||||
if (clusters.length < 2) return nothing;
|
||||
@@ -2695,18 +2698,18 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
* renderMatchDetails, so each column shows its own values plainly.
|
||||
*/
|
||||
private renderComparison(cand: CandidateView, clusters: VersionCluster[] = []) {
|
||||
const locals = this.score?.localTracks ?? [];
|
||||
const locals = (this.score?.localTracks ?? []) ?? [];
|
||||
|
||||
// localIndex -> its alignment, so a folder row knows whether it
|
||||
// paired and (if so) how confidently.
|
||||
const alignByLocal = new Map<number, AlignmentView>();
|
||||
for (const a of cand.alignments) {
|
||||
for (const a of (cand.alignments ?? [])) {
|
||||
if (a.localIndex >= 0) alignByLocal.set(a.localIndex, a);
|
||||
}
|
||||
|
||||
// Candidate side: every alignment that has a candidate track
|
||||
// (paired or missing-from-folder), in candidate order.
|
||||
const candRows = cand.alignments
|
||||
const candRows = (cand.alignments ?? [])
|
||||
.filter((a) => a.status !== 'unmatched' && a.candidatePosition > 0)
|
||||
.slice()
|
||||
.sort((x, y) =>
|
||||
@@ -2856,7 +2859,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
${clusters.map((cluster) => {
|
||||
const best = cluster.candidates[cluster.bestIdx]!;
|
||||
const active = cluster.candidates.includes(activeCand);
|
||||
const idx = this.score?.candidates.indexOf(best) ?? -1;
|
||||
const idx = (this.score?.candidates ?? []).indexOf(best) ?? -1;
|
||||
return html`
|
||||
<div class="cand-chip ${active ? 'selected' : ''}"
|
||||
title=${cluster.label}
|
||||
@@ -2949,7 +2952,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.score || this.score.candidates.length === 0) {
|
||||
if (!this.score || (this.score.candidates ?? []).length === 0) {
|
||||
return html`
|
||||
<div class="main">
|
||||
<div class="empty">
|
||||
@@ -2965,7 +2968,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
return html`<div class="main"><div class="empty">Candidate index out of range.</div></div>`;
|
||||
}
|
||||
|
||||
const clusters = this.clusterVersions(this.score.candidates);
|
||||
const clusters = this.clusterVersions((this.score.candidates ?? []));
|
||||
|
||||
return html`
|
||||
<div class="main">
|
||||
|
||||
@@ -2,14 +2,14 @@ import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import type { explore } from '@go/models';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
import {
|
||||
AddLibrary,
|
||||
RenameLibrary,
|
||||
RemoveLibrary,
|
||||
GetRemovalImpact,
|
||||
GetAllLibrariesWithTrackCounts,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import {
|
||||
GetScanConcurrency,
|
||||
SetScanConcurrency,
|
||||
@@ -17,17 +17,17 @@ import {
|
||||
SetDefaultPage,
|
||||
GetQueueFallback,
|
||||
SetQueueFallback,
|
||||
} from '@go/config/Config';
|
||||
import { GetIndexStatus } from '@go/explore/Service';
|
||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||
} from '@go/config/config.js';
|
||||
import { GetIndexStatus } from '@go/explore/service.js';
|
||||
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||
import { notificationStore } from '@store/notification-store';
|
||||
import { describeError, explainError } from '@utils/describe-error';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { ThemeController } from '@store/controllers/theme-controller';
|
||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { GetAllPlaylists } from '@go/playlist/Service';
|
||||
import type { playlist } from '@go/models';
|
||||
import { GetAllPlaylists } from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { Events } from '../../events';
|
||||
import {
|
||||
SHORTCUT_CATEGORIES,
|
||||
@@ -49,6 +49,7 @@ import './shortcut-capture';
|
||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||
import { shortcutsStore } from '../../store/shortcuts-store';
|
||||
import { ShortcutsController } from '../../store/controllers/shortcuts-controller';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
const SCROLL_STORAGE_KEY = 'yj-now-playing-scroll-mode';
|
||||
const SCROLL_CHANGE_EVENT = 'yj-scroll-mode-changed';
|
||||
@@ -1012,9 +1013,9 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
const ok = await confirmAction({
|
||||
title: 'Remove library',
|
||||
message: `Remove “${libName}”?`,
|
||||
impact: `This deletes ${impact.trackCount} tracks, affects `
|
||||
+ `${impact.playlistsAffected} playlists and removes `
|
||||
+ `${impact.queueItemCount} queue items.`,
|
||||
impact: `This deletes ${impact?.trackCount ?? 0} tracks, affects `
|
||||
+ `${impact?.playlistsAffected ?? 0} playlists and removes `
|
||||
+ `${impact?.queueItemCount ?? 0} queue items.`,
|
||||
confirmLabel: 'Remove',
|
||||
danger: true,
|
||||
});
|
||||
@@ -1187,8 +1188,7 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
|
||||
private async loadPlaylists(): Promise<void> {
|
||||
try {
|
||||
this.playlists =
|
||||
await GetAllPlaylists();
|
||||
this.playlists = await list(GetAllPlaylists());
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to load playlists:',
|
||||
@@ -1456,10 +1456,10 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
<span class="index-stat">updated ${this.timeAgo(s.lastBuilt)}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${s.tiers?.length > 0 && s.tiers.some((t) => t.state === 'running' || t.state === 'pending' || t.state === 'error')
|
||||
${(s.tiers?.length ?? 0) > 0 && (s.tiers ?? []).some((t) => t.state === 'running' || t.state === 'pending' || t.state === 'error')
|
||||
? html`
|
||||
<div class="index-tiers">
|
||||
${s.tiers.map(
|
||||
${(s.tiers ?? []).map(
|
||||
(t) => html`
|
||||
<div class="index-tier">
|
||||
<span class="tier-icon">${this.tierIcon(t.state)}</span>
|
||||
|
||||
@@ -14,10 +14,12 @@ import type {
|
||||
ProviderField,
|
||||
} from '@store/download-store';
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/Config';
|
||||
import { SetPreferences } from '@go/download/Service';
|
||||
import type { download } from '@go/models';
|
||||
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/config.js';
|
||||
import { SetPreferences } from '@go/download/service.js';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import { Format } from '@go/download/models.js';
|
||||
import { compact } from '@utils/binding';
|
||||
import { describeError, explainError } from '@utils/describe-error';
|
||||
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
|
||||
import './config-section';
|
||||
@@ -28,15 +30,15 @@ import './config-section';
|
||||
* deliberately excluded — it names "no format detected", not a format a
|
||||
* user could opt into.
|
||||
*/
|
||||
const AUTO_DOWNLOAD_FORMATS: { value: string; label: string }[] = [
|
||||
{ value: 'flac', label: 'FLAC' },
|
||||
{ value: 'alac', label: 'ALAC' },
|
||||
{ value: 'wav', label: 'WAV' },
|
||||
{ value: 'mp3', label: 'MP3' },
|
||||
{ value: 'aac', label: 'AAC' },
|
||||
{ value: 'ogg', label: 'OGG' },
|
||||
{ value: 'opus', label: 'Opus' },
|
||||
{ value: 'wma', label: 'WMA' },
|
||||
const AUTO_DOWNLOAD_FORMATS: { value: Format; label: string }[] = [
|
||||
{ value: Format.FormatFLAC, label: 'FLAC' },
|
||||
{ value: Format.FormatALAC, label: 'ALAC' },
|
||||
{ value: Format.FormatWAV, label: 'WAV' },
|
||||
{ value: Format.FormatMP3, label: 'MP3' },
|
||||
{ value: Format.FormatAAC, label: 'AAC' },
|
||||
{ value: Format.FormatOGG, label: 'OGG' },
|
||||
{ value: Format.FormatOpus, label: 'Opus' },
|
||||
{ value: Format.FormatWMA, label: 'WMA' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -607,7 +609,7 @@ export class DownloadClients extends LitElement {
|
||||
// Secrets are never sent back to the frontend, so their fields
|
||||
// start blank; a blank secret on save means "leave it alone"
|
||||
// rather than "clear it".
|
||||
this.draft = { ...(provider.settings ?? {}) };
|
||||
this.draft = compact(provider.settings);
|
||||
}
|
||||
|
||||
private cancelEdit = () => {
|
||||
@@ -748,7 +750,7 @@ export class DownloadClients extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private toggleFormat(format: string, checked: boolean): void {
|
||||
private toggleFormat(format: Format, checked: boolean): void {
|
||||
const current = this.prefs.allowedFormats ?? [];
|
||||
const allowedFormats = checked
|
||||
? [...current, format]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html, svg, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
|
||||
@@ -2,9 +2,10 @@ import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumTracksByLibrary,
|
||||
GetFilePathsByAlbums,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { library } from '@go/models';
|
||||
import { dict, list } from '@utils/binding';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
|
||||
/**
|
||||
@@ -58,9 +59,11 @@ export class AlbumSelectionManager {
|
||||
const libId =
|
||||
libraryStore.getSelectedLibraryId();
|
||||
|
||||
return libId !== null
|
||||
? GetAlbumTracksByLibrary(albumId, libId)
|
||||
: GetAlbumTracks(albumId);
|
||||
return list(
|
||||
libId !== null
|
||||
? GetAlbumTracksByLibrary(albumId, libId)
|
||||
: GetAlbumTracks(albumId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +88,7 @@ export class AlbumSelectionManager {
|
||||
const libId =
|
||||
libraryStore.getSelectedLibraryId();
|
||||
|
||||
return GetFilePathsByAlbums(ids, libId ?? 0);
|
||||
return dict(GetFilePathsByAlbums(ids, libId ?? 0));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
|
||||
/**
|
||||
* Discriminated context menu target so we know whether the
|
||||
|
||||
@@ -14,8 +14,8 @@ import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumTracksByLibrary,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||
@@ -74,6 +74,7 @@ import type {
|
||||
GridEntry,
|
||||
SortDirection,
|
||||
} from './cover-grid-types.js';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
@customElement('cover-grid')
|
||||
export class CoverGrid
|
||||
@@ -933,12 +934,14 @@ export class CoverGrid
|
||||
const libId =
|
||||
this.libraryCtrl.selectedLibraryId;
|
||||
|
||||
const tracks = libId !== null
|
||||
? await GetAlbumTracksByLibrary(
|
||||
album.ID,
|
||||
libId,
|
||||
)
|
||||
: await GetAlbumTracks(album.ID);
|
||||
const tracks = await list(
|
||||
libId !== null
|
||||
? GetAlbumTracksByLibrary(
|
||||
album.ID,
|
||||
libId,
|
||||
)
|
||||
: GetAlbumTracks(album.ID),
|
||||
);
|
||||
|
||||
if (this.expandedAlbumId === album.ID) {
|
||||
this.expandedTracks = tracks;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitElement } from 'lit';
|
||||
import type { LitVirtualizer } from '@lit-labs/virtualizer';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import type { LibraryController } from '@store/controllers/library-controller';
|
||||
|
||||
import type { GridEntry } from './cover-grid-types.js';
|
||||
|
||||
@@ -7,7 +7,7 @@ import '@awesome.me/webawesome/dist/components/callout/callout.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import type { DownloadCandidate } from '@store/download-store';
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import type { download } from '@go/models';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import './candidate-row';
|
||||
import { explainError } from '@utils/describe-error';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
|
||||
@@ -686,7 +686,7 @@ export class DownloadsView extends ViewLifecycleMixin(LitElement) {
|
||||
/** Provider/progress summary for a download's second line. */
|
||||
private downloadDetail(view: DownloadRecord): string {
|
||||
const providers = [
|
||||
...new Set(view.items.map((item) => item.candidate?.origin).filter(Boolean)),
|
||||
...new Set((view.items ?? []).map((item) => item.candidate?.origin).filter(Boolean)),
|
||||
];
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, state, query } from 'lit/decorators.js';
|
||||
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@awesome.me/webawesome/dist/components/switch/switch.js';
|
||||
import { AddTracksToPlaylist } from '@go/playlist/Service';
|
||||
import { AddTracksToPlaylist } from '@go/playlist/service.js';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
LookupReleaseGroup,
|
||||
BrowseReleases,
|
||||
GetThumbnail,
|
||||
} from '@go/explore/Service';
|
||||
} from '@go/explore/service.js';
|
||||
import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumCompleteness,
|
||||
GetFilePathsByAlbums,
|
||||
GetFilePathsByRecordingMBIDs,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
import type { download, explore } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type MBRelease = explore.MBRelease;
|
||||
type MBTrack = explore.MBTrack;
|
||||
@@ -46,6 +47,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import { dict, dictByName } from '@utils/binding';
|
||||
|
||||
/**
|
||||
* The region the album header's own failures are rendered in.
|
||||
@@ -2248,9 +2250,8 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
// to an empty set and the Play button silently does nothing.
|
||||
// That is exactly what the first version of this did.
|
||||
if (this.localAlbumId > 0) {
|
||||
const byAlbum = await GetFilePathsByAlbums(
|
||||
[this.localAlbumId],
|
||||
libraryID,
|
||||
const byAlbum = await dict(
|
||||
GetFilePathsByAlbums([this.localAlbumId], libraryID),
|
||||
);
|
||||
|
||||
return byAlbum[this.localAlbumId] ?? [];
|
||||
@@ -2266,7 +2267,9 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
|
||||
if (mbids.length === 0) return [];
|
||||
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs(mbids, libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs(mbids, libraryID),
|
||||
);
|
||||
|
||||
// Walked in tracklist order rather than flattened, because the
|
||||
// grouping is what lets the caller keep its own order. A
|
||||
@@ -2349,7 +2352,9 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
if (!track.inLibrary || !track.mbid) return null;
|
||||
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs([track.mbid], libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs([track.mbid], libraryID),
|
||||
);
|
||||
|
||||
return byMBID[track.mbid]?.[0] ?? null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user