Files
metadata-agregator/docs/research/meelo/analysis/DATA.md
T
Alexander a1f6701bac feat: initial implementation of metadata aggregator
- gRPC service with MusicBrainz provider
- PostgreSQL schema with migrations
- Service layer with database-first caching
- Repository pattern for data access
- YAML configuration support
- Research documentation for 17 music metadata projects
2026-04-28 16:28:53 +02:00

1081 lines
28 KiB
Markdown

# Meelo Data Model
## Database Overview
Meelo uses PostgreSQL with Prisma ORM. The schema defines 30+ models representing music metadata, relationships, and system entities. The design prioritizes flexibility and accuracy over simplicity.
**Schema File**: `server/prisma/schema.prisma` (910 lines)
**Migrations**: Automatic via Prisma Migrate
**Indexes**: Foreign keys, unique constraints, frequently queried fields
## Core Principles
### Album vs Release
Albums are abstract concepts. Releases are physical/digital manifestations. One album can have multiple releases (original, remaster, deluxe edition). This mirrors real-world music organization.
### Song vs Track
Songs are compositions. Tracks are recordings. One song can have multiple tracks (studio version, live version, acoustic version). Tracks link to source files.
### Versioning
Songs can belong to song groups, representing different versions of the same composition (original, cover, remix). This enables tracking relationships between versions.
### External Metadata
Descriptions, ratings, and external IDs are stored separately from core entities. This allows multiple sources without polluting main tables.
## Entity Relationship Diagram
```
User ──┬─── Playlist ─── PlaylistEntry ─── Track
└─── UserScrobbler
Artist ──┬─── Album ─── Release ─── Track
├─── Song ──┬─── Track
│ └─── SongGroup
├─── Video
└─── ArtistArea ─── Area
Album ──┬─── AlbumGenre ─── Genre
└─── AlbumLabel ─── Label
Song ──┬─── SongArtist (featuring)
├─── SongGenre ─── Genre
└─── Lyrics
Track ─── File ─── Library
ExternalMetadata ─── ExternalSource
LocalIdentifiers
Illustration
```
## User Model
Represents system users with authentication and authorization.
```prisma
model User {
id Int @id @default(autoincrement())
username String @unique
password String // bcrypt hash
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
playlists Playlist[]
scrobblers UserScrobbler[]
}
```
**Fields**:
- `username`: Unique identifier for login
- `password`: Bcrypt hash with salt rounds = 10
- `isAdmin`: Admin flag for privileged operations
- `createdAt`: Registration timestamp
**Relationships**:
- One-to-many with Playlist (user owns playlists)
- One-to-many with UserScrobbler (user connects scrobblers)
**Constraints**:
- Username must be unique
- Password must be hashed (never stored plain)
- First user becomes admin automatically
## Artist Model
Represents musical artists (individuals or groups).
```prisma
model Artist {
id Int @id @default(autoincrement())
name String
slug String @unique
sortName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
illustrationId Int? @unique
illustration Illustration? @relation(fields: [illustrationId])
albums Album[]
songs Song[]
videos Video[]
areas ArtistArea[]
externalMetadata ExternalMetadata?
localIdentifiers LocalIdentifiers?
}
```
**Fields**:
- `name`: Display name (e.g., "The Beatles")
- `slug`: URL-safe identifier (e.g., "the-beatles")
- `sortName`: Name for alphabetical sorting (e.g., "Beatles, The")
- `illustrationId`: Foreign key to artist image
**Relationships**:
- One-to-one with Illustration (artist image)
- One-to-many with Album (artist's albums)
- One-to-many with Song (artist's songs)
- One-to-many with Video (artist's videos)
- Many-to-many with Area via ArtistArea (geographic associations)
- One-to-one with ExternalMetadata (descriptions, ratings)
- One-to-one with LocalIdentifiers (MusicBrainz ID, etc.)
**Constraints**:
- Slug must be unique
- Name is required
## Album Model
Represents abstract album concepts.
```prisma
model Album {
id Int @id @default(autoincrement())
name String
slug String @unique
type AlbumType
releaseDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
artistId Int
artist Artist @relation(fields: [artistId])
illustrationId Int? @unique
illustration Illustration? @relation(fields: [illustrationId])
releases Release[]
genres AlbumGenre[]
labels AlbumLabel[]
externalMetadata ExternalMetadata?
localIdentifiers LocalIdentifiers?
}
```
**Fields**:
- `name`: Album title
- `slug`: URL-safe identifier
- `type`: Album type enum (see below)
- `releaseDate`: Original release date
- `artistId`: Foreign key to primary artist
**Album Types** (enum):
- `studio`: Studio album
- `live`: Live album
- `compilation`: Compilation
- `soundtrack`: Soundtrack
- `ep`: Extended play
- `single`: Single
- `remix`: Remix album
- `bootleg`: Bootleg
- `interview`: Interview
- `other`: Other
**Relationships**:
- Many-to-one with Artist (album belongs to artist)
- One-to-many with Release (album has multiple releases)
- Many-to-many with Genre via AlbumGenre
- Many-to-many with Label via AlbumLabel
- One-to-one with Illustration (album cover)
- One-to-one with ExternalMetadata
- One-to-one with LocalIdentifiers
## Release Model
Represents physical or digital album releases.
```prisma
model Release {
id Int @id @default(autoincrement())
name String
releaseDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
albumId Int
album Album @relation(fields: [albumId])
labelId Int?
label Label? @relation(fields: [labelId])
tracks Track[]
extensions Json? // catalogNumber, barcode, etc.
}
```
**Fields**:
- `name`: Release name (e.g., "Abbey Road (2019 Remaster)")
- `releaseDate`: This release's date (may differ from album)
- `albumId`: Foreign key to parent album
- `labelId`: Foreign key to record label
- `extensions`: JSON field for additional metadata (catalog number, barcode, format)
**Relationships**:
- Many-to-one with Album (release belongs to album)
- Many-to-one with Label (release issued by label)
- One-to-many with Track (release contains tracks)
**Extensions JSON**:
```json
{
"catalogNumber": "PCS 7088",
"barcode": "5099969945724",
"format": "CD",
"country": "UK"
}
```
## Song Model
Represents musical compositions.
```prisma
model Song {
id Int @id @default(autoincrement())
name String
slug String @unique
type SongType
bpm Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
artistId Int
artist Artist @relation(fields: [artistId])
groupId Int?
group SongGroup? @relation(fields: [groupId])
tracks Track[]
featuring SongArtist[]
genres SongGenre[]
lyrics Lyrics?
externalMetadata ExternalMetadata?
localIdentifiers LocalIdentifiers?
}
```
**Fields**:
- `name`: Song title
- `slug`: URL-safe identifier
- `type`: Song type enum (see below)
- `bpm`: Beats per minute (tempo)
- `artistId`: Foreign key to primary artist
- `groupId`: Foreign key to song group (for versions)
**Song Types** (enum):
- `original`: Original recording
- `live`: Live performance
- `acoustic`: Acoustic version
- `remix`: Remix
- `cover`: Cover version
- `demo`: Demo recording
- `instrumental`: Instrumental
- `karaoke`: Karaoke version
- `radio_edit`: Radio edit
- `extended`: Extended version
- `clean`: Clean version
- `explicit`: Explicit version
**Relationships**:
- Many-to-one with Artist (song's main artist)
- Many-to-one with SongGroup (song belongs to version group)
- One-to-many with Track (song has multiple recordings)
- Many-to-many with Artist via SongArtist (featuring artists)
- Many-to-many with Genre via SongGenre
- One-to-one with Lyrics
- One-to-one with ExternalMetadata
- One-to-one with LocalIdentifiers
## Track Model
Represents individual audio or video recordings.
```prisma
model Track {
id Int @id @default(autoincrement())
name String
type TrackType
duration Int // seconds
bitrate Int? // bits per second
codec String?
ripSource RipSource?
isBonus Boolean @default(false)
isRemastered Boolean @default(false)
discIndex Int?
trackIndex Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
songId Int
song Song @relation(fields: [songId])
releaseId Int
release Release @relation(fields: [releaseId])
sourceFileId Int @unique
sourceFile File @relation(fields: [sourceFileId])
playlistEntries PlaylistEntry[]
}
```
**Fields**:
- `name`: Track name (may differ from song name)
- `type`: Audio or video
- `duration`: Length in seconds
- `bitrate`: Bitrate in bits per second
- `codec`: Audio/video codec (mp3, flac, h264, etc.)
- `ripSource`: Source of rip (CD, vinyl, web, etc.)
- `isBonus`: Bonus track flag
- `isRemastered`: Remastered flag
- `discIndex`: Disc number (for multi-disc releases)
- `trackIndex`: Track number on disc
- `songId`: Foreign key to song
- `releaseId`: Foreign key to release
- `sourceFileId`: Foreign key to file
**Track Types** (enum):
- `audio`: Audio track
- `video`: Video track
**Rip Source Types** (enum):
- `cd`: CD rip
- `vinyl`: Vinyl rip
- `web`: Web download
- `stream`: Stream rip
- `other`: Other source
**Relationships**:
- Many-to-one with Song (track is recording of song)
- Many-to-one with Release (track belongs to release)
- One-to-one with File (track links to source file)
- One-to-many with PlaylistEntry (track appears in playlists)
## File Model
Represents physical files on disk.
```prisma
model File {
id Int @id @default(autoincrement())
path String @unique
checksum String
fingerprint String? // AcoustID fingerprint
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
libraryId Int
library Library @relation(fields: [libraryId])
track Track?
}
```
**Fields**:
- `path`: Absolute file path
- `checksum`: SHA256 hash for duplicate detection
- `fingerprint`: AcoustID fingerprint for matching
- `libraryId`: Foreign key to library
**Relationships**:
- Many-to-one with Library (file belongs to library)
- One-to-one with Track (file may be linked to track)
**Constraints**:
- Path must be unique
- Checksum used for duplicate detection
- Fingerprint used for MusicBrainz matching
## Library Model
Represents music library directories.
```prisma
model Library {
id Int @id @default(autoincrement())
name String
path String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
files File[]
}
```
**Fields**:
- `name`: Display name
- `path`: Absolute directory path
**Relationships**:
- One-to-many with File (library contains files)
**Constraints**:
- Path must be unique
- Path must be absolute
- Scanner watches this directory
## Video Model
Represents music videos.
```prisma
model Video {
id Int @id @default(autoincrement())
name String
slug String @unique
type VideoType
duration Int // seconds
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
artistId Int
artist Artist @relation(fields: [artistId])
songId Int?
song Song? @relation(fields: [songId])
sourceFileId Int @unique
sourceFile File @relation(fields: [sourceFileId])
}
```
**Fields**:
- `name`: Video title
- `slug`: URL-safe identifier
- `type`: Video type enum (see below)
- `duration`: Length in seconds
- `artistId`: Foreign key to artist
- `songId`: Foreign key to song (optional, for non-song videos)
- `sourceFileId`: Foreign key to file
**Video Types** (enum):
- `official`: Official music video
- `live`: Live performance
- `lyric`: Lyric video
- `audio`: Audio-only video
- `behind_the_scenes`: Behind the scenes
- `interview`: Interview
- `documentary`: Documentary
- `fan_made`: Fan-made video
- `other`: Other
**Relationships**:
- Many-to-one with Artist (video belongs to artist)
- Many-to-one with Song (video may be for song)
- One-to-one with File (video links to source file)
## SongGroup Model
Represents groups of song versions.
```prisma
model SongGroup {
id Int @id @default(autoincrement())
name String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
songs Song[]
}
```
**Fields**:
- `name`: Group name (e.g., "Hallelujah")
- `slug`: URL-safe identifier
**Relationships**:
- One-to-many with Song (group contains song versions)
**Use Case**:
Group original, covers, remixes of same composition. Example:
- "Hallelujah" by Leonard Cohen (original)
- "Hallelujah" by Jeff Buckley (cover)
- "Hallelujah" by Pentatonix (cover)
## Genre Model
Represents music genres.
```prisma
model Genre {
id Int @id @default(autoincrement())
name String @unique
slug String @unique
albums AlbumGenre[]
songs SongGenre[]
}
```
**Fields**:
- `name`: Genre name (e.g., "Rock")
- `slug`: URL-safe identifier
**Relationships**:
- Many-to-many with Album via AlbumGenre
- Many-to-many with Song via SongGenre
## Label Model
Represents record labels.
```prisma
model Label {
id Int @id @default(autoincrement())
name String @unique
slug String @unique
releases Release[]
albums AlbumLabel[]
}
```
**Fields**:
- `name`: Label name (e.g., "Apple Records")
- `slug`: URL-safe identifier
**Relationships**:
- One-to-many with Release (label issues releases)
- Many-to-many with Album via AlbumLabel
## Area Model
Represents geographic areas.
```prisma
model Area {
id Int @id @default(autoincrement())
name String
type AreaType
iso3166 String? // ISO 3166 code
parentId Int?
parent Area? @relation("AreaHierarchy", fields: [parentId])
children Area[] @relation("AreaHierarchy")
artists ArtistArea[]
}
```
**Fields**:
- `name`: Area name (e.g., "Liverpool")
- `type`: Area type enum (see below)
- `iso3166`: ISO 3166 code (e.g., "GB-LIV")
- `parentId`: Foreign key to parent area (for hierarchy)
**Area Types** (enum):
- `country`: Country
- `subdivision`: State/province
- `county`: County
- `municipality`: City/town
- `city`: City
- `district`: District
- `island`: Island
**Relationships**:
- Self-referential many-to-one (area has parent)
- Self-referential one-to-many (area has children)
- Many-to-many with Artist via ArtistArea
**Hierarchy Example**:
```
United Kingdom (country)
└─ England (subdivision)
└─ Merseyside (county)
└─ Liverpool (city)
```
## Lyrics Model
Represents song lyrics.
```prisma
model Lyrics {
id Int @id @default(autoincrement())
plain String? @db.Text
synced Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
songId Int @unique
song Song @relation(fields: [songId])
}
```
**Fields**:
- `plain`: Plain text lyrics
- `synced`: Synced lyrics in JSON format (see below)
- `songId`: Foreign key to song
**Synced Lyrics Format**:
```json
[
{ "time": 0, "text": "Here come old flat top" },
{ "time": 3500, "text": "He come grooving up slowly" },
{ "time": 7200, "text": "He got joo-joo eyeball" }
]
```
Time in milliseconds. Used for karaoke-style display.
**Relationships**:
- One-to-one with Song (song has lyrics)
## Playlist Model
Represents user playlists.
```prisma
model Playlist {
id Int @id @default(autoincrement())
name String
slug String @unique
isPublic Boolean @default(false)
allowChanges Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ownerId Int
owner User @relation(fields: [ownerId])
entries PlaylistEntry[]
}
```
**Fields**:
- `name`: Playlist name
- `slug`: URL-safe identifier
- `isPublic`: Public visibility flag
- `allowChanges`: Allow non-owners to modify flag
- `ownerId`: Foreign key to owner
**Relationships**:
- Many-to-one with User (playlist belongs to user)
- One-to-many with PlaylistEntry (playlist contains entries)
**Access Control**:
- Owner can always modify
- Others can modify if `allowChanges=true`
- Others can view if `isPublic=true`
## PlaylistEntry Model
Represents tracks in playlists.
```prisma
model PlaylistEntry {
id Int @id @default(autoincrement())
index Int
createdAt DateTime @default(now())
playlistId Int
playlist Playlist @relation(fields: [playlistId])
trackId Int
track Track @relation(fields: [trackId])
@@unique([playlistId, index])
}
```
**Fields**:
- `index`: Position in playlist (0-based)
- `playlistId`: Foreign key to playlist
- `trackId`: Foreign key to track
**Relationships**:
- Many-to-one with Playlist (entry belongs to playlist)
- Many-to-one with Track (entry references track)
**Constraints**:
- Unique combination of playlistId and index (no duplicate positions)
## Illustration Model
Represents images (artist photos, album covers).
```prisma
model Illustration {
id Int @id @default(autoincrement())
url String
blurhash String
colors String[]
aspectRatio Float
type IllustrationType
createdAt DateTime @default(now())
artist Artist?
album Album?
}
```
**Fields**:
- `url`: Image URL or path
- `blurhash`: BlurHash string for placeholder
- `colors`: Dominant colors (hex codes)
- `aspectRatio`: Width/height ratio
- `type`: Illustration type enum (see below)
**Illustration Types** (enum):
- `artist`: Artist photo
- `album`: Album cover
- `banner`: Banner image
**Relationships**:
- One-to-one with Artist (artist has illustration)
- One-to-one with Album (album has illustration)
**BlurHash**:
Compact representation of image for placeholder. Decoded client-side for instant preview while loading full image.
## ExternalMetadata Model
Represents metadata from external providers.
```prisma
model ExternalMetadata {
id Int @id @default(autoincrement())
description String? @db.Text
rating Int? // 0-100
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
artistId Int? @unique
artist Artist? @relation(fields: [artistId])
albumId Int? @unique
album Album? @relation(fields: [albumId])
songId Int? @unique
song Song? @relation(fields: [songId])
sources ExternalSource[]
}
```
**Fields**:
- `description`: Aggregated description from providers
- `rating`: Aggregated rating (0-100 scale)
- `artistId`, `albumId`, `songId`: Foreign keys (one of these set)
**Relationships**:
- One-to-one with Artist, Album, or Song
- One-to-many with ExternalSource (metadata has sources)
**Rating Scale**:
Normalized to 0-100 regardless of provider scale. Example:
- Metacritic: 85/100 → 85
- AllMusic: 4.5/5 → 90
## ExternalSource Model
Represents individual provider sources.
```prisma
model ExternalSource {
id Int @id @default(autoincrement())
provider String
url String
externalMetadataId Int
externalMetadata ExternalMetadata @relation(fields: [externalMetadataId])
}
```
**Fields**:
- `provider`: Provider name (musicbrainz, genius, wikipedia, etc.)
- `url`: Source URL
- `externalMetadataId`: Foreign key to parent metadata
**Relationships**:
- Many-to-one with ExternalMetadata (source belongs to metadata)
**Providers**:
- `musicbrainz`: MusicBrainz
- `genius`: Genius
- `wikipedia`: Wikipedia
- `wikidata`: Wikidata
- `discogs`: Discogs
- `allmusic`: AllMusic
- `metacritic`: Metacritic
- `lrclib`: LrcLib
## LocalIdentifiers Model
Represents external IDs for matching.
```prisma
model LocalIdentifiers {
id Int @id @default(autoincrement())
musicbrainzId String?
acoustidId String?
discogsId String?
artistId Int? @unique
artist Artist? @relation(fields: [artistId])
albumId Int? @unique
album Album? @relation(fields: [albumId])
songId Int? @unique
song Song? @relation(fields: [songId])
}
```
**Fields**:
- `musicbrainzId`: MusicBrainz MBID
- `acoustidId`: AcoustID fingerprint
- `discogsId`: Discogs ID
- `artistId`, `albumId`, `songId`: Foreign keys (one of these set)
**Relationships**:
- One-to-one with Artist, Album, or Song
**Use Case**:
Store external IDs for cross-referencing and matching. AcoustID used for initial matching, MusicBrainz ID for enrichment.
## UserScrobbler Model
Represents user scrobbler connections.
```prisma
model UserScrobbler {
id Int @id @default(autoincrement())
provider String
username String
data Json // provider-specific data (tokens, etc.)
connectedAt DateTime @default(now())
userId Int
user User @relation(fields: [userId])
@@unique([userId, provider])
}
```
**Fields**:
- `provider`: Scrobbler provider (lastfm, listenbrainz)
- `username`: Username on provider
- `data`: JSON field for provider-specific data
- `userId`: Foreign key to user
**Relationships**:
- Many-to-one with User (user has scrobblers)
**Constraints**:
- Unique combination of userId and provider (one connection per provider)
**Data JSON**:
```json
{
"accessToken": "...",
"sessionKey": "...",
"expiresAt": 1705320600
}
```
## Join Tables
### ArtistArea
Links artists to geographic areas.
```prisma
model ArtistArea {
artistId Int
artist Artist @relation(fields: [artistId])
areaId Int
area Area @relation(fields: [areaId])
@@id([artistId, areaId])
}
```
### AlbumGenre
Links albums to genres.
```prisma
model AlbumGenre {
albumId Int
album Album @relation(fields: [albumId])
genreId Int
genre Genre @relation(fields: [genreId])
@@id([albumId, genreId])
}
```
### AlbumLabel
Links albums to labels.
```prisma
model AlbumLabel {
albumId Int
album Album @relation(fields: [albumId])
labelId Int
label Label @relation(fields: [labelId])
@@id([albumId, labelId])
}
```
### SongArtist
Links songs to featuring artists.
```prisma
model SongArtist {
songId Int
song Song @relation(fields: [songId])
artistId Int
artist Artist @relation(fields: [artistId])
@@id([songId, artistId])
}
```
### SongGenre
Links songs to genres.
```prisma
model SongGenre {
songId Int
song Song @relation(fields: [songId])
genreId Int
genre Genre @relation(fields: [genreId])
@@id([songId, genreId])
}
```
## MeiliSearch Indexes
MeiliSearch indexes four entity types for full-text search.
### Artist Index
**Indexed Fields**:
- `id`, `name`, `slug`, `sortName`
**Searchable Attributes**:
- `name`, `sortName`
**Filterable Attributes**:
- None
**Sortable Attributes**:
- `name`, `sortName`
### Album Index
**Indexed Fields**:
- `id`, `name`, `slug`, `type`, `releaseDate`, `artistName`
**Searchable Attributes**:
- `name`, `artistName`
**Filterable Attributes**:
- `type`, `releaseDate`
**Sortable Attributes**:
- `name`, `releaseDate`
### Song Index
**Indexed Fields**:
- `id`, `name`, `slug`, `type`, `artistName`
**Searchable Attributes**:
- `name`, `artistName`
**Filterable Attributes**:
- `type`
**Sortable Attributes**:
- `name`
### Video Index
**Indexed Fields**:
- `id`, `name`, `slug`, `type`, `artistName`
**Searchable Attributes**:
- `name`, `artistName`
**Filterable Attributes**:
- `type`
**Sortable Attributes**:
- `name`
## Data Integrity
### Cascading Deletes
Prisma handles cascading deletes:
- Delete artist → delete albums, songs, videos
- Delete album → delete releases
- Delete release → delete tracks
- Delete track → orphan file (not deleted)
- Delete library → orphan files (not deleted)
### Orphan Cleanup
Scanner's `/clean` endpoint removes orphaned files and tracks.
### Unique Constraints
- Artist slug
- Album slug
- Song slug
- Video slug
- Playlist slug
- File path
- Genre name
- Label name
- User username
### Foreign Key Constraints
All foreign keys enforced by PostgreSQL. Invalid references rejected.
## Data Migration
Prisma Migrate handles schema changes:
1. Modify `schema.prisma`
2. Run `prisma migrate dev --name <migration_name>`
3. Prisma generates SQL migration
4. Migration applied to database
5. Prisma Client regenerated
Migrations stored in `prisma/migrations/` directory.
## Seeding
Initial data seeded on first run:
- Admin user (if none exists)
- Default genres (Rock, Pop, Jazz, etc.)
- Area hierarchy (countries, cities)
Seed script: `prisma/seed.ts`
## Performance Considerations
### Indexes
Prisma creates indexes on:
- Primary keys (automatic)
- Foreign keys (automatic)
- Unique constraints (automatic)
- Custom indexes for frequently queried fields
### Query Optimization
- Use `include` for eager loading (avoid N+1)
- Use `select` to fetch only needed fields
- Use pagination (`skip`, `take`) for large result sets
- Use `where` filters to reduce result size
### Connection Pooling
Prisma uses connection pooling. Configure in `schema.prisma`:
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
pool_size = 10
}
```
## Backup and Restore
### Backup
```bash
docker exec meelo-db pg_dump -U postgres meelo > backup.sql
```
### Restore
```bash
docker exec -i meelo-db psql -U postgres meelo < backup.sql
```
### Volume Backup
```bash
docker run --rm -v meelo_db:/data -v $(pwd):/backup alpine tar czf /backup/db.tar.gz /data
```
## Summary
Meelo's data model is the most sophisticated among self-hosted music servers. The Album/Release and Song/Track distinctions enable accurate representation of real-world music organization. Song groups track versions and covers. External metadata and local identifiers separate provider data from core entities. The schema supports complex queries (artist with all albums, releases, tracks, genres) while maintaining referential integrity. MeiliSearch indexes provide fast full-text search. Prisma ORM handles migrations, type safety, and query optimization.