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

28 KiB

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.

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).

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.

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.

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:

{
  "catalogNumber": "PCS 7088",
  "barcode": "5099969945724",
  "format": "CD",
  "country": "UK"
}

Song Model

Represents musical compositions.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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:

[
  { "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.

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.

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).

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.

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.

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.

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.

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:

{
  "accessToken": "...",
  "sessionKey": "...",
  "expiresAt": 1705320600
}

Join Tables

ArtistArea

Links artists to geographic areas.

model ArtistArea {
  artistId          Int
  artist            Artist           @relation(fields: [artistId])
  areaId            Int
  area              Area             @relation(fields: [areaId])
  
  @@id([artistId, areaId])
}

AlbumGenre

Links albums to genres.

model AlbumGenre {
  albumId           Int
  album             Album            @relation(fields: [albumId])
  genreId           Int
  genre             Genre            @relation(fields: [genreId])
  
  @@id([albumId, genreId])
}

AlbumLabel

Links albums to labels.

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.

model SongArtist {
  songId            Int
  song              Song             @relation(fields: [songId])
  artistId          Int
  artist            Artist           @relation(fields: [artistId])
  
  @@id([songId, artistId])
}

SongGenre

Links songs to genres.

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:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  pool_size = 10
}

Backup and Restore

Backup

docker exec meelo-db pg_dump -U postgres meelo > backup.sql

Restore

docker exec -i meelo-db psql -U postgres meelo < backup.sql

Volume Backup

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.