Compare commits
10
Commits
4471db3aef
...
1128881e8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1128881e8d | ||
|
|
cad3d1339b | ||
|
|
453d5df0da | ||
|
|
84963e38bd | ||
|
|
deb3f3da7e | ||
|
|
60779c41c3 | ||
|
|
a4ada725a2 | ||
|
|
04114eabae | ||
|
|
162c68769f | ||
|
|
c9905fbcff |
No files matched your search
@@ -22,9 +22,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
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 \
|
pacman -Syu --noconfirm --needed \
|
||||||
base-devel git go nodejs pnpm curl sudo \
|
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
|
- name: Create unprivileged build user
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+14
-19
@@ -171,7 +171,7 @@ jobs:
|
|||||||
working-directory: /src
|
working-directory: /src
|
||||||
run: make ui-test
|
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.
|
# so the codegen pre-commit hook does not cover it.
|
||||||
- name: Bindings are current
|
- name: Bindings are current
|
||||||
working-directory: /src
|
working-directory: /src
|
||||||
@@ -184,7 +184,9 @@ jobs:
|
|||||||
run: make skill-check
|
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:
|
e2e:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -220,7 +222,7 @@ jobs:
|
|||||||
apt-get install -y -qq --no-install-recommends \
|
apt-get install -y -qq --no-install-recommends \
|
||||||
ca-certificates curl git jq build-essential pkg-config \
|
ca-certificates curl git jq build-essential pkg-config \
|
||||||
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev \
|
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
|
alsa-utils libasound2-plugins pulseaudio pulseaudio-utils
|
||||||
|
|
||||||
- name: Clone repo at this commit
|
- name: Clone repo at this commit
|
||||||
@@ -247,24 +249,17 @@ jobs:
|
|||||||
apt-get install -y -qq --no-install-recommends nodejs
|
apt-get install -y -qq --no-install-recommends nodejs
|
||||||
corepack enable
|
corepack enable
|
||||||
|
|
||||||
# scripts/seed-sandbox.sh drives the real AddLibrary binding
|
# @playwright/cli is gone with v2. seed-sandbox.sh drove the real
|
||||||
# through playwright-cli, so the CLI has to be on PATH.
|
# AddLibrary binding through a browser because `window.go` was the
|
||||||
- name: Playwright CLI
|
# only way in; v3 answers the same call over HTTP, so the seed is
|
||||||
run: npm install -g @playwright/cli
|
# curl now and needs no CLI, no second Chromium and no shared
|
||||||
|
# PLAYWRIGHT_BROWSERS_PATH revision dance.
|
||||||
- name: Browsers
|
- name: Browsers
|
||||||
working-directory: /src/e2e
|
working-directory: /src/e2e
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
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
|
pnpm install --frozen-lockfile
|
||||||
npx playwright install --with-deps chromium webkit
|
npx playwright install --with-deps chromium webkit
|
||||||
playwright-cli install-browser chromium
|
|
||||||
|
|
||||||
# oto/v3 talks to libasound directly, and a container has no
|
# oto/v3 talks to libasound directly, and a container has no
|
||||||
# PulseAudio socket to fall back on — so it needs a default device
|
# 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
|
# 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
|
# 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
|
# audio takes 13.5 s. Verified under the private session bus
|
||||||
# Xvfb that dev-headless.sh runs the app in. It needs no system
|
# dev-headless.sh runs the app in. It needs no system D-Bus and
|
||||||
# D-Bus and no kernel module, which is why it is reachable from a
|
# no kernel module, which is why it is reachable from a container
|
||||||
# container at all.
|
# at all.
|
||||||
- name: Real-time audio sink
|
- name: Real-time audio sink
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
|
|||||||
+16
@@ -54,3 +54,19 @@ coverage/
|
|||||||
.cache/
|
.cache/
|
||||||
tmp/
|
tmp/
|
||||||
bin/
|
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
|
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.
|
reference, because you need them *before* the failure, not after.
|
||||||
|
|
||||||
- **Time out every binding call.** A bound Go method called with wrong
|
- **Call a binding through the bridge.** `window.go` does not exist
|
||||||
argument types makes the backend log `error parsing arguments` and
|
under Wails v3 — the bindings are bundled modules, not a global — so
|
||||||
**never fire the callback**, so the promise hangs forever. Use
|
use `window.__yjEvents.call(path, args, ms)` (browser) or
|
||||||
`window.__yjEvents.call(path, args, ms)` (browser) or `callBinding`
|
`callBinding` (specs). Both post to the runtime's own endpoint by
|
||||||
(specs), never a bare `window.go.…`. When one hangs anyway,
|
method name, so they work on any page, including one with no init
|
||||||
`make dev-logs` — `.dev/app.log` is the only place the reason appears.
|
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>`
|
- **Nothing is clickable on a fresh `YJ_HOME`.** `<first-run-wizard>`
|
||||||
intercepts all pointer events until a library exists, and the click
|
intercepts all pointer events until a library exists, and the click
|
||||||
fails with a Playwright interception error that reads like a selector
|
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:
|
when iterating on a single package:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test -tags webkit2_41 ./backend/player/ # the app build
|
go test ./backend/player/ # the app build
|
||||||
go test -tags webkit2_41 -run TestName ./backend/player/
|
go test -run TestName ./backend/player/
|
||||||
go test -tags "webkit2_41 indexbuild" ./backend/explore/... ./cmd/... # dump importer
|
go test -tags indexbuild ./backend/explore/... ./cmd/... # dump importer
|
||||||
go test -tags "webkit2_41 dev" ./backend/testctl/... # control surface
|
go test -tags dev ./backend/testctl/... # control surface
|
||||||
```
|
```
|
||||||
|
|
||||||
Forgetting the tag gives a build error that looks like a missing
|
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
|
- **`snapshot` writes a file, it does not print the tree.** The
|
||||||
command prints a path under `outputDir`; read that. Only the tail
|
command prints a path under `outputDir`; read that. Only the tail
|
||||||
is echoed.
|
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
|
(`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
|
- **`getByRole('button', { name })` matches substrings.** "Play" also
|
||||||
matches "Add queue to playlist"; transport controls need
|
matches "Add queue to playlist"; transport controls need
|
||||||
`exact: true`.
|
`exact: true`.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Then:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make generate # sqlc + templ
|
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
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# The component and store tier (`make ui-test`)
|
# The component and store tier (`make ui-test`)
|
||||||
|
|
||||||
313 tests in a real Chromium in ~2 s with no Wails, no backend, no
|
757 tests in a real Chromium with no Wails, no backend, no seeded
|
||||||
seeded library and no virtual display. This is the cheapest coverage
|
library and no virtual display. This is the cheapest coverage available
|
||||||
available and where the bulk of UI regression belongs.
|
and where the bulk of UI regression belongs.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ui-setup # once: the Vitest provider's own Chromium
|
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
|
## How it works
|
||||||
|
|
||||||
`frontend/wailsjs/` is a pure passthrough — every binding is
|
Wails v3 routes every runtime call — bindings, event emits, window,
|
||||||
`window.go[svc][Type][Method](args)`, every runtime call is
|
dialogs, clipboard — through one IPC transport, and `setTransport()` is
|
||||||
`window.runtime.X(...)`. So `frontend/test/support/wails-fake.ts`
|
a public seam for replacing it. So
|
||||||
replaces **those two globals and nothing else**, and the tests then
|
`frontend/test/support/wails-fake.ts` replaces **that and nothing
|
||||||
exercise the *real* generated bindings and the *real* store code. No
|
else**, and the tests then exercise the *real* generated bindings, the
|
||||||
module mocking, and no second description of the Wails layer.
|
*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
|
```ts
|
||||||
emit(Events.QueueChanged, payload); // push a backend event
|
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
|
const el = await fixture('now-playing'); // mount; shadow()/text() query it
|
||||||
```
|
```
|
||||||
|
|
||||||
The dispatcher mirrors wails' own `desktop/events.js`, including
|
Delivery is not mirrored — `emit()` goes through the runtime's own
|
||||||
`maxCallbacks` expiry and the fact that a frontend `EventsEmit`
|
`window._wails.dispatchWailsEvent`, which is the entry point the
|
||||||
notifies local listeners *before* Go.
|
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
|
- **Store singletons are constructed at module import**, before any test
|
||||||
can stub. `test/setup.ts` therefore carries import-time defaults for
|
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
|
- **`@lit-labs/virtualizer` never produces two identical frames**, so
|
||||||
`toMatchScreenshot` on `<queue-panel>` fails with "could not capture a
|
`toMatchScreenshot` on `<queue-panel>` fails with "could not capture a
|
||||||
stable screenshot" rather than a diff. Assert on its rows instead.
|
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
|
Visual baselines are font-hinting and compositing sensitive, which is
|
||||||
why they are opt-in: they only mean anything on the machine that
|
why they are opt-in: they only mean anything on the machine that
|
||||||
@@ -61,14 +84,15 @@ recorded them.
|
|||||||
|
|
||||||
## Bindings
|
## 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
|
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.
|
method first shows up at runtime, as a call that never settles.
|
||||||
|
|
||||||
```bash
|
```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
|
make bindings # regenerate for real
|
||||||
```
|
```
|
||||||
|
|
||||||
The generator rewrites `wailsjs/runtime/*` as mode 755 every run; that
|
No build tags are passed: the generator is a static analyser that sees
|
||||||
is churn, not drift, and the check ignores it.
|
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
|
# 009 — Wails v3 migration
|
||||||
|
|
||||||
**Status:** Phases 0–3 complete. The Go side is entirely on v3: nothing
|
**Status:** Phases 0–7 complete. CI needed nothing (it had ridden along
|
||||||
in the tree imports `wails/v2`, all three lint and test configurations
|
with Phases 1–6); **packaging needed everything** — both recipes still
|
||||||
are green, and `go build .` produces a running binary. **Phase 4
|
called v2's CLI, installed from v2's output path, and built from a
|
||||||
(bindings) is next, and until it lands the app builds but the frontend
|
scaffold's `yjref` metadata. Locally green: `make lint` and `make test`
|
||||||
cannot talk to it** — `frontend/wailsjs/` is v2's tree and nothing
|
(all three build configurations), `tsc --noEmit`, `make build-prod`,
|
||||||
regenerates it.
|
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`.
|
**Branch:** `wails-v3`, off `main` at `edb13a6`.
|
||||||
**Created:** 2026-08-13
|
**Created:** 2026-08-13
|
||||||
**Phase 0 run:** 2026-08-13 against **v3.0.0-beta.8**
|
**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`
|
`make build-prod` still strips and UPX-compresses; `make skill-check`
|
||||||
passes; `grep -r webkit2_41` returns nothing.
|
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.
|
**Est.** Half a session. Low risk, high churn.
|
||||||
|
|
||||||
### Phase 1 — what actually landed
|
### 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.
|
**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)
|
## 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
|
**Est.** One session. This is where the official estimate stops
|
||||||
applying.
|
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
|
## Phase 6 — E2E harness and testctl
|
||||||
@@ -666,6 +858,118 @@ spec edits.
|
|||||||
|
|
||||||
**Est.** One to two sessions. The largest and riskiest phase.
|
**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
|
## Phase 7 — CI and packaging
|
||||||
@@ -693,6 +997,83 @@ spec edits.
|
|||||||
|
|
||||||
**Est.** Half a session.
|
**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)
|
## 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 |
|
| 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 |
|
| 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) |
|
| 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** | *Confirmed.* Bindings nest by import path; codemod required |
|
| 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** | *New.* Pin tags in Phase 4 step 6 |
|
| 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 |
|
| 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** |
|
| 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` |
|
||||||
| v3 event ordering differs from v2's | medium | Re-derive the fake; don't port it |
|
| `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 |
|
| `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 |
|
| 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 |
|
| 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
|
* other events arrive from Go whenever they arrive. An assertion that
|
||||||
* sleeps and hopes is flaky; an assertion that awaits the event is not.
|
* 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
|
* record every backend -> frontend event, in order, with payloads
|
||||||
* wait a promise that settles on a matching event (or rejects
|
* wait a promise that settles on a matching event (or rejects
|
||||||
* with the list of events that *did* arrive, which is the
|
* with the list of events that *did* arrive, which is the
|
||||||
* single most useful failure message this harness can give)
|
* single most useful failure message this harness can give)
|
||||||
* call a bound Go method that is guaranteed to settle: a binding
|
* call a bound Go method, by name, over the runtime's own HTTP
|
||||||
* invoked with wrong argument types makes the backend log
|
* endpoint — no dependence on the app's bundle
|
||||||
* "error parsing arguments" and never fire the callback, so
|
* bindings every binding call the *app* made, which is what turns
|
||||||
* the in-page promise hangs forever. Timing out here fixes
|
* "did that refetch the library" from an inference into a
|
||||||
* that once instead of in every eval.
|
* fact (e2e/perf/measure.mjs labels and reads these)
|
||||||
*
|
*
|
||||||
* WHERE IT HOOKS. Not EventsOn. Every backend event enters the page
|
* WHERE IT HOOKS. Two places, and neither is `EventsOn`.
|
||||||
* at exactly one place — wails' ipc_websocket.js does
|
|
||||||
*
|
*
|
||||||
* 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
|
* Outbound, `fetch`: v3 routes every runtime call — binding calls, event
|
||||||
* single choke point captures all 46 events whether or not the app
|
* emits, window and dialog calls — through one POST to /wails/runtime.
|
||||||
* subscribes to them, and needs one wrap rather than 46.
|
* 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
|
||||||
* `window.wails` does not exist yet when this script runs, so we install
|
* an object graph, and cannot miss one made before the harness looked.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* INSTALL EXACTLY ONCE. Listeners registered by one `eval` survive into
|
* INSTALL EXACTLY ONCE. Listeners registered by one `eval` survive into
|
||||||
* the next, so a recorder that re-registers double-counts. Tests call
|
* the next, so a recorder that re-registers double-counts. Tests call
|
||||||
@@ -44,8 +47,27 @@
|
|||||||
|
|
||||||
const LIMIT = 2000;
|
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;
|
let seq = 0;
|
||||||
const log = [];
|
const log = [];
|
||||||
|
const bindings = [];
|
||||||
const waiters = new Set();
|
const waiters = new Set();
|
||||||
|
|
||||||
const summarize = () => {
|
const summarize = () => {
|
||||||
@@ -56,6 +78,25 @@
|
|||||||
return counts;
|
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 record = (name, data, dir) => {
|
||||||
const entry = { seq: ++seq, name, data, dir, t: Date.now() };
|
const entry = { seq: ++seq, name, data, dir, t: Date.now() };
|
||||||
log.push(entry);
|
log.push(entry);
|
||||||
@@ -89,7 +130,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
version: 1,
|
version: 2,
|
||||||
|
|
||||||
/** Every recorded event, oldest first. */
|
/** Every recorded event, oldest first. */
|
||||||
get log() {
|
get log() {
|
||||||
@@ -101,10 +142,30 @@
|
|||||||
return seq;
|
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() {
|
reset() {
|
||||||
const n = log.length;
|
const n = log.length;
|
||||||
log.length = 0;
|
log.length = 0;
|
||||||
|
bindings.length = 0;
|
||||||
return n;
|
return n;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -177,13 +238,11 @@
|
|||||||
async ready(timeoutMs) {
|
async ready(timeoutMs) {
|
||||||
const deadline = Date.now() + (timeoutMs || 15000);
|
const deadline = Date.now() + (timeoutMs || 15000);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (window.go?.queue?.Queue?.GetState) {
|
try {
|
||||||
try {
|
await api.call("queue.Queue.GetState", [], 2000);
|
||||||
await api.call("queue.Queue.GetState", [], 2000);
|
return true;
|
||||||
return true;
|
} catch {
|
||||||
} catch {
|
/* backend not up yet */
|
||||||
/* backend not up yet */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (Date.now() > deadline) {
|
if (Date.now() > deadline) {
|
||||||
throw new Error("__yjEvents.ready timed out");
|
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])
|
* await __yjEvents.call('player.Player.SetVolume', [42])
|
||||||
*
|
*
|
||||||
* A binding called with the wrong argument types never fires its
|
* This posts to the runtime's own endpoint rather than reaching
|
||||||
* callback — the reason appears only in .dev/app.log. Without a
|
* into the page for a binding function, because v3 has no
|
||||||
* timeout the caller waits forever; with one it gets told where
|
* `window.go` and the generated bindings are ordinary bundled
|
||||||
* to look.
|
* 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) {
|
call(path, args, timeoutMs) {
|
||||||
const parts = String(path).split(".");
|
const request = nativeFetch("/wails/runtime", {
|
||||||
let fn = window.go;
|
method: "POST",
|
||||||
for (const p of parts) {
|
headers: {
|
||||||
fn = fn?.[p];
|
"Content-Type": "application/json",
|
||||||
}
|
"x-wails-client-id": window._wails?.clientId ?? "",
|
||||||
if (typeof fn !== "function") {
|
},
|
||||||
return Promise.reject(
|
body: JSON.stringify({
|
||||||
new Error(`__yjEvents.call: no such binding: ${path}`),
|
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([
|
return Promise.race([
|
||||||
Promise.resolve(fn(...(args || []))),
|
request,
|
||||||
new Promise((_, reject) =>
|
new Promise((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`__yjEvents.call(${path}) did not settle in ` +
|
`__yjEvents.call(${path}) did not settle in ` +
|
||||||
`${timeoutMs || 10000}ms — almost always wrong ` +
|
`${timeoutMs || 10000}ms — the runtime endpoint ` +
|
||||||
`argument types; check .dev/app.log for ` +
|
`hung, which is not how a bad argument fails; ` +
|
||||||
`"error parsing arguments"`,
|
`check .dev/app.log`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timeoutMs || 10000,
|
timeoutMs || 10000,
|
||||||
@@ -241,62 +328,92 @@
|
|||||||
writable: false,
|
writable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wrap `obj[method]` once, routing every invocation through `tap`.
|
// ── Inbound ──────────────────────────────────────────────────────
|
||||||
const wrap = (obj, method, tap) => {
|
//
|
||||||
const original = obj[method];
|
// The runtime keeps whatever `window._wails` already is, so creating
|
||||||
if (typeof original !== "function" || original.__yjWrapped) {
|
// it here and defining an accessor on the one property we care about
|
||||||
return;
|
// means the wrap happens the moment the runtime module is evaluated.
|
||||||
}
|
window._wails = window._wails || {};
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Install an accessor that wraps on first assignment, then collapses
|
let dispatch;
|
||||||
// back into an ordinary property.
|
|
||||||
const hookOnAssign = (name, onAssign) => {
|
Object.defineProperty(window._wails, "dispatchWailsEvent", {
|
||||||
let value;
|
configurable: true,
|
||||||
Object.defineProperty(window, name, {
|
enumerable: true,
|
||||||
configurable: true,
|
get: () => dispatch,
|
||||||
enumerable: true,
|
set: (fn) => {
|
||||||
get: () => value,
|
dispatch = function (event) {
|
||||||
set: (v) => {
|
|
||||||
value = v;
|
|
||||||
try {
|
try {
|
||||||
onAssign(v);
|
record(event?.name, argsOf(event?.data), "in");
|
||||||
} catch {
|
} catch {
|
||||||
/* ditto */
|
/* a broken recorder must never break the app */
|
||||||
}
|
}
|
||||||
Object.defineProperty(window, name, {
|
return fn.apply(this, arguments);
|
||||||
value: v,
|
};
|
||||||
configurable: true,
|
},
|
||||||
enumerable: true,
|
});
|
||||||
writable: true,
|
|
||||||
});
|
// ── 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 LABEL=<n> # Measure a running app; writes .dev/perf/<n>.json
|
||||||
make perf-compare BEFORE=<a> AFTER=<b> # Print the before/after table
|
make perf-compare BEFORE=<a> AFTER=<b> # Print the before/after table
|
||||||
make build-dev # Debug build with symbols
|
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 generate # Run code generators (sqlc + templ via go generate)
|
||||||
make e2e # Playwright smoke suite against a running dev-headless app
|
make e2e # Playwright smoke suite against a running dev-headless app
|
||||||
make e2e-setup # Install the e2e runner + its browser (once)
|
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-test # Vitest component/store suite in a real browser (no app)
|
||||||
make ui-visual # Same, including toMatchScreenshot comparisons
|
make ui-visual # Same, including toMatchScreenshot comparisons
|
||||||
make ui-setup # Install the Vitest provider's own Chromium (once)
|
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 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 commit-check # Fail if a commit subject is not a Conventional Commit
|
||||||
make lint # golangci-lint v2 (strict), all three build configurations
|
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
|
### 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
|
```bash
|
||||||
go test -tags webkit2_41 ./... # All tests
|
go test ./... # All tests
|
||||||
go test -tags webkit2_41 ./backend/player/ # Single package
|
go test ./backend/player/ # Single package
|
||||||
go test -tags webkit2_41 -run TestName ./backend/player/ # Single test
|
go test -run TestName ./backend/player/ # Single test
|
||||||
```
|
```
|
||||||
|
|
||||||
The central index builder is behind a second tag and is **not** covered
|
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:
|
needs it spelled out:
|
||||||
|
|
||||||
```bash
|
```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
|
`backend/testctl` is behind a third tag and needs its own pass too
|
||||||
(`make test` runs all three):
|
(`make test` runs all three):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test -tags "webkit2_41 dev" ./backend/testctl/...
|
go test -tags dev ./backend/testctl/...
|
||||||
```
|
```
|
||||||
|
|
||||||
Audio playback integration tests require `YELLOWJACKET_INTEGRATION=1`.
|
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.
|
path, and skip themselves when it has not been generated.
|
||||||
|
|
||||||
The app itself can be run without a blocking window — `make
|
The app itself can be run without a blocking window — `make
|
||||||
dev-headless` — and driven with `playwright-cli` against the dev server
|
dev-headless` — and driven with `playwright-cli` against it on
|
||||||
on `:34115`, which is the real app with real bindings on `window.go`,
|
`:34115`. That is wails v3's first-class `-tags server` mode: the real
|
||||||
bridged to the same Go backend a desktop window would use.
|
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
|
**The operational half of all this lives in the
|
||||||
`yellowjacket-dev` skill** (`.pi/skills/yellowjacket-dev/`): which tier
|
`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,
|
backend event on `window.__yjEvents`. Half this app is push-driven,
|
||||||
so assertions **await an event, not a timeout**:
|
so assertions **await an event, not a timeout**:
|
||||||
`await window.__yjEvents.wait('LibraryScanComplete', {timeoutMs: 60000})`.
|
`await window.__yjEvents.wait('LibraryScanComplete', {timeoutMs: 60000})`.
|
||||||
It also provides `ready()` and `call('queue.Queue.GetState', [])`,
|
It also provides `ready()` and `call('queue.Queue.GetState', [])`.
|
||||||
which times out instead of hanging.
|
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
|
- **The dev-only control surface**, `backend/testctl`, mounted at
|
||||||
`/__test/` on the same port: `health`, `db/snapshot`, `db/restore`,
|
`/__test/` on the same port: `health`, `db/snapshot`, `db/restore`,
|
||||||
`emit` (force any backend event, which renders push-driven views
|
`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
|
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.
|
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
|
**The cheapest tier needs none of that.** `make ui-test` runs 757
|
||||||
Vitest tests in a real Chromium in ~2 s with no Wails, no backend, no
|
Vitest tests in a real Chromium with no Wails, no backend, no seeded
|
||||||
seeded library and no virtual display, because `frontend/wailsjs/` is a
|
library and no virtual display, because **v3 routes every runtime call
|
||||||
pure passthrough to `window.go` / `window.runtime` and
|
— bindings, event emits, window, dialogs, clipboard — through one IPC
|
||||||
`frontend/test/support/wails-fake.ts` replaces just those two globals —
|
transport**, and `frontend/test/support/wails-fake.ts` replaces it via
|
||||||
so the tests exercise the real generated bindings and the real store
|
`setTransport()`, which is public documented API. So the fake covers
|
||||||
code.
|
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`
|
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
|
(~3.5 s warm, ~20 s on a cold build cache, also a pre-commit hook)
|
||||||
tree; `make bindings` regenerates it for real.
|
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
|
**Seeds are produced by running the app**, never by hand-writing a
|
||||||
`config.toml` and DB rows — the same discipline `sql/schemas/` gets,
|
`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
|
## 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/`):
|
**Backend packages** (under `backend/`):
|
||||||
- `player` — Audio playback via beep. `BufferedStreamer` provides a ring buffer for smooth seeking.
|
- `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
|
keeping only `explore.ArtistImageKeepNames()` and refusing an empty
|
||||||
keep set for the reason the covers sweep refuses an empty live set.
|
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
|
**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
|
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.
|
something invisible.
|
||||||
|
|
||||||
Emit through **`events.Emit(ctx, name, data...)`**, never
|
Emit through **`events.Emit(ctx, name, data...)`**, never
|
||||||
`runtime.EventsEmit` — wails `log.Fatalf`s (unrecoverably) on any
|
`app.Event.Emit`. `TestNoDirectRuntimeEmits` fails the build on a
|
||||||
context that does not carry its runtime, which includes every
|
direct call anywhere outside `backend/events`.
|
||||||
`context.Background()`, so a direct call cannot run under test and can
|
|
||||||
kill the app from a background worker. `TestNoDirectRuntimeEmits` fails
|
**Its justification changed with v3 and is now the weaker one.** Under
|
||||||
the build on a direct call anywhere outside `backend/events`.
|
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
|
That wrapper is what makes services testable in-process: install a
|
||||||
recorder with `events.WithSink(ctx, rec)` and assert on the payload the
|
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),
|
`make lint` and `make test` (three build configurations each),
|
||||||
`tsc --noEmit`, `make ui-test`, `make bindings-check`,
|
`tsc --noEmit`, `make ui-test`, `make bindings-check`,
|
||||||
`make skill-check`.
|
`make skill-check`.
|
||||||
- **`e2e`** — under Xvfb and a private D-Bus: fixtures, a seed built by
|
- **`e2e`** — under a private D-Bus and **no display at all**, since
|
||||||
running the app, `make dev-headless`, then the Playwright suite
|
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
|
against **both** Chromium and WebKit. Playwright's Linux WebKit links
|
||||||
Ubuntu 24.04 libraries that Arch does not provide, so CI is the only
|
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
|
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
|
`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.
|
explore artifact, matching what `scripts/seed-sandbox.sh` already does.
|
||||||
|
|
||||||
**`make lint`'s tag sets must stay identical to `make test`'s.**
|
**`make lint`'s tag sets must stay identical to `make test`'s**, or
|
||||||
Without `webkit2_41` wails resolves `webkit2gtk-4.0`, which Arch still
|
lint is checking configurations nothing builds. There are three, and
|
||||||
ships and Ubuntu 24.04 does not — so a mismatch lints a configuration
|
the app's is now the *default* tag set: v3 resolves GTK4 +
|
||||||
that only builds on one developer's distro, and says nothing about what
|
WebKitGTK 6.0, which Arch and ubuntu:24.04 both ship, so the
|
||||||
ships.
|
`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).
|
# point elsewhere (or unset it there to share the real user dirs).
|
||||||
DEV_YJ_HOME ?= $(HOME)/.local/share/yellowjacket-dev
|
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
|
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
|
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) ──────────────────────────────────────
|
# ── Headless harness (plan 005) ──────────────────────────────────────
|
||||||
# The same dev server `make dev` runs, minus the blocking GTK window:
|
# The same app `make dev` runs, minus the window: v3's `-tags server`
|
||||||
# Xvfb gives it the display it insists on, and the script returns once
|
# is a first-class headless mode that needs no display at all, so the
|
||||||
# :34115 answers. This is the only entry point an agent can use, since
|
# Xvfb this used to require is gone. The script returns once :34115
|
||||||
# every other one blocks the terminal forever.
|
# 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)
|
dev-headless: ## Start the app headless in the background (SEED=<name> to seed)
|
||||||
@./scripts/dev-headless.sh $(if $(SEED),--seed $(SEED),) $(HEADLESS_ARGS)
|
@./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)
|
ui-setup: ## Install the Vitest browser provider's own Chromium (once)
|
||||||
@cd frontend && pnpm install && npx playwright install chromium
|
@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
|
# pre-commit codegen check does not cover them: a renamed Go struct
|
||||||
# field currently surfaces at runtime, in a window.
|
# field would otherwise surface at runtime, inside 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.
|
|
||||||
bindings-check: ## Fail if the generated bindings are stale
|
bindings-check: ## Fail if the generated bindings are stale
|
||||||
@./scripts/bindings-check.sh
|
@./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))
|
@./scripts/commit-check.sh $(if $(RANGE),--range $(RANGE))
|
||||||
|
|
||||||
# v3 generates TypeScript into frontend/bindings/, nested by Go import
|
# v3 generates TypeScript into frontend/bindings/, nested by Go import
|
||||||
# path, rather than v2's frontend/wailsjs/. The 93 frontend import
|
# path, rather than v2's frontend/wailsjs/. The `@go` alias absorbs the
|
||||||
# sites still point at the old tree, so this writes the new one beside
|
# constant prefix, so a call site imports '@go/library/library.js'.
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# The tag set is pinned deliberately: the generator is a static
|
# No -f flag: the tag set is the default one, deliberately, because the
|
||||||
# analyser, so it only sees the configuration it is told about, and the
|
# generator is a static analyser that sees only the configuration it is
|
||||||
# one that matters is the one users run.
|
# 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
|
bindings: ## Regenerate frontend/bindings from the bound Go services
|
||||||
go tool wails3 generate bindings -clean=true -ts -i
|
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 \
|
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." ;; \
|
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; \
|
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
|
# Named, persistent sandboxes: `make sandbox foo` runs dev against
|
||||||
# $(FRESH_HOME_BASE)/yellowjacket-sandbox-foo, creating it on first use
|
# $(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 \
|
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." ;; \
|
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; \
|
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
|
sandboxes: ## List existing named sandboxes
|
||||||
@ls -d "$(SANDBOX_DIR)"-* 2>/dev/null \
|
@ls -d "$(SANDBOX_DIR)"-* 2>/dev/null \
|
||||||
@@ -238,10 +240,10 @@ sandboxes: ## List existing named sandboxes
|
|||||||
.PHONY: sandbox sandbox-rm sandboxes
|
.PHONY: sandbox sandbox-rm sandboxes
|
||||||
|
|
||||||
build-dev: generate
|
build-dev: generate
|
||||||
go tool wails3 task build DEV=true
|
PATH="$(TOOLBIN):$$PATH" go tool wails3 task build DEV=true
|
||||||
|
|
||||||
build-prod: generate
|
build-prod: generate
|
||||||
go tool wails3 task build
|
PATH="$(TOOLBIN):$$PATH" go tool wails3 task build
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
cd frontend && pnpm install && pnpm build
|
cd frontend && pnpm install && pnpm build
|
||||||
|
|||||||
@@ -78,16 +78,25 @@ YellowJacket is built with [Go](https://go.dev/) and a
|
|||||||
| Go | 1.25+ |
|
| Go | 1.25+ |
|
||||||
| Node.js | 22+ |
|
| Node.js | 22+ |
|
||||||
| pnpm | 10+ |
|
| 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
|
```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
|
A machine without `webkitgtk-6.0` can still build with `-tags gtk3` against the
|
||||||
environment.
|
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**
|
**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
|
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
|
// by relevance (popularity-blended). Returns nil when the index
|
||||||
// hasn't finished its initial build.
|
// hasn't finished its initial build.
|
||||||
func (si *SearchIndex) Search(ctx context.Context, query string, limit int) []SearchIndexResult {
|
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
|
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'
|
version: '3'
|
||||||
|
|
||||||
# This information is used to generate the build assets.
|
# 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:
|
info:
|
||||||
companyName: "yellowjacket" # The name of the company
|
companyName: "yellowjacket" # The name of the company
|
||||||
productName: "yellowjacket" # The name of the application
|
productName: "yellowjacket" # The name of the application
|
||||||
productIdentifier: "app.yellowjacket" # The unique product identifier
|
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
|
copyright: "(c) 2026, yellowjacket" # Copyright text
|
||||||
comments: "yj@yellowjacket.app" # Comments
|
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)
|
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
|
||||||
# # Should match the name of your .icon file without the extension
|
# # Should match the name of your .icon file without the extension
|
||||||
# # If not set and Assets.car exists, defaults to "appicon"
|
# # 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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>APPL</string>
|
<string>yellowjacket</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>My Product</string>
|
<string>yj@yellowjacket.app</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>yjref</string>
|
<string>icons</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIconName</key>
|
||||||
<string>com.example.yjref</string>
|
<string>appicon</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>0.1.0</string>
|
<string>app.yellowjacket</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleName</key>
|
||||||
<string>This is a comment</string>
|
<string>yellowjacket</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>0.1.0</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>icons</string>
|
<string>1.3.0</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>appicon</string>
|
<string>1.3.0</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0.0</string>
|
<string>12.0.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<string>true</string>
|
<dict>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<string>© 2026, My Company</string>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
</dict>
|
||||||
<dict>
|
<key>NSHighResolutionCapable</key>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<string>true</string>
|
||||||
<true/>
|
<key>NSHumanReadableCopyright</key>
|
||||||
</dict>
|
<string>(c) 2026, yellowjacket</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>APPL</string>
|
<string>yellowjacket</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>My Product</string>
|
<string>yj@yellowjacket.app</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>yjref</string>
|
<string>icons</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIconName</key>
|
||||||
<string>com.example.yjref</string>
|
<string>appicon</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>0.1.0</string>
|
<string>app.yellowjacket</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleName</key>
|
||||||
<string>This is a comment</string>
|
<string>yellowjacket</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>0.1.0</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>icons</string>
|
<string>1.3.0</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>appicon</string>
|
<string>1.3.0</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0.0</string>
|
<string>12.0.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2026, My Company</string>
|
<string>(c) 2026, yellowjacket</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -43,7 +43,10 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"'
|
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"'
|
||||||
vars:
|
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}}'
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
env:
|
env:
|
||||||
|
|||||||
Binary file not shown.
@@ -59,7 +59,15 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
|
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
|
||||||
vars:
|
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}}'
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
env:
|
env:
|
||||||
|
|||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Name=My Product
|
Name=yellowjacket
|
||||||
Comment=A yjref application
|
Comment=Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag
|
||||||
# The Exec line includes %u to pass the URL to the application
|
# 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
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=yjref
|
Icon=yellowjacket
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
StartupWMClass=yjref
|
StartupWMClass=yellowjacket
|
||||||
|
|
||||||
|
|
||||||
+14
-11
@@ -3,26 +3,29 @@
|
|||||||
#
|
#
|
||||||
# The lines below are called `modelines`. See `:help modeline`
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
name: "yjref"
|
name: "yellowjacket"
|
||||||
arch: ${GOARCH}
|
arch: ${GOARCH}
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "0.1.0"
|
version: "1.3.0"
|
||||||
section: "default"
|
section: "default"
|
||||||
priority: "extra"
|
priority: "extra"
|
||||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
description: "A yjref application"
|
description: "Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag"
|
||||||
vendor: "My Company"
|
vendor: "yellowjacket"
|
||||||
homepage: "https://wails.io"
|
# homepage and license are NOT derived from build/config.yml, so
|
||||||
license: "MIT"
|
# `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"
|
release: "1"
|
||||||
|
|
||||||
contents:
|
contents:
|
||||||
- src: "./bin/yjref"
|
- src: "./bin/yellowjacket"
|
||||||
dst: "/usr/local/bin/yjref"
|
dst: "/usr/local/bin/yellowjacket"
|
||||||
- src: "./build/appicon.png"
|
- src: "./build/appicon.png"
|
||||||
dst: "/usr/share/icons/hicolor/128x128/apps/yjref.png"
|
dst: "/usr/share/icons/hicolor/128x128/apps/yellowjacket.png"
|
||||||
- src: "./build/linux/yjref.desktop"
|
- src: "./build/linux/yellowjacket.desktop"
|
||||||
dst: "/usr/share/applications/yjref.desktop"
|
dst: "/usr/share/applications/yellowjacket.desktop"
|
||||||
|
|
||||||
# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
|
# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
|
||||||
depends:
|
depends:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"fixed": {
|
"fixed": {
|
||||||
"file_version": "0.1.0"
|
"file_version": "1.3.0"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.1.0",
|
"ProductVersion": "1.3.0",
|
||||||
"CompanyName": "My Company",
|
"CompanyName": "yellowjacket",
|
||||||
"FileDescription": "A yjref application",
|
"FileDescription": "Cross-platform desktop music player — local library, MusicBrainz explore & auto-tag",
|
||||||
"LegalCopyright": "© 2026, My Company",
|
"LegalCopyright": "(c) 2026, yellowjacket",
|
||||||
"ProductName": "My Product",
|
"ProductName": "yellowjacket",
|
||||||
"Comments": "This is a comment"
|
"Comments": "yj@yellowjacket.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,19 +5,19 @@
|
|||||||
!include "FileFunc.nsh"
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
!ifndef INFO_PROJECTNAME
|
!ifndef INFO_PROJECTNAME
|
||||||
!define INFO_PROJECTNAME "yjref"
|
!define INFO_PROJECTNAME "yellowjacket"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COMPANYNAME
|
!ifndef INFO_COMPANYNAME
|
||||||
!define INFO_COMPANYNAME "My Company"
|
!define INFO_COMPANYNAME "yellowjacket"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTNAME
|
!ifndef INFO_PRODUCTNAME
|
||||||
!define INFO_PRODUCTNAME "My Product"
|
!define INFO_PRODUCTNAME "yellowjacket"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTVERSION
|
!ifndef INFO_PRODUCTVERSION
|
||||||
!define INFO_PRODUCTVERSION "0.1.0"
|
!define INFO_PRODUCTVERSION "1.3.0"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COPYRIGHT
|
!ifndef INFO_COPYRIGHT
|
||||||
!define INFO_COPYRIGHT "© 2026, My Company"
|
!define INFO_COPYRIGHT "(c) 2026, yellowjacket"
|
||||||
!endif
|
!endif
|
||||||
!ifndef PRODUCT_EXECUTABLE
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<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>
|
<dependency>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
<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 { fileURLToPath } from 'node:url';
|
||||||
import { chromium } from '@playwright/test';
|
import { chromium } from '@playwright/test';
|
||||||
|
|
||||||
|
import { methodIDs } from '../support/method-ids.mjs';
|
||||||
|
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
const REPO = resolve(HERE, '../..');
|
const REPO = resolve(HERE, '../..');
|
||||||
const OUT_DIR = resolve(REPO, '.dev/perf');
|
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';
|
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
|
// The browse script visited for the heap measurement. Deliberately the
|
||||||
// views the audit named as retaining: explore (two unbounded caches),
|
// views the audit named as retaining: explore (two unbounded caches),
|
||||||
// artists and genres (per-frame work), settings (the 3 s ticker).
|
// 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
|
* Say which bindings a user action provoked, and how much they
|
||||||
* user action provoked and how much they returned.
|
* returned.
|
||||||
*
|
*
|
||||||
* Post-hoc wrapping is safe because `frontend/wailsjs` looks its target
|
* This used to walk `window.go` and wrap every bound method in place,
|
||||||
* up at call time (`window['go']['library']['Library']['GetAllTracks']()`),
|
* which worked because v2's generated stubs looked their target up at
|
||||||
* so a store holding an imported wrapper still lands here.
|
* 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 = `() => {
|
const INSTRUMENT = `(names) => {
|
||||||
// Runs twice: once as an initScript (before window.go exists, which
|
if (window.__yjPerf) return;
|
||||||
// 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;
|
|
||||||
|
|
||||||
if (first) {
|
window.__yjEvents.measureBytes = true;
|
||||||
const calls = [];
|
|
||||||
window.__yjPerf = {
|
|
||||||
calls,
|
|
||||||
reset: () => { calls.length = 0; },
|
|
||||||
since: (t) => calls.filter((c) => c.start >= t),
|
|
||||||
longtasks: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const calls = window.__yjPerf.calls;
|
window.__yjPerf = {
|
||||||
|
get calls() {
|
||||||
const wrap = (obj, key, path) => {
|
return window.__yjEvents.bindings.map((c) => ({
|
||||||
const fn = obj[key];
|
path: names[c.methodID] || ('#' + c.methodID),
|
||||||
if (typeof fn !== 'function' || fn.__yjPerfWrapped) return;
|
methodID: c.methodID,
|
||||||
const wrapped = function (...args) {
|
start: c.start,
|
||||||
const start = performance.now();
|
ms: c.ms,
|
||||||
let out;
|
bytes: c.bytes,
|
||||||
try { out = fn.apply(this, args); } catch (e) { throw e; }
|
}));
|
||||||
return Promise.resolve(out).then((v) => {
|
},
|
||||||
let bytes = 0;
|
reset: () => { window.__yjEvents.reset(); },
|
||||||
try { bytes = JSON.stringify(v ?? null).length; } catch { bytes = -1; }
|
since: (t) => window.__yjPerf.calls.filter((c) => c.start >= t),
|
||||||
calls.push({ path, start, ms: performance.now() - start, bytes });
|
longtasks: [],
|
||||||
return v;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
wrapped.__yjPerfWrapped = true;
|
|
||||||
obj[key] = wrapped;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
// 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.
|
// parse on the main thread shows up here and nowhere else.
|
||||||
try {
|
try {
|
||||||
@@ -352,7 +341,15 @@ async function measureTrackChange(page) {
|
|||||||
const paths = (tracks ?? []).slice(0, 4).map((t) => t.FilePath);
|
const paths = (tracks ?? []).slice(0, 4).map((t) => t.FilePath);
|
||||||
if (paths.length < 2) return { error: 'library too small to measure' };
|
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('queue.Queue.PlayIndex', [0], 15000);
|
||||||
|
|
||||||
// Settle mid-track before starting to record. Starting playback
|
// Settle mid-track before starting to record. Starting playback
|
||||||
@@ -1046,14 +1043,15 @@ const SCROLL_SETTLE_MS = 260;
|
|||||||
async function measureScroll(page) {
|
async function measureScroll(page) {
|
||||||
// -- M3: the track list, with the Art column staged on. --
|
// -- M3: the track list, with the Art column staged on. --
|
||||||
const priorColumns = await page.evaluate(async () => {
|
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: 'albumArt' }, { id: 'trackName' },
|
||||||
{ id: 'artistName' }, { id: 'trackLength' }],
|
{ id: 'artistName' }, { id: 'trackLength' }],
|
||||||
);
|
], 15000);
|
||||||
|
|
||||||
return prior.map((c) => ({ id: c.id }));
|
return (prior ?? []).map((c) => ({ id: c.id }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollView = async (view, tag) => {
|
const scrollView = async (view, tag) => {
|
||||||
@@ -1141,7 +1139,9 @@ async function measureScroll(page) {
|
|||||||
const artists = await scrollView('artists', 'artists-view');
|
const artists = await scrollView('artists', 'artists-view');
|
||||||
|
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
(cols) => window.go.config.Config.SetTrackListColumns(cols),
|
(cols) => window.__yjEvents.call(
|
||||||
|
'config.Config.SetTrackListColumns', [cols], 15000,
|
||||||
|
),
|
||||||
priorColumns,
|
priorColumns,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1605,7 +1605,15 @@ async function measurePlayerBarPass(page) {
|
|||||||
|
|
||||||
if (paths.length < 2) return { error: 'library too small to measure' };
|
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('queue.Queue.PlayIndex', [0], 15000);
|
||||||
await ev.call('player.Player.Pause', [], 5000).catch(() => {});
|
await ev.call('player.Player.Pause', [], 5000).catch(() => {});
|
||||||
await new Promise((r) => setTimeout(r, 600));
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
@@ -1804,7 +1812,9 @@ async function run(label) {
|
|||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||||
await context.addInitScript({ path: BRIDGE });
|
await context.addInitScript({ path: BRIDGE });
|
||||||
await context.addInitScript(`(${INSTRUMENT})()`);
|
await context.addInitScript(
|
||||||
|
`(${INSTRUMENT})(${JSON.stringify(METHOD_NAMES)})`,
|
||||||
|
);
|
||||||
|
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const client = await context.newCDPSession(page);
|
const client = await context.newCDPSession(page);
|
||||||
@@ -1813,8 +1823,10 @@ async function run(label) {
|
|||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
await page.goto(BASE_URL, { waitUntil: 'load' });
|
await page.goto(BASE_URL, { waitUntil: 'load' });
|
||||||
await page.evaluate(() => window.__yjEvents.ready(30000));
|
await page.evaluate(() => window.__yjEvents.ready(30000));
|
||||||
// Wrapping runs before `window.go` exists; re-run now that it does.
|
// No second instrumentation pass. The old one existed because
|
||||||
await page.evaluate(`(${INSTRUMENT})()`);
|
// 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 = {
|
const report = {
|
||||||
label,
|
label,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These specs drive the *real* application: the Wails dev server on
|
* These specs drive the *real* application: Wails v3's server mode on
|
||||||
* :34115 serves the real frontend with real bindings on `window.go`,
|
* :34115 serves the real frontend with the real generated bindings,
|
||||||
* bridged to the same Go backend a desktop window would use. Nothing
|
* bridged to the same Go backend a desktop window would use. Nothing
|
||||||
* here is mocked.
|
* here is mocked.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ import type { Page } from '@playwright/test';
|
|||||||
* none, so Play was wired, labelled correctly, clicked cleanly and
|
* none, so Play was wired, labelled correctly, clicked cleanly and
|
||||||
* queued **nothing**. Every component test still passed.
|
* 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.describe('playing an album from its page', () => {
|
||||||
test.beforeEach(async ({ app }) => {
|
test.beforeEach(async ({ app }) => {
|
||||||
await openFirstAlbum(app);
|
await openFirstAlbum(app);
|
||||||
@@ -55,16 +63,6 @@ test.describe('playing an album from its page', () => {
|
|||||||
await expect.poll(() => queueLength(app)).toBe(before * 2);
|
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 }) => {
|
test('the ticks are badges, not keyboard stops', async ({ app }) => {
|
||||||
// Every one of them was a <button> whose click handler was a
|
// Every one of them was a <button> whose click handler was a
|
||||||
// stopPropagation() and a comment saying to wire up the download
|
// stopPropagation() and a comment saying to wire up the download
|
||||||
|
|||||||
+25
-10
@@ -15,12 +15,18 @@ import {
|
|||||||
*/
|
*/
|
||||||
test.describe('harness', () => {
|
test.describe('harness', () => {
|
||||||
test('the app is the real app, not a mock', async ({ app }) => {
|
test('the app is the real app, not a mock', async ({ app }) => {
|
||||||
// All 11 bound services land on window.go through the dev server.
|
// v2 landed every bound service on `window.go`, so "is this real"
|
||||||
const services = await app.evaluate(() => Object.keys(window.go));
|
// 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(runtime).toEqual({ dispatch: 'function', client: 'string' });
|
||||||
expect.arrayContaining(['queue', 'player', 'library', 'explore']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const state = await callBinding<{ tracks: unknown[] }>(
|
const state = await callBinding<{ tracks: unknown[] }>(
|
||||||
app,
|
app,
|
||||||
@@ -28,6 +34,12 @@ test.describe('harness', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state).toHaveProperty('tracks');
|
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 ({
|
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 }) => {
|
test('a binding called with wrong types fails fast', async ({ app }) => {
|
||||||
// player.UserVolume is an int. Passing a float makes the backend
|
// player.UserVolume is an int. Under v2 a float made the backend
|
||||||
// log "error parsing arguments" and never fire the callback; without
|
// log "error parsing arguments" and never fire the callback, so
|
||||||
// a timeout the promise never settles and the spec hangs until the
|
// this asserted that the harness's own timeout fired — the failure
|
||||||
// suite gives up.
|
// 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 () => {
|
const failure = await app.evaluate(async () => {
|
||||||
try {
|
try {
|
||||||
await window.__yjEvents.call(
|
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 }) => {
|
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.
|
* 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. */
|
/** Long enough for a fixture track (2–6 s) to finish by itself. */
|
||||||
const FINISH_TIMEOUT = 60_000;
|
const FINISH_TIMEOUT = 60_000;
|
||||||
|
|
||||||
/** Instrument the library bindings so "was anything refetched" is a fact. */
|
/**
|
||||||
const COUNT_LIBRARY_CALLS = `(() => {
|
* "Was anything refetched" is a fact, not an inference.
|
||||||
const w = window;
|
*
|
||||||
if (w.__yjCalls) { w.__yjCalls.length = 0; return; }
|
* This used to wrap every method on `window.go.library.Library` in
|
||||||
w.__yjCalls = [];
|
* place. v3 has no such object, and does not need one: the harness
|
||||||
const lib = w.go.library.Library;
|
* bridge records every binding call off the single POST they all go
|
||||||
for (const key of Object.keys(lib)) {
|
* through, so the question is answered by reading that log rather than
|
||||||
const fn = lib[key];
|
* by instrumenting a target first. `resetEvents` clears it.
|
||||||
if (typeof fn !== 'function' || fn.__counted) continue;
|
*/
|
||||||
const wrapped = function (...args) {
|
const libraryCalls = async (app: Page): Promise<string[]> =>
|
||||||
w.__yjCalls.push(key);
|
(await bindingCalls(app)).filter((c) => c.startsWith('library.Library.'));
|
||||||
return fn.apply(this, args);
|
|
||||||
};
|
|
||||||
wrapped.__counted = true;
|
|
||||||
lib[key] = wrapped;
|
|
||||||
}
|
|
||||||
})()`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select rows by dispatching on the row rather than clicking it.
|
* 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);
|
expect(paths.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false]);
|
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||||
await app.evaluate(COUNT_LIBRARY_CALLS);
|
|
||||||
await resetEvents(app);
|
await resetEvents(app);
|
||||||
|
|
||||||
await callBinding(app, 'queue.Queue.PlayIndex', [0]);
|
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',
|
'a play emitted the retag event, which invalidates every cache',
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
|
|
||||||
const refetched = await app.evaluate(
|
const refetched = await libraryCalls(app);
|
||||||
() => (window as unknown as { __yjCalls: string[] }).__yjCalls,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
refetched.filter((c) => c.startsWith('GetAll')),
|
refetched.filter((c) => c.startsWith('library.Library.GetAll')),
|
||||||
'a play refetched a collection',
|
'a play refetched a collection',
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ test.describe('a finished track', () => {
|
|||||||
|
|
||||||
const paths = await app.evaluate(firstPaths, 2);
|
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 resetEvents(app);
|
||||||
await callBinding(app, 'queue.Queue.PlayIndex', [0]);
|
await callBinding(app, 'queue.Queue.PlayIndex', [0]);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
resetEvents,
|
resetEvents,
|
||||||
waitForEvent,
|
waitForEvent,
|
||||||
LONG_TRACK,
|
LONG_TRACK,
|
||||||
|
NO_QUEUE_SOURCE,
|
||||||
} from '../support/fixtures.js';
|
} from '../support/fixtures.js';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
@@ -161,6 +162,7 @@ test.describe('a finished queue keeps its context', () => {
|
|||||||
[rows[0].file_path],
|
[rows[0].file_path],
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
|
NO_QUEUE_SOURCE,
|
||||||
]);
|
]);
|
||||||
await waitForEvent(app, 'QueueChanged');
|
await waitForEvent(app, 'QueueChanged');
|
||||||
await callBinding(app, 'queue.Queue.Play');
|
await callBinding(app, 'queue.Queue.Play');
|
||||||
@@ -199,7 +201,7 @@ test.describe('a track that will not play says so', () => {
|
|||||||
try {
|
try {
|
||||||
await callBinding(app, 'queue.Queue.Clear');
|
await callBinding(app, 'queue.Queue.Clear');
|
||||||
await resetEvents(app);
|
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 waitForEvent(app, 'QueueChanged');
|
||||||
await callBinding(app, 'queue.Queue.Play');
|
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';
|
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);
|
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.
|
// A closed panel renders no list at all, so there is no row to focus.
|
||||||
await app.locator('#queue-button').click();
|
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';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +70,7 @@ async function playTheLongOne(app: Page): Promise<void> {
|
|||||||
|
|
||||||
expect(paths.length).toBeGreaterThan(0);
|
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');
|
await waitForEvent(app, 'TrackChanged');
|
||||||
|
|
||||||
// The scroll cycle is armed 1500 ms after the geometry is measured,
|
// 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`,
|
* Deliberately not `callBinding`: that goes through
|
||||||
* which only exists on a page the `app` fixture created — a bare
|
* `window.__yjEvents`, which only exists on a page the `app` fixture
|
||||||
* `browser.newPage()` has no init script, so the bridge is undefined and
|
* created — a bare `browser.newPage()` has no init script, so the
|
||||||
* the cleanup throws where nobody is looking. The first version of this
|
* bridge is undefined and the cleanup throws where nobody is looking.
|
||||||
* did exactly that and left the request behind, which failed the *next*
|
* The first version of this did exactly that and left the request
|
||||||
* run of this same spec.
|
* 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) {
|
async function clearRequest(page: import('@playwright/test').Page) {
|
||||||
await page.evaluate(async (mbid) => {
|
await page.evaluate(async (mbid) => {
|
||||||
const go = (
|
const call = async (method: string, args: unknown[]) => {
|
||||||
window as unknown as {
|
const res = await fetch('/wails/runtime', {
|
||||||
go?: {
|
method: 'POST',
|
||||||
download?: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
Service?: {
|
body: JSON.stringify({
|
||||||
ListRequests(): Promise<{ id: number; mbid: string }[]>;
|
object: 0,
|
||||||
RemoveRequest(id: number): Promise<void>;
|
method: 0,
|
||||||
};
|
args: {
|
||||||
};
|
'call-id': `clear-${method}`,
|
||||||
};
|
methodName: `yellowjacket/backend/${method}`,
|
||||||
}
|
args,
|
||||||
).go;
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
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) {
|
for (const row of rows) {
|
||||||
if (row.mbid?.toLowerCase() === mbid.toLowerCase()) {
|
if (row.mbid?.toLowerCase() === mbid.toLowerCase()) {
|
||||||
await svc.RemoveRequest(row.id);
|
await call('download.Service.RemoveRequest', [row.id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, MBID);
|
}, MBID);
|
||||||
|
|||||||
+49
-6
@@ -3,12 +3,26 @@ import { dirname, resolve } from 'node:path';
|
|||||||
|
|
||||||
import { test as base, expect, type Page } from '@playwright/test';
|
import { test as base, expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
import { nameOf } from './method-ids.mjs';
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
/** The same bridge `playwright-cli` loads, so an exploratory session and
|
/** The same bridge `playwright-cli` loads, so an exploratory session and
|
||||||
* a committed spec see an identical page. */
|
* a committed spec see an identical page. */
|
||||||
const INIT_SCRIPT = resolve(here, '../../.playwright/init-events.js');
|
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`).
|
* 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"
|
* v3 rejects a bad call rather than never firing its callback the way
|
||||||
* and never fire the callback, so an unguarded call hangs until the
|
* v2 did: a wrong argument type comes back as a TypeError naming the
|
||||||
* whole spec times out with no clue why. This fails in seconds and
|
* argument, a wrong count as `expects 4 arguments, got 3`, an unknown
|
||||||
* says where to look.
|
* 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>(
|
export async function callBinding<T = unknown>(
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -81,6 +96,21 @@ export async function callBinding<T = unknown>(
|
|||||||
) as Promise<T>;
|
) 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). */
|
/** Thin client for the dev-only /__test/ surface (backend/testctl). */
|
||||||
export class TestCtl {
|
export class TestCtl {
|
||||||
constructor(private readonly baseURL: string) {}
|
constructor(private readonly baseURL: string) {}
|
||||||
@@ -166,7 +196,20 @@ declare global {
|
|||||||
): Promise<YjEvent>;
|
): Promise<YjEvent>;
|
||||||
ready(timeoutMs?: number): Promise<boolean>;
|
ready(timeoutMs?: number): Promise<boolean>;
|
||||||
call(path: string, args?: unknown[], timeoutMs?: number): Promise<any>;
|
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,
|
"noEmit": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["node"],
|
"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
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// 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
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
export function GetShelves() {
|
import * as Config from "./config.js";
|
||||||
return window['go']['home']['Service']['GetShelves']();
|
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 { registerBundledIcons } from './src/icons';
|
||||||
import { queueStore } from '@store/queue-store';
|
import { queueStore } from '@store/queue-store';
|
||||||
import { searchStore } from '@store/search-store';
|
import { searchStore } from '@store/search-store';
|
||||||
import * as Player from '@go/player/Player';
|
import * as Player from '@go/player/player.js';
|
||||||
import * as Queue from '@go/queue/Queue';
|
import * as Queue from '@go/queue/queue.js';
|
||||||
import { GetDefaultPage } from '@go/config/Config';
|
import { GetDefaultPage } from '@go/config/config.js';
|
||||||
// Importing the theme store triggers initialization: it fetches the saved
|
// Importing the theme store triggers initialization: it fetches the saved
|
||||||
// theme from the backend and applies CSS custom properties to :root.
|
// theme from the backend and applies CSS custom properties to :root.
|
||||||
import '@store/theme-store';
|
import '@store/theme-store';
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:dev": "vite build --mode development",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@awesome.me/webawesome": "^3.2.1",
|
"@awesome.me/webawesome": "^3.2.1",
|
||||||
"@lit-labs/signals": "^0.2.0",
|
"@lit-labs/signals": "^0.2.0",
|
||||||
"@lit-labs/virtualizer": "^2.1.1",
|
"@lit-labs/virtualizer": "^2.1.1",
|
||||||
|
"@wailsio/runtime": "3.0.0-beta.8",
|
||||||
"lit": "^3.2.1"
|
"lit": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+8
@@ -17,6 +17,9 @@ importers:
|
|||||||
'@lit-labs/virtualizer':
|
'@lit-labs/virtualizer':
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
'@wailsio/runtime':
|
||||||
|
specifier: 3.0.0-beta.8
|
||||||
|
version: 3.0.0-beta.8
|
||||||
lit:
|
lit:
|
||||||
specifier: ^3.2.1
|
specifier: ^3.2.1
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@@ -543,6 +546,9 @@ packages:
|
|||||||
'@vscode/web-custom-data@0.4.13':
|
'@vscode/web-custom-data@0.4.13':
|
||||||
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
|
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
|
||||||
|
|
||||||
|
'@wailsio/runtime@3.0.0-beta.8':
|
||||||
|
resolution: {integrity: sha512-c9PZJcOR9z1a6cxtBS2q5cygNajlxDE8Oxv/vvcTFYRbZSoOGBszq4uzlkPTOd0Bot1xkM81jnpaBgKWRpBh2g==}
|
||||||
|
|
||||||
ajv@8.18.0:
|
ajv@8.18.0:
|
||||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||||
|
|
||||||
@@ -1734,6 +1740,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vscode/web-custom-data@0.4.13': {}
|
'@vscode/web-custom-data@0.4.13': {}
|
||||||
|
|
||||||
|
'@wailsio/runtime@3.0.0-beta.8': {}
|
||||||
|
|
||||||
ajv@8.18.0:
|
ajv@8.18.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import {
|
|||||||
property,
|
property,
|
||||||
state,
|
state,
|
||||||
} from 'lit/decorators.js';
|
} 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 { LibraryController } from '@store/controllers/library-controller';
|
||||||
import {
|
import {
|
||||||
GetArtistImageURL,
|
GetArtistImageURL,
|
||||||
GetArtistImageCachedPath,
|
GetArtistImageCachedPath,
|
||||||
GetArtistMBID,
|
GetArtistMBID,
|
||||||
} from '@go/explore/Service';
|
} from '@go/explore/service.js';
|
||||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
import '@components/cover-grid/cover-grid.js';
|
import '@components/cover-grid/cover-grid.js';
|
||||||
import { designTokens } from '../../styles/tokens.css';
|
import { designTokens } from '../../styles/tokens.css';
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
GetAlbumsByArtist,
|
GetAlbumsByArtist,
|
||||||
GetAlbumsByArtistByLibrary,
|
GetAlbumsByArtistByLibrary,
|
||||||
GetFilePathsByAlbums,
|
GetFilePathsByAlbums,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/library.js';
|
||||||
import { library } from '@go/models';
|
import * as library from '@go/library/models.js';
|
||||||
import { LibraryController } from '@store/controllers/library-controller';
|
import { LibraryController } from '@store/controllers/library-controller';
|
||||||
import { libraryStore } from '@store/library-store';
|
import { libraryStore } from '@store/library-store';
|
||||||
import { SearchController } from '@store/controllers/search-controller';
|
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 type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||||
import '@components/playlist-picker/playlist-picker.js';
|
import '@components/playlist-picker/playlist-picker.js';
|
||||||
|
import { dict, list } from '@utils/binding';
|
||||||
|
|
||||||
/** Pixels to change card width per scroll tick. */
|
/** Pixels to change card width per scroll tick. */
|
||||||
const ZOOM_STEP = 16;
|
const ZOOM_STEP = 16;
|
||||||
@@ -1055,18 +1056,21 @@ export class ArtistsView
|
|||||||
const libId =
|
const libId =
|
||||||
this.libraryCtrl.selectedLibraryId;
|
this.libraryCtrl.selectedLibraryId;
|
||||||
|
|
||||||
const albums = libId !== null
|
const albums = await list(
|
||||||
? await GetAlbumsByArtistByLibrary(
|
libId !== null
|
||||||
artist.ID,
|
? GetAlbumsByArtistByLibrary(
|
||||||
libId,
|
artist.ID,
|
||||||
)
|
libId,
|
||||||
: await GetAlbumsByArtist(artist.ID);
|
)
|
||||||
|
: GetAlbumsByArtist(artist.ID),
|
||||||
|
);
|
||||||
|
|
||||||
const byAlbum =
|
const byAlbum = await dict(
|
||||||
await GetFilePathsByAlbums(
|
GetFilePathsByAlbums(
|
||||||
albums.map((a) => a.ID),
|
albums.map((a) => a.ID),
|
||||||
libId ?? 0,
|
libId ?? 0,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const allPaths: string[] = [];
|
const allPaths: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
ClearCompletedEntries,
|
ClearCompletedEntries,
|
||||||
SearchCandidates,
|
SearchCandidates,
|
||||||
SelectSearchCandidate,
|
SelectSearchCandidate,
|
||||||
} from '@go/autotagservice/Service';
|
} from '@go/autotagservice/service.js';
|
||||||
import type { autotagservice } from '@go/models';
|
import type * as autotagservice from '@go/autotagservice/models.js';
|
||||||
import { EventsOn } from '@runtime/runtime';
|
import { EventsOn } from '@runtime/runtime';
|
||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
import { inlineDiff, normalizeStrict, isCosmeticDiff } from '../../utils/text-diff';
|
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 { ViewLifecycleMixin } from '../../utils/view-lifecycle';
|
||||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||||
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
||||||
|
import { list } from '@utils/binding';
|
||||||
|
|
||||||
type PendingItem = autotagservice.PendingItem;
|
type PendingItem = autotagservice.PendingItem;
|
||||||
type ScoreView = autotagservice.ScoreView;
|
type ScoreView = autotagservice.ScoreView;
|
||||||
@@ -1483,8 +1484,10 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
this.searchError = '';
|
this.searchError = '';
|
||||||
this.searchRan = true;
|
this.searchRan = true;
|
||||||
try {
|
try {
|
||||||
this.searchResults = await SearchCandidates(
|
this.searchResults = await list(
|
||||||
this.searchKind, query, this.searchArtist.trim(),
|
SearchCandidates(
|
||||||
|
this.searchKind, query, this.searchArtist.trim(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('autotag: candidate search failed', err);
|
console.error('autotag: candidate search failed', err);
|
||||||
@@ -1755,12 +1758,12 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private topScore(): number {
|
private topScore(): number {
|
||||||
if (!this.score || this.score.candidates.length === 0) return 0;
|
if (!this.score || (this.score.candidates ?? []).length === 0) return 0;
|
||||||
return this.score.candidates[0]?.score ?? 0;
|
return (this.score.candidates ?? [])[0]?.score ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onApply(): Promise<void> {
|
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)) {
|
if (!this.hasLibraryWarningBeenAcked(this.current.libraryId)) {
|
||||||
await this.confirmWarningThenApply();
|
await this.confirmWarningThenApply();
|
||||||
|
|
||||||
@@ -1773,7 +1776,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
private async executeApply(): Promise<void> {
|
private async executeApply(): Promise<void> {
|
||||||
if (!this.current || !this.score) return;
|
if (!this.current || !this.score) return;
|
||||||
|
|
||||||
const cand = this.score.candidates[this.selectedCandidateIdx];
|
const cand = (this.score.candidates ?? [])[this.selectedCandidateIdx];
|
||||||
if (!cand) {
|
if (!cand) {
|
||||||
this.errorMessage = 'No candidate selected.';
|
this.errorMessage = 'No candidate selected.';
|
||||||
return;
|
return;
|
||||||
@@ -1781,7 +1784,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
|
|
||||||
const groupKey = this.current.groupKey;
|
const groupKey = this.current.groupKey;
|
||||||
const mbid = cand.releaseMbid || cand.releaseGroupMbid;
|
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
|
// Pre-mark the folder as running so the sidebar icon flips
|
||||||
// to the progress ring immediately — the Started event will
|
// to the progress ring immediately — the Started event will
|
||||||
@@ -1955,16 +1958,16 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
*/
|
*/
|
||||||
private currentCandidate(): CandidateView | null {
|
private currentCandidate(): CandidateView | null {
|
||||||
if (!this.score) return 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> {
|
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;
|
this.selectedCandidateIdx = idx;
|
||||||
// Lazy-load cover art for non-top candidates. The backend
|
// Lazy-load cover art for non-top candidates. The backend
|
||||||
// populates art for the top one eagerly; everything else
|
// populates art for the top one eagerly; everything else
|
||||||
// arrives empty until the user picks it.
|
// arrives empty until the user picks it.
|
||||||
const cand = this.score.candidates[idx]!;
|
const cand = (this.score.candidates ?? [])[idx]!;
|
||||||
if (!cand.coverArtUrl) {
|
if (!cand.coverArtUrl) {
|
||||||
try {
|
try {
|
||||||
const url = await GetCandidateCoverArt(
|
const url = await GetCandidateCoverArt(
|
||||||
@@ -2252,7 +2255,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
if (!cluster || cluster.candidates.length <= 1) return nothing;
|
if (!cluster || cluster.candidates.length <= 1) return nothing;
|
||||||
|
|
||||||
const editions = cluster.candidates
|
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);
|
.filter((e) => e.idx >= 0);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -2316,11 +2319,11 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
// the score forgives, the UI mutes.
|
// the score forgives, the UI mutes.
|
||||||
const SUBTLE_LENGTH_MAX_MS = 5000;
|
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') {
|
if (a.status === 'matched' || a.status === 'mismatched') {
|
||||||
paired++;
|
paired++;
|
||||||
const local = a.localIndex >= 0
|
const local = a.localIndex >= 0
|
||||||
? this.score?.localTracks[a.localIndex] ?? null
|
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||||
: null;
|
: null;
|
||||||
if (local) {
|
if (local) {
|
||||||
if (local.title !== a.candidateTitle) {
|
if (local.title !== a.candidateTitle) {
|
||||||
@@ -2374,7 +2377,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
missingTitles.push(a.candidateTitle || '(untitled)');
|
missingTitles.push(a.candidateTitle || '(untitled)');
|
||||||
} else if (a.status === 'unmatched') {
|
} else if (a.status === 'unmatched') {
|
||||||
const local = a.localIndex >= 0
|
const local = a.localIndex >= 0
|
||||||
? this.score?.localTracks[a.localIndex] ?? null
|
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||||
: null;
|
: null;
|
||||||
extraTitles.push(a.localTitle || local?.title || '(untitled)');
|
extraTitles.push(a.localTitle || local?.title || '(untitled)');
|
||||||
}
|
}
|
||||||
@@ -2659,7 +2662,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderLowConfidenceBanner(clusters: VersionCluster[]) {
|
private renderLowConfidenceBanner(clusters: VersionCluster[]) {
|
||||||
const top = this.score?.candidates[0];
|
const top = (this.score?.candidates ?? [])[0];
|
||||||
if (!top) return nothing;
|
if (!top) return nothing;
|
||||||
if (top.score >= LOW_CONFIDENCE_THRESHOLD) return nothing;
|
if (top.score >= LOW_CONFIDENCE_THRESHOLD) return nothing;
|
||||||
if (clusters.length < 2) 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.
|
* renderMatchDetails, so each column shows its own values plainly.
|
||||||
*/
|
*/
|
||||||
private renderComparison(cand: CandidateView, clusters: VersionCluster[] = []) {
|
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
|
// localIndex -> its alignment, so a folder row knows whether it
|
||||||
// paired and (if so) how confidently.
|
// paired and (if so) how confidently.
|
||||||
const alignByLocal = new Map<number, AlignmentView>();
|
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);
|
if (a.localIndex >= 0) alignByLocal.set(a.localIndex, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Candidate side: every alignment that has a candidate track
|
// Candidate side: every alignment that has a candidate track
|
||||||
// (paired or missing-from-folder), in candidate order.
|
// (paired or missing-from-folder), in candidate order.
|
||||||
const candRows = cand.alignments
|
const candRows = (cand.alignments ?? [])
|
||||||
.filter((a) => a.status !== 'unmatched' && a.candidatePosition > 0)
|
.filter((a) => a.status !== 'unmatched' && a.candidatePosition > 0)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((x, y) =>
|
.sort((x, y) =>
|
||||||
@@ -2856,7 +2859,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
|||||||
${clusters.map((cluster) => {
|
${clusters.map((cluster) => {
|
||||||
const best = cluster.candidates[cluster.bestIdx]!;
|
const best = cluster.candidates[cluster.bestIdx]!;
|
||||||
const active = cluster.candidates.includes(activeCand);
|
const active = cluster.candidates.includes(activeCand);
|
||||||
const idx = this.score?.candidates.indexOf(best) ?? -1;
|
const idx = (this.score?.candidates ?? []).indexOf(best) ?? -1;
|
||||||
return html`
|
return html`
|
||||||
<div class="cand-chip ${active ? 'selected' : ''}"
|
<div class="cand-chip ${active ? 'selected' : ''}"
|
||||||
title=${cluster.label}
|
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`
|
return html`
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="empty">
|
<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>`;
|
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`
|
return html`
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { LitElement, html, css, nothing } from 'lit';
|
|||||||
import { customElement, state } from 'lit/decorators.js';
|
import { customElement, state } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { EventsOn } from '@runtime/runtime';
|
import { EventsOn } from '@runtime/runtime';
|
||||||
import type { explore } from '@go/models';
|
import type * as explore from '@go/explore/models.js';
|
||||||
import {
|
import {
|
||||||
AddLibrary,
|
AddLibrary,
|
||||||
RenameLibrary,
|
RenameLibrary,
|
||||||
RemoveLibrary,
|
RemoveLibrary,
|
||||||
GetRemovalImpact,
|
GetRemovalImpact,
|
||||||
GetAllLibrariesWithTrackCounts,
|
GetAllLibrariesWithTrackCounts,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/library.js';
|
||||||
import {
|
import {
|
||||||
GetScanConcurrency,
|
GetScanConcurrency,
|
||||||
SetScanConcurrency,
|
SetScanConcurrency,
|
||||||
@@ -17,17 +17,17 @@ import {
|
|||||||
SetDefaultPage,
|
SetDefaultPage,
|
||||||
GetQueueFallback,
|
GetQueueFallback,
|
||||||
SetQueueFallback,
|
SetQueueFallback,
|
||||||
} from '@go/config/Config';
|
} from '@go/config/config.js';
|
||||||
import { GetIndexStatus } from '@go/explore/Service';
|
import { GetIndexStatus } from '@go/explore/service.js';
|
||||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||||
import { notificationStore } from '@store/notification-store';
|
import { notificationStore } from '@store/notification-store';
|
||||||
import { describeError, explainError } from '@utils/describe-error';
|
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 { ThemeController } from '@store/controllers/theme-controller';
|
||||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import { GetAllPlaylists } from '@go/playlist/Service';
|
import { GetAllPlaylists } from '@go/playlist/service.js';
|
||||||
import type { playlist } from '@go/models';
|
import type * as playlist from '@go/playlist/models.js';
|
||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
import {
|
import {
|
||||||
SHORTCUT_CATEGORIES,
|
SHORTCUT_CATEGORIES,
|
||||||
@@ -49,6 +49,7 @@ import './shortcut-capture';
|
|||||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||||
import { shortcutsStore } from '../../store/shortcuts-store';
|
import { shortcutsStore } from '../../store/shortcuts-store';
|
||||||
import { ShortcutsController } from '../../store/controllers/shortcuts-controller';
|
import { ShortcutsController } from '../../store/controllers/shortcuts-controller';
|
||||||
|
import { list } from '@utils/binding';
|
||||||
|
|
||||||
const SCROLL_STORAGE_KEY = 'yj-now-playing-scroll-mode';
|
const SCROLL_STORAGE_KEY = 'yj-now-playing-scroll-mode';
|
||||||
const SCROLL_CHANGE_EVENT = 'yj-scroll-mode-changed';
|
const SCROLL_CHANGE_EVENT = 'yj-scroll-mode-changed';
|
||||||
@@ -1012,9 +1013,9 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
|||||||
const ok = await confirmAction({
|
const ok = await confirmAction({
|
||||||
title: 'Remove library',
|
title: 'Remove library',
|
||||||
message: `Remove “${libName}”?`,
|
message: `Remove “${libName}”?`,
|
||||||
impact: `This deletes ${impact.trackCount} tracks, affects `
|
impact: `This deletes ${impact?.trackCount ?? 0} tracks, affects `
|
||||||
+ `${impact.playlistsAffected} playlists and removes `
|
+ `${impact?.playlistsAffected ?? 0} playlists and removes `
|
||||||
+ `${impact.queueItemCount} queue items.`,
|
+ `${impact?.queueItemCount ?? 0} queue items.`,
|
||||||
confirmLabel: 'Remove',
|
confirmLabel: 'Remove',
|
||||||
danger: true,
|
danger: true,
|
||||||
});
|
});
|
||||||
@@ -1187,8 +1188,7 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
|||||||
|
|
||||||
private async loadPlaylists(): Promise<void> {
|
private async loadPlaylists(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.playlists =
|
this.playlists = await list(GetAllPlaylists());
|
||||||
await GetAllPlaylists();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
'Failed to load playlists:',
|
'Failed to load playlists:',
|
||||||
@@ -1456,10 +1456,10 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
|||||||
<span class="index-stat">updated ${this.timeAgo(s.lastBuilt)}</span>`
|
<span class="index-stat">updated ${this.timeAgo(s.lastBuilt)}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</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`
|
? html`
|
||||||
<div class="index-tiers">
|
<div class="index-tiers">
|
||||||
${s.tiers.map(
|
${(s.tiers ?? []).map(
|
||||||
(t) => html`
|
(t) => html`
|
||||||
<div class="index-tier">
|
<div class="index-tier">
|
||||||
<span class="tier-icon">${this.tierIcon(t.state)}</span>
|
<span class="tier-icon">${this.tierIcon(t.state)}</span>
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import type {
|
|||||||
ProviderField,
|
ProviderField,
|
||||||
} from '@store/download-store';
|
} from '@store/download-store';
|
||||||
import { downloadStore } from '@store/download-store';
|
import { downloadStore } from '@store/download-store';
|
||||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||||
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/Config';
|
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/config.js';
|
||||||
import { SetPreferences } from '@go/download/Service';
|
import { SetPreferences } from '@go/download/service.js';
|
||||||
import type { download } from '@go/models';
|
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 { describeError, explainError } from '@utils/describe-error';
|
||||||
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
|
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
|
||||||
import './config-section';
|
import './config-section';
|
||||||
@@ -28,15 +30,15 @@ import './config-section';
|
|||||||
* deliberately excluded — it names "no format detected", not a format a
|
* deliberately excluded — it names "no format detected", not a format a
|
||||||
* user could opt into.
|
* user could opt into.
|
||||||
*/
|
*/
|
||||||
const AUTO_DOWNLOAD_FORMATS: { value: string; label: string }[] = [
|
const AUTO_DOWNLOAD_FORMATS: { value: Format; label: string }[] = [
|
||||||
{ value: 'flac', label: 'FLAC' },
|
{ value: Format.FormatFLAC, label: 'FLAC' },
|
||||||
{ value: 'alac', label: 'ALAC' },
|
{ value: Format.FormatALAC, label: 'ALAC' },
|
||||||
{ value: 'wav', label: 'WAV' },
|
{ value: Format.FormatWAV, label: 'WAV' },
|
||||||
{ value: 'mp3', label: 'MP3' },
|
{ value: Format.FormatMP3, label: 'MP3' },
|
||||||
{ value: 'aac', label: 'AAC' },
|
{ value: Format.FormatAAC, label: 'AAC' },
|
||||||
{ value: 'ogg', label: 'OGG' },
|
{ value: Format.FormatOGG, label: 'OGG' },
|
||||||
{ value: 'opus', label: 'Opus' },
|
{ value: Format.FormatOpus, label: 'Opus' },
|
||||||
{ value: 'wma', label: 'WMA' },
|
{ 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
|
// Secrets are never sent back to the frontend, so their fields
|
||||||
// start blank; a blank secret on save means "leave it alone"
|
// start blank; a blank secret on save means "leave it alone"
|
||||||
// rather than "clear it".
|
// rather than "clear it".
|
||||||
this.draft = { ...(provider.settings ?? {}) };
|
this.draft = compact(provider.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelEdit = () => {
|
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 current = this.prefs.allowedFormats ?? [];
|
||||||
const allowedFormats = checked
|
const allowedFormats = checked
|
||||||
? [...current, format]
|
? [...current, format]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LitElement, html, svg, css } from 'lit';
|
import { LitElement, html, svg, css } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.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 { PlayerController } from '@store/controllers/player-controller';
|
||||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import { formatMilliseconds } from '@utils/time';
|
import { formatMilliseconds } from '@utils/time';
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import {
|
|||||||
GetAlbumTracks,
|
GetAlbumTracks,
|
||||||
GetAlbumTracksByLibrary,
|
GetAlbumTracksByLibrary,
|
||||||
GetFilePathsByAlbums,
|
GetFilePathsByAlbums,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/library.js';
|
||||||
import { libraryStore } from '@store/library-store';
|
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';
|
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,9 +59,11 @@ export class AlbumSelectionManager {
|
|||||||
const libId =
|
const libId =
|
||||||
libraryStore.getSelectedLibraryId();
|
libraryStore.getSelectedLibraryId();
|
||||||
|
|
||||||
return libId !== null
|
return list(
|
||||||
? GetAlbumTracksByLibrary(albumId, libId)
|
libId !== null
|
||||||
: GetAlbumTracks(albumId);
|
? GetAlbumTracksByLibrary(albumId, libId)
|
||||||
|
: GetAlbumTracks(albumId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +88,7 @@ export class AlbumSelectionManager {
|
|||||||
const libId =
|
const libId =
|
||||||
libraryStore.getSelectedLibraryId();
|
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
|
* Discriminated context menu target so we know whether the
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
|||||||
import {
|
import {
|
||||||
GetAlbumTracks,
|
GetAlbumTracks,
|
||||||
GetAlbumTracksByLibrary,
|
GetAlbumTracksByLibrary,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/library.js';
|
||||||
import { library } from '@go/models';
|
import * as library from '@go/library/models.js';
|
||||||
import { LibraryController } from '@store/controllers/library-controller';
|
import { LibraryController } from '@store/controllers/library-controller';
|
||||||
import { SearchController } from '@store/controllers/search-controller';
|
import { SearchController } from '@store/controllers/search-controller';
|
||||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||||
@@ -74,6 +74,7 @@ import type {
|
|||||||
GridEntry,
|
GridEntry,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
} from './cover-grid-types.js';
|
} from './cover-grid-types.js';
|
||||||
|
import { list } from '@utils/binding';
|
||||||
|
|
||||||
@customElement('cover-grid')
|
@customElement('cover-grid')
|
||||||
export class CoverGrid
|
export class CoverGrid
|
||||||
@@ -933,12 +934,14 @@ export class CoverGrid
|
|||||||
const libId =
|
const libId =
|
||||||
this.libraryCtrl.selectedLibraryId;
|
this.libraryCtrl.selectedLibraryId;
|
||||||
|
|
||||||
const tracks = libId !== null
|
const tracks = await list(
|
||||||
? await GetAlbumTracksByLibrary(
|
libId !== null
|
||||||
album.ID,
|
? GetAlbumTracksByLibrary(
|
||||||
libId,
|
album.ID,
|
||||||
)
|
libId,
|
||||||
: await GetAlbumTracks(album.ID);
|
)
|
||||||
|
: GetAlbumTracks(album.ID),
|
||||||
|
);
|
||||||
|
|
||||||
if (this.expandedAlbumId === album.ID) {
|
if (this.expandedAlbumId === album.ID) {
|
||||||
this.expandedTracks = tracks;
|
this.expandedTracks = tracks;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LitElement } from 'lit';
|
import type { LitElement } from 'lit';
|
||||||
import type { LitVirtualizer } from '@lit-labs/virtualizer';
|
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 { LibraryController } from '@store/controllers/library-controller';
|
||||||
|
|
||||||
import type { GridEntry } from './cover-grid-types.js';
|
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 { designTokens } from '../../styles/tokens.css';
|
||||||
import type { DownloadCandidate } from '@store/download-store';
|
import type { DownloadCandidate } from '@store/download-store';
|
||||||
import { downloadStore } 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 './candidate-row';
|
||||||
import { explainError } from '@utils/describe-error';
|
import { explainError } from '@utils/describe-error';
|
||||||
import { nameDialogsIn } from '@utils/name-dialog';
|
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. */
|
/** Provider/progress summary for a download's second line. */
|
||||||
private downloadDetail(view: DownloadRecord): string {
|
private downloadDetail(view: DownloadRecord): string {
|
||||||
const providers = [
|
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[] = [];
|
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/dialog/dialog.js';
|
||||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
import '@awesome.me/webawesome/dist/components/switch/switch.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 { formatMilliseconds } from '@utils/time';
|
||||||
import { nameDialogsIn } from '@utils/name-dialog';
|
import { nameDialogsIn } from '@utils/name-dialog';
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import {
|
|||||||
LookupReleaseGroup,
|
LookupReleaseGroup,
|
||||||
BrowseReleases,
|
BrowseReleases,
|
||||||
GetThumbnail,
|
GetThumbnail,
|
||||||
} from '@go/explore/Service';
|
} from '@go/explore/service.js';
|
||||||
import {
|
import {
|
||||||
GetAlbumTracks,
|
GetAlbumTracks,
|
||||||
GetAlbumCompleteness,
|
GetAlbumCompleteness,
|
||||||
GetFilePathsByAlbums,
|
GetFilePathsByAlbums,
|
||||||
GetFilePathsByRecordingMBIDs,
|
GetFilePathsByRecordingMBIDs,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/library.js';
|
||||||
import { library } from '@go/models';
|
import * as library from '@go/library/models.js';
|
||||||
import type { download, explore } from '@go/models';
|
import type * as download from '@go/download/models.js';
|
||||||
|
import type * as explore from '@go/explore/models.js';
|
||||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||||
type MBRelease = explore.MBRelease;
|
type MBRelease = explore.MBRelease;
|
||||||
type MBTrack = explore.MBTrack;
|
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 '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
import type WaPopup from '@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 '@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.
|
* 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.
|
// to an empty set and the Play button silently does nothing.
|
||||||
// That is exactly what the first version of this did.
|
// That is exactly what the first version of this did.
|
||||||
if (this.localAlbumId > 0) {
|
if (this.localAlbumId > 0) {
|
||||||
const byAlbum = await GetFilePathsByAlbums(
|
const byAlbum = await dict(
|
||||||
[this.localAlbumId],
|
GetFilePathsByAlbums([this.localAlbumId], libraryID),
|
||||||
libraryID,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return byAlbum[this.localAlbumId] ?? [];
|
return byAlbum[this.localAlbumId] ?? [];
|
||||||
@@ -2266,7 +2267,9 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
|||||||
|
|
||||||
if (mbids.length === 0) return [];
|
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
|
// Walked in tracklist order rather than flattened, because the
|
||||||
// grouping is what lets the caller keep its own order. A
|
// 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;
|
if (!track.inLibrary || !track.mbid) return null;
|
||||||
|
|
||||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
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;
|
return byMBID[track.mbid]?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user