feat: the library's folders are set in Admin › Library, and changing a folder moves its tracks with it, keeping their plays, likes and playlists (#6129)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m20s
release / go (push) Failing after 1m37s
release / integration (push) Failing after 4m56s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 20:37:03 -04:00
co-authored by Claude Opus 5.5
parent 453a977e48
commit edcb3a57ef
25 changed files with 1102 additions and 57 deletions
+162
View File
@@ -0,0 +1,162 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: library_settings.sql
package dbq
import (
"context"
)
const countTracksUnder = `-- name: CountTracksUnder :one
SELECT count(*) FROM tracks WHERE starts_with(file_path, $1::text || '/')
`
func (q *Queries) CountTracksUnder(ctx context.Context, root string) (int64, error) {
row := q.db.QueryRow(ctx, countTracksUnder, root)
var count int64
err := row.Scan(&count)
return count, err
}
const getLibrarySettings = `-- name: GetLibrarySettings :one
SELECT id, scan_paths, updated_at FROM library_settings WHERE id = true
`
func (q *Queries) GetLibrarySettings(ctx context.Context) (LibrarySetting, error) {
row := q.db.QueryRow(ctx, getLibrarySettings)
var i LibrarySetting
err := row.Scan(&i.ID, &i.ScanPaths, &i.UpdatedAt)
return i, err
}
const rerootAlbumCoverPaths = `-- name: RerootAlbumCoverPaths :execrows
UPDATE albums
SET cover_art_path = $1::text || substr(cover_art_path, length($2::text) + 1)
WHERE starts_with(cover_art_path, $2::text || '/')
`
type RerootAlbumCoverPathsParams struct {
NewRoot string
OldRoot string
}
func (q *Queries) RerootAlbumCoverPaths(ctx context.Context, arg RerootAlbumCoverPathsParams) (int64, error) {
result, err := q.db.Exec(ctx, rerootAlbumCoverPaths, arg.NewRoot, arg.OldRoot)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const rerootGrabDestinations = `-- name: RerootGrabDestinations :execrows
UPDATE grabs
SET destination = $1::text || substr(destination, length($2::text) + 1)
WHERE destination = $2::text
OR starts_with(destination, $2::text || '/')
`
type RerootGrabDestinationsParams struct {
NewRoot string
OldRoot string
}
func (q *Queries) RerootGrabDestinations(ctx context.Context, arg RerootGrabDestinationsParams) (int64, error) {
result, err := q.db.Exec(ctx, rerootGrabDestinations, arg.NewRoot, arg.OldRoot)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const rerootImportRoot = `-- name: RerootImportRoot :execrows
UPDATE acquisition_settings
SET import_root = $1::text
WHERE import_root = $2::text
`
type RerootImportRootParams struct {
NewRoot string
OldRoot string
}
func (q *Queries) RerootImportRoot(ctx context.Context, arg RerootImportRootParams) (int64, error) {
result, err := q.db.Exec(ctx, rerootImportRoot, arg.NewRoot, arg.OldRoot)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const rerootTrackPaths = `-- name: RerootTrackPaths :execrows
UPDATE tracks
SET file_path = $1::text || substr(file_path, length($2::text) + 1)
WHERE starts_with(file_path, $2::text || '/')
`
type RerootTrackPathsParams struct {
NewRoot string
OldRoot string
}
// A library folder that moved (#6129): every path under old_root is rewritten
// to the same place under new_root, so the tracks keep their ids and with them
// their plays, likes and playlists. updated_at is left alone on purpose: the
// scan compares it with each file's mtime, and the files have not changed.
func (q *Queries) RerootTrackPaths(ctx context.Context, arg RerootTrackPathsParams) (int64, error) {
result, err := q.db.Exec(ctx, rerootTrackPaths, arg.NewRoot, arg.OldRoot)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const sampleTrackPathsUnder = `-- name: SampleTrackPathsUnder :many
SELECT file_path FROM tracks
WHERE starts_with(file_path, $1::text || '/') AND missing_since IS NULL
ORDER BY random()
LIMIT $2::int
`
type SampleTrackPathsUnderParams struct {
Root string
N int32
}
// A few present tracks under a folder, to check a move before rewriting it.
func (q *Queries) SampleTrackPathsUnder(ctx context.Context, arg SampleTrackPathsUnderParams) ([]string, error) {
rows, err := q.db.Query(ctx, sampleTrackPathsUnder, arg.Root, arg.N)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var file_path string
if err := rows.Scan(&file_path); err != nil {
return nil, err
}
items = append(items, file_path)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateLibraryScanPaths = `-- name: UpdateLibraryScanPaths :one
UPDATE library_settings
SET scan_paths = $1::text[],
updated_at = now()
WHERE id = true
RETURNING id, scan_paths, updated_at
`
func (q *Queries) UpdateLibraryScanPaths(ctx context.Context, scanPaths []string) (LibrarySetting, error) {
row := q.db.QueryRow(ctx, updateLibraryScanPaths, scanPaths)
var i LibrarySetting
err := row.Scan(&i.ID, &i.ScanPaths, &i.UpdatedAt)
return i, err
}
+6
View File
@@ -506,6 +506,12 @@ type LibraryChange struct {
ChangedAt pgtype.Timestamptz
}
type LibrarySetting struct {
ID bool
ScanPaths []string
UpdatedAt pgtype.Timestamptz
}
type ListenbrainzSimilarRecording struct {
SeedTrackID pgtype.UUID
RecordingMbid string
@@ -0,0 +1 @@
DROP TABLE IF EXISTS library_settings;
@@ -0,0 +1,11 @@
-- The library's folders, set in Settings (#6129). Until someone saves them
-- there, scan_paths is NULL and MINSTREL_LIBRARY_SCAN_PATHS names the folders,
-- as it always has; from the first save the row is what the server reads.
CREATE TABLE library_settings (
id boolean PRIMARY KEY DEFAULT true,
scan_paths text[],
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT library_settings_singleton CHECK (id = true)
);
INSERT INTO library_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;
+45
View File
@@ -0,0 +1,45 @@
-- name: GetLibrarySettings :one
SELECT * FROM library_settings WHERE id = true;
-- name: UpdateLibraryScanPaths :one
UPDATE library_settings
SET scan_paths = sqlc.arg(scan_paths)::text[],
updated_at = now()
WHERE id = true
RETURNING *;
-- A library folder that moved (#6129): every path under old_root is rewritten
-- to the same place under new_root, so the tracks keep their ids and with them
-- their plays, likes and playlists. updated_at is left alone on purpose: the
-- scan compares it with each file's mtime, and the files have not changed.
-- name: RerootTrackPaths :execrows
UPDATE tracks
SET file_path = sqlc.arg(new_root)::text || substr(file_path, length(sqlc.arg(old_root)::text) + 1)
WHERE starts_with(file_path, sqlc.arg(old_root)::text || '/');
-- name: RerootAlbumCoverPaths :execrows
UPDATE albums
SET cover_art_path = sqlc.arg(new_root)::text || substr(cover_art_path, length(sqlc.arg(old_root)::text) + 1)
WHERE starts_with(cover_art_path, sqlc.arg(old_root)::text || '/');
-- name: RerootGrabDestinations :execrows
UPDATE grabs
SET destination = sqlc.arg(new_root)::text || substr(destination, length(sqlc.arg(old_root)::text) + 1)
WHERE destination = sqlc.arg(old_root)::text
OR starts_with(destination, sqlc.arg(old_root)::text || '/');
-- name: RerootImportRoot :execrows
UPDATE acquisition_settings
SET import_root = sqlc.arg(new_root)::text
WHERE import_root = sqlc.arg(old_root)::text;
-- name: SampleTrackPathsUnder :many
-- A few present tracks under a folder, to check a move before rewriting it.
SELECT file_path FROM tracks
WHERE starts_with(file_path, sqlc.arg(root)::text || '/') AND missing_since IS NULL
ORDER BY random()
LIMIT sqlc.arg(n)::int;
-- name: CountTracksUnder :one
SELECT count(*) FROM tracks WHERE starts_with(file_path, sqlc.arg(root)::text || '/');