Files
mrleo1nidandClaude Opus 5.5 c583991d14
ci / api / api (push) Successful in 26s
ci / mobile / mobile (Metro-бандл Android) (push) Failing after 22s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 37s
ci / ts / ts (packages + web) (push) Successful in 51s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 53s
feat(api): library sources, scanner and catalog (M2)
- schema for sources, path patterns, media files, artists, albums,
  tracks, genres (migration 00003)
- path patterns (RE2 with named groups, skip), merge with tags by
  source priority, fallbacks from the file name and folder, name
  normalization and multi-artist split
- scanner: walk, skip unchanged files by size and mtime, ffprobe in 8
  workers with a single catalog writer, renames by content hash keep
  the track, duplicates fold into one track with the best file as
  primary, missing files are kept 30 days
- storage-agent mounts NFS with validated server, export and an option
  whitelist, no shell; test mount counts audio files
- admin API: sources CRUD, connection test, patterns with preview,
  scan, library status, file errors, host folder picker; scheduled
  scans and health checks; mounts are restored on start
- catalog API: artist, artist albums, album with tracks, track
- api image is alpine with ffmpeg for ffprobe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:48:37 +03:00

136 lines
4.3 KiB
SQL

-- +goose Up
CREATE TYPE source_type AS ENUM ('nfs', 'local');
CREATE TYPE source_status AS ENUM ('pending', 'mounted', 'error', 'disabled');
CREATE TYPE tag_priority AS ENUM ('tags_first', 'path_first');
CREATE TABLE library_sources (
id uuid PRIMARY KEY,
name text NOT NULL,
type source_type NOT NULL,
nfs_server text,
nfs_export text,
nfs_options text NOT NULL DEFAULT 'ro,nfsvers=4.1,soft,timeo=150,retrans=3',
local_path text,
tag_priority tag_priority NOT NULL DEFAULT 'tags_first',
accepts_uploads boolean NOT NULL DEFAULT false,
write_template text,
scan_interval interval,
status source_status NOT NULL DEFAULT 'pending',
status_message text,
last_scan_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE path_patterns (
id uuid PRIMARY KEY,
source_id uuid NOT NULL REFERENCES library_sources(id) ON DELETE CASCADE,
position int NOT NULL,
regex text NOT NULL,
is_enabled boolean NOT NULL DEFAULT true,
UNIQUE (source_id, position)
);
CREATE TABLE artists (
id uuid PRIMARY KEY,
name text NOT NULL,
sort_name text NOT NULL,
name_norm text NOT NULL UNIQUE,
image_path text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE albums (
id uuid PRIMARY KEY,
title text NOT NULL,
title_norm text NOT NULL,
album_artist_id uuid NOT NULL REFERENCES artists(id),
year int,
cover_path text,
cover_colors text[],
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (album_artist_id, title_norm)
);
CREATE TABLE media_files (
id uuid PRIMARY KEY,
source_id uuid NOT NULL REFERENCES library_sources(id) ON DELETE CASCADE,
rel_path text NOT NULL,
size_bytes bigint NOT NULL,
mtime timestamptz NOT NULL,
content_hash bytea,
sha256 bytea,
upload_file_id uuid,
format text NOT NULL,
codec text,
bitrate int,
sample_rate int,
bit_depth int,
channels int,
duration_ms int NOT NULL,
track_id uuid,
missing_since timestamptz,
scan_error text,
UNIQUE (source_id, rel_path)
);
CREATE INDEX media_files_content_hash_idx ON media_files (content_hash);
CREATE INDEX media_files_sha256_idx ON media_files (sha256);
CREATE INDEX media_files_track_idx ON media_files (track_id);
CREATE TABLE tracks (
id uuid PRIMARY KEY,
title text NOT NULL,
title_norm text NOT NULL,
album_id uuid NOT NULL REFERENCES albums(id),
disc_no int,
track_no int,
year int,
duration_ms int NOT NULL,
primary_file_id uuid REFERENCES media_files(id) ON DELETE SET NULL,
explicit boolean,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX tracks_album_idx ON tracks (album_id, disc_no, track_no);
-- Дубли: тот же альбом, название и длительность ±2 с ищутся по (album_id, title_norm).
CREATE INDEX tracks_dedup_idx ON tracks (album_id, title_norm);
ALTER TABLE media_files ADD FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE SET NULL;
CREATE TABLE track_artists (
track_id uuid REFERENCES tracks(id) ON DELETE CASCADE,
artist_id uuid REFERENCES artists(id) ON DELETE CASCADE,
role text NOT NULL DEFAULT 'main',
position int NOT NULL,
PRIMARY KEY (track_id, artist_id, role)
);
CREATE INDEX track_artists_artist_idx ON track_artists (artist_id);
CREATE TABLE genres (
id serial PRIMARY KEY,
name text NOT NULL,
name_norm text NOT NULL UNIQUE,
parent_id int REFERENCES genres(id)
);
CREATE TABLE track_genres (
track_id uuid REFERENCES tracks(id) ON DELETE CASCADE,
genre_id int REFERENCES genres(id),
source text NOT NULL,
weight real NOT NULL DEFAULT 1,
PRIMARY KEY (track_id, genre_id, source)
);
-- +goose Down
DROP TABLE track_genres;
DROP TABLE genres;
DROP TABLE track_artists;
ALTER TABLE media_files DROP CONSTRAINT media_files_track_id_fkey;
DROP TABLE tracks;
DROP TABLE media_files;
DROP TABLE albums;
DROP TABLE artists;
DROP TABLE path_patterns;
DROP TABLE library_sources;
DROP TYPE tag_priority;
DROP TYPE source_status;
DROP TYPE source_type;