diff --git a/.dockerignore b/.dockerignore
index eb9362e..5026ba4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,18 +14,12 @@ compose.yaml
*.pprof
main
conf.yaml
-output.css
-tailwindcss
.env
-# Node.js (frontend build artifacts)
-node_modules
-package.json
-package-lock.json
-
-# Test files
+# Test files and fixtures
*_test.go
-integration_test.go
+internal/testutil
+*_dump.sql
# OS
.DS_Store
diff --git a/.gitignore b/.gitignore
index f5a43fb..9be0b89 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,13 +3,8 @@ tmp
.idea
*templ.go
conf.yaml
-output.css
compose.yaml
-tailwindcss
.env
-node_modules
-package.json
-package-lock.json
cpu.pprof
main
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/MusicServer.iml b/.idea/MusicServer.iml
deleted file mode 100644
index d840f1f..0000000
--- a/.idea/MusicServer.iml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a1..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
deleted file mode 100644
index 9476a4a..0000000
--- a/.idea/dataSources.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://192.168.86.100:5432/music_prod
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://192.168.86.100:5432/music_test
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://ssh.sanplex.xyz:9432/music_test
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://ssh.sanplex.xyz:9432/music_prod
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://localhost:5432/music_test2
- $ProjectFileDir$
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 9ce91d9..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index cc1dd39..5001ed6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,36 +1,39 @@
-# Stage 1: Build backend
-FROM golang:1.25-alpine as build_go
-RUN apk add --no-cache curl
+# The frontend is built and published separately; override to pin a version.
+ARG FRONTEND_IMAGE=gitea.sanplex.xyz/sansan/musicfrontend:latest
+
+# Stage 1: Build the backend on the build machine and cross-compile for the target platform,
+# so multi-platform builds need no emulation.
+FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
+ARG TARGETOS TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
-RUN find . -name "*_test.go" -delete && \
- find . -name "integration_test.go" -delete
-RUN go install github.com/a-h/templ/cmd/templ@latest
-RUN templ generate
-RUN go build -o main cmd/main.go
+RUN go tool templ generate
+RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
+ go build -trimpath -ldflags="-s -w" -tags timetzdata -o main ./cmd
-# Stage 2: Final image
-FROM golang:1.25-alpine
+# Stage 2: Frontend static files (the same for every platform)
+FROM --platform=$BUILDPLATFORM ${FRONTEND_IMAGE} AS frontend
+# Empty API_HOSTNAME makes the frontend use relative paths
+RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > /usr/share/nginx/html/config.js
+
+# Stage 3: Final image. No RUN steps here, so no emulation is needed for other architectures.
+FROM alpine:3.22
+COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+WORKDIR /app
+COPY --from=build /app/main .
+COPY --from=frontend /usr/share/nginx/html /frontend
+COPY ./songs/ ./songs/
+
+# The music volume must be readable by uid 1000, and writable for the .id file cleanup during sync.
+USER 1000:1000
EXPOSE 8080
VOLUME /sorted
VOLUME /characters
-COPY --from=build_go /app/main .
-COPY --from=gitea.sanplex.xyz/sansan/musicfrontend:latest /usr/share/nginx/html /frontend
-# Generate config.js with empty API_HOSTNAME (relative paths)
-RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > /frontend/config.js
-COPY ./songs/ ./songs/
+# Required: DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME, MUSIC_PATH, CHARACTERS_PATH
+ENV PORT=8080
-ENV PORT 8080
-ENV DB_HOST ""
-ENV DB_PORT ""
-ENV DB_USERNAME ""
-ENV DB_PASSWORD ""
-ENV DB_NAME ""
-ENV MUSIC_PATH ""
-ENV CHARACTERS_PATH ""
-
-CMD ./main
+CMD ["./main"]
diff --git a/app.yaml b/app.yaml
deleted file mode 100644
index 083a3d4..0000000
--- a/app.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-application: musicserver
-version: 2.2.0
-runtime: go115
-api_version: go1
-
-handlers:
-- url: /.*
- script: _go_app
\ No newline at end of file
diff --git a/init.sh b/init.sh
deleted file mode 100644
index f055fe3..0000000
--- a/init.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#! /bin/sh
-
-#envsubst < ./frontend/src/arne.js
-
-sed -i 's^$HOSTNAME^'"$HOSTNAME"'^g' ./frontend/src/arne.js
-
-cat ./frontend/src/arne.js
-
-cd ./frontend/
-
-npm install
-
-npm run build
-
-cd ..
-
-./MusicServer
\ No newline at end of file
diff --git a/justfile b/justfile
index 3aa992b..4cc0a4d 100644
--- a/justfile
+++ b/justfile
@@ -1,104 +1,50 @@
set dotenv-load
-# Build the application
+# Build and run the unit tests
all: build test
-templ-install:
- @if ! command -v templ > /dev/null; then \
- read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
- if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
- go install github.com/a-h/templ/cmd/templ@latest; \
- if [ ! -x "$$(command -v templ)" ]; then \
- echo "templ installation failed. Exiting..."; \
- exit 1; \
- fi; \
- else \
- echo "You chose not to install templ. Exiting..."; \
- exit 1; \
- fi; \
- fi
-
-templ-build: templ-install
+# Generate templ components (version pinned in go.mod)
+templ-build:
@echo "Building templ..."
- @templ generate
-
-tailwind-macos:
- @if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64 -o tailwindcss; fi
- @chmod +x tailwindcss
-
-tailwind-linux:
- @if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss; fi
- @chmod +x tailwindcss
-
-tailwind-build:
- @echo "Building tailwind..."
- @./tailwindcss -i cmd/web/assets/css/input.css -o cmd/web/assets/css/output.css
+ @go tool templ generate
sqlc-generate:
- @echo "Generating sqlc schema..."
+ @echo "Generating sqlc code..."
@sqlc generate
migrate-create name:
@migrate create -ext sql -dir internal/db/migrations -seq {{name}}
-swag-install:
- @if ! command -v swag > /dev/null; then \
- read -p "Swag is not installed on your machine. Do you want to install it? [Y/n] " choice; \
- if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
- go install github.com/swaggo/swag/cmd/swag@latest; \
- if [ ! -x "$$(command -v swag)" ]; then \
- echo "swag installation failed. Exiting..."; \
- exit 1; \
- fi; \
- else \
- echo "You chose not to install swag. Exiting..."; \
- exit 1; \
- fi; \
- fi
-
-swag-generate: swag-install
+# Generate OpenAPI docs (version pinned in go.mod)
+swag-generate:
@echo "Generating OpenAPI docs..."
- @swag init -g internal/server/routes.go -o cmd/docs
+ @go tool swag init -g internal/server/routes.go -o cmd/docs --parseInternal
-frontend-install:
- @if ! command -v npm > /dev/null; then \
- echo "npm is not installed on your machine. Please install Node.js first."; \
- exit 1; \
- fi
- @cd cmd/frontend && npm install
-
-frontend-build: frontend-install
- @echo "Building frontend..."
- @cd cmd/frontend && npm run build
-
-[no-cd]
build: sqlc-generate templ-build swag-generate
- @echo "Building..."
- @go build -o main cmd/main.go
+ @echo "Building..."
+ @go build -o main cmd/main.go
-run:
- @templ generate
+run: templ-build
@go run cmd/main.go
build-run: build
- @go run cmd/main.go
+ @./main
-test: build
- @echo "Starting test database container..."
- @docker compose -f compose.test.yaml up -d
- @sleep 10
- @echo "Running integration tests..."
- @just test-integration
- @echo "Stopping test database container..."
- @just test-integration-down
+# Unit tests: no database or Docker needed
+test:
+ @go test -race ./...
+
+# Integration tests against PostgreSQL in Docker (testcontainers), using the active docker context.
+# Pass a pattern to run a subset, e.g. `just test-integration TestSQLDumps`
+test-integration pattern=".":
+ @DOCKER_HOST="${DOCKER_HOST:-$(docker context inspect --format '{{{{.Endpoints.docker.Host}}')}" \
+ go test -tags integration -race -timeout 15m -run '{{pattern}}' ./...
-# Clean the binary
clean:
@echo "Cleaning..."
@rm -f main
docker-build:
- @echo "Building Docker image with docker..."
@docker build -t music-server .
docker-run:
@@ -107,82 +53,13 @@ docker-run:
docker-down:
@docker compose down
-# Run integration tests with docker
-# Starts a test PostgreSQL container, runs tests, then cleans up
-test-integration:
- @echo "Cleaning old test database..."
- @docker compose -f compose.test.yaml down -v
- @echo "Starting fresh test database container..."
- @docker compose -f compose.test.yaml up -d
- @sleep 10
- @echo "Running integration tests..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net DB_HOST=localhost DB_PORT=5433 DB_USERNAME=testuser DB_PASSWORD=testpass DB_NAME=music_server_test MUSIC_PATH=/Users/sebastian/projects/MusicServer/testMusic CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters PORT=8081 LOG_LEVEL=debug LOG_JSON=false go test -v -timeout 30m -p 1 -parallel 1 ./internal/...
-
-# Alternative: Run integration tests using testcontainers with docker provider
-test-integration-tc:
- @echo "Creating test network (if it doesn't exist)..."
- @docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
- @echo "Running integration tests with testcontainers (docker provider)..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 30m .
-
-# Run SQL dump migration tests
-# Tests that the application can start with each SQL dump and migrations work correctly
-test-sql-dump:
- @echo "Creating test network (if it doesn't exist)..."
- @docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
- @echo "Running SQL dump migration tests..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 15m -run "TestMusic.*Dump" ./internal/server/
-
-# Run individual SQL dump tests
-test-sql-dump-ref:
- @echo "Creating test network (if it doesn't exist)..."
- @docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
- @echo "Running SQL dump migration test for ref dump..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicRefDump" ./internal/server/
-
-test-sql-dump-test:
- @echo "Creating test network (if it doesn't exist)..."
- @docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
- @echo "Running SQL dump migration test for test dump..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicTestDump" ./internal/server/
-
-test-sql-dump-prod:
- @echo "Creating test network (if it doesn't exist)..."
- @docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
- @echo "Running SQL dump migration test for prod dump..."
- @DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicProdDump" ./internal/server/
-
-# Stop and remove test database container
-test-integration-down:
- @echo "Stopping test database container..."
- @docker compose -f compose.test.yaml down -v
-
-# Build and run with Docker Compose
-build-run-docker:
- @echo "Building and running with Docker Compose..."
- @docker compose up --build
-
-# Shutdown containers
-stop-docker:
- @echo "Stopping containers..."
- @docker compose down
-
watch-templ:
- templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
+ @go tool templ generate --watch --proxy="http://localhost:8080" --cmd="go run ./cmd"
-# Live Reload
+# Live reload
watch:
- @if command -v air > /dev/null; then \
- air; \
- echo "Watching...";\
- else \
- read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
- if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
- go install github.com/air-verse/air@latest; \
- air; \
- echo "Watching...";\
- else \
- echo "You chose not to install air. Exiting..."; \
- exit 1; \
- fi; \
+ @if ! command -v air > /dev/null; then \
+ echo "air is not installed. Install it with: go install github.com/air-verse/air@latest"; \
+ exit 1; \
fi
+ @air
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index 4ec3451..0000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- "./cmd/web/**/*.html", "./cmd/web/**/*.templ",
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-}
-