services:
  db:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: muzick
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./backend/src/db/schema.sql:/docker-entrypoint-initdb.d/schema.sql

  search:
    image: typesense/typesense:0.25.1
    restart: always
    ports:
      - "127.0.0.1:8108:8108"
    volumes:
      - ./data/typesense:/data
    command: --data-dir /data --api-key=${TYPESENSE_API_KEY}

  backend:
    build: ./backend
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgresql://user:${DB_PASSWORD}@db:5432/muzick
      REDIS_URL: redis://:${REDIS_PASSWORD}@infra-redis:6379
      TYPESENSE_API_KEY: ${TYPESENSE_API_KEY}
      MUZICK_API_KEY: ${MUZICK_API_KEY}
      MUZICK_ADMIN_KEY: ${MUZICK_ADMIN_KEY}
      MUSIC_DIR: /music
    volumes:
      # READ-ONLY, deliberately. Nothing in the API request path may write to
      # the library. Hard deletion of disliked files happens only in the worker,
      # which is the sole service with an rw mount.
      - /mnt/hdd1/media/Music:/music:ro
    depends_on:
      - db
      - search
    networks:
      - default
      - infra-net

  frontend:
    build: ./frontend
    restart: unless-stopped
    ports:
      - "127.0.0.1:5174:80"
    environment:
      MUZICK_API_KEY: ${MUZICK_API_KEY}
      MUZICK_ADMIN_KEY: ${MUZICK_ADMIN_KEY}
    depends_on:
      - backend

  worker:
    build:
      context: ./workers
      # Enabled deliberately: the auto-seed discovery loop cannot acquire a
      # candidate without the downloader present.
      args:
        INSTALL_YTDLP: "true"
    restart: unless-stopped
    network_mode: host
    environment:
      DATABASE_URL: postgresql://user:${DB_PASSWORD}@127.0.0.1:5432/muzick
      REDIS_URL: redis://:${REDIS_PASSWORD}@127.0.0.1:6379
      MUSICBRAINZ_CONTACT: ${MUSICBRAINZ_CONTACT}
      LASTFM_API_KEY: ${LASTFM_API_KEY}
      LASTFM_SHARED_SECRET: ${LASTFM_SHARED_SECRET}
      DISCOGS_TOKEN: ${DISCOGS_TOKEN}
      SOCKS_PROXY_URL: ${SOCKS_PROXY_URL}
      MUSIC_DIR: /music
      # System E acquisition, enabled. The allow-list is the only thing deciding
      # where audio may come from, and it binds search-resolved URLs too.
      # `www.youtube.com` is what yt-dlp reports as webpage_url for a ytsearch
      # hit. Narrow this list to shrink the blast radius; empty it to stop
      # acquisition without a rebuild.
      MUZICK_ACQUISITION_ENABLED: "true"
      MUZICK_ACQUISITION_YTDLP_PATH: /usr/bin/yt-dlp
      MUZICK_ACQUISITION_ALLOWED_HOSTS: www.youtube.com
      MUZICK_ACQUISITION_DIR: .recommendations
      #
      # Candidate generation crons (defaults shown, both daily).
      # NEW_RELEASE_CRON: "30 5 * * *"
      # RECOMMENDATION_CRON: "30 6 * * *"
      # Hard-deletion gates for the dislike lifecycle (invariant §C). All three
      # default to the safe value inside cleanup.service.ts; they are listed here
      # as documentation and are intentionally left unset.
      #
      # MASTER SWITCH — leave unset/false. While off, the cleanup sweep logs
      # exactly which files it WOULD delete and changes nothing at all. Set to
      # true by hand, only after reviewing a dry-run log:
      #   MUZICK_ALLOW_HARD_DELETE: "false"
      # Blast-radius cap per sweep (default 5):
      #   MUZICK_HARD_DELETE_MAX_PER_SWEEP: "5"
      # Days a track must sit in HIDDEN before its file is eligible (default 7):
      #   MUZICK_HARD_DELETE_GRACE_DAYS: "7"
    volumes:
      # READ-WRITE, and the only rw mount of the library in the stack. The worker
      # is the sole process permitted to unlink a music file, and only via the
      # gated cleanup sweep. The backend keeps `:ro`.
      - /mnt/hdd1/media/Music:/music:rw

networks:
  infra-net:
    external: true
    name: infra-net
