Android: leave the volume to the system, and give Now Playing 29px back #174

Merged
logan merged 4 commits from 64-android-system-volume into main 2026-08-21 05:50:07 +00:00
Collaborator

Closes #64 and #172. Reordering argued on #73 first — #64 is a Phase 4
"independent" item and Phase 3 still has #58 and #51 outstanding; #172's
own Direction names #64 as the one pure-gain vertical win on Now Playing
and #51 is the verification pass that should follow it.

Commits

commit issue what
867ced8 feat(player): leave the volume to the system where the system owns it #64 the backend half: level pinned, every route to a level refused, nothing persisted, ducking untouched
2be6fb3 feat(player): draw no volume control where there is no volume #64, #172 the frontend half: volume-control asks the player and renders nothing
b43172a docs(player): record who owns the volume, and what it gave back CLAUDE.md and .planning/NOTES.md

The decision that mattered

The issue's first Finding says volume-control "already stands down at
narrow widths". That is true of one of its two copies — the bar's,
by width — and now-playing-view renders one that is visible on a phone
deliberately, with a comment saying so. So the real question was what
predicate replaces that comment, and three options were on the issue.

It could not be a width. The backend half of the same issue pins the
level at maximum, and a width rule then leaves an inert slider on the
platform the issue is about: an Android tablet at >=600px gets the
bottom bar and its copy. That is a control that cannot act, which
library-status-indicator already settled is worse than none. The same
rule is wrong the other way below 600px, where a narrow desktop window
has no hardware keys to fall back on.

So the frontend asks the player. The predicate is named after the
capability, not the platform
SystemOwnsVolume — which is what
makes it testable: only platformOwnsVolume is behind a build tag, in
two files that declare nothing else, and everything else is decided
against a field a Go test sets either way. That is mediacontrols'
split with androidpayload.go's reasoning. The frontend's absent
branch is exercised in the component tier with a stubbed binding, on an
ordinary Linux runner.

index.css keeps its phone rule for the bar, restated as what a
stylesheet can actually answer: room. Five controls and a slider do not
fit a 360px bar.

Mute is refused too — it is a level of zero by another name, and with no
control rendered it would be the one state on such a platform the user
could not get out of. Persistence round-trips the stored level rather
than recording the synthetic maximum; the alternative, a second query
omitting the column, buys nothing and is a second write path.

SetDuck is untouched, which is the issue's second Finding. It
applies its attenuation by re-applying the user's level through
setVolumeLocked, so pinning that level leaves the arithmetic exactly
as it was.

#172, re-measured

Measured at 424x439 by flipping platformOwnsVolume to true in the
!android file and rebuilding — the real binding, the real store, the
real component, everything except the tag itself.

element before after
header 48 48
album art 39 68
title / artist / album 63 63
transport 172 143
— seek bar 19 19
— player-controls 116 116
— volume-control 21 0

29px — the 21px control plus the 8px flex gap a hidden box stops
drawing. #172's "~30px of pure gain", confirmed. The art is 74% larger
and no longer the smallest element on the screen.

It is still the second-smallest, and that is #51's question, which is
what #172's Direction says
: "decide whether the art gets a floor …
which is #51's question, and this issue is evidence for it rather than a
competing answer." Closing it as that evidence rather than leaving two
issues asking the same thing.

Verification

Run, and what each was for:

  • make test (3 build configurations) — the four new Go tests, and
    TestSetDuck still passing beside them.
  • make ui-test — 976 tests, 89 files, including the two new ones.
  • make e2ethe whole suite, 220 tests, chromium. Not a formality:
    the last three sessions each broke something adjacent, and both
    existing volume assertions (bottom-bar.spec.ts:114,
    phone-shell.spec.ts:238) are what catch an inverted polarity.
  • make lint (3 configurations), make css-check, make bindings-check,
    make skill-check, tsc --noEmit in both frontend/ and e2e/.
    Formalities, but bindings-check is not — this adds a bound method.

Proving the specs bite. volume-ownership.test.ts's two tests were
run against the reverted volume-control.ts and both fail, on the
two different halves they assert:

× renders no control at all, and no empty shadow root to find
  expected [ <button …> ] to have a length of +0 but got 1
× leaves the rest of the phone transport alone
  a mounted volume-control is not hidden: expected false to be true

The Go tests are all new behaviour and so reproduce trivially;
TestAnUnpinnedPlayerStillChangesItsVolume exists because without it
the pinned test passes on a player that refuses everything, which is
what a mis-wired field would produce.

The e2e addition asserts the mechanism this tier can see — that the
control's presence follows the player's answer — and cannot fail on the
old build, since the binding did not exist. It is a guard, not a
reproduction, and the file says so.

What I could not verify at the time. No device was attached when this was written. One was afterwards, and three of the four items below are now closed — see the device-verification comment. What remains is the duck, and for a sharper reason: it is unreachable above API 25 by design, so no modern phone can test it.

Left as written, because the reasoning is what justified shipping without a device:

  • The build-tagged constant itself is compiled by nothing here.
    TestPlatformVolumeOwnershipIsDeclaredOncePerPlatform sweeps both
    files' source instead, which catches deletion and an edited value but
    is not a compiler.
  • Ducking against a real audio-focus change is device-only.
    TestSystemVolumeStillDucks checks the arithmetic — attenuate,
    restore, and a refused SetVolume not disturbing the offset — which
    is the property, not the integration.
  • The one behaviour change a desktop user could see is none: the
    binding answers false there and every existing volume spec passes
    unchanged.

Screenshots

424x439, before and after, in a comment below.

Closes #64 and #172. Reordering argued on #73 first — #64 is a Phase 4 "independent" item and Phase 3 still has #58 and #51 outstanding; #172's own Direction names #64 as the one pure-gain vertical win on Now Playing and #51 is the verification pass that should follow it. ## Commits | commit | issue | what | |---|---|---| | `867ced8` feat(player): leave the volume to the system where the system owns it | #64 | the backend half: level pinned, every route to a level refused, nothing persisted, ducking untouched | | `2be6fb3` feat(player): draw no volume control where there is no volume | #64, #172 | the frontend half: `volume-control` asks the player and renders nothing | | `b43172a` docs(player): record who owns the volume, and what it gave back | — | `CLAUDE.md` and `.planning/NOTES.md` | ## The decision that mattered The issue's first Finding says `volume-control` "already stands down at narrow widths". That is true of **one** of its two copies — the bar's, by width — and `now-playing-view` renders one that is visible on a phone *deliberately*, with a comment saying so. So the real question was what predicate replaces that comment, and three options were on the issue. **It could not be a width.** The backend half of the same issue pins the level at maximum, and a width rule then leaves an inert slider on the platform the issue is about: an Android **tablet** at >=600px gets the bottom bar and its copy. That is a control that cannot act, which `library-status-indicator` already settled is worse than none. The same rule is wrong the other way below 600px, where a narrow desktop window has no hardware keys to fall back on. So the frontend asks the player. **The predicate is named after the capability, not the platform** — `SystemOwnsVolume` — which is what makes it testable: only `platformOwnsVolume` is behind a build tag, in two files that declare nothing else, and everything else is decided against a field a Go test sets either way. That is `mediacontrols`' split with `androidpayload.go`'s reasoning. The frontend's *absent* branch is exercised in the component tier with a stubbed binding, on an ordinary Linux runner. `index.css` keeps its phone rule for the bar, restated as what a stylesheet can actually answer: room. Five controls and a slider do not fit a 360px bar. Mute is refused too — it is a level of zero by another name, and with no control rendered it would be the one state on such a platform the user could not get out of. Persistence round-trips the stored level rather than recording the synthetic maximum; the alternative, a second query omitting the column, buys nothing and is a second write path. **`SetDuck` is untouched**, which is the issue's second Finding. It applies its attenuation by re-applying the *user's* level through `setVolumeLocked`, so pinning that level leaves the arithmetic exactly as it was. ## #172, re-measured Measured at 424x439 by flipping `platformOwnsVolume` to `true` in the `!android` file and rebuilding — the real binding, the real store, the real component, everything except the tag itself. | element | before | after | |---|---|---| | header | 48 | 48 | | **album art** | **39** | **68** | | title / artist / album | 63 | 63 | | **transport** | **172** | **143** | | — seek bar | 19 | 19 | | — player-controls | 116 | 116 | | — volume-control | 21 | **0** | **29px** — the 21px control plus the 8px flex gap a hidden box stops drawing. #172's "~30px of pure gain", confirmed. The art is 74% larger and no longer the smallest element on the screen. It is still the second-smallest, and that is **#51's question, which is what #172's Direction says**: "decide whether the art gets a floor … which is #51's question, and this issue is evidence for it rather than a competing answer." Closing it as that evidence rather than leaving two issues asking the same thing. ## Verification Run, and what each was for: - `make test` (3 build configurations) — the four new Go tests, and `TestSetDuck` still passing beside them. - `make ui-test` — 976 tests, 89 files, including the two new ones. - `make e2e` — **the whole suite, 220 tests, chromium**. Not a formality: the last three sessions each broke something adjacent, and both existing volume assertions (`bottom-bar.spec.ts:114`, `phone-shell.spec.ts:238`) are what catch an inverted polarity. - `make lint` (3 configurations), `make css-check`, `make bindings-check`, `make skill-check`, `tsc --noEmit` in **both** `frontend/` and `e2e/`. Formalities, but `bindings-check` is not — this adds a bound method. **Proving the specs bite.** `volume-ownership.test.ts`'s two tests were run against the reverted `volume-control.ts` and **both fail**, on the two different halves they assert: ``` × renders no control at all, and no empty shadow root to find expected [ <button …> ] to have a length of +0 but got 1 × leaves the rest of the phone transport alone a mounted volume-control is not hidden: expected false to be true ``` The Go tests are all new behaviour and so reproduce trivially; `TestAnUnpinnedPlayerStillChangesItsVolume` exists because without it the pinned test passes on a player that refuses everything, which is what a mis-wired field would produce. The e2e addition asserts the *mechanism this tier can see* — that the control's presence follows the player's answer — and cannot fail on the old build, since the binding did not exist. It is a guard, not a reproduction, and the file says so. **What I could not verify at the time.** No device was attached when this was written. One was afterwards, and three of the four items below are now closed — see the device-verification comment. What remains is the duck, and for a sharper reason: it is unreachable above API 25 by design, so no modern phone can test it. Left as written, because the reasoning is what justified shipping without a device: - The build-tagged constant itself is compiled by nothing here. `TestPlatformVolumeOwnershipIsDeclaredOncePerPlatform` sweeps both files' source instead, which catches deletion and an edited value but is not a compiler. - **Ducking against a real audio-focus change is device-only.** `TestSystemVolumeStillDucks` checks the arithmetic — attenuate, restore, and a refused `SetVolume` not disturbing the offset — which is the property, not the integration. - The one behaviour change a *desktop* user could see is none: the binding answers `false` there and every existing volume spec passes unchanged. ## Screenshots 424x439, before and after, in a comment below.
logan added 3 commits 2026-08-21 04:54:56 +00:00
On Android the hardware keys are the volume control and the framework
mixes our stream against the device level, so a second control inside
the app moves something the user already moved.  Where that is true the
player's level sits at maximum, SetVolume / ChangeVolume / MuteToggle
are refused, and nothing persists a level nobody chose: restore
remembers the stored value instead of applying it, and saveState writes
that same value back rather than recording the synthetic maximum.

Mute is in that list because it is a level of zero by another name --
and because with no control rendered it would be the one state on such
a platform the user could not get out of.

The predicate is named after the capability rather than the platform,
because that is what makes it testable.  Only platformOwnsVolume is
behind a build tag, in two files that declare nothing else; everything
else is decided against Player.systemVolume, a field a test sets either
way.  That is mediacontrols' split, with androidpayload.go's reasoning
for keeping the contract out of a tagged file, and the tagged pair is
covered by a source sweep since no tier here compiles both halves.

SetDuck is deliberately untouched: it applies its attenuation by
re-applying the *user's* level through setVolumeLocked, so pinning that
level to maximum leaves the offset arithmetic exactly as it was.  It is
the only thing that may still move the output on such a platform, and
TestSystemVolumeStillDucks is that property rather than a comment.
volume-control asks the player whether there is a volume of ours to
control, and renders nothing when there is not.  The decision is in the
control rather than at either mount point because there are two, and
one of them -- the bottom bar's -- lives in index.html, which has no
module scope to make it conditional.

It could not have been a width, and that is the whole design decision.
Every other stand-down rule in this app is keyed on a viewport, because
a width is what a browser can answer and what every tier can test.
This one is a property of the build: keyed on width, an Android tablet
at 600px or more draws the bar's slider over a level the backend has
pinned -- a control that cannot act, on exactly the platform the rule
exists for, which library-status-indicator settled is worse than none.
The same rule is wrong the other way below 600px, where a narrow
desktop window has no hardware keys to fall back on.  index.css keeps
its phone rule, which is now about room and says so.

Rendering nothing and hiding the host are both needed and are separate
assertions: an empty shadow root is what stops a by-role or positional
query finding a button that cannot act, and :host([hidden]) is what
stops the element taking a flex item's worth of the transport.  The
host rule has to be written down, since :host { display: inline-flex }
outranks the UA's [hidden].

Measured at 424x439 by flipping the constant and rebuilding: the album
art goes 39px to 68px and the transport 172px to 143px -- 29px, being
the 21px control plus the 8px gap a hidden box stops drawing.  The
bar's centring is unaffected, since #23's outer columns are the same
min() expression rather than content-sized.

volume-ownership.test.ts is the tier that can exercise the Android
rendering, on an ordinary Linux runner, because the predicate is a
stubbable backend answer.  Both of its tests were confirmed to fail on
the build before this.

Closes #64
Closes #172
docs(player): record who owns the volume, and what it gave back
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m34s
CI / e2e (pull_request) Successful in 8m47s
b43172a60c
CLAUDE.md's volume paragraph ended "#64 asks for it to be gone on
Android outright, which is a platform question the frontend cannot
currently ask", which is no longer true -- it can, and the paragraph
now says why the answer is a capability rather than a viewport and what
that costs.  The mediacontrols entry gains the corollary: on that
platform "the user's level" is a constant, and the duck is the one
thing that may still move the output.

NOTES.md carries the measurements: the per-element budget at 424x439
before and after, the :host([hidden]) specificity trap, the fact that
the bar's centring survives the control going away, and what no tier
here could check.
Author
Collaborator

Now Playing at 424x439, the reference device's viewport. Both from
the running app, seeded and playing; the "after" is the real rendering
with platformOwnsVolume flipped true and rebuilt, not a simulated
hide.

before after
before after

The slider and its mute icon are gone from the bottom-left of the
transport, and the art absorbs the 29px — it is flex: 1 1 auto, which
is why the whole gain lands there rather than being shared out.

Worth reading rather than measuring: the art is now large enough to
read as artwork instead of as a stripe, and the block above the
transport has not moved relative to itself. Three bugs in the last three
sessions were found this way and by nothing else, which is why this is
here.

**Now Playing at 424x439**, the reference device's viewport. Both from the running app, seeded and playing; the "after" is the real rendering with `platformOwnsVolume` flipped true and rebuilt, not a simulated hide. | before | after | |---|---| | ![before](https://git.ljones.me/attachments/45245d8e-aba5-4d03-a5f6-8547142f7518) | ![after](https://git.ljones.me/attachments/90277c76-fb23-4fdd-a81b-861d070c43fa) | The slider and its mute icon are gone from the bottom-left of the transport, and the art absorbs the 29px — it is `flex: 1 1 auto`, which is why the whole gain lands there rather than being shared out. Worth reading rather than measuring: the art is now large enough to read as artwork instead of as a stripe, and the block above the transport has not moved relative to itself. Three bugs in the last three sessions were found this way and by nothing else, which is why this is here.
Author
Collaborator

CI green, run 17357.

  • check — every step success, including Commit messages, Lint,
    Test, Bindings are current and Documented make targets exist.
  • e2e — success on both engines. E2E — webkit reports
    completed / success, not skipped: that step carries
    if: ${{ !cancelled() }}, has silently skipped before, and has been
    the only tier to catch a defect in each of the last two sessions, so
    it is checked rather than inferred from the run's conclusion. Read
    from /api/v1/…/actions/runs/17357/jobs, since gitea_ci job_logs
    404s on this build.
  • The sink plays at real time passed, so the playback specs the
    transport work touches were measuring a clock that moves.
**CI green**, run `17357`. - `check` — every step success, including `Commit messages`, `Lint`, `Test`, `Bindings are current` and `Documented make targets exist`. - `e2e` — success on **both** engines. `E2E — webkit` reports `completed / success`, not `skipped`: that step carries `if: ${{ !cancelled() }}`, has silently skipped before, and has been the only tier to catch a defect in each of the last two sessions, so it is checked rather than inferred from the run's conclusion. Read from `/api/v1/…/actions/runs/17357/jobs`, since `gitea_ci job_logs` 404s on this build. - `The sink plays at real time` passed, so the playback specs the transport work touches were measuring a clock that moves.
logan added 1 commit 2026-08-21 05:34:51 +00:00
docs(player): close three of the four gaps with a real device
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m26s
CI / e2e (pull_request) Successful in 9m5s
f26b44db08
A Light Phone III (Android 14, SDK 34, arm64, Chrome 113 at 424x439)
was attached after the PR was opened, so what it listed as
unverifiable was checked rather than left as a caveat.

SystemOwnsVolume answers true on the device -- the build tag, the
constant, the field and the generated binding, end to end, which is the
one thing a source sweep only approximates and which nothing else here
compiles at all.  The control is absent in both mount points on the
real engine, and the transport measures 143px, exactly what the
desktop-headless "after" predicted.  A stored volume of 37 survives a
session that demonstrably rewrote the row.

The duck is the one that stays open, and now for a stated reason rather
than for want of hardware: the foreground service omits
setWillPauseWhenDucked from Oreo, so the framework attenuates us itself
and never sends AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -- the device logs
`requestAudioFocus() ... flags=0x0` saying so.  minSdk is 21, so that
path is live code on Android 5.0 to 7.1 and unreachable above it.
Asking for a modern phone will not test it.
Author
Collaborator

Device verification — three of the four gaps closed

A Light Phone III (TLP301, Android 14 / SDK 34, arm64, WebView
Chrome 113 at 424x439 — the reference device) was attached after this PR
was opened, so what the body lists as unverifiable was re-checked rather
than left standing. f26b44d records it.

1. The whole chain resolves on the device.

__yj.call("player.Player.SystemOwnsVolume", []) -> true

Build tag → platformOwnsVolumePlayer.systemVolume → the
generated binding, end to end. That is the thing the source sweep only
approximates, and the device is the only tier that compiles the
android file at all. (The NDK cross-compile
GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./backend/... also
passes, and is worth running first — it catches a type error but not a
wrong constant.)

2. The control is absent in both mount points, on the real engine.
.bottom-bar volume-control and now-playing-view's copy are both
hidden with an empty shadow root and a 0x0 box. Transport measures
143px on the device — exactly what the desktop-headless "after"
predicted. The art is 75px rather than 68 because this fixture's
.names block is one line shorter, not because anything differs.

device

3. Nothing persists a level nobody chose, and this one needed care:
the default 50 surviving proves nothing, since 50 is what a fresh row
holds anyway. So the stored value was set to 37 in the device's own
database, the app relaunched, and a queue change driven to make the row
be rewritten. Reading it back, the WAL has to be pulled with the
db
— the main file alone still showed the old last_track_path and
reads exactly like a write that never happened. With yj.db-wal beside
it: last_track_path is the new track, so saveState ran, and
volume is still 37.

4. The duck stays unverified — but now for a stated reason rather
than for want of hardware.
WailsForegroundService builds its
AudioFocusRequest without setWillPauseWhenDucked from Oreo, so the
framework attenuates the stream itself and never delivers
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK. The device's own log says so:

MediaFocusControl: requestAudioFocus() from uid/pid 10172/28537
  AA=USAGE_MEDIA/CONTENT_TYPE_MUSIC ... req=1 flags=0x0

flags=0x0 is the absent AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS.
minSdk is 21, so the Go-side duck is live code rather than dead —
reachable on Android 5.0 to 7.1 and on nothing newer. So #64's "verify
ducking still attenuates and restores" cannot be done on any modern
device, and asking for a better phone will not help; it needs an API
21–25 one. The arithmetic remains covered by
TestSystemVolumeStillDucks, including that a refused SetVolume
does not disturb the offset.

Two things found on the way

Neither is caused by this change.

  • A fresh install downloads the real catalog, and that is 209px of
    the screen while it does.
    YJ_CORE_INDEX_URL is stubbed in
    dev-headless.sh and in CI but is real on a device, so the first
    measurement taken here was of a screen with job-band on it and the
    art at 0px. Worth knowing before someone reports it as #172
    getting worse: it is the environment.
  • #175 — the first-run wizard does not dismiss when a library
    appears by a route other than its own, and stays up intercepting
    every pointer event. Filed with the reproduction.

The device now has the debug build (app.yellowjacket.dev, a sibling
id — nothing was uninstalled) and a scratch library under
/sdcard/Music/yj.

## Device verification — three of the four gaps closed A **Light Phone III** (TLP301, Android 14 / SDK 34, arm64, WebView Chrome 113 at 424x439 — the reference device) was attached after this PR was opened, so what the body lists as unverifiable was re-checked rather than left standing. `f26b44d` records it. **1. The whole chain resolves on the device.** ``` __yj.call("player.Player.SystemOwnsVolume", []) -> true ``` Build tag → `platformOwnsVolume` → `Player.systemVolume` → the generated binding, end to end. That is the thing the source sweep only approximates, and the device is the only tier that compiles the `android` file at all. (The NDK cross-compile `GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./backend/...` also passes, and is worth running first — it catches a type error but not a wrong constant.) **2. The control is absent in both mount points, on the real engine.** `.bottom-bar volume-control` and `now-playing-view`'s copy are both `hidden` with an empty shadow root and a 0x0 box. Transport measures **143px** on the device — exactly what the desktop-headless "after" predicted. The art is 75px rather than 68 because this fixture's `.names` block is one line shorter, not because anything differs. ![device](https://git.ljones.me/attachments/967a127f-579a-406e-8970-f1cfaf02a7b6) **3. Nothing persists a level nobody chose**, and this one needed care: the default 50 surviving proves nothing, since 50 is what a fresh row holds anyway. So the stored value was set to **37** in the device's own database, the app relaunched, and a queue change driven to make the row be rewritten. Reading it back, **the WAL has to be pulled with the db** — the main file alone still showed the old `last_track_path` and reads exactly like a write that never happened. With `yj.db-wal` beside it: `last_track_path` is the new track, so `saveState` ran, and `volume` is still `37`. **4. The duck stays unverified — but now for a stated reason rather than for want of hardware.** `WailsForegroundService` builds its `AudioFocusRequest` without `setWillPauseWhenDucked` from Oreo, so the framework attenuates the stream itself and never delivers `AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK`. The device's own log says so: ``` MediaFocusControl: requestAudioFocus() from uid/pid 10172/28537 AA=USAGE_MEDIA/CONTENT_TYPE_MUSIC ... req=1 flags=0x0 ``` `flags=0x0` is the absent `AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS`. **`minSdk` is 21**, so the Go-side duck is live code rather than dead — reachable on Android 5.0 to 7.1 and on nothing newer. So #64's "verify ducking still attenuates and restores" cannot be done on *any* modern device, and asking for a better phone will not help; it needs an API 21–25 one. The arithmetic remains covered by `TestSystemVolumeStillDucks`, including that a *refused* `SetVolume` does not disturb the offset. ## Two things found on the way Neither is caused by this change. - **A fresh install downloads the real catalog, and that is 209px of the screen while it does.** `YJ_CORE_INDEX_URL` is stubbed in `dev-headless.sh` and in CI but is real on a device, so the first measurement taken here was of a screen with `job-band` on it and the art at **0px**. Worth knowing before someone reports it as #172 getting worse: it is the environment. - **#175** — the first-run wizard does not dismiss when a library appears by a route other than its own, and stays up intercepting every pointer event. Filed with the reproduction. The device now has the debug build (`app.yellowjacket.dev`, a sibling id — nothing was uninstalled) and a scratch library under `/sdcard/Music/yj`.
logan merged commit 2e29e67664 into main 2026-08-21 05:50:07 +00:00
Sign in to join this conversation.