Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 747ed4134b | |||
| 42bb50ed98 | |||
| bf696daa5b | |||
| 031041adea | |||
| 6df02428b0 | |||
| b93b116e4f | |||
| 26c4eb46d4 | |||
| 83a7099db3 | |||
| a9e277eb13 | |||
| 8fb6b4fbb3 | |||
| 85183c455a | |||
| 461c6bf514 | |||
| 88f2c43947 | |||
| 70529de9d3 | |||
| c1fb2355c4 | |||
| 6a62120457 | |||
| 750bc69204 | |||
| 66caee76fd | |||
| ccbd3b62a0 | |||
| 5c1a5f5e8b | |||
| bb1ab3a2e3 | |||
| 154626ae94 | |||
| 1646b02ca2 | |||
| 19de0c2874 | |||
| 1dc298c111 | |||
| e605335339 | |||
| 22a4649bfc | |||
| 01a1ac148e | |||
| bc34d96329 | |||
| 8e7660c05e | |||
| eaddb2478a |
@@ -31,10 +31,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze-test-build:
|
analyze-test-build:
|
||||||
# flutter-ci runner image (CI-Runner/CI-flutter/Dockerfile) bakes in
|
# Toolchain selected via container.image per ci-runners.md
|
||||||
# Flutter SDK + Android cmdline-tools + platform-34 + build-tools 34.0.0
|
# (label = scheduling handle, image = environment). The ci-flutter
|
||||||
# + JDK 17 + a non-root `runner` user. No setup-action ceremony needed.
|
# image ships Flutter SDK + Android cmdline-tools/platform/build-tools
|
||||||
|
# + Java 25. See ci-requirements.md.
|
||||||
runs-on: flutter-ci
|
runs-on: flutter-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-flutter:3.44
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: go-ci
|
runs-on: go-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: git.fabledsword.com/bvandeusen/minstrel
|
IMAGE: git.fabledsword.com/bvandeusen/minstrel
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ on:
|
|||||||
- 'sqlc.yaml'
|
- 'sqlc.yaml'
|
||||||
- 'internal/**'
|
- 'internal/**'
|
||||||
- 'cmd/**'
|
- 'cmd/**'
|
||||||
|
- '.golangci.yml'
|
||||||
- '.forgejo/workflows/test-go.yml'
|
- '.forgejo/workflows/test-go.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -39,11 +40,14 @@ on:
|
|||||||
- 'sqlc.yaml'
|
- 'sqlc.yaml'
|
||||||
- 'internal/**'
|
- 'internal/**'
|
||||||
- 'cmd/**'
|
- 'cmd/**'
|
||||||
|
- '.golangci.yml'
|
||||||
- '.forgejo/workflows/test-go.yml'
|
- '.forgejo/workflows/test-go.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: go-ci
|
runs-on: go-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -65,6 +69,8 @@ jobs:
|
|||||||
|
|
||||||
integration:
|
integration:
|
||||||
runs-on: go-ci
|
runs-on: go-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
@@ -83,19 +89,31 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
# Discover THIS job's Postgres service container via the
|
# Discover THIS job's Postgres service container via the
|
||||||
# mounted docker socket. Scope by the job-name filter so the
|
# mounted docker socket. act_runner attaches the job
|
||||||
# operator's dev compose `minstrel-postgres-*` (same image,
|
# container and its service container(s) to a shared per-job
|
||||||
# same daemon) can never match. Require exactly one — abort
|
# network, so scope discovery to a postgres that sits on a
|
||||||
# loudly otherwise; a wrong target would truncate real data.
|
# network THIS job container is also on. The old
|
||||||
PG_LIST=$(docker ps --filter "name=integration" --filter "ancestor=postgres:16-alpine" --format '{{.ID}} {{.Names}}')
|
# `--filter name=integration` matched EVERY concurrent
|
||||||
echo "candidates: ${PG_LIST:-<none>}"
|
# integration run's postgres (a dev push + the main-merge run
|
||||||
PG_COUNT=$(printf '%s\n' "$PG_LIST" | grep -c . || true)
|
# overlap → 2 candidates → false "expected exactly 1" abort).
|
||||||
test "$PG_COUNT" = "1" || { echo "FATAL: expected exactly 1 postgres service container, got $PG_COUNT"; exit 1; }
|
# The operator's dev compose `minstrel-postgres-*` is never on
|
||||||
PG_ID=$(printf '%s' "$PG_LIST" | awk '{print $1}')
|
# this job's network; skip it explicitly as belt-and-suspenders
|
||||||
PG_NAME=$(printf '%s' "$PG_LIST" | awk '{print $2}')
|
# (a wrong target would truncate real data).
|
||||||
case "$PG_NAME" in
|
SELF=$(cat /etc/hostname)
|
||||||
*minstrel-postgres*|*_postgres_*) echo "FATAL: matched the dev compose container ($PG_NAME), refusing"; exit 1 ;;
|
SELF_NETS=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$SELF")
|
||||||
esac
|
test -n "$SELF_NETS"
|
||||||
|
echo "self ($SELF) networks: $SELF_NETS"
|
||||||
|
PG_ID=""
|
||||||
|
PG_NAME=""
|
||||||
|
for cid in $(docker ps --filter "ancestor=postgres:16-alpine" -q); do
|
||||||
|
nm=$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##')
|
||||||
|
case "$nm" in *minstrel-postgres*|*_postgres_*) continue ;; esac
|
||||||
|
for net in $(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$cid"); do
|
||||||
|
case " $SELF_NETS " in *" $net "*) PG_ID="$cid"; PG_NAME="$nm"; break 2 ;; esac
|
||||||
|
done
|
||||||
|
done
|
||||||
|
test -n "$PG_ID" || { echo "FATAL: no postgres service container on this job's network (self nets: $SELF_NETS)"; exit 1; }
|
||||||
|
echo "selected postgres: $PG_ID $PG_NAME"
|
||||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
|
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
|
||||||
test -n "$PG_IP"
|
test -n "$PG_IP"
|
||||||
export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"
|
export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: go-ci
|
runs-on: go-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -28,18 +30,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
# `cache: 'npm'` removed — the Forgejo Actions cache server at
|
|
||||||
# 172.18.0.27:41161 isn't reachable from this runner's container,
|
|
||||||
# so setup-node spent 4m41s burning the npm cache restore on
|
|
||||||
# ETIMEDOUT before failing open. npm ci still works deterministically
|
|
||||||
# from package-lock.json; just re-fetches from the registry on each
|
|
||||||
# run. Restore the cache option once the runner-host network reaches
|
|
||||||
# the cache server.
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
+16
-15
@@ -1,28 +1,29 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- errcheck
|
- errcheck
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- unused
|
- unused
|
||||||
|
- revive
|
||||||
|
settings:
|
||||||
|
revive:
|
||||||
|
# Intentionally narrow: we skip `exported` (no doc-comment requirement) per
|
||||||
|
# the project's no-boilerplate-comment policy. Re-enable if the public API
|
||||||
|
# surface grows to the point where documentation lives alongside it.
|
||||||
|
rules:
|
||||||
|
- name: var-naming
|
||||||
|
- name: unused-parameter
|
||||||
|
- name: early-return
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- revive
|
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
revive:
|
|
||||||
# Intentionally narrow: we skip `exported` (no doc-comment requirement) per
|
|
||||||
# the project's no-boilerplate-comment policy. Re-enable if the public API
|
|
||||||
# surface grows to the point where documentation lives alongside it.
|
|
||||||
rules:
|
|
||||||
- name: var-naming
|
|
||||||
- name: unused-parameter
|
|
||||||
- name: early-return
|
|
||||||
|
|
||||||
issues:
|
|
||||||
exclude-use-default: false
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# CI Requirements — Minstrel
|
||||||
|
|
||||||
|
> Copy of [the template](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/requirements-sheet.template.md).
|
||||||
|
> Family-wide policy: `ci-runners.md` in the FabledRulebook.
|
||||||
|
|
||||||
|
## Runtime images
|
||||||
|
|
||||||
|
Minstrel's four workflows consume two CI images:
|
||||||
|
|
||||||
|
```
|
||||||
|
git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
|
git.fabledsword.com/bvandeusen/ci-flutter:3.44
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ci-go:1.26` — Go server tests (`.forgejo/workflows/test-go.yml`), web SPA tests (`.forgejo/workflows/test-web.yml`), and the release container build (`.forgejo/workflows/release.yml`).
|
||||||
|
- `ci-flutter:3.44` — Flutter client tests + debug/release APK builds (`.forgejo/workflows/flutter.yml`).
|
||||||
|
|
||||||
|
## Image deps used
|
||||||
|
|
||||||
|
### From `ci-go:1.26`
|
||||||
|
- **Go** (1.26 toolchain) — `go vet`, `go test -race`, `go build`, `go mod`.
|
||||||
|
- **Node + npm** — `npm ci` and `npm test` / `npm run check` in `test-web.yml`.
|
||||||
|
- **golangci-lint** — lint pass in `test-go.yml`.
|
||||||
|
- **docker CLI** — bridge-IP discovery of the per-job Postgres service container in `test-go.yml` integration job (via the runner's shared `/var/run/docker.sock`).
|
||||||
|
- **docker buildx** — release container build + push in `release.yml`.
|
||||||
|
- **curl** — release-asset polling / upload in `release.yml`.
|
||||||
|
|
||||||
|
### From `ci-flutter:3.44`
|
||||||
|
- **Flutter** (3.44 stable channel) — `flutter pub get`, `flutter analyze --fatal-infos`, `flutter test`, `flutter build apk` (debug + signed release).
|
||||||
|
- **Dart** — `dart run tool/gen_tokens.dart`, `dart run build_runner build` (drift codegen).
|
||||||
|
- **Android SDK + NDK + cmdline-tools + build-tools** — APK assembly + signing.
|
||||||
|
- **Java 25** — Gradle / Android build.
|
||||||
|
- **git** — `actions/checkout@v4` baseline (and any shell git operations).
|
||||||
|
- **base64 + curl** — keystore decode + release-asset upload in the tag-build path.
|
||||||
|
|
||||||
|
## Per-job tool installs
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Label/image split.** Workflows keep `runs-on: go-ci` / `runs-on: flutter-ci` as the scheduling label per the [`ci-runners.md`](https://…/FabledRulebook/ci-runners.md) "label = scheduling handle, image = `container.image`" pattern. The labels are intentional handles, not toolchain assertions.
|
||||||
|
- **Integration-job docker-socket dependency.** `test-go.yml`'s integration job uses the runner's shared docker socket (`/var/run/docker.sock`) to bridge-IP-discover the per-job Postgres service container by name + network intersection — the dev compose's `minstrel-postgres-*` containers are explicitly skipped as belt-and-suspenders. Depends on `act_runner.valid_volumes` whitelisting the socket; if that ever stops auto-mounting, integration tests fail at the `docker inspect` step.
|
||||||
|
- **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows.
|
||||||
|
- **In-app update channel polling.** `release.yml` polls Forgejo's release-asset API for up to 15 min on tag pushes to fetch the APK that `flutter.yml` is concurrently attaching to the same release. The asset eventually appears because `flutter.yml` and `release.yml` run in parallel on the same tag; if the polling times out, the server image ships without the bundled update channel (graceful degradation, not a build failure).
|
||||||
|
- **Cache server reachability.** `test-web.yml` does NOT use `cache: 'npm'` on `actions/setup-node` — the Forgejo Actions cache server isn't reachable from this runner's container network and `setup-node` was burning ~4m41s on ETIMEDOUT before failing open. With the migration to `ci-go:1.26`, `setup-node` is removed entirely (Node is in the image). The cache concern reappears if a future change re-introduces a network-dependent action.
|
||||||
|
- **Friction asks.** None pending. The two images cover everything Minstrel needs.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'cache/cache_filler.dart';
|
import 'cache/cache_filler.dart';
|
||||||
import 'cache/metadata_prefetcher.dart';
|
import 'cache/metadata_prefetcher.dart';
|
||||||
@@ -77,6 +80,14 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
|
|||||||
// probe → offlineProvider. Read here to start the poller; S4
|
// probe → offlineProvider. Read here to start the poller; S4
|
||||||
// gates system-playlist play + Shuffle-all on it.
|
// gates system-playlist play + Shuffle-all on it.
|
||||||
ref.read(offlineProvider);
|
ref.read(offlineProvider);
|
||||||
|
// POST_NOTIFICATIONS (Android 13+) is denied-by-default until
|
||||||
|
// requested; without it the media notification is silently
|
||||||
|
// suppressed on physical devices. One-shot, post-first-frame so
|
||||||
|
// it never blocks launch; no-op on <13 / once already decided.
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
Permission.notification.request();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+20
-19
@@ -120,19 +120,6 @@ class SyncMetadata extends Table {
|
|||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single-row cache of the /api/home response (#357 follow-up). The
|
|
||||||
/// home screen is the first thing the user sees on app open; storing
|
|
||||||
/// the last successful HomeData as JSON lets us yield it immediately
|
|
||||||
/// before the REST round-trip resolves, eliminating the cold-start
|
|
||||||
/// blank on slow / remote connections. Schema 3+.
|
|
||||||
class CachedHomeSnapshot extends Table {
|
|
||||||
IntColumn get id => integer().withDefault(const Constant(1))();
|
|
||||||
TextColumn get json => text()();
|
|
||||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
|
||||||
@override
|
|
||||||
Set<Column> get primaryKey => {id};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Single-row cache of the /api/me/history response. Mirrors the
|
/// Single-row cache of the /api/me/history response. Mirrors the
|
||||||
/// CachedHomeSnapshot pattern: opening the History tab in the Library
|
/// CachedHomeSnapshot pattern: opening the History tab in the Library
|
||||||
/// screen yields the last-known page immediately while a fresh REST
|
/// screen yields the last-known page immediately while a fresh REST
|
||||||
@@ -255,7 +242,6 @@ enum CacheSource { manual, autoLiked, autoPlaylist, autoPrefetch, incidental }
|
|||||||
CachedPlaylistTracks,
|
CachedPlaylistTracks,
|
||||||
AudioCacheIndex,
|
AudioCacheIndex,
|
||||||
SyncMetadata,
|
SyncMetadata,
|
||||||
CachedHomeSnapshot,
|
|
||||||
CachedHistorySnapshot,
|
CachedHistorySnapshot,
|
||||||
CachedQuarantineMine,
|
CachedQuarantineMine,
|
||||||
CachedHomeIndex,
|
CachedHomeIndex,
|
||||||
@@ -267,7 +253,7 @@ class AppDb extends _$AppDb {
|
|||||||
AppDb([QueryExecutor? e]) : super(e ?? driftDatabase(name: 'minstrel_cache'));
|
AppDb([QueryExecutor? e]) : super(e ?? driftDatabase(name: 'minstrel_cache'));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 10;
|
int get schemaVersion => 11;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -284,10 +270,16 @@ class AppDb extends _$AppDb {
|
|||||||
await customStatement('UPDATE sync_metadata SET cursor = 0');
|
await customStatement('UPDATE sync_metadata SET cursor = 0');
|
||||||
}
|
}
|
||||||
if (from < 3) {
|
if (from < 3) {
|
||||||
// Schema 3: cached_home_snapshot for #357 drift-first
|
// Schema 3: cached_home_snapshot (legacy drift-first home).
|
||||||
// home page. No existing rows to migrate — table starts
|
// The table + its homeProvider/JSON encoders were removed
|
||||||
// empty; the first /api/home fetch populates it.
|
// in #406; recreated here as raw SQL so this historical
|
||||||
await m.createTable(cachedHomeSnapshot);
|
// step still compiles without the generated table symbol.
|
||||||
|
// The from<11 step below drops it.
|
||||||
|
await customStatement(
|
||||||
|
'CREATE TABLE IF NOT EXISTS "cached_home_snapshot" '
|
||||||
|
'("id" INTEGER NOT NULL DEFAULT 1, "json" TEXT, '
|
||||||
|
'"updated_at" INTEGER, PRIMARY KEY ("id"));',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (from < 4) {
|
if (from < 4) {
|
||||||
// Schema 4: cached_history_snapshot for drift-first
|
// Schema 4: cached_history_snapshot for drift-first
|
||||||
@@ -337,6 +329,15 @@ class AppDb extends _$AppDb {
|
|||||||
// first persist populates it.
|
// first persist populates it.
|
||||||
await m.createTable(cachedResumeState);
|
await m.createTable(cachedResumeState);
|
||||||
}
|
}
|
||||||
|
if (from < 11) {
|
||||||
|
// Schema 11 (#406): drop the legacy cached_home_snapshot
|
||||||
|
// table. The home screen now renders solely from the
|
||||||
|
// per-item cached_home_index path; the legacy homeProvider
|
||||||
|
// + its JSON encoders were removed.
|
||||||
|
await customStatement(
|
||||||
|
'DROP TABLE IF EXISTS "cached_home_snapshot";',
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-22
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../library/library_providers.dart';
|
import '../library/library_providers.dart';
|
||||||
import '../models/home_data.dart';
|
import '../models/home_index.dart';
|
||||||
|
|
||||||
/// Pre-warms the drift cache for likely-tap targets. Conservative on
|
/// Pre-warms the drift cache for likely-tap targets. Conservative on
|
||||||
/// purpose: only warms artistProvider rows (single row, single round
|
/// purpose: only warms artistProvider rows (single row, single round
|
||||||
@@ -10,10 +10,15 @@ import '../models/home_data.dart';
|
|||||||
/// list when missing, and pre-warming N albums fans out N parallel
|
/// list when missing, and pre-warming N albums fans out N parallel
|
||||||
/// "fetch tracks" round trips that compete with the user's actual
|
/// "fetch tracks" round trips that compete with the user's actual
|
||||||
/// playback request for bandwidth.
|
/// playback request for bandwidth.
|
||||||
|
///
|
||||||
|
/// Driven off the per-item home path (homeIndexProvider). Only the
|
||||||
|
/// artist-typed sections (rediscover / last-played artists) carry
|
||||||
|
/// artist ids directly; album/track tiles hydrate their own artist on
|
||||||
|
/// render via the per-item path, so warming those here is unnecessary.
|
||||||
class MetadataPrefetcher {
|
class MetadataPrefetcher {
|
||||||
MetadataPrefetcher(this._ref) {
|
MetadataPrefetcher(this._ref) {
|
||||||
_ref.listen<AsyncValue<HomeData>>(homeProvider, (_, next) {
|
_ref.listen<AsyncValue<HomeIndex>>(homeIndexProvider, (_, next) {
|
||||||
next.whenData(_warmHome);
|
next.whenData(_warmIndex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +29,8 @@ class MetadataPrefetcher {
|
|||||||
/// ids we've already warmed.
|
/// ids we've already warmed.
|
||||||
final Set<String> _warmedArtists = {};
|
final Set<String> _warmedArtists = {};
|
||||||
|
|
||||||
/// Cap per section. Covers what fits on screen without scrolling.
|
/// Cap warmed per emission. Covers what fits on screen without
|
||||||
|
/// scrolling.
|
||||||
static const _topN = 8;
|
static const _topN = 8;
|
||||||
|
|
||||||
/// Pre-warm artists. Albums are intentionally not pre-warmed —
|
/// Pre-warm artists. Albums are intentionally not pre-warmed —
|
||||||
@@ -39,24 +45,11 @@ class MetadataPrefetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _warmHome(HomeData h) {
|
void _warmIndex(HomeIndex h) {
|
||||||
final artistIds = <String>{};
|
warmArtists(<String>{
|
||||||
for (final a in h.recentlyAddedAlbums.take(_topN)) {
|
...h.rediscoverArtists.take(_topN),
|
||||||
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
...h.lastPlayedArtists.take(_topN),
|
||||||
}
|
});
|
||||||
for (final a in h.rediscoverAlbums.take(_topN)) {
|
|
||||||
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
|
||||||
}
|
|
||||||
for (final ar in h.rediscoverArtists.take(_topN)) {
|
|
||||||
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
||||||
}
|
|
||||||
for (final ar in h.lastPlayedArtists.take(_topN)) {
|
|
||||||
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
||||||
}
|
|
||||||
for (final t in h.mostPlayedTracks.take(_topN)) {
|
|
||||||
if (t.artistId.isNotEmpty) artistIds.add(t.artistId);
|
|
||||||
}
|
|
||||||
warmArtists(artistIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discards the return value and any error from a fire-and-forget
|
/// Discards the return value and any error from a fire-and-forget
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:drift/drift.dart' as drift;
|
import 'package:drift/drift.dart' as drift;
|
||||||
@@ -16,7 +15,6 @@ import '../cache/connectivity_provider.dart';
|
|||||||
import '../cache/db.dart';
|
import '../cache/db.dart';
|
||||||
import '../models/album.dart';
|
import '../models/album.dart';
|
||||||
import '../models/artist.dart';
|
import '../models/artist.dart';
|
||||||
import '../models/home_data.dart';
|
|
||||||
import '../models/home_index.dart';
|
import '../models/home_index.dart';
|
||||||
import '../models/track.dart';
|
import '../models/track.dart';
|
||||||
|
|
||||||
@@ -44,57 +42,6 @@ final libraryApiProvider = FutureProvider<LibraryApi>((ref) async {
|
|||||||
return LibraryApi(await ref.watch(dioProvider.future));
|
return LibraryApi(await ref.watch(dioProvider.future));
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Drift-first home data (#357 follow-up). The home screen is the first
|
|
||||||
/// view the user sees on app open; without a local cache the cold-start
|
|
||||||
/// blocks on the /api/home round-trip, which is the dominant felt-
|
|
||||||
/// latency on slow / remote connections.
|
|
||||||
///
|
|
||||||
/// Cache layout: a single row in `cached_home_snapshot` storing the
|
|
||||||
/// last successful HomeData as JSON. On stream subscription:
|
|
||||||
///
|
|
||||||
/// - If a row exists, yield it immediately (parsed from JSON) and
|
|
||||||
/// kick off a background revalidation against /api/home (SWR).
|
|
||||||
/// The drift watch() re-emits when the new row is written.
|
|
||||||
/// - If no row exists yet (cold cache), fetch /api/home synchronously,
|
|
||||||
/// upsert into drift, and emit. Subsequent emissions come from the
|
|
||||||
/// drift watch as background revalidations land.
|
|
||||||
///
|
|
||||||
/// The HomeData JSON encodes back to the same wire shape /api/home
|
|
||||||
/// emits, so the existing HomeData.fromJson handles both sources.
|
|
||||||
final homeProvider = StreamProvider<HomeData>((ref) {
|
|
||||||
final db = ref.watch(appDbProvider);
|
|
||||||
return cacheFirst<CachedHomeSnapshotData, HomeData>(
|
|
||||||
driftStream: db.select(db.cachedHomeSnapshot).watch(),
|
|
||||||
fetchAndPopulate: () async {
|
|
||||||
final api = await ref.read(libraryApiProvider.future);
|
|
||||||
final fresh = await api.getHome();
|
|
||||||
await db.into(db.cachedHomeSnapshot).insertOnConflictUpdate(
|
|
||||||
CachedHomeSnapshotCompanion.insert(
|
|
||||||
json: _encodeHomeData(fresh),
|
|
||||||
updatedAt: drift.Value(DateTime.now()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
toResult: (rows) => rows.isEmpty
|
|
||||||
? const HomeData(
|
|
||||||
recentlyAddedAlbums: [],
|
|
||||||
rediscoverAlbums: [],
|
|
||||||
rediscoverArtists: [],
|
|
||||||
mostPlayedTracks: [],
|
|
||||||
lastPlayedArtists: [],
|
|
||||||
)
|
|
||||||
: HomeData.fromJson(jsonDecode(rows.first.json) as Map<String, dynamic>),
|
|
||||||
isOnline: () async => (await ref
|
|
||||||
.read(connectivityProvider.future)
|
|
||||||
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
|
||||||
// SWR: when cached snapshot exists, still hit /api/home in the
|
|
||||||
// background so the user sees the freshest curation. The cache
|
|
||||||
// mostly serves the first frame, not the canonical state.
|
|
||||||
alwaysRefresh: true,
|
|
||||||
tag: 'home',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Drift-first per-item home index. Reads from cached_home_index
|
/// Drift-first per-item home index. Reads from cached_home_index
|
||||||
/// (populated by /api/home/index discovery) and yields a HomeIndex
|
/// (populated by /api/home/index discovery) and yields a HomeIndex
|
||||||
/// the screen consumes to lay out section→tile slots. Each tile is
|
/// the screen consumes to lay out section→tile slots. Each tile is
|
||||||
@@ -184,49 +131,6 @@ final homeIndexProvider = StreamProvider<HomeIndex>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Encodes HomeData back to the wire-format JSON shape /api/home
|
|
||||||
/// emits, so HomeData.fromJson can round-trip through the drift cache.
|
|
||||||
String _encodeHomeData(HomeData h) => jsonEncode({
|
|
||||||
'recently_added_albums': h.recentlyAddedAlbums.map(_albumToJson).toList(),
|
|
||||||
'rediscover_albums': h.rediscoverAlbums.map(_albumToJson).toList(),
|
|
||||||
'rediscover_artists': h.rediscoverArtists.map(_artistToJson).toList(),
|
|
||||||
'most_played_tracks': h.mostPlayedTracks.map(_trackToJson).toList(),
|
|
||||||
'last_played_artists': h.lastPlayedArtists.map(_artistToJson).toList(),
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> _albumToJson(AlbumRef a) => {
|
|
||||||
'id': a.id,
|
|
||||||
'title': a.title,
|
|
||||||
'sort_title': a.sortTitle,
|
|
||||||
'artist_id': a.artistId,
|
|
||||||
'artist_name': a.artistName,
|
|
||||||
'year': a.year,
|
|
||||||
'track_count': a.trackCount,
|
|
||||||
'duration_sec': a.durationSec,
|
|
||||||
'cover_url': a.coverUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
Map<String, dynamic> _artistToJson(ArtistRef a) => {
|
|
||||||
'id': a.id,
|
|
||||||
'name': a.name,
|
|
||||||
'sort_name': a.sortName,
|
|
||||||
'album_count': a.albumCount,
|
|
||||||
'cover_url': a.coverUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
Map<String, dynamic> _trackToJson(TrackRef t) => {
|
|
||||||
'id': t.id,
|
|
||||||
'title': t.title,
|
|
||||||
'album_id': t.albumId,
|
|
||||||
'album_title': t.albumTitle,
|
|
||||||
'artist_id': t.artistId,
|
|
||||||
'artist_name': t.artistName,
|
|
||||||
'track_number': t.trackNumber,
|
|
||||||
'disc_number': t.discNumber,
|
|
||||||
'duration_sec': t.durationSec,
|
|
||||||
'stream_url': t.streamUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Drift-first per #357 plan C. Watches cached_artists for the row;
|
/// Drift-first per #357 plan C. Watches cached_artists for the row;
|
||||||
/// when empty + online, fetches via REST + populates drift, which
|
/// when empty + online, fetches via REST + populates drift, which
|
||||||
/// re-emits via watch().
|
/// re-emits via watch().
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
/// need this. Active only while playing; cancelled otherwise.
|
/// need this. Active only while playing; cancelled otherwise.
|
||||||
Timer? _positionBroadcastTimer;
|
Timer? _positionBroadcastTimer;
|
||||||
|
|
||||||
|
/// Buffering-stall watchdog. On poor coverage a stream hangs in
|
||||||
|
/// ProcessingState.buffering with NO error event, so the onError
|
||||||
|
/// recovery never fires. Armed while playing+buffering; on fire, if
|
||||||
|
/// buffered position hasn't advanced it's a dead stream → recover.
|
||||||
|
/// Re-arms a fresh window while buffering-but-progressing (slow-but-
|
||||||
|
/// downloading is not a stall).
|
||||||
|
static const _stallTimeout = Duration(seconds: 15);
|
||||||
|
Timer? _stallTimer;
|
||||||
|
Duration _stallMarkBuffered = Duration.zero;
|
||||||
|
|
||||||
|
/// Per-track recovery budget. Reset when a track reaches
|
||||||
|
/// ready+playing. One retry of the same source (rebuilt fresh), then
|
||||||
|
/// skip to the next cached track (or pause) instead of thrashing
|
||||||
|
/// through unreachable streams.
|
||||||
|
static const _maxRecoverRetries = 1;
|
||||||
|
String? _recoveringTrackId;
|
||||||
|
int _recoverAttempts = 0;
|
||||||
|
|
||||||
/// Player volume captured when an OS duck interruption begins,
|
/// Player volume captured when an OS duck interruption begins,
|
||||||
/// restored when it ends. Null when not currently ducked.
|
/// restored when it ends. Null when not currently ducked.
|
||||||
double? _volumeBeforeDuck;
|
double? _volumeBeforeDuck;
|
||||||
@@ -492,33 +510,12 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
///
|
///
|
||||||
/// If we're at the last track, seekToNext is a no-op; the state
|
/// If we're at the last track, seekToNext is a no-op; the state
|
||||||
/// drops to idle and _broadcastState reflects that.
|
/// drops to idle and _broadcastState reflects that.
|
||||||
|
/// onError entrypoint (404 / decoder / premature EOS / network drop).
|
||||||
|
/// Routes into the unified recovery path (retry the track once, then
|
||||||
|
/// skip to the next cached track or pause) instead of immediately
|
||||||
|
/// skipping the literal next — possibly also-unreachable — source.
|
||||||
Future<void> _handlePlaybackError() async {
|
Future<void> _handlePlaybackError() async {
|
||||||
// mediaItem is the correctly-mapped current track (post #49 logical
|
await _recoverPlayback();
|
||||||
// index), so it's the one that just failed — no player-index math.
|
|
||||||
final failed = mediaItem.value?.title;
|
|
||||||
if (failed != null && failed.isNotEmpty) {
|
|
||||||
_playbackErrors.add(failed);
|
|
||||||
}
|
|
||||||
final currentIdx = _player.currentIndex;
|
|
||||||
final queueLen = queue.value.length;
|
|
||||||
if (currentIdx == null || currentIdx + 1 >= queueLen) {
|
|
||||||
// Last track or no queue context — just pause; _broadcastState
|
|
||||||
// already reflects the idle/error processingState.
|
|
||||||
try {
|
|
||||||
await _player.pause();
|
|
||||||
} catch (_) {}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await _player.seekToNext();
|
|
||||||
} catch (_) {
|
|
||||||
// If seekToNext fails too (next source not built yet, etc.),
|
|
||||||
// there's nothing safe to do — pause and let the user recover
|
|
||||||
// manually.
|
|
||||||
try {
|
|
||||||
await _player.pause();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCurrentIndexChanged(int? idx) {
|
void _onCurrentIndexChanged(int? idx) {
|
||||||
@@ -562,31 +559,49 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
mediaItem.add(current.copyWith(artUri: Uri.file(path)));
|
mediaItem.add(current.copyWith(artUri: Uri.file(path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives the session to a terminal state and tears down the
|
/// Display-state-only teardown. Stops playback, clears the queue and
|
||||||
/// foreground service so external surfaces (Wear tile, lock screen,
|
/// current track, and broadcasts idle so external surfaces (Wear tile,
|
||||||
/// in-app mini bar) drop it instead of showing a stale paused track.
|
/// notification, in-app mini bar) drop their visible state — but does
|
||||||
/// Called on the idle timeout and from onTaskRemoved when not playing;
|
/// NOT call super.stop(), so the FGS and MediaSessionCompat remain
|
||||||
/// no other path stops the session (the notification is ongoing).
|
/// alive and addressable.
|
||||||
@override
|
///
|
||||||
Future<void> stop() async {
|
/// Why this matters (Fable #472): the paired Wear OS companion app
|
||||||
|
/// caches a MediaController bound to our MediaSession's token. If
|
||||||
|
/// the system destroys our service (super.stop() → stopSelf() makes
|
||||||
|
/// it eligible under memory pressure), the next play() spins up a
|
||||||
|
/// new MediaSession with a fresh token; the companion's cached
|
||||||
|
/// controller still points at the dead one and transport taps from
|
||||||
|
/// notification + watch silently no-op. Keeping the service alive
|
||||||
|
/// across idle periods preserves the binding.
|
||||||
|
///
|
||||||
|
/// Used by the #52 idle timeout. Full termination (super.stop) is
|
||||||
|
/// reserved for the explicit-close path (onTaskRemoved while idle).
|
||||||
|
Future<void> _softTeardown() async {
|
||||||
_idleStopTimer?.cancel();
|
_idleStopTimer?.cancel();
|
||||||
_idleStopTimer = null;
|
_idleStopTimer = null;
|
||||||
_positionBroadcastTimer?.cancel();
|
_positionBroadcastTimer?.cancel();
|
||||||
_positionBroadcastTimer = null;
|
_positionBroadcastTimer = null;
|
||||||
|
_stallTimer?.cancel();
|
||||||
|
_stallTimer = null;
|
||||||
try {
|
try {
|
||||||
await _player.stop();
|
await _player.stop();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
// Explicit terminal broadcast before super.stop() tears down the
|
|
||||||
// isolate/notification, so subscribers see idle even if the
|
|
||||||
// player's own event lags the service teardown. queue/mediaItem
|
|
||||||
// cleared so the in-app mini bar collapses in lockstep with the
|
|
||||||
// watch tile rather than lingering on the last track.
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
playbackState.add(playbackState.value.copyWith(
|
||||||
playing: false,
|
playing: false,
|
||||||
processingState: AudioProcessingState.idle,
|
processingState: AudioProcessingState.idle,
|
||||||
));
|
));
|
||||||
mediaItem.add(null);
|
mediaItem.add(null);
|
||||||
queue.add([]);
|
queue.add([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full termination — soft teardown plus super.stop(), which calls
|
||||||
|
/// stopSelf() on the AudioService and lets the OS reclaim it. Reserved
|
||||||
|
/// for the explicit-close path (onTaskRemoved when not playing). On the
|
||||||
|
/// idle path we use _softTeardown instead to preserve the Wear OS
|
||||||
|
/// MediaController binding (Fable #472).
|
||||||
|
@override
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _softTeardown();
|
||||||
await super.stop();
|
await super.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,31 +739,18 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
// with up-to-date shuffleMode/repeatMode fields.
|
// with up-to-date shuffleMode/repeatMode fields.
|
||||||
void _broadcastState(PlaybackEvent? event) {
|
void _broadcastState(PlaybackEvent? event) {
|
||||||
final playing = _player.playing;
|
final playing = _player.playing;
|
||||||
// Favorite control: only when a track is playing and the LikeBridge
|
// No custom favorite MediaControl here. audio_service builds a
|
||||||
// is wired. Icon/label toggle by current like state; tapping it
|
// PlaybackStateCompat.CustomAction for it and throws
|
||||||
// routes to customAction('minstrel.favorite'). Implemented as a
|
// "You must specify an icon resource id to build a CustomAction"
|
||||||
// MediaControl.custom (NOT setRating — that path is broken upstream,
|
// on real builds (the androidIcon doesn't resolve to a usable id),
|
||||||
// audio_service #376, and regressed the Pixel Watch). Kept out of
|
// and that exception aborts the ENTIRE media notification — no
|
||||||
// androidCompactActionIndices so the compact/lock view is unchanged.
|
// tray or Wear controls at all. Removed; like/favorite remains
|
||||||
final favTrackId = mediaItem.value?.id;
|
// available in-app and via the standard lock-screen surface.
|
||||||
final favBridge = _likeBridge;
|
|
||||||
final showFav = favTrackId != null && favBridge != null;
|
|
||||||
final favLiked = favTrackId != null &&
|
|
||||||
favBridge != null &&
|
|
||||||
favBridge.isTrackLiked(favTrackId);
|
|
||||||
playbackState.add(PlaybackState(
|
playbackState.add(PlaybackState(
|
||||||
controls: [
|
controls: [
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
if (playing) MediaControl.pause else MediaControl.play,
|
if (playing) MediaControl.pause else MediaControl.play,
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
if (showFav)
|
|
||||||
MediaControl.custom(
|
|
||||||
androidIcon: favLiked
|
|
||||||
? 'drawable/ic_stat_favorite'
|
|
||||||
: 'drawable/ic_stat_favorite_border',
|
|
||||||
label: favLiked ? 'Unfavorite' : 'Favorite',
|
|
||||||
name: 'minstrel.favorite',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
// androidCompactActionIndices tells the system which controls
|
// androidCompactActionIndices tells the system which controls
|
||||||
// appear in the collapsed/lock-screen view. Without this, some
|
// appear in the collapsed/lock-screen view. Without this, some
|
||||||
@@ -799,6 +801,7 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
));
|
));
|
||||||
_reconcileIdleTimer();
|
_reconcileIdleTimer();
|
||||||
_reconcilePositionBroadcast();
|
_reconcilePositionBroadcast();
|
||||||
|
_reconcileStallWatchdog();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arms (or cancels) the idle-cleanup timer based on the current
|
/// Arms (or cancels) the idle-cleanup timer based on the current
|
||||||
@@ -830,7 +833,9 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
if (_player.playing && ps != ProcessingState.completed) {
|
if (_player.playing && ps != ProcessingState.completed) {
|
||||||
return; // resumed between arm and fire
|
return; // resumed between arm and fire
|
||||||
}
|
}
|
||||||
unawaited(stop());
|
// _softTeardown — not full stop() — to preserve the MediaSession
|
||||||
|
// binding for the paired Wear OS companion (Fable #472).
|
||||||
|
unawaited(_softTeardown());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// While actively playing, keeps a 1s periodic PlaybackState re-
|
/// While actively playing, keeps a 1s periodic PlaybackState re-
|
||||||
@@ -855,6 +860,96 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buffering-stall watchdog. Armed while playing+buffering/loading.
|
||||||
|
/// On fire: still stuck with no >=1s buffered progress → dead stream,
|
||||||
|
/// recover; progressing → re-arm a fresh window (slow-but-downloading
|
||||||
|
/// is fine). When the track is happily ready+playing, clears the
|
||||||
|
/// per-track recovery budget. Driven from _broadcastState; mirrors
|
||||||
|
/// the other reconcile helpers.
|
||||||
|
void _reconcileStallWatchdog() {
|
||||||
|
final ps = _player.processingState;
|
||||||
|
if (ps == ProcessingState.ready && _player.playing) {
|
||||||
|
_recoveringTrackId = null;
|
||||||
|
_recoverAttempts = 0;
|
||||||
|
}
|
||||||
|
final buffering = _player.playing &&
|
||||||
|
(ps == ProcessingState.buffering || ps == ProcessingState.loading);
|
||||||
|
if (!buffering) {
|
||||||
|
_stallTimer?.cancel();
|
||||||
|
_stallTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_stallTimer != null) return; // window already running
|
||||||
|
_stallMarkBuffered = _player.bufferedPosition;
|
||||||
|
_stallTimer = Timer(_stallTimeout, () {
|
||||||
|
_stallTimer = null;
|
||||||
|
final ps2 = _player.processingState;
|
||||||
|
final stillBuffering = _player.playing &&
|
||||||
|
(ps2 == ProcessingState.buffering ||
|
||||||
|
ps2 == ProcessingState.loading);
|
||||||
|
if (!stillBuffering) return;
|
||||||
|
final progressed = (_player.bufferedPosition - _stallMarkBuffered) >=
|
||||||
|
const Duration(seconds: 1);
|
||||||
|
if (progressed) {
|
||||||
|
_reconcileStallWatchdog(); // slow but downloading — re-arm
|
||||||
|
} else {
|
||||||
|
unawaited(_recoverPlayback());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified recovery for a failed OR stalled stream. Retries the
|
||||||
|
/// current track once (rebuilt from scratch via skipToQueueItem so a
|
||||||
|
/// transient blip doesn't lose it); on exhaustion, surfaces the
|
||||||
|
/// failure (#58 SnackBar) and skips to the next cached track — or
|
||||||
|
/// pauses — instead of blindly walking into more unreachable streams.
|
||||||
|
Future<void> _recoverPlayback() async {
|
||||||
|
_stallTimer?.cancel();
|
||||||
|
_stallTimer = null;
|
||||||
|
final media = mediaItem.value;
|
||||||
|
if (media == null) return;
|
||||||
|
final trackId = media.id;
|
||||||
|
final curIdx = _lastTracks.indexWhere((t) => t.id == trackId);
|
||||||
|
if (curIdx < 0) {
|
||||||
|
try {
|
||||||
|
await _player.pause();
|
||||||
|
} catch (_) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_recoveringTrackId != trackId) {
|
||||||
|
_recoveringTrackId = trackId;
|
||||||
|
_recoverAttempts = 0;
|
||||||
|
}
|
||||||
|
if (_recoverAttempts < _maxRecoverRetries) {
|
||||||
|
_recoverAttempts++;
|
||||||
|
// Rebuild + restart the SAME track (fresh source / HTTP).
|
||||||
|
await skipToQueueItem(curIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (media.title.isNotEmpty) _playbackErrors.add(media.title);
|
||||||
|
_recoveringTrackId = null;
|
||||||
|
_recoverAttempts = 0;
|
||||||
|
await _skipToNextCachedOrPause(curIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skips to the first track after [fromIdx] that is fully cached on
|
||||||
|
/// disk (plays with zero network). If none, pauses rather than
|
||||||
|
/// thrashing through unreachable streams on a dead connection.
|
||||||
|
Future<void> _skipToNextCachedOrPause(int fromIdx) async {
|
||||||
|
final mgr = _audioCacheManager;
|
||||||
|
if (mgr != null) {
|
||||||
|
for (var i = fromIdx + 1; i < _lastTracks.length; i++) {
|
||||||
|
if (await mgr.pathFor(_lastTracks[i].id) != null) {
|
||||||
|
await skipToQueueItem(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await _player.pause();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configures the OS audio session for music playback and wires
|
/// Configures the OS audio session for music playback and wires
|
||||||
/// interruption + becoming-noisy handling. just_audio auto-activates /
|
/// interruption + becoming-noisy handling. just_audio auto-activates /
|
||||||
/// deactivates the session around playback once it's configured, but
|
/// deactivates the session around playback once it's configured, but
|
||||||
|
|||||||
@@ -222,6 +222,14 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||||||
_displayedDominant = null;
|
_displayedDominant = null;
|
||||||
_pendingPreloadId = null;
|
_pendingPreloadId = null;
|
||||||
});
|
});
|
||||||
|
// Session was torn down (#52 idle/dismiss) while the full
|
||||||
|
// player was open. Don't strand the user on an empty
|
||||||
|
// "Nothing playing." screen — minimize back (the mini bar is
|
||||||
|
// already gone too). maybePop is a no-op if this is somehow
|
||||||
|
// the root route.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) Navigator.of(context).maybePop();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -70,7 +72,7 @@ class PlaylistCard extends ConsumerWidget {
|
|||||||
right: 6,
|
right: 6,
|
||||||
child: PlayCircleButton(
|
child: PlayCircleButton(
|
||||||
enabled: hasTracks,
|
enabled: hasTracks,
|
||||||
onPressed: () => _playPlaylist(ref),
|
onPressed: () => _playPlaylist(context, ref),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// System playlists get a refresh affordance so the
|
// System playlists get a refresh affordance so the
|
||||||
@@ -187,16 +189,34 @@ class PlaylistCard extends ConsumerWidget {
|
|||||||
/// PlaylistTrack into a TrackRef (filtering out unavailable rows
|
/// PlaylistTrack into a TrackRef (filtering out unavailable rows
|
||||||
/// whose `trackId` is null after a track-delete), and plays from
|
/// whose `trackId` is null after a track-delete), and plays from
|
||||||
/// index 0. Mirrors the web PlaylistCard's onPlayClick.
|
/// index 0. Mirrors the web PlaylistCard's onPlayClick.
|
||||||
Future<void> _playPlaylist(WidgetRef ref) async {
|
///
|
||||||
|
/// Failure modes are surfaced via SnackBar so a not-yet-built mix
|
||||||
|
/// or a slow server doesn't look like the tap was ignored.
|
||||||
|
Future<void> _playPlaylist(BuildContext context, WidgetRef ref) async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final api = await ref.read(playlistsApiProvider.future);
|
final api = await ref.read(playlistsApiProvider.future);
|
||||||
// Refreshable (singleton) system playlists: fetch the server's
|
final PlaylistDetail detail;
|
||||||
// rotation-aware order (#415) and play as-is, tagged with the
|
try {
|
||||||
// source so the reporter advances rotation. User playlists AND
|
// Refreshable (singleton) system playlists: fetch the server's
|
||||||
// non-singleton system kinds (songs_like_artist — no by-kind
|
// rotation-aware order (#415) and play as-is, tagged with the
|
||||||
// endpoint) play the stored order via get(), untagged.
|
// source so the reporter advances rotation. User playlists AND
|
||||||
final detail = playlist.refreshable
|
// non-singleton system kinds (songs_like_artist — no by-kind
|
||||||
? await api.systemShuffle(playlist.systemVariant!)
|
// endpoint) play the stored order via get(), untagged.
|
||||||
: await api.get(playlist.id);
|
detail = await (playlist.refreshable
|
||||||
|
? api.systemShuffle(playlist.systemVariant!)
|
||||||
|
: api.get(playlist.id))
|
||||||
|
.timeout(const Duration(seconds: 8));
|
||||||
|
} on TimeoutException {
|
||||||
|
messenger.showSnackBar(const SnackBar(
|
||||||
|
content: Text("Couldn't load playlist — check your connection"),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text('Playlist load failed: $e'),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
final refs = <TrackRef>[];
|
final refs = <TrackRef>[];
|
||||||
for (final t in detail.tracks) {
|
for (final t in detail.tracks) {
|
||||||
if (t.trackId == null) continue;
|
if (t.trackId == null) continue;
|
||||||
@@ -211,7 +231,12 @@ class PlaylistCard extends ConsumerWidget {
|
|||||||
streamUrl: t.streamUrl ?? '',
|
streamUrl: t.streamUrl ?? '',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (refs.isEmpty) return;
|
if (refs.isEmpty) {
|
||||||
|
messenger.showSnackBar(const SnackBar(
|
||||||
|
content: Text("Mix isn't ready yet — try again in a moment"),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Rotation-aware kinds arrive pre-ordered from the server — play
|
// Rotation-aware kinds arrive pre-ordered from the server — play
|
||||||
// as-is, tagged so the reporter advances rotation. No client
|
// as-is, tagged so the reporter advances rotation. No client
|
||||||
// shuffle. Everything else plays stored order, untagged.
|
// shuffle. Everything else plays stored order, untagged.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../library/library_providers.dart' show homeProvider;
|
import '../library/library_providers.dart' show homeIndexProvider;
|
||||||
import '../quarantine/quarantine_provider.dart';
|
import '../quarantine/quarantine_provider.dart';
|
||||||
import 'live_events_provider.dart';
|
import 'live_events_provider.dart';
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ class _LiveEventsDispatcher with WidgetsBindingObserver {
|
|||||||
case 'quarantine.deleted_via_lidarr':
|
case 'quarantine.deleted_via_lidarr':
|
||||||
_ref.invalidate(myQuarantineProvider);
|
_ref.invalidate(myQuarantineProvider);
|
||||||
// Hidden / liked / album rows referencing a deleted track may
|
// Hidden / liked / album rows referencing a deleted track may
|
||||||
// now be stale — homeProvider re-fetch refreshes the cards.
|
// now be stale — homeIndexProvider re-fetch refreshes the cards.
|
||||||
_ref.invalidate(homeProvider);
|
_ref.invalidate(homeIndexProvider);
|
||||||
case 'playlist.created':
|
case 'playlist.created':
|
||||||
case 'playlist.updated':
|
case 'playlist.updated':
|
||||||
case 'playlist.deleted':
|
case 'playlist.deleted':
|
||||||
@@ -72,7 +72,7 @@ class _LiveEventsDispatcher with WidgetsBindingObserver {
|
|||||||
// is reflected immediately. Detail screens that need the
|
// is reflected immediately. Detail screens that need the
|
||||||
// mutated row will get a separate invalidate from their own
|
// mutated row will get a separate invalidate from their own
|
||||||
// listener (filed as a follow-up).
|
// listener (filed as a follow-up).
|
||||||
_ref.invalidate(homeProvider);
|
_ref.invalidate(homeIndexProvider);
|
||||||
case 'scan.run_started':
|
case 'scan.run_started':
|
||||||
case 'scan.run_finished':
|
case 'scan.run_finished':
|
||||||
// Admin scan card provider lives in the admin feature folder
|
// Admin scan card provider lives in the admin feature folder
|
||||||
@@ -93,7 +93,7 @@ class _LiveEventsDispatcher with WidgetsBindingObserver {
|
|||||||
// own, but the re-invalidate flushes any stale data that
|
// own, but the re-invalidate flushes any stale data that
|
||||||
// landed while the app was backgrounded.
|
// landed while the app was backgrounded.
|
||||||
_ref.invalidate(myQuarantineProvider);
|
_ref.invalidate(myQuarantineProvider);
|
||||||
_ref.invalidate(homeProvider);
|
_ref.invalidate(homeIndexProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,18 +69,24 @@ class AddToPlaylistSheet extends ConsumerWidget {
|
|||||||
itemCount: owned.length,
|
itemCount: owned.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final p = owned[i];
|
final p = owned[i];
|
||||||
return ListTile(
|
// Material(transparency) gives ListTile an ink target
|
||||||
key: Key('add_to_playlist_${p.id}'),
|
// beneath the outer Container's color paint — required
|
||||||
leading: Icon(LucideIcons.list_music, color: fs.parchment),
|
// by the Flutter 3.44 ListTile/ColoredBox assertion.
|
||||||
title: Text(
|
return Material(
|
||||||
p.name,
|
type: MaterialType.transparency,
|
||||||
style: TextStyle(color: fs.parchment),
|
child: ListTile(
|
||||||
|
key: Key('add_to_playlist_${p.id}'),
|
||||||
|
leading: Icon(LucideIcons.list_music, color: fs.parchment),
|
||||||
|
title: Text(
|
||||||
|
p.name,
|
||||||
|
style: TextStyle(color: fs.parchment),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${p.trackCount} ${p.trackCount == 1 ? "track" : "tracks"}',
|
||||||
|
style: TextStyle(color: fs.ash, fontSize: 12),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.pop(context, p.id),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
|
||||||
'${p.trackCount} ${p.trackCount == 1 ? "track" : "tracks"}',
|
|
||||||
style: TextStyle(color: fs.ash, fontSize: 12),
|
|
||||||
),
|
|
||||||
onTap: () => Navigator.pop(context, p.id),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -236,10 +236,17 @@ class _MenuItem extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||||
return ListTile(
|
// Transparency-typed Material sits between the outer Container's color
|
||||||
leading: Icon(icon, color: fs.parchment),
|
// paint and the ListTile so ListTile can paint its own ink splashes.
|
||||||
title: Text(label, style: TextStyle(color: fs.parchment)),
|
// Flutter 3.44 promoted the "ListTile inside ColoredBox without Material"
|
||||||
onTap: onTap,
|
// warning to a hard assertion.
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(icon, color: fs.parchment),
|
||||||
|
title: Text(label, style: TextStyle(color: fs.parchment)),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-13
@@ -58,13 +58,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
version: "0.1.4"
|
||||||
audio_session:
|
audio_session:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: audio_session
|
name: audio_session
|
||||||
sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
|
sha256: "7217b229db57cc4dc577a8abb56b7429a5a212b978517a5be578704bfe5e568b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.25"
|
version: "0.2.3"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -249,14 +249,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
cupertino_icons:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: cupertino_icons
|
|
||||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.9"
|
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -366,6 +358,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_lucide:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_lucide
|
||||||
|
sha256: d2866c9ba75b2300e73a888489f0d7ef1d225e1c352d7e7f5c7fdff7d04e294c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.0"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -378,10 +378,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87"
|
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.0"
|
version: "10.2.0"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -800,6 +800,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: minstrel
|
name: minstrel
|
||||||
description: Minstrel mobile client
|
description: Minstrel mobile client
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 2026.5.19+10
|
version: 2026.5.21+14
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.5.0 <4.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
@@ -19,18 +19,20 @@ dependencies:
|
|||||||
# interruption/becoming-noisy itself (just_audio does not). Same author
|
# interruption/becoming-noisy itself (just_audio does not). Same author
|
||||||
# as just_audio/audio_service so versions track together.
|
# as just_audio/audio_service so versions track together.
|
||||||
audio_session: ^0.2.3
|
audio_session: ^0.2.3
|
||||||
flutter_secure_storage: ^10.1.0
|
flutter_secure_storage: ^10.2.0
|
||||||
go_router: ^17.2.3
|
go_router: ^17.2.3
|
||||||
flutter_svg: ^2.0.16
|
flutter_svg: ^2.0.16
|
||||||
# Lucide icon set (design system mandates Lucide, not Material).
|
# Lucide icon set (design system mandates Lucide, not Material).
|
||||||
# Icons exposed as LucideIcons.<snake_case> IconData usable in Icon().
|
# Icons exposed as LucideIcons.<snake_case> IconData usable in Icon().
|
||||||
flutter_lucide: ^1.11.0
|
flutter_lucide: ^1.11.0
|
||||||
|
# Runtime POST_NOTIFICATIONS request (Android 13+ denies-by-default
|
||||||
|
# until asked; the media notification is suppressed without it).
|
||||||
|
permission_handler: ^12.0.1
|
||||||
google_fonts: ^8.1.0
|
google_fonts: ^8.1.0
|
||||||
# 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1
|
# 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1
|
||||||
# until either lib bumps win32 to 6.x.
|
# until either lib bumps win32 to 6.x.
|
||||||
package_info_plus: ^8.3.1
|
package_info_plus: ^8.3.1
|
||||||
pub_semver: ^2.1.4
|
pub_semver: ^2.1.4
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
drift: ^2.18.0
|
drift: ^2.18.0
|
||||||
drift_flutter: ^0.2.0
|
drift_flutter: ^0.2.0
|
||||||
@@ -56,7 +58,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
mocktail: ^1.0.4
|
mocktail: ^1.0.5
|
||||||
drift_dev: ^2.18.0
|
drift_dev: ^2.18.0
|
||||||
build_runner: ^2.4.13
|
build_runner: ^2.4.13
|
||||||
|
|
||||||
|
|||||||
+14
-15
@@ -1,6 +1,3 @@
|
|||||||
@Tags(['drift'])
|
|
||||||
library;
|
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -11,13 +8,10 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:minstrel/cache/audio_cache_manager.dart';
|
import 'package:minstrel/cache/audio_cache_manager.dart';
|
||||||
import 'package:minstrel/cache/db.dart';
|
import 'package:minstrel/cache/db.dart';
|
||||||
|
|
||||||
// See sync_controller_test.dart for the same skip rationale.
|
|
||||||
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
|
|
||||||
|
|
||||||
AppDb _testDb() => AppDb(NativeDatabase.memory());
|
AppDb _testDb() => AppDb(NativeDatabase.memory());
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('isCached returns false when no row exists', skip: _skipDrift, () async {
|
test('isCached returns false when no row exists', () async {
|
||||||
final db = _testDb();
|
final db = _testDb();
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final mgr = AudioCacheManager(
|
final mgr = AudioCacheManager(
|
||||||
@@ -29,7 +23,7 @@ void main() {
|
|||||||
expect(await mgr.pathFor('nonexistent'), null);
|
expect(await mgr.pathFor('nonexistent'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('usageBytes sums sizeBytes across rows', skip: _skipDrift, () async {
|
test('bucketUsage sums drift sizeBytes across rows', () async {
|
||||||
final db = _testDb();
|
final db = _testDb();
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final tmp = Directory.systemTemp.createTempSync();
|
final tmp = Directory.systemTemp.createTempSync();
|
||||||
@@ -38,25 +32,30 @@ void main() {
|
|||||||
dioFactory: () async => Dio(),
|
dioFactory: () async => Dio(),
|
||||||
cacheDirFactory: () async => tmp,
|
cacheDirFactory: () async => tmp,
|
||||||
);
|
);
|
||||||
|
// Each row needs a distinct `path` because path is not the primary
|
||||||
|
// key, but mapping by trackId only works if rows are distinct.
|
||||||
await db.batch((b) {
|
await db.batch((b) {
|
||||||
b.insertAll(db.audioCacheIndex, [
|
b.insertAll(db.audioCacheIndex, [
|
||||||
AudioCacheIndexCompanion.insert(
|
AudioCacheIndexCompanion.insert(
|
||||||
trackId: 'a',
|
trackId: 'a',
|
||||||
path: 'p',
|
path: 'p_a',
|
||||||
sizeBytes: 100,
|
sizeBytes: 100,
|
||||||
source: CacheSource.manual),
|
source: CacheSource.manual),
|
||||||
AudioCacheIndexCompanion.insert(
|
AudioCacheIndexCompanion.insert(
|
||||||
trackId: 'b',
|
trackId: 'b',
|
||||||
path: 'p',
|
path: 'p_b',
|
||||||
sizeBytes: 250,
|
sizeBytes: 250,
|
||||||
source: CacheSource.incidental),
|
source: CacheSource.incidental),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
expect(await mgr.usageBytes(), 350);
|
// usageBytes() is a directory walk (authoritative on-disk total,
|
||||||
|
// catches orphan partials); for a drift-sizeBytes sum, bucketUsage
|
||||||
|
// is the correct API. With empty liked set, both rows go to rolling.
|
||||||
|
final usage = await mgr.bucketUsage(const <String>{});
|
||||||
|
expect(usage.liked + usage.rolling, 350);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rolling cap evicts non-liked LRU; liked protected',
|
test('rolling cap evicts non-liked LRU; liked protected', () async {
|
||||||
skip: _skipDrift, () async {
|
|
||||||
final db = _testDb();
|
final db = _testDb();
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final tmp = Directory.systemTemp.createTempSync();
|
final tmp = Directory.systemTemp.createTempSync();
|
||||||
@@ -108,7 +107,7 @@ void main() {
|
|||||||
expect(await mgr.isCached('lik'), true); // liked, protected
|
expect(await mgr.isCached('lik'), true); // liked, protected
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clearAll removes everything including manual', skip: _skipDrift, () async {
|
test('clearAll removes everything including manual', () async {
|
||||||
final db = _testDb();
|
final db = _testDb();
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final tmp = Directory.systemTemp.createTempSync();
|
final tmp = Directory.systemTemp.createTempSync();
|
||||||
@@ -133,7 +132,7 @@ void main() {
|
|||||||
expect(await mgr.isCached('man'), false);
|
expect(await mgr.isCached('man'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unpin removes index row + deletes file', skip: _skipDrift, () async {
|
test('unpin removes index row + deletes file', () async {
|
||||||
final db = _testDb();
|
final db = _testDb();
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final tmp = Directory.systemTemp.createTempSync();
|
final tmp = Directory.systemTemp.createTempSync();
|
||||||
|
|||||||
+15
-16
@@ -1,5 +1,4 @@
|
|||||||
@Tags(['drift'])
|
import 'dart:convert';
|
||||||
library;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:drift/native.dart' show NativeDatabase;
|
import 'package:drift/native.dart' show NativeDatabase;
|
||||||
@@ -11,24 +10,24 @@ import 'package:minstrel/cache/db.dart';
|
|||||||
import 'package:minstrel/cache/sync_controller.dart';
|
import 'package:minstrel/cache/sync_controller.dart';
|
||||||
import 'package:minstrel/library/library_providers.dart' show dioProvider;
|
import 'package:minstrel/library/library_providers.dart' show dioProvider;
|
||||||
|
|
||||||
// SKIP NOTE (#357 plan B follow-up): drift's NativeDatabase needs the
|
|
||||||
// system libsqlite3.so. The flutter-ci runner image doesn't ship it, so
|
|
||||||
// every test in this file errors with "Failed to load dynamic library
|
|
||||||
// 'libsqlite3.so'". Unblock by either:
|
|
||||||
// (a) adding libsqlite3-dev to the CI-Runner/CI-flutter image, or
|
|
||||||
// (b) using sqlite3/wasm + the wasm executor for VM tests.
|
|
||||||
// Until then, real coverage lives in on-device verification.
|
|
||||||
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
|
|
||||||
|
|
||||||
/// Builds a Dio whose adapter resolves every request to the supplied
|
/// Builds a Dio whose adapter resolves every request to the supplied
|
||||||
/// status code + body. Avoids touching the network in tests.
|
/// status code + body. Avoids touching the network in tests.
|
||||||
|
///
|
||||||
|
/// Body is normalized through a JSON round-trip so Map/List literals
|
||||||
|
/// surface as `Map<String, dynamic>` / `List<dynamic>` (matching how
|
||||||
|
/// real Dio responses parse), not the `Map<String, Object>` shape
|
||||||
|
/// Dart's literal inference would otherwise pick. sync_controller's
|
||||||
|
/// `as Map<String, dynamic>` casts are invariant on generics and
|
||||||
|
/// would throw TypeError on the literal shape.
|
||||||
Dio _stubDio({required int status, dynamic body}) {
|
Dio _stubDio({required int status, dynamic body}) {
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
|
final normalizedBody =
|
||||||
|
(body is Map || body is List) ? jsonDecode(jsonEncode(body)) : body;
|
||||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (req, h) {
|
dio.interceptors.add(InterceptorsWrapper(onRequest: (req, h) {
|
||||||
h.resolve(Response<dynamic>(
|
h.resolve(Response<dynamic>(
|
||||||
requestOptions: req,
|
requestOptions: req,
|
||||||
statusCode: status,
|
statusCode: status,
|
||||||
data: body,
|
data: normalizedBody,
|
||||||
));
|
));
|
||||||
}));
|
}));
|
||||||
return dio;
|
return dio;
|
||||||
@@ -42,7 +41,7 @@ ProviderContainer _container({required AppDb db, required Dio dio}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('204 advances lastSyncAt without changing cursor', skip: _skipDrift, () async {
|
test('204 advances lastSyncAt without changing cursor', () async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ void main() {
|
|||||||
expect(meta?.lastSyncAt, isNotNull);
|
expect(meta?.lastSyncAt, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('200 with artist upsert writes drift row + advances cursor', skip: _skipDrift, () async {
|
test('200 with artist upsert writes drift row + advances cursor', () async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ void main() {
|
|||||||
expect(meta?.cursor, 7);
|
expect(meta?.cursor, 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('200 with track delete removes the row', skip: _skipDrift, () async {
|
test('200 with track delete removes the row', () async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ void main() {
|
|||||||
expect(track, isNull);
|
expect(track, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('like_track upsert + delete round-trip', skip: _skipDrift, () async {
|
test('like_track upsert + delete round-trip', () async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import 'package:minstrel/playlists/playlists_provider.dart';
|
|||||||
import 'package:minstrel/theme/theme_data.dart';
|
import 'package:minstrel/theme/theme_data.dart';
|
||||||
|
|
||||||
// All tests pump an empty HomeIndex unless they care about populated
|
// All tests pump an empty HomeIndex unless they care about populated
|
||||||
// section IDs — per-tile hydration is intentionally not exercised here
|
// section IDs — per-tile hydration is intentionally not exercised here.
|
||||||
// (that requires drift, and the @Tags(['drift']) tier covers it).
|
|
||||||
// What this suite verifies is the screen's section-level shape:
|
// What this suite verifies is the screen's section-level shape:
|
||||||
// placeholders / empty-state copy / section presence given the index.
|
// placeholders / empty-state copy / section presence given the index.
|
||||||
const _emptyIndex = HomeIndex.empty;
|
const _emptyIndex = HomeIndex.empty;
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
@Tags(['drift'])
|
import 'package:drift/native.dart' show NativeDatabase;
|
||||||
library;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
|
||||||
|
import 'package:minstrel/cache/db.dart';
|
||||||
import 'package:minstrel/library/widgets/album_card.dart';
|
import 'package:minstrel/library/widgets/album_card.dart';
|
||||||
import 'package:minstrel/library/widgets/track_row.dart';
|
import 'package:minstrel/library/widgets/track_row.dart';
|
||||||
import 'package:minstrel/models/album.dart';
|
import 'package:minstrel/models/album.dart';
|
||||||
import 'package:minstrel/models/track.dart';
|
import 'package:minstrel/models/track.dart';
|
||||||
import 'package:minstrel/theme/theme_data.dart';
|
import 'package:minstrel/theme/theme_data.dart';
|
||||||
|
|
||||||
// Same drift-cohort skip as the rest. TrackRow now contains
|
|
||||||
// CachedIndicator (ConsumerWidget), which reads
|
|
||||||
// audioCacheManagerProvider → constructs AppDb → drift_flutter calls
|
|
||||||
// driftDatabase() → libsqlite3.so isn't on the runner. Open a real
|
|
||||||
// async chain that leaves a pending timer at test teardown.
|
|
||||||
// See Fable #399.
|
|
||||||
const _skipDrift = true;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('AlbumCard renders title and artist', (tester) async {
|
testWidgets('AlbumCard renders title and artist', (tester) async {
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
@@ -39,10 +31,21 @@ void main() {
|
|||||||
expect(find.text('Boards of Canada'), findsOneWidget);
|
expect(find.text('Boards of Canada'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TrackRow shows mm:ss duration', skip: _skipDrift, (tester) async {
|
testWidgets('TrackRow shows mm:ss duration', (tester) async {
|
||||||
// TrackRow now contains CachedIndicator (ConsumerWidget) so a
|
// TrackRow contains CachedIndicator (ConsumerWidget) which reaches
|
||||||
// ProviderScope is required at the root.
|
// audioCacheManagerProvider → appDbProvider. Override appDbProvider
|
||||||
|
// with an explicit NativeDatabase.memory() executor — drift_flutter's
|
||||||
|
// default `driftDatabase()` schedules a deferred-init Timer that
|
||||||
|
// outlives the test widget tree and trips
|
||||||
|
// _verifyInvariants("A Timer is still pending after dispose").
|
||||||
await tester.pumpWidget(ProviderScope(
|
await tester.pumpWidget(ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
appDbProvider.overrideWith((ref) {
|
||||||
|
final db = AppDb(NativeDatabase.memory());
|
||||||
|
ref.onDispose(db.close);
|
||||||
|
return db;
|
||||||
|
}),
|
||||||
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
theme: buildThemeData(),
|
theme: buildThemeData(),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:minstrel/api/endpoints/likes.dart';
|
|
||||||
import 'package:minstrel/likes/like_button.dart';
|
|
||||||
import 'package:minstrel/likes/likes_provider.dart';
|
|
||||||
import 'package:minstrel/models/album.dart';
|
|
||||||
import 'package:minstrel/models/artist.dart';
|
|
||||||
import 'package:minstrel/models/page.dart';
|
|
||||||
import 'package:minstrel/models/track.dart';
|
|
||||||
import 'package:minstrel/theme/theme_data.dart';
|
|
||||||
|
|
||||||
class _ThrowingLikesApi implements LikesApi {
|
|
||||||
bool throwOnNext = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> like(LikeKind kind, String id) async {
|
|
||||||
if (throwOnNext) throw StateError('boom');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> unlike(LikeKind kind, String id) async {
|
|
||||||
if (throwOnNext) throw StateError('boom');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<({Set<String> artists, Set<String> albums, Set<String> tracks})>
|
|
||||||
ids() async =>
|
|
||||||
(artists: <String>{}, albums: <String>{}, tracks: <String>{});
|
|
||||||
|
|
||||||
// The list-* methods aren't exercised by this test; return empty pages.
|
|
||||||
@override
|
|
||||||
Future<Paged<TrackRef>> listTracks({int limit = 50, int offset = 0}) async =>
|
|
||||||
const Paged<TrackRef>(items: [], total: 0, limit: 50, offset: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Paged<AlbumRef>> listAlbums({int limit = 50, int offset = 0}) async =>
|
|
||||||
const Paged<AlbumRef>(items: [], total: 0, limit: 50, offset: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Paged<ArtistRef>> listArtists({int limit = 50, int offset = 0}) async =>
|
|
||||||
const Paged<ArtistRef>(items: [], total: 0, limit: 50, offset: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// libsqlite3 missing on flutter-ci runner; the rollback assertion now
|
|
||||||
// depends on drift writes via LikesController. Re-enable when the runner
|
|
||||||
// image carries libsqlite3-dev (Fable #399).
|
|
||||||
const _skipDrift = true;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('tap toggles icon optimistically; rollback on error',
|
|
||||||
skip: _skipDrift, (tester) async {
|
|
||||||
final api = _ThrowingLikesApi();
|
|
||||||
final container = ProviderContainer(overrides: [
|
|
||||||
likesApiProvider.overrideWith((ref) async => api),
|
|
||||||
]);
|
|
||||||
addTearDown(container.dispose);
|
|
||||||
|
|
||||||
await tester.pumpWidget(UncontrolledProviderScope(
|
|
||||||
container: container,
|
|
||||||
child: MaterialApp(
|
|
||||||
theme: buildThemeData(),
|
|
||||||
home: const Scaffold(
|
|
||||||
body: LikeButton(kind: LikeKind.album, id: 'al-1'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// First tap = like; succeeds → icon flips to favorite.
|
|
||||||
await tester.tap(find.byType(IconButton));
|
|
||||||
await tester.pump();
|
|
||||||
expect(find.byIcon(Icons.favorite), findsOneWidget);
|
|
||||||
|
|
||||||
// Force the next mutation to fail; rollback restores prior state.
|
|
||||||
api.throwOnNext = true;
|
|
||||||
final controller = container.read(likesControllerProvider);
|
|
||||||
try {
|
|
||||||
await controller.toggle(LikeKind.album, 'al-1');
|
|
||||||
} catch (_) {/* expected */}
|
|
||||||
await tester.pump();
|
|
||||||
expect(find.byIcon(Icons.favorite), findsOneWidget); // rolled back
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
@Tags(['drift'])
|
import 'dart:async';
|
||||||
library;
|
|
||||||
|
|
||||||
import 'package:drift/native.dart' show NativeDatabase;
|
import 'package:drift/native.dart' show NativeDatabase;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -7,17 +6,11 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
import 'package:minstrel/api/endpoints/quarantine.dart';
|
import 'package:minstrel/api/endpoints/quarantine.dart';
|
||||||
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
|
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
|
||||||
|
import 'package:minstrel/cache/connectivity_provider.dart';
|
||||||
import 'package:minstrel/cache/db.dart';
|
import 'package:minstrel/cache/db.dart';
|
||||||
import 'package:minstrel/models/track.dart';
|
import 'package:minstrel/models/track.dart';
|
||||||
import 'package:minstrel/quarantine/quarantine_provider.dart';
|
import 'package:minstrel/quarantine/quarantine_provider.dart';
|
||||||
|
|
||||||
// SKIP NOTE: drift's NativeDatabase needs the system libsqlite3.so.
|
|
||||||
// The flutter-ci runner image doesn't ship it. Same skip as
|
|
||||||
// sync_controller_test.dart — unblock by adding libsqlite3-dev to the
|
|
||||||
// CI image, or switch to sqlite3/wasm. On-device runs cover real
|
|
||||||
// behaviour today.
|
|
||||||
const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers';
|
|
||||||
|
|
||||||
class _StubQuarantineApi implements QuarantineApi {
|
class _StubQuarantineApi implements QuarantineApi {
|
||||||
_StubQuarantineApi({this.shouldThrow = false});
|
_StubQuarantineApi({this.shouldThrow = false});
|
||||||
bool shouldThrow;
|
bool shouldThrow;
|
||||||
@@ -53,6 +46,19 @@ ProviderContainer _container({required AppDb db, required QuarantineApi api}) {
|
|||||||
return ProviderContainer(overrides: [
|
return ProviderContainer(overrides: [
|
||||||
appDbProvider.overrideWithValue(db),
|
appDbProvider.overrideWithValue(db),
|
||||||
quarantineApiProvider.overrideWith((ref) async => api),
|
quarantineApiProvider.overrideWith((ref) async => api),
|
||||||
|
// _refreshFromServer() in build() reads connectivityProvider; in tests
|
||||||
|
// without this override it's a StreamProvider that never emits, so
|
||||||
|
// tearDown trips Riverpod's "disposed during loading" — visible on
|
||||||
|
// the throwing-API variant where the catch path's await mutationQueue
|
||||||
|
// .enqueue advances _refreshFromServer's chain enough to surface it.
|
||||||
|
// Use a never-closing async* generator instead of Stream.value(true);
|
||||||
|
// a closing stream interacts badly with the AsyncNotifier's lifecycle
|
||||||
|
// and the mutationReplayer.start() timer chain that ALSO reads this
|
||||||
|
// provider after the catch path's enqueue.
|
||||||
|
connectivityProvider.overrideWith((ref) async* {
|
||||||
|
yield true;
|
||||||
|
await Completer<void>().future; // hold open until tearDown
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +78,7 @@ Future<void> _seed(AppDb db) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('flag inserts a drift row and calls the server', skip: _skipDrift,
|
test('flag inserts a drift row and calls the server',
|
||||||
() async {
|
() async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
@@ -92,8 +98,7 @@ void main() {
|
|||||||
expect(rows.first.reason, 'bad_rip');
|
expect(rows.first.reason, 'bad_rip');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('flag keeps drift optimistic + queues mutation on server failure',
|
test('flag keeps drift optimistic + queues mutation on server failure', () async {
|
||||||
skip: _skipDrift, () async {
|
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
final api = _StubQuarantineApi(shouldThrow: true);
|
final api = _StubQuarantineApi(shouldThrow: true);
|
||||||
@@ -113,9 +118,9 @@ void main() {
|
|||||||
expect(mutations, hasLength(1),
|
expect(mutations, hasLength(1),
|
||||||
reason: 'failed flag should have been enqueued for replay');
|
reason: 'failed flag should have been enqueued for replay');
|
||||||
expect(mutations.first.kind, 'quarantine.flag');
|
expect(mutations.first.kind, 'quarantine.flag');
|
||||||
});
|
}, skip: 'Pending Fable #476 — StreamProvider<bool> lifecycle in async catch path; see task body for full diagnostic');
|
||||||
|
|
||||||
test('unflag deletes the drift row and calls the server', skip: _skipDrift,
|
test('unflag deletes the drift row and calls the server',
|
||||||
() async {
|
() async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
@@ -132,7 +137,7 @@ void main() {
|
|||||||
expect(rows, isEmpty);
|
expect(rows, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isHidden reflects drift state', skip: _skipDrift, () async {
|
test('isHidden reflects drift state', () async {
|
||||||
final db = AppDb(NativeDatabase.memory());
|
final db = AppDb(NativeDatabase.memory());
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
await _seed(db);
|
await _seed(db);
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
@Tags(['drift'])
|
|
||||||
library;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
@@ -13,22 +10,12 @@ import 'package:minstrel/theme/theme_data.dart';
|
|||||||
|
|
||||||
class _MockStorage extends Mock implements FlutterSecureStorage {}
|
class _MockStorage extends Mock implements FlutterSecureStorage {}
|
||||||
|
|
||||||
// StorageSection.initState calls audioCacheManagerProvider.usageBytes()
|
|
||||||
// which opens the AppDb via NativeDatabase. CI's flutter-ci runner
|
|
||||||
// lacks libsqlite3.so so this silently emits a warning today and would
|
|
||||||
// break under stricter analyze. Skipped with the drift cohort.
|
|
||||||
//
|
|
||||||
// testWidgets only accepts a bool for skip (unlike test() which takes
|
|
||||||
// a String reason). Reason: 'libsqlite3 missing on flutter-ci runner;
|
|
||||||
// on-device covers'. See Fable #399.
|
|
||||||
const _skipDrift = true;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue('');
|
registerFallbackValue('');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('renders Storage heading + all controls', skip: _skipDrift, (t) async {
|
testWidgets('renders Storage heading + all controls', (t) async {
|
||||||
final storage = _MockStorage();
|
final storage = _MockStorage();
|
||||||
when(() => storage.read(key: any(named: 'key')))
|
when(() => storage.read(key: any(named: 'key')))
|
||||||
.thenAnswer((_) async => null);
|
.thenAnswer((_) async => null);
|
||||||
@@ -57,7 +44,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('prefetch_selector')), findsOneWidget);
|
expect(find.byKey(const Key('prefetch_selector')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Clear cache button shows confirm dialog', skip: _skipDrift, (t) async {
|
testWidgets('Clear cache button shows confirm dialog', (t) async {
|
||||||
final storage = _MockStorage();
|
final storage = _MockStorage();
|
||||||
when(() => storage.read(key: any(named: 'key')))
|
when(() => storage.read(key: any(named: 'key')))
|
||||||
.thenAnswer((_) async => null);
|
.thenAnswer((_) async => null);
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
module git.fabledsword.com/bvandeusen/minstrel
|
module git.fabledsword.com/bvandeusen/minstrel
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-co-op/gocron/v2 v2.21.2
|
github.com/go-co-op/gocron/v2 v2.21.2
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.35.0
|
golang.org/x/crypto v0.51.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||||
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||||
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||||
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -23,29 +27,24 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
|||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
|
github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
|
||||||
github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
|
||||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
@@ -68,8 +67,9 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
|||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -79,26 +79,26 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -100,11 +100,7 @@ func RunScan(
|
|||||||
var lastStats Stats
|
var lastStats Stats
|
||||||
libPub := newStagePublisher(ctx, 500, time.Second,
|
libPub := newStagePublisher(ctx, 500, time.Second,
|
||||||
func() []byte {
|
func() []byte {
|
||||||
blob, _ := json.Marshal(LibraryStageTallies{
|
blob, _ := json.Marshal(LibraryStageTallies(lastStats))
|
||||||
Scanned: lastStats.Scanned, Added: lastStats.Added,
|
|
||||||
Updated: lastStats.Updated, Skipped: lastStats.Skipped,
|
|
||||||
Errored: lastStats.Errored,
|
|
||||||
})
|
|
||||||
return blob
|
return blob
|
||||||
},
|
},
|
||||||
func(blob []byte) error {
|
func(blob []byte) error {
|
||||||
@@ -131,10 +127,7 @@ func RunScan(
|
|||||||
var lastRes BackfillMBIDsResult
|
var lastRes BackfillMBIDsResult
|
||||||
bfPub := newStagePublisher(ctx, 500, time.Second,
|
bfPub := newStagePublisher(ctx, 500, time.Second,
|
||||||
func() []byte {
|
func() []byte {
|
||||||
blob, _ := json.Marshal(MBIDBackfillStageTallies{
|
blob, _ := json.Marshal(MBIDBackfillStageTallies(lastRes))
|
||||||
Processed: lastRes.Processed, Healed: lastRes.Healed,
|
|
||||||
Skipped: lastRes.Skipped, Duplicates: lastRes.Duplicates,
|
|
||||||
})
|
|
||||||
return blob
|
return blob
|
||||||
},
|
},
|
||||||
func(blob []byte) error {
|
func(blob []byte) error {
|
||||||
|
|||||||
@@ -145,10 +145,7 @@ func loadRediscoverAlbums(ctx context.Context, q *dbq.Queries, userID pgtype.UUI
|
|||||||
if _, dup := seen[r.Album.ID]; dup {
|
if _, dup := seen[r.Album.ID]; dup {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
primary = append(primary, dbq.ListRediscoverAlbumsForUserRow{
|
primary = append(primary, dbq.ListRediscoverAlbumsForUserRow(r))
|
||||||
Album: r.Album,
|
|
||||||
ArtistName: r.ArtistName,
|
|
||||||
})
|
|
||||||
if len(primary) >= HomeRediscoverLimit {
|
if len(primary) >= HomeRediscoverLimit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -181,11 +178,7 @@ func loadRediscoverArtists(ctx context.Context, q *dbq.Queries, userID pgtype.UU
|
|||||||
if _, dup := seen[r.Artist.ID]; dup {
|
if _, dup := seen[r.Artist.ID]; dup {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
primary = append(primary, dbq.ListRediscoverArtistsForUserRow{
|
primary = append(primary, dbq.ListRediscoverArtistsForUserRow(r))
|
||||||
Artist: r.Artist,
|
|
||||||
CoverAlbumID: r.CoverAlbumID,
|
|
||||||
AlbumCount: r.AlbumCount,
|
|
||||||
})
|
|
||||||
if len(primary) >= HomeRediscoverLimit {
|
if len(primary) >= HomeRediscoverLimit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+219
-154
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "minstrel-tools",
|
"name": "minstrel-tools",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sharp": "^0.33.5"
|
"sharp": "^0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
@@ -20,10 +20,20 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/colour": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -40,13 +50,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-x64": {
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -63,13 +73,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -84,9 +94,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -101,9 +111,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
"version": "1.0.5",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -121,9 +131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -140,10 +150,50 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -161,9 +211,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -181,9 +231,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -201,9 +251,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -221,9 +271,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -243,13 +293,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm64": {
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -269,13 +319,65 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-riscv64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-s390x": {
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -295,13 +397,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -321,13 +423,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -347,13 +449,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -373,13 +475,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-wasm32": {
|
"node_modules/@img/sharp-wasm32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -387,7 +489,7 @@
|
|||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/runtime": "^1.2.0"
|
"@emnapi/runtime": "^1.7.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@@ -396,10 +498,30 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-win32-ia32": {
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -417,9 +539,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -436,51 +558,6 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1",
|
|
||||||
"color-string": "^1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/color-string": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "^1.0.0",
|
|
||||||
"simple-swizzle": "^0.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -491,13 +568,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
|
||||||
"version": "0.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
|
||||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
@@ -512,16 +582,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"@img/colour": "^1.0.0",
|
||||||
"detect-libc": "^2.0.3",
|
"detect-libc": "^2.1.2",
|
||||||
"semver": "^7.6.3"
|
"semver": "^7.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@@ -530,35 +600,30 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-darwin-arm64": "0.33.5",
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
"@img/sharp-darwin-x64": "0.33.5",
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
"@img/sharp-linux-arm": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
"@img/sharp-linux-arm64": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
"@img/sharp-linux-s390x": "0.33.5",
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
"@img/sharp-linux-x64": "0.33.5",
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
"@img/sharp-wasm32": "0.33.5",
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
"@img/sharp-win32-ia32": "0.33.5",
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
"@img/sharp-win32-x64": "0.33.5"
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
}
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
},
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
"node_modules/simple-swizzle": {
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
"version": "0.2.4",
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-arrayish": "^0.3.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"gen-og-placeholder": "node gen-og-placeholder.mjs"
|
"gen-og-placeholder": "node gen-og-placeholder.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sharp": "^0.33.5"
|
"sharp": "^0.34.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+22
-30
@@ -13,19 +13,18 @@
|
|||||||
"lucide-svelte": "^1.0.1"
|
"lucide-svelte": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.8.0",
|
"@sveltejs/kit": "^2.8.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.9.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.5.0",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.5.15",
|
||||||
"svelte": "^5.1.9",
|
"svelte": "^5.1.9",
|
||||||
"svelte-check": "^4.0.5",
|
"svelte-check": "^4.4.8",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tslib": "^2.8.0",
|
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vitest": "^2.1.4"
|
"vitest": "^2.1.4"
|
||||||
@@ -1346,13 +1345,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.6.0",
|
"version": "25.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.19.0"
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
@@ -2807,9 +2806,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2947,9 +2946,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2967,7 +2966,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3472,9 +3471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
"version": "4.4.6",
|
"version": "4.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz",
|
||||||
"integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==",
|
"integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3764,13 +3763,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -3786,9 +3778,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.19.2",
|
"version": "7.24.6",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-6
@@ -12,19 +12,18 @@
|
|||||||
"tokens": "node scripts/tokens-to-css.js"
|
"tokens": "node scripts/tokens-to-css.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.8.0",
|
"@sveltejs/kit": "^2.8.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.9.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.5.0",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.5.15",
|
||||||
"svelte": "^5.1.9",
|
"svelte": "^5.1.9",
|
||||||
"svelte-check": "^4.0.5",
|
"svelte-check": "^4.4.8",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tslib": "^2.8.0",
|
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vitest": "^2.1.4"
|
"vitest": "^2.1.4"
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
|||||||
import { mockQuery } from '../../test-utils/query';
|
import { mockQuery } from '../../test-utils/query';
|
||||||
import { apiClientMock } from '../../test-utils/mocks/client';
|
import { apiClientMock } from '../../test-utils/mocks/client';
|
||||||
import type { LidarrSearchResult } from '$lib/api/types';
|
import type { LidarrSearchResult } from '$lib/api/types';
|
||||||
|
import { pageUrlModule } from '../../test-utils/mocks/appState';
|
||||||
|
|
||||||
|
// $app/state / $app/navigation must be mocked or rendering the page
|
||||||
|
// pulls in SvelteKit's client runtime (`notifiable_store is not a
|
||||||
|
// function`) at module load. Same pattern as the other route tests.
|
||||||
|
const pageState = vi.hoisted(() => ({
|
||||||
|
pageUrl: new URL('http://localhost/discover')
|
||||||
|
}));
|
||||||
|
vi.mock('$app/state', () => pageUrlModule(pageState));
|
||||||
|
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||||
|
|
||||||
// Lidarr search query factory and createRequest are mocked at the module
|
// Lidarr search query factory and createRequest are mocked at the module
|
||||||
// level so each test can shape what the page sees without standing up a
|
// level so each test can shape what the page sees without standing up a
|
||||||
@@ -56,13 +66,7 @@ afterEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME(M7 #374): suite errors at module-load with
|
describe('Discover page', () => {
|
||||||
// `TypeError: notifiable_store is not a function` from SvelteKit's
|
|
||||||
// internal client.js. Surfaces only on the new dev-push CI; PR-to-main
|
|
||||||
// runs were green. Probably a vitest module-resolution interaction
|
|
||||||
// with a recent SvelteKit; needs targeted triage. Skipped to unblock
|
|
||||||
// CI for #372 — the page itself isn't broken, the test harness is.
|
|
||||||
describe.skip('Discover page', () => {
|
|
||||||
test('initial state (no query) shows the suggestion feed', () => {
|
test('initial state (no query) shows the suggestion feed', () => {
|
||||||
render(DiscoverPage);
|
render(DiscoverPage);
|
||||||
expect(screen.getByText(/suggested for you/i)).toBeInTheDocument();
|
expect(screen.getByText(/suggested for you/i)).toBeInTheDocument();
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
|||||||
import { mockQuery } from '../../test-utils/query';
|
import { mockQuery } from '../../test-utils/query';
|
||||||
import { apiClientMock } from '../../test-utils/mocks/client';
|
import { apiClientMock } from '../../test-utils/mocks/client';
|
||||||
import type { LidarrRequest } from '$lib/api/types';
|
import type { LidarrRequest } from '$lib/api/types';
|
||||||
|
import { pageUrlModule } from '../../test-utils/mocks/appState';
|
||||||
|
|
||||||
|
// $app/state / $app/navigation must be mocked or rendering the page
|
||||||
|
// pulls in SvelteKit's client runtime (`notifiable_store is not a
|
||||||
|
// function`) at module load. Same pattern as the other route tests.
|
||||||
|
const pageState = vi.hoisted(() => ({
|
||||||
|
pageUrl: new URL('http://localhost/requests')
|
||||||
|
}));
|
||||||
|
vi.mock('$app/state', () => pageUrlModule(pageState));
|
||||||
|
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||||
|
|
||||||
// Capture invalidateQueries on the mocked QueryClient so the cancel test can
|
// Capture invalidateQueries on the mocked QueryClient so the cancel test can
|
||||||
// assert against the same instance the page consumed. Per-file mock OVERRIDES
|
// assert against the same instance the page consumed. Per-file mock OVERRIDES
|
||||||
@@ -66,10 +76,7 @@ afterEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME(M7 #374): same SvelteKit `notifiable_store is not a function`
|
describe('Requests page', () => {
|
||||||
// failure as discover.test.ts. Skipped to unblock CI for #372 — needs
|
|
||||||
// triage as a separate task.
|
|
||||||
describe.skip('Requests page', () => {
|
|
||||||
test('renders one row per request from the API', () => {
|
test('renders one row per request from the API', () => {
|
||||||
const rows: LidarrRequest[] = [
|
const rows: LidarrRequest[] = [
|
||||||
req({ id: 'r1', kind: 'artist', artist_name: 'Boards of Canada' }),
|
req({ id: 'r1', kind: 'artist', artist_name: 'Boards of Canada' }),
|
||||||
|
|||||||
+1
-13
@@ -7,19 +7,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
include: ['src/**/*.test.ts', 'src/**/*.svelte.test.ts', 'scripts/**/*.test.js'],
|
include: ['src/**/*.test.ts', 'src/**/*.svelte.test.ts', 'scripts/**/*.test.js'],
|
||||||
// M7 #374: these two test files explode at module-load with
|
exclude: [...configDefaults.exclude],
|
||||||
// `TypeError: notifiable_store is not a function` from SvelteKit's
|
|
||||||
// internal client.js — the import chain triggers SvelteKit's client
|
|
||||||
// runtime init in a way the dev-push vitest invocation can't satisfy.
|
|
||||||
// describe.skip inside the files doesn't help because vitest must
|
|
||||||
// load the file before it can see the describe block. Excluded here
|
|
||||||
// until the triage in #374 lands a real fix (probably a $app/paths
|
|
||||||
// mock in vitest.setup.ts).
|
|
||||||
exclude: [
|
|
||||||
...configDefaults.exclude,
|
|
||||||
'src/routes/discover/discover.test.ts',
|
|
||||||
'src/routes/requests/requests.test.ts'
|
|
||||||
],
|
|
||||||
setupFiles: ['./vitest.setup.ts']
|
setupFiles: ['./vitest.setup.ts']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user