5 Commits

Author SHA1 Message Date
Sansan c63202242b feat: Complete DI cleanup - migrate test helpers to Database struct
- Update internal/db/test_helpers.go to use Database struct instead of globals
- Update internal/server/test_helpers.go to use TestDatabase.Pool
- Add TODO comment to old Dbpool/Ctx globals in dbHelper.go
- Remove db.Testf() usage from production code (kept for deprecated /dbtest endpoint)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-01 20:06:47 +02:00
Sansan 3418f492f5 feat: Add deprecation middleware for legacy endpoints
- Create middleware/deprecation.go with DeprecationMiddleware
- Adds Warning and Deprecation headers to old endpoints
- Apply middleware to all non-/api/v1 routes:
  /version, /dbtest, /health, /character*, /download*, /sync/*,
  /music/*
- Message: 'Deprecated: This endpoint is deprecated. Use /api/v1/ endpoints instead.'

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-01 19:41:17 +02:00
Sansan f4d1c3cf28 feat: Implement Statistics API with 8 endpoints under /api/v1/statistics/
- Add statistics.sql with 8 SQL queries for play count statistics
- Generate repository code via sqlc
- Add backend/statistics.go with business logic
- Add server/statistics_handler.go with Echo handlers
- Register protected routes under /api/v1/statistics/ with token auth
- Endpoints: games/most-played, games/least-played, games/never-played,
  games/last-played, games/oldest-played, songs/most-played,
  songs/least-played, summary

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-01 19:40:22 +02:00
Sansan 98c1948eff feat: Remove global db.Dbpool with dependency injection (Phase 0)
- Add Database struct in internal/db/database.go with Pool, Ctx, and RunMigrations()
- Update server.go to use Database struct with NewServerInstance()
- Add backend.go with InitBackend(), BackendRepo(), BackendCtx(), BackendPool()
- Update music.go and sync.go to use BackendRepo() and BackendCtx() instead of db.Dbpool/db.Ctx
- Update token_handler.go to accept pool parameter
- Update routes.go to use s.db.Pool for middleware
- Update cmd/main.go to use NewServerInstance() and HTTPServer()
- Update test_helpers.go to initialize backend with test database
- Update test files to use backend.BackendPool() and backend.BackendCtx()

Benefits:
- Easier to mock database for unit tests
- Follows Go best practices (dependency injection)
- Better architecture with explicit dependencies
- RunMigrations() replaces old Migrate_db() function

Note: Global db.Dbpool and db.Ctx still exist in dbHelper.go for backward compatibility
with test_helpers.go, but production code no longer uses them.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-01 18:50:05 +02:00
Sansan 3e37303979 feat: Implement Session Token System with /api/v1 base path
- Add migration 000004 for sessions table and performance indexes
- Create session.sql queries for CRUD operations
- Generate session repository code with sqlc
- Create token auth middleware for Echo framework
- Create token handler with create/delete/cleanup endpoints
- Add /api/v1 router with token authentication infrastructure
- Update dbHelper.go to use Up() instead of Migrate(2)
- Update server.go to initialize token handler
- Existing endpoints remain functional (to be deprecated)

New endpoints:
- POST /api/v1/token - Create new session token
- DELETE /api/v1/token - Invalidate token
- POST /api/v1/token/cleanup - Remove expired sessions

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-01 18:07:28 +02:00
48 changed files with 1176 additions and 3664 deletions
+11 -17
View File
@@ -1,35 +1,26 @@
# Stage 1: Build frontend
FROM node:18-alpine AS frontend-builder
RUN apk add --no-cache git
WORKDIR /app
RUN git clone https://gitea.sanplex.xyz/Sansan/MusicFrontend.git
WORKDIR /app/MusicFrontend
RUN npm install
RUN npm run build
# Generate config.js with empty API_HOSTNAME (relative paths)
RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > dist/config.js
# Stage 2: Build backend
FROM golang:1.25-alpine as build_go FROM golang:1.25-alpine as build_go
RUN apk add --no-cache curl RUN apk add --no-cache curl
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go install github.com/a-h/templ/cmd/templ@latest RUN go install github.com/a-h/templ/cmd/templ@latest
RUN templ generate RUN templ generate
RUN go build -o main cmd/main.go RUN go build -o main cmd/main.go
# Stage 3: Final image # Stage 2, distribution container
FROM golang:1.25-alpine FROM golang:1.25-alpine
EXPOSE 8080 EXPOSE 8080
VOLUME /sorted VOLUME /sorted
VOLUME /frontend
VOLUME /characters VOLUME /characters
COPY --from=build_go /app/main .
COPY --from=frontend-builder /app/MusicFrontend/dist /frontend
COPY ./songs/ ./songs/
ENV PORT 8080 ENV PORT 8080
ENV DB_HOST "" ENV DB_HOST ""
ENV DB_PORT "" ENV DB_PORT ""
@@ -39,4 +30,7 @@ ENV DB_NAME ""
ENV MUSIC_PATH "" ENV MUSIC_PATH ""
ENV CHARACTERS_PATH "" ENV CHARACTERS_PATH ""
COPY --from=build_go /app/main .
COPY ./songs/ ./songs/
CMD ./main CMD ./main
+39 -676
View File
@@ -23,539 +23,6 @@ var doc = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/api/v1/statistics/games/last-played": {
"get": {
"description": "Returns the most recently played games",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get last played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/least-played": {
"get": {
"description": "Returns the top N least played games with their songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get least played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/most-played": {
"get": {
"description": "Returns the top N most played games with their songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get most played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/never-played": {
"get": {
"description": "Returns all games that have never been played (times_played = 0)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get never played games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/oldest-played": {
"get": {
"description": "Returns the least recently played games (that have been played at least once)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get oldest played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/songs/least-played": {
"get": {
"description": "Returns the top N least played songs with their game info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get least played songs",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/songs/most-played": {
"get": {
"description": "Returns the top N most played songs with their game info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get most played songs",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/summary": {
"get": {
"description": "Returns overall statistics about the music library",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get statistics summary",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/backend.StatisticsSummary"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/token": {
"post": {
"description": "Returns a new session token for API access",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Create session token",
"parameters": [
{
"description": "Client type",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.TokenRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.TokenResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes the current session token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Invalidate session token",
"parameters": [
{
"type": "string",
"description": "Bearer token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/token/cleanup": {
"post": {
"description": "Removes all expired session tokens from the database",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Cleanup expired sessions",
"parameters": [
{
"type": "string",
"description": "Bearer token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/character": { "/character": {
"get": { "get": {
"description": "Returns the image for a specific character", "description": "Returns the image for a specific character",
@@ -614,6 +81,29 @@ var doc = `{
} }
} }
}, },
"/dbtest": {
"get": {
"description": "Tests the database connection",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"database"
],
"summary": "Test database connection",
"responses": {
"200": {
"description": "TestedDB",
"schema": {
"type": "string"
}
}
}
}
},
"/download": { "/download": {
"get": { "get": {
"description": "Checks for the latest version of the application", "description": "Checks for the latest version of the application",
@@ -834,7 +324,7 @@ var doc = `{
"tags": [ "tags": [
"music" "music"
], ],
"summary": "Get all soundtracks", "summary": "Get all games",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -867,7 +357,7 @@ var doc = `{
"tags": [ "tags": [
"music" "music"
], ],
"summary": "Get all soundtracks random", "summary": "Get all games random",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -1207,10 +697,10 @@ var doc = `{
"tags": [ "tags": [
"sync" "sync"
], ],
"summary": "Sync soundtracks with only changes", "summary": "Sync games with only changes",
"responses": { "responses": {
"200": { "200": {
"description": "Start syncing soundtracks", "description": "Start syncing games",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1239,7 +729,7 @@ var doc = `{
"summary": "Sync all games fully", "summary": "Sync all games fully",
"responses": { "responses": {
"200": { "200": {
"description": "Start syncing soundtracks full", "description": "Start syncing games full",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1289,10 +779,10 @@ var doc = `{
"tags": [ "tags": [
"sync" "sync"
], ],
"summary": "Reset soundtracks database", "summary": "Reset games database",
"responses": { "responses": {
"200": { "200": {
"description": "Soundtracks and songs are deleted from the database", "description": "Games and songs are deleted from the database",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1308,7 +798,7 @@ var doc = `{
}, },
"/version": { "/version": {
"get": { "get": {
"description": "get latest version info", "description": "get string by ID",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -1316,9 +806,9 @@ var doc = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"version" "accounts"
], ],
"summary": "Getting the latest version of the backend", "summary": "Getting the version of the backend",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -1334,154 +824,27 @@ var doc = `{
} }
} }
} }
},
"/version/history": {
"get": {
"description": "get version history",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"version"
],
"summary": "Getting the version history of the backend",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.VersionData"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"backend.GameWithSongs": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
},
"game_last_played": {
"type": "string"
},
"game_name": {
"type": "string"
},
"game_played": {
"type": "integer"
},
"songs": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
}
},
"backend.SongInfoForStats": {
"type": "object",
"properties": {
"file_name": {
"type": "string"
},
"game_id": {
"type": "integer"
},
"game_name": {
"type": "string"
},
"path": {
"type": "string"
},
"song_name": {
"type": "string"
},
"times_played": {
"type": "integer"
}
}
},
"backend.StatisticsSummary": {
"type": "object",
"properties": {
"avg_game_plays": {
"type": "number"
},
"max_game_plays": {
"type": "integer"
},
"min_game_plays": {
"type": "integer"
},
"never_played_games": {
"type": "integer"
},
"played_games": {
"type": "integer"
},
"total_game_plays": {
"type": "integer"
},
"total_games": {
"type": "integer"
}
}
},
"backend.VersionData": { "backend.VersionData": {
"type": "object", "type": "object",
"properties": { "properties": {
"changelog": { "changelog": {
"type": "string",
"example": "account name"
},
"history": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/backend.VersionData"
}, }
"example": [
"[\"Initial release\"",
"\"Bug fixes\"]"
]
}, },
"version": { "version": {
"type": "string", "type": "string",
"example": "1.0.0" "example": "1.0.0"
} }
} }
},
"server.TokenRequest": {
"type": "object",
"properties": {
"client_type": {
"description": "Optional: \"web\", \"mobile\", \"api\"",
"type": "string"
}
}
},
"server.TokenResponse": {
"type": "object",
"properties": {
"client_type": {
"type": "string"
},
"expires_at": {
"type": "string"
},
"token": {
"type": "string"
}
}
} }
} }
}` }`
+39 -676
View File
@@ -4,539 +4,6 @@
"contact": {} "contact": {}
}, },
"paths": { "paths": {
"/api/v1/statistics/games/last-played": {
"get": {
"description": "Returns the most recently played games",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get last played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/least-played": {
"get": {
"description": "Returns the top N least played games with their songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get least played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/most-played": {
"get": {
"description": "Returns the top N most played games with their songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get most played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/never-played": {
"get": {
"description": "Returns all games that have never been played (times_played = 0)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get never played games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/games/oldest-played": {
"get": {
"description": "Returns the least recently played games (that have been played at least once)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get oldest played games",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.GameWithSongs"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/songs/least-played": {
"get": {
"description": "Returns the top N least played songs with their game info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get least played songs",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/songs/most-played": {
"get": {
"description": "Returns the top N most played songs with their game info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get most played songs",
"parameters": [
{
"type": "integer",
"description": "Number of results (default: 10)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/statistics/summary": {
"get": {
"description": "Returns overall statistics about the music library",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"statistics"
],
"summary": "Get statistics summary",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/backend.StatisticsSummary"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/token": {
"post": {
"description": "Returns a new session token for API access",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Create session token",
"parameters": [
{
"description": "Client type",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.TokenRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.TokenResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes the current session token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Invalidate session token",
"parameters": [
{
"type": "string",
"description": "Bearer token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/token/cleanup": {
"post": {
"description": "Removes all expired session tokens from the database",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Cleanup expired sessions",
"parameters": [
{
"type": "string",
"description": "Bearer token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/character": { "/character": {
"get": { "get": {
"description": "Returns the image for a specific character", "description": "Returns the image for a specific character",
@@ -595,6 +62,29 @@
} }
} }
}, },
"/dbtest": {
"get": {
"description": "Tests the database connection",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"database"
],
"summary": "Test database connection",
"responses": {
"200": {
"description": "TestedDB",
"schema": {
"type": "string"
}
}
}
}
},
"/download": { "/download": {
"get": { "get": {
"description": "Checks for the latest version of the application", "description": "Checks for the latest version of the application",
@@ -815,7 +305,7 @@
"tags": [ "tags": [
"music" "music"
], ],
"summary": "Get all soundtracks", "summary": "Get all games",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -848,7 +338,7 @@
"tags": [ "tags": [
"music" "music"
], ],
"summary": "Get all soundtracks random", "summary": "Get all games random",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -1188,10 +678,10 @@
"tags": [ "tags": [
"sync" "sync"
], ],
"summary": "Sync soundtracks with only changes", "summary": "Sync games with only changes",
"responses": { "responses": {
"200": { "200": {
"description": "Start syncing soundtracks", "description": "Start syncing games",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1220,7 +710,7 @@
"summary": "Sync all games fully", "summary": "Sync all games fully",
"responses": { "responses": {
"200": { "200": {
"description": "Start syncing soundtracks full", "description": "Start syncing games full",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1270,10 +760,10 @@
"tags": [ "tags": [
"sync" "sync"
], ],
"summary": "Reset soundtracks database", "summary": "Reset games database",
"responses": { "responses": {
"200": { "200": {
"description": "Soundtracks and songs are deleted from the database", "description": "Games and songs are deleted from the database",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -1289,7 +779,7 @@
}, },
"/version": { "/version": {
"get": { "get": {
"description": "get latest version info", "description": "get string by ID",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -1297,9 +787,9 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"version" "accounts"
], ],
"summary": "Getting the latest version of the backend", "summary": "Getting the version of the backend",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -1315,154 +805,27 @@
} }
} }
} }
},
"/version/history": {
"get": {
"description": "get version history",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"version"
],
"summary": "Getting the version history of the backend",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.VersionData"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"backend.GameWithSongs": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
},
"game_last_played": {
"type": "string"
},
"game_name": {
"type": "string"
},
"game_played": {
"type": "integer"
},
"songs": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.SongInfoForStats"
}
}
}
},
"backend.SongInfoForStats": {
"type": "object",
"properties": {
"file_name": {
"type": "string"
},
"game_id": {
"type": "integer"
},
"game_name": {
"type": "string"
},
"path": {
"type": "string"
},
"song_name": {
"type": "string"
},
"times_played": {
"type": "integer"
}
}
},
"backend.StatisticsSummary": {
"type": "object",
"properties": {
"avg_game_plays": {
"type": "number"
},
"max_game_plays": {
"type": "integer"
},
"min_game_plays": {
"type": "integer"
},
"never_played_games": {
"type": "integer"
},
"played_games": {
"type": "integer"
},
"total_game_plays": {
"type": "integer"
},
"total_games": {
"type": "integer"
}
}
},
"backend.VersionData": { "backend.VersionData": {
"type": "object", "type": "object",
"properties": { "properties": {
"changelog": { "changelog": {
"type": "string",
"example": "account name"
},
"history": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/backend.VersionData"
}, }
"example": [
"[\"Initial release\"",
"\"Bug fixes\"]"
]
}, },
"version": { "version": {
"type": "string", "type": "string",
"example": "1.0.0" "example": "1.0.0"
} }
} }
},
"server.TokenRequest": {
"type": "object",
"properties": {
"client_type": {
"description": "Optional: \"web\", \"mobile\", \"api\"",
"type": "string"
}
}
},
"server.TokenResponse": {
"type": "object",
"properties": {
"client_type": {
"type": "string"
},
"expires_at": {
"type": "string"
},
"token": {
"type": "string"
}
}
} }
} }
} }
+29 -448
View File
@@ -1,433 +1,20 @@
definitions: definitions:
backend.GameWithSongs:
properties:
game_id:
type: integer
game_last_played:
type: string
game_name:
type: string
game_played:
type: integer
songs:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
type: object
backend.SongInfoForStats:
properties:
file_name:
type: string
game_id:
type: integer
game_name:
type: string
path:
type: string
song_name:
type: string
times_played:
type: integer
type: object
backend.StatisticsSummary:
properties:
avg_game_plays:
type: number
max_game_plays:
type: integer
min_game_plays:
type: integer
never_played_games:
type: integer
played_games:
type: integer
total_game_plays:
type: integer
total_games:
type: integer
type: object
backend.VersionData: backend.VersionData:
properties: properties:
changelog: changelog:
example: example: account name
- '["Initial release"' type: string
- '"Bug fixes"]' history:
items: items:
type: string $ref: '#/definitions/backend.VersionData'
type: array type: array
version: version:
example: 1.0.0 example: 1.0.0
type: string type: string
type: object type: object
server.TokenRequest:
properties:
client_type:
description: 'Optional: "web", "mobile", "api"'
type: string
type: object
server.TokenResponse:
properties:
client_type:
type: string
expires_at:
type: string
token:
type: string
type: object
info: info:
contact: {} contact: {}
paths: paths:
/api/v1/statistics/games/last-played:
get:
consumes:
- application/json
description: Returns the most recently played games
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.GameWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get last played games
tags:
- statistics
/api/v1/statistics/games/least-played:
get:
consumes:
- application/json
description: Returns the top N least played games with their songs
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.GameWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get least played games
tags:
- statistics
/api/v1/statistics/games/most-played:
get:
consumes:
- application/json
description: Returns the top N most played games with their songs
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.GameWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get most played games
tags:
- statistics
/api/v1/statistics/games/never-played:
get:
consumes:
- application/json
description: Returns all games that have never been played (times_played = 0)
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.GameWithSongs'
type: array
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get never played games
tags:
- statistics
/api/v1/statistics/games/oldest-played:
get:
consumes:
- application/json
description: Returns the least recently played games (that have been played
at least once)
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.GameWithSongs'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get oldest played games
tags:
- statistics
/api/v1/statistics/songs/least-played:
get:
consumes:
- application/json
description: Returns the top N least played songs with their game info
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get least played songs
tags:
- statistics
/api/v1/statistics/songs/most-played:
get:
consumes:
- application/json
description: Returns the top N most played songs with their game info
parameters:
- description: 'Number of results (default: 10)'
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.SongInfoForStats'
type: array
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get most played songs
tags:
- statistics
/api/v1/statistics/summary:
get:
consumes:
- application/json
description: Returns overall statistics about the music library
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/backend.StatisticsSummary'
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get statistics summary
tags:
- statistics
/api/v1/token:
delete:
consumes:
- application/json
description: Deletes the current session token
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Invalidate session token
tags:
- auth
post:
consumes:
- application/json
description: Returns a new session token for API access
parameters:
- description: Client type
in: body
name: request
required: true
schema:
$ref: '#/definitions/server.TokenRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/server.TokenResponse'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Create session token
tags:
- auth
/api/v1/token/cleanup:
post:
consumes:
- application/json
description: Removes all expired session tokens from the database
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Cleanup expired sessions
tags:
- auth
/character: /character:
get: get:
consumes: consumes:
@@ -466,6 +53,21 @@ paths:
summary: Get list of characters summary: Get list of characters
tags: tags:
- characters - characters
/dbtest:
get:
consumes:
- application/json
description: Tests the database connection
produces:
- application/json
responses:
"200":
description: TestedDB
schema:
type: string
summary: Test database connection
tags:
- database
/download: /download:
get: get:
consumes: consumes:
@@ -621,7 +223,7 @@ paths:
description: Syncing is in progress description: Syncing is in progress
schema: schema:
type: string type: string
summary: Get all soundtracks summary: Get all games
tags: tags:
- music - music
/music/all/random: /music/all/random:
@@ -643,7 +245,7 @@ paths:
description: Syncing is in progress description: Syncing is in progress
schema: schema:
type: string type: string
summary: Get all soundtracks random summary: Get all games random
tags: tags:
- music - music
/music/info: /music/info:
@@ -857,14 +459,14 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: Start syncing soundtracks description: Start syncing games
schema: schema:
type: string type: string
"423": "423":
description: Syncing is in progress description: Syncing is in progress
schema: schema:
type: string type: string
summary: Sync soundtracks with only changes summary: Sync games with only changes
tags: tags:
- sync - sync
/sync/full: /sync/full:
@@ -876,7 +478,7 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: Start syncing soundtracks full description: Start syncing games full
schema: schema:
type: string type: string
"423": "423":
@@ -911,21 +513,21 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: Soundtracks and songs are deleted from the database description: Games and songs are deleted from the database
schema: schema:
type: string type: string
"423": "423":
description: Syncing is in progress description: Syncing is in progress
schema: schema:
type: string type: string
summary: Reset soundtracks database summary: Reset games database
tags: tags:
- sync - sync
/version: /version:
get: get:
consumes: consumes:
- application/json - application/json
description: get latest version info description: get string by ID
produces: produces:
- application/json - application/json
responses: responses:
@@ -937,28 +539,7 @@ paths:
description: Not Found description: Not Found
schema: schema:
type: string type: string
summary: Getting the latest version of the backend summary: Getting the version of the backend
tags: tags:
- version - accounts
/version/history:
get:
consumes:
- application/json
description: get version history
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/backend.VersionData'
type: array
"404":
description: Not Found
schema:
type: string
summary: Getting the version history of the backend
tags:
- version
swagger: "2.0" swagger: "2.0"
+10 -65
View File
@@ -1,33 +1,5 @@
/* Pure CSS styles for Music Search */ /* Pure CSS styles for Music Search */
:root {
/* Light mode colors (default) */
--bg-primary: #f3f4f6;
--bg-secondary: #e5e7eb;
--bg-tertiary: #dcfce7;
--text-primary: #000;
--text-secondary: #374151;
--border-primary: #9ca3af;
--border-focus: #6b7280;
--accent-primary: #f97316;
--accent-hover: #ea580c;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
/* Dark mode colors matching frontend */
--bg-primary: #555;
--bg-secondary: #333;
--bg-tertiary: #2a2a2a;
--text-primary: #fff;
--text-secondary: #ff9c00;
--border-primary: #666;
--border-focus: #ff9c00;
--accent-primary: #ff9c00;
--accent-hover: #e68a00;
--shadow-color: rgba(0, 0, 0, 0.3);
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -38,9 +10,7 @@ html, body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.5; line-height: 1.5;
background-color: var(--bg-primary); background-color: #f3f4f6;
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
} }
main { main {
@@ -59,15 +29,15 @@ main {
max-width: 600px; max-width: 600px;
font-size: 1.5rem; font-size: 1.5rem;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid var(--border-primary); border: 1px solid #9ca3af;
border-radius: 0.5rem; border-radius: 0.5rem;
background-color: var(--bg-secondary); background-color: #e5e7eb;
color: var(--text-primary); color: #000;
} }
#search_term:focus { #search_term:focus {
outline: none; outline: none;
border-color: var(--border-focus); border-color: #6b7280;
} }
#clear { #clear {
@@ -75,48 +45,23 @@ main {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
background-color: var(--accent-primary); background-color: #f97316;
color: var(--text-primary); color: #fff;
cursor: pointer; cursor: pointer;
margin-left: 1rem; margin-left: 1rem;
} }
#clear:hover { #clear:hover {
background-color: var(--accent-hover); background-color: #ea580c;
} }
#games-container { #games-container {
font-size: 1.5rem; font-size: 1.5rem;
} }
.game-text {
color: var(--text-primary);
word-break: break-word;
}
/* Dark mode toggle */
#dark-mode-toggle {
position: fixed;
top: 1rem;
right: 1rem;
font-size: 1.2rem;
padding: 0.4rem 0.8rem;
border: none;
border-radius: 0.5rem;
background-color: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
z-index: 1000;
transition: all 0.3s ease;
}
#dark-mode-toggle:hover {
background-color: var(--border-primary);
}
/* Game result cards */ /* Game result cards */
.bg-green-100 { .bg-green-100 {
background-color: var(--bg-tertiary); background-color: #dcfce7;
} }
.p-4 { .p-4 {
@@ -124,7 +69,7 @@ main {
} }
.shadow-md { .shadow-md {
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
} }
.rounded-lg { .rounded-lg {
+1 -1
View File
@@ -30,7 +30,7 @@ func FindGameWebHandler(w http.ResponseWriter, r *http.Request) {
func search(searchText string) { func search(searchText string) {
games_added = nil games_added = nil
games := backend.GetAllSoundtracks() games := backend.GetAllGames()
for _, game := range games { for _, game := range games {
if is_match_exact(searchText, game) { if is_match_exact(searchText, game) {
add_game(game) add_game(game)
+1 -23
View File
@@ -2,7 +2,6 @@ package web
templ HelloForm() { templ HelloForm() {
@Base() { @Base() {
<button id="dark-mode-toggle">🌙</button>
<div id="search-container"> <div id="search-container">
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/> <input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
<button type="button" id="clear" name="clear">Clear</button> <button type="button" id="clear" name="clear">Clear</button>
@@ -13,29 +12,8 @@ templ HelloForm() {
if (document.readyState == 'complete') { if (document.readyState == 'complete') {
htmx.ajax('POST', '/find', '#games-container'); htmx.ajax('POST', '/find', '#games-container');
document.getElementById("search_term").focus(); document.getElementById("search_term").focus();
// Initialize dark mode from localStorage (default to dark)
const savedTheme = localStorage.getItem('theme') || 'dark';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
document.getElementById('dark-mode-toggle').textContent = '☀️';
}
} }
}); });
// Dark mode toggle functionality
document.getElementById("dark-mode-toggle").addEventListener("click", function() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update toggle button text
this.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
document.getElementById("clear").addEventListener("click", function (event) { document.getElementById("clear").addEventListener("click", function (event) {
document.getElementById("search_term").value = ""; document.getElementById("search_term").value = "";
htmx.ajax('POST', '/find', '#games-container'); htmx.ajax('POST', '/find', '#games-container');
@@ -48,7 +26,7 @@ templ HelloForm() {
templ FoundGames(games []string) { templ FoundGames(games []string) {
for _, game := range games { for _, game := range games {
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6"> <div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
<p class="game-text">{ game }</p> <p>{ game }</p>
</div> </div>
} }
} }
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"os" "os"
"strings" "strings"
"go.uber.org/zap"
"music-server/internal/logging" "music-server/internal/logging"
"go.uber.org/zap"
) )
func GetCharacterList() []string { func GetCharacterList() []string {
@@ -30,10 +30,10 @@ func GetCharacterList() []string {
func GetCharacter(character string) string { func GetCharacter(character string) string {
charactersPath := os.Getenv("CHARACTERS_PATH") charactersPath := os.Getenv("CHARACTERS_PATH")
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath))
// Clean the path - remove trailing slashes and then add one for consistency // Clean the path - remove trailing slashes and then add one for consistency
charactersPath = strings.TrimSuffix(charactersPath, "/") charactersPath = strings.TrimSuffix(charactersPath, "/")
charactersPath += "/" charactersPath += "/"
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath+character))
return charactersPath + character return charactersPath + character
} }
+95
View File
@@ -0,0 +1,95 @@
package backend
import (
"music-server/internal/db"
)
func TestDB() {
db.Testf()
}
type VersionData struct {
Version string `json:"version" example:"1.0.0"`
Changelog string `json:"changelog" example:"account name"`
History []VersionData `json:"history"`
}
func GetVersionHistory() VersionData {
data := VersionData{Version: "4.5.0",
Changelog: "#1 - Created request to check newest version of the app\n" +
"#2 - Added request to download the newest version of the app\n" +
"#3 - Added request to check progress during sync\n" +
"#4 - Now blocking all request while sync is in progress\n" +
"#5 - Implemented ants for thread pooling\n" +
"#6 - Changed the sync request to now only start the sync",
History: []VersionData{
{
Version: "4.0.0",
Changelog: "Changed framework from gin to Echo\n" +
"Reorganized the code\n" +
"Implemented sqlc\n" +
"Added support to send character images from the server\n" +
"Added function to create a new database of no one exists",
},
{
Version: "3.2",
Changelog: "Upgraded Go version and the version of all dependencies. Fixed som more bugs.",
},
{
Version: "3.1",
Changelog: "Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend.",
},
{
Version: "3.0",
Changelog: "Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application.",
},
{
Version: "2.3.0",
Changelog: "Images should not be included in the database, removes songs where the path doesn't work.",
},
{
Version: "2.2.0",
Changelog: "Changed the structure of the whole application, should be no changes to functionality.",
},
{
Version: "2.1.4",
Changelog: "Game list should now be sorted, a new endpoint with the game list in random order have been added.",
},
{
Version: "2.1.3",
Changelog: "Added a check to see if song exists before returning it, if not a new song will be picked up.",
},
{
Version: "2.1.2",
Changelog: "Added test server to swagger file.",
},
{
Version: "2.1.1",
Changelog: "Fixed bug where wrong song was showed as currently played.",
},
{
Version: "2.1.0",
Changelog: "Added /addQue to add the last received song to the songQue. " +
"Changed /rand and /rand/low to not add song to the que. " +
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
},
{
Version: "2.0.3",
Changelog: "Another small change that should fix the caching problem.",
},
{
Version: "2.0.2",
Changelog: "Hopefully fixed the caching problem with random.",
},
{
Version: "2.0.1",
Changelog: "Fixed CORS",
},
{
Version: "2.0.0",
Changelog: "Rebuilt the application in Go.",
},
},
}
return data
}
+29 -29
View File
@@ -22,7 +22,7 @@ type SongInfo struct {
var currentSong = -1 var currentSong = -1
var gamesNew []repository.Soundtrack var gamesNew []repository.Game
var songQueNew []repository.Song var songQueNew []repository.Song
@@ -37,10 +37,10 @@ func initRepo() {
} }
} }
func getAllGames() []repository.Soundtrack { func getAllGames() []repository.Game {
if len(gamesNew) == 0 { if len(gamesNew) == 0 {
initRepo() initRepo()
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx()) gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
} }
return gamesNew return gamesNew
@@ -59,7 +59,7 @@ func Reset() {
songQueNew = nil songQueNew = nil
currentSong = -1 currentSong = -1
initRepo() initRepo()
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx()) gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
} }
func AddLatestToQue() { func AddLatestToQue() {
@@ -77,8 +77,8 @@ func AddLatestPlayed() {
currentSongData := songQueNew[currentSong] currentSongData := songQueNew[currentSong]
initRepo() initRepo()
BackendRepo().AddSoundtrackPlayed(BackendCtx(), currentSongData.SoundtrackID) BackendRepo().AddGamePlayed(BackendCtx(), currentSongData.GameID)
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: currentSongData.SoundtrackID, SongName: currentSongData.SongName}) BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: currentSongData.GameID, SongName: currentSongData.SongName})
} }
func SetPlayed(songNumber int) { func SetPlayed(songNumber int) {
@@ -87,8 +87,8 @@ func SetPlayed(songNumber int) {
} }
songData := songQueNew[songNumber] songData := songQueNew[songNumber]
initRepo() initRepo()
BackendRepo().AddSoundtrackPlayed(BackendCtx(), songData.SoundtrackID) BackendRepo().AddGamePlayed(BackendCtx(), songData.GameID)
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: songData.SoundtrackID, SongName: songData.SongName}) BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: songData.GameID, SongName: songData.SongName})
} }
func GetRandomSong() string { func GetRandomSong() string {
@@ -105,7 +105,7 @@ func GetRandomSong() string {
func GetRandomSongLowChance() string { func GetRandomSongLowChance() string {
getAllGames() getAllGames()
var listOfGames []repository.Soundtrack var listOfGames []repository.Game
var averagePlayed = getAveragePlayed() var averagePlayed = getAveragePlayed()
@@ -131,7 +131,7 @@ func GetRandomSongClassic() string {
var listOfAllSongs []repository.Song var listOfAllSongs []repository.Song
for _, game := range gamesNew { for _, game := range gamesNew {
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID) songList, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
listOfAllSongs = append(listOfAllSongs, songList...) listOfAllSongs = append(listOfAllSongs, songList...)
} }
@@ -139,13 +139,13 @@ func GetRandomSongClassic() string {
var song repository.Song var song repository.Song
for !songFound { for !songFound {
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))] song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
gameData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID) gameData, err := BackendRepo().GetGameById(BackendCtx(), song.GameID)
if err != nil { if err != nil {
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path}) BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
logging.GetLogger().Warn("Song not found, removed from database", logging.GetLogger().Warn("Song not found, removed from database",
zap.String("song", song.SongName), zap.String("song", song.SongName),
zap.String("game", gameData.SoundtrackName), zap.String("game", gameData.GameName),
zap.String("filename", *song.FileName)) zap.String("filename", *song.FileName))
continue continue
} }
@@ -154,10 +154,10 @@ func GetRandomSongClassic() string {
openFile, err := os.Open(song.Path) openFile, err := os.Open(song.Path)
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) { if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
//File not found //File not found
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path}) BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
logging.GetLogger().Warn("Song not found, removed from database", logging.GetLogger().Warn("Song not found, removed from database",
zap.String("song", song.SongName), zap.String("song", song.SongName),
zap.String("game", gameData.SoundtrackName), zap.String("game", gameData.GameName),
zap.String("filename", *song.FileName)) zap.String("filename", *song.FileName))
} else { } else {
songFound = true songFound = true
@@ -180,7 +180,7 @@ func GetSongInfo() SongInfo {
currentGameData := getCurrentGame(currentSongData) currentGameData := getCurrentGame(currentSongData)
return SongInfo{ return SongInfo{
Game: currentGameData.SoundtrackName, Game: currentGameData.GameName,
GamePlayed: currentGameData.TimesPlayed, GamePlayed: currentGameData.TimesPlayed,
Song: currentSongData.SongName, Song: currentSongData.SongName,
SongPlayed: currentSongData.TimesPlayed, SongPlayed: currentSongData.TimesPlayed,
@@ -195,7 +195,7 @@ func GetPlayedSongs() []SongInfo {
for i, song := range songQueNew { for i, song := range songQueNew {
gameData := getCurrentGame(song) gameData := getCurrentGame(song)
songList = append(songList, SongInfo{ songList = append(songList, SongInfo{
Game: gameData.SoundtrackName, Game: gameData.GameName,
GamePlayed: gameData.TimesPlayed, GamePlayed: gameData.TimesPlayed,
Song: song.SongName, Song: song.SongName,
SongPlayed: song.TimesPlayed, SongPlayed: song.TimesPlayed,
@@ -217,22 +217,22 @@ func GetSong(song string) string {
return songData.Path return songData.Path
} }
func GetAllSoundtracks() []string { func GetAllGames() []string {
getAllGames() getAllGames()
var jsonArray []string var jsonArray []string
for _, game := range gamesNew { for _, game := range gamesNew {
jsonArray = append(jsonArray, game.SoundtrackName) jsonArray = append(jsonArray, game.GameName)
} }
return jsonArray return jsonArray
} }
func GetAllSoundtracksRandom() []string { func GetAllGamesRandom() []string {
getAllGames() getAllGames()
var jsonArray []string var jsonArray []string
for _, game := range gamesNew { for _, game := range gamesNew {
jsonArray = append(jsonArray, game.SoundtrackName) jsonArray = append(jsonArray, game.GameName)
} }
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] }) rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
return jsonArray return jsonArray
@@ -266,12 +266,12 @@ func GetPreviousSong() string {
} }
} }
func getSongFromList(games []repository.Soundtrack) repository.Song { func getSongFromList(games []repository.Game) repository.Song {
songFound := false songFound := false
var song repository.Song var song repository.Song
for !songFound { for !songFound {
game := getRandomGame(games) game := getRandomGame(games)
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID) songs, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
if len(songs) == 0 { if len(songs) == 0 {
continue continue
} }
@@ -282,10 +282,10 @@ func getSongFromList(games []repository.Soundtrack) repository.Song {
openFile, err := os.Open(song.Path) openFile, err := os.Open(song.Path)
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) { if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
//File not found //File not found
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path}) BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
logging.GetLogger().Warn("Song not found, removed from database", logging.GetLogger().Warn("Song not found, removed from database",
zap.String("song", song.SongName), zap.String("song", song.SongName),
zap.String("game", game.SoundtrackName), zap.String("game", game.GameName),
zap.Any("filename", song.FileName)) zap.Any("filename", song.FileName))
} else { } else {
songFound = true songFound = true
@@ -299,13 +299,13 @@ func getSongFromList(games []repository.Soundtrack) repository.Song {
return song return song
} }
func getCurrentGame(currentSongData repository.Song) repository.Soundtrack { func getCurrentGame(currentSongData repository.Song) repository.Game {
for _, game := range gamesNew { for _, game := range gamesNew {
if game.ID == currentSongData.SoundtrackID { if game.ID == currentSongData.GameID {
return game return game
} }
} }
return repository.Soundtrack{} return repository.Game{}
} }
func getAveragePlayed() int32 { func getAveragePlayed() int32 {
@@ -317,6 +317,6 @@ func getAveragePlayed() int32 {
return sum / int32(len(gamesNew)) return sum / int32(len(gamesNew))
} }
func getRandomGame(listOfGames []repository.Soundtrack) repository.Soundtrack { func getRandomGame(listOfGames []repository.Game) repository.Game {
return listOfGames[rand.Intn(len(listOfGames))] return listOfGames[rand.Intn(len(listOfGames))]
} }
+37 -35
View File
@@ -9,10 +9,10 @@ import (
// Test the average calculation logic directly without database access // Test the average calculation logic directly without database access
func TestCalculateAverage(t *testing.T) { func TestCalculateAverage(t *testing.T) {
games := []repository.Soundtrack{ games := []repository.Game{
{SoundtrackName: "Game1", TimesPlayed: 10}, {GameName: "Game1", TimesPlayed: 10},
{SoundtrackName: "Game2", TimesPlayed: 20}, {GameName: "Game2", TimesPlayed: 20},
{SoundtrackName: "Game3", TimesPlayed: 30}, {GameName: "Game3", TimesPlayed: 30},
} }
var sum int32 var sum int32
@@ -28,7 +28,7 @@ func TestCalculateAverage(t *testing.T) {
} }
func TestCalculateAverageEmpty(t *testing.T) { func TestCalculateAverageEmpty(t *testing.T) {
games := []repository.Soundtrack{} games := []repository.Game{}
if len(games) == 0 { if len(games) == 0 {
result := int32(0) result := int32(0)
@@ -52,8 +52,8 @@ func TestCalculateAverageEmpty(t *testing.T) {
} }
func TestCalculateAverageSingle(t *testing.T) { func TestCalculateAverageSingle(t *testing.T) {
games := []repository.Soundtrack{ games := []repository.Game{
{SoundtrackName: "Game1", TimesPlayed: 42}, {GameName: "Game1", TimesPlayed: 42},
} }
var sum int32 var sum int32
@@ -69,10 +69,10 @@ func TestCalculateAverageSingle(t *testing.T) {
} }
func TestGetRandomGame(t *testing.T) { func TestGetRandomGame(t *testing.T) {
games := []repository.Soundtrack{ games := []repository.Game{
{SoundtrackName: "Game1", TimesPlayed: 10}, {GameName: "Game1", TimesPlayed: 10},
{SoundtrackName: "Game2", TimesPlayed: 20}, {GameName: "Game2", TimesPlayed: 20},
{SoundtrackName: "Game3", TimesPlayed: 30}, {GameName: "Game3", TimesPlayed: 30},
} }
// Set seed for reproducible tests // Set seed for reproducible tests
@@ -80,93 +80,93 @@ func TestGetRandomGame(t *testing.T) {
result := games[rand.Intn(len(games))] result := games[rand.Intn(len(games))]
if result.SoundtrackName == "" { if result.GameName == "" {
t.Error("random game selection returned empty game") t.Error("random game selection returned empty game")
} }
found := false found := false
for _, g := range games { for _, g := range games {
if g.SoundtrackName == result.SoundtrackName { if g.GameName == result.GameName {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Errorf("random game selection returned game not in list: %v", result.SoundtrackName) t.Errorf("random game selection returned game not in list: %v", result.GameName)
} }
} }
func TestFindGameByID(t *testing.T) { func TestFindGameByID(t *testing.T) {
games := []repository.Soundtrack{ games := []repository.Game{
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10}, {ID: 1, GameName: "Game1", TimesPlayed: 10},
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20}, {ID: 2, GameName: "Game2", TimesPlayed: 20},
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30}, {ID: 3, GameName: "Game3", TimesPlayed: 30},
} }
tests := []struct { tests := []struct {
name string name string
games []repository.Soundtrack games []repository.Game
gameID int32 gameID int32
expected repository.Soundtrack expected repository.Game
}{ }{
{ {
name: "existing game", name: "existing game",
games: games, games: games,
gameID: 2, gameID: 2,
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20}, expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
}, },
{ {
name: "non-existing game", name: "non-existing game",
games: games, games: games,
gameID: 99, gameID: 99,
expected: repository.Soundtrack{}, expected: repository.Game{},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var result repository.Soundtrack var result repository.Game
for _, game := range tt.games { for _, game := range tt.games {
if game.ID == tt.gameID { if game.ID == tt.gameID {
result = game result = game
break break
} }
} }
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName { if result.ID != tt.expected.ID || result.GameName != tt.expected.GameName {
t.Errorf("findGameByID() = %v, want %v", result, tt.expected) t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
} }
}) })
} }
} }
func TestExtractSoundtrackNames(t *testing.T) { func TestExtractGameNames(t *testing.T) {
games := []repository.Soundtrack{ games := []repository.Game{
{SoundtrackName: "Game1", TimesPlayed: 10}, {GameName: "Game1", TimesPlayed: 10},
{SoundtrackName: "Game2", TimesPlayed: 20}, {GameName: "Game2", TimesPlayed: 20},
{SoundtrackName: "Game3", TimesPlayed: 30}, {GameName: "Game3", TimesPlayed: 30},
} }
var result []string var result []string
for _, game := range games { for _, game := range games {
result = append(result, game.SoundtrackName) result = append(result, game.GameName)
} }
expected := []string{"Game1", "Game2", "Game3"} expected := []string{"Game1", "Game2", "Game3"}
if len(result) != len(expected) { if len(result) != len(expected) {
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected)) t.Errorf("extractGameNames() length = %d, want %d", len(result), len(expected))
return return
} }
for i, v := range result { for i, v := range result {
if v != expected[i] { if v != expected[i] {
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i]) t.Errorf("extractGameNames()[%d] = %v, want %v", i, v, expected[i])
} }
} }
} }
func TestShuffleSoundtrackNames(t *testing.T) { func TestShuffleGameNames(t *testing.T) {
games := []string{"Game1", "Game2", "Game3"} games := []string{"Game1", "Game2", "Game3"}
// Test that shuffle doesn't lose any elements // Test that shuffle doesn't lose any elements
@@ -181,7 +181,7 @@ func TestShuffleSoundtrackNames(t *testing.T) {
} }
if len(games) != len(original) { if len(games) != len(original) {
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(games)) t.Errorf("shuffleGameNames() changed length from %d to %d", len(original), len(games))
return return
} }
@@ -195,7 +195,9 @@ func TestShuffleSoundtrackNames(t *testing.T) {
} }
} }
if !found { if !found {
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig) t.Errorf("shuffleGameNames() lost element: %v", orig)
} }
} }
} }
+37 -37
View File
@@ -11,17 +11,17 @@ import (
// GameWithSongs represents a game with its songs for statistics // GameWithSongs represents a game with its songs for statistics
type GameWithSongs struct { type GameWithSongs struct {
SoundtrackID int32 `json:"game_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"game_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"game_played"` GamePlayed int32 `json:"game_played"`
SoundtrackLastPlayed *time.Time `json:"game_last_played,omitempty"` GameLastPlayed *time.Time `json:"game_last_played,omitempty"`
Songs []SongInfoForStats `json:"songs"` Songs []SongInfoForStats `json:"songs"`
} }
// SongInfoForStats represents a song with game info for statistics // SongInfoForStats represents a song with game info for statistics
type SongInfoForStats struct { type SongInfoForStats struct {
SoundtrackID int32 `json:"game_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"game_name"` GameName string `json:"game_name"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
Path string `json:"path"` Path string `json:"path"`
TimesPlayed int32 `json:"times_played"` TimesPlayed int32 `json:"times_played"`
@@ -72,10 +72,10 @@ func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWith
} }
} }
result = append(result, GameWithSongs{ result = append(result, GameWithSongs{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SoundtrackPlayed: row.SoundtrackPlayed, GamePlayed: row.GamePlayed,
SoundtrackLastPlayed: row.SoundtrackLastPlayed, GameLastPlayed: row.GameLastPlayed,
Songs: songs, Songs: songs,
}) })
} }
@@ -101,10 +101,10 @@ func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWit
} }
} }
result = append(result, GameWithSongs{ result = append(result, GameWithSongs{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SoundtrackPlayed: row.SoundtrackPlayed, GamePlayed: row.GamePlayed,
SoundtrackLastPlayed: row.SoundtrackLastPlayed, GameLastPlayed: row.GameLastPlayed,
Songs: songs, Songs: songs,
}) })
} }
@@ -124,8 +124,8 @@ func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoF
var result []SongInfoForStats var result []SongInfoForStats
for _, row := range rows { for _, row := range rows {
result = append(result, SongInfoForStats{ result = append(result, SongInfoForStats{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SongName: row.SongName, SongName: row.SongName,
Path: row.Path, Path: row.Path,
TimesPlayed: row.TimesPlayed, TimesPlayed: row.TimesPlayed,
@@ -148,8 +148,8 @@ func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfo
var result []SongInfoForStats var result []SongInfoForStats
for _, row := range rows { for _, row := range rows {
result = append(result, SongInfoForStats{ result = append(result, SongInfoForStats{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SongName: row.SongName, SongName: row.SongName,
Path: row.Path, Path: row.Path,
TimesPlayed: row.TimesPlayed, TimesPlayed: row.TimesPlayed,
@@ -178,10 +178,10 @@ func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
} }
} }
result = append(result, GameWithSongs{ result = append(result, GameWithSongs{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SoundtrackPlayed: row.SoundtrackPlayed, GamePlayed: row.GamePlayed,
SoundtrackLastPlayed: nil, GameLastPlayed: nil,
Songs: songs, Songs: songs,
}) })
} }
@@ -207,10 +207,10 @@ func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, er
} }
} }
result = append(result, GameWithSongs{ result = append(result, GameWithSongs{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SoundtrackPlayed: row.SoundtrackPlayed, GamePlayed: row.GamePlayed,
SoundtrackLastPlayed: row.SoundtrackLastPlayed, GameLastPlayed: row.GameLastPlayed,
Songs: songs, Songs: songs,
}) })
} }
@@ -236,10 +236,10 @@ func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs,
} }
} }
result = append(result, GameWithSongs{ result = append(result, GameWithSongs{
SoundtrackID: row.SoundtrackID, GameID: row.GameID,
SoundtrackName: row.SoundtrackName, GameName: row.GameName,
SoundtrackPlayed: row.SoundtrackPlayed, GamePlayed: row.GamePlayed,
SoundtrackLastPlayed: row.SoundtrackLastPlayed, GameLastPlayed: row.GameLastPlayed,
Songs: songs, Songs: songs,
}) })
} }
@@ -257,13 +257,13 @@ func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
} }
return &StatisticsSummary{ return &StatisticsSummary{
TotalGames: int64(row.TotalSoundtracks), TotalGames: int64(row.TotalGames),
PlayedGames: int64(row.PlayedSoundtracks), PlayedGames: int64(row.PlayedGames),
NeverPlayedGames: int64(row.NeverPlayedSoundtracks), NeverPlayedGames: int64(row.NeverPlayedGames),
TotalGamePlays: int64(row.TotalSoundtrackPlays), TotalGamePlays: int64(row.TotalGamePlays),
AvgGamePlays: float64(row.AvgSoundtrackPlays), AvgGamePlays: float64(row.AvgGamePlays),
MaxGamePlays: int64(row.MaxSoundtrackPlays), MaxGamePlays: int64(row.MaxGamePlays),
MinGamePlays: int64(row.MinSoundtrackPlays), MinGamePlays: int64(row.MinGamePlays),
}, nil }, nil
} }
+59 -64
View File
@@ -30,22 +30,16 @@ var start time.Time
var totalTime time.Duration var totalTime time.Duration
var timeSpent time.Duration var timeSpent time.Duration
var allGames []repository.Soundtrack var allGames []repository.Game
var gamesBeforeSync []repository.Soundtrack var gamesBeforeSync []repository.Game
var gamesAfterSync []repository.Soundtrack var gamesAfterSync []repository.Game
var gamesAdded []string var gamesAdded []string
var gamesReAdded []string var gamesReAdded []string
var gamesChangedTitle map[string]string var gamesChangedTitle map[string]string
var gamesChangedContent []string var gamesChangedContent []string
var gamesRemoved []string var gamesRemoved []string
var catchedErrors []string var catchedErrors []string
var brokenSongs []string
type brokenSong struct {
SoundtrackID int32
Path string
}
var brokenSongs []brokenSong
var pool *ants.Pool var pool *ants.Pool
var poolSong *ants.Pool var poolSong *ants.Pool
@@ -86,7 +80,7 @@ func (gs GameStatus) String() string {
func ResetDB() { func ResetDB() {
repo.ClearSongs(BackendCtx()) repo.ClearSongs(BackendCtx())
repo.ClearSoundtracks(BackendCtx()) repo.ClearGames(BackendCtx())
} }
func SyncProgress() ProgressResponse { func SyncProgress() ProgressResponse {
@@ -130,13 +124,13 @@ func SyncResult() SyncResponse {
for _, beforeGame := range gamesBeforeSync { for _, beforeGame := range gamesBeforeSync {
var found = false var found = false
for _, afterGame := range gamesAfterSync { for _, afterGame := range gamesAfterSync {
if beforeGame.SoundtrackName == afterGame.SoundtrackName { if beforeGame.GameName == afterGame.GameName {
found = true found = true
break break
} }
} }
if !found { if !found {
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.SoundtrackName) gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.GameName)
} }
} }
@@ -175,17 +169,19 @@ func SyncResult() SyncResponse {
} }
} }
func SyncSoundtracksNewFull() { func SyncGamesNewFull() {
syncGamesNew(true) syncGamesNew(true)
Reset() Reset()
} }
func SyncSoundtracksNewOnlyChanges() { func SyncGamesNewOnlyChanges() {
syncGamesNew(false) syncGamesNew(false)
Reset() Reset()
} }
func syncGamesNew(full bool) { func syncGamesNew(full bool) {
Syncing = true
musicPath := os.Getenv("MUSIC_PATH") musicPath := os.Getenv("MUSIC_PATH")
fmt.Printf("dir: %s\n", musicPath) fmt.Printf("dir: %s\n", musicPath)
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath)) logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
@@ -197,7 +193,7 @@ func syncGamesNew(full bool) {
initRepo() initRepo()
start = time.Now() start = time.Now()
foldersToSkip := []string{".sync", "characters", "dist", "old"} foldersToSkip := []string{".sync", "dist", "old", "characters"}
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip)) logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
var err error var err error
@@ -209,19 +205,20 @@ func syncGamesNew(full bool) {
catchedErrors = nil catchedErrors = nil
brokenSongs = nil brokenSongs = nil
gamesBeforeSync, err = repo.FindAllSoundtracks(BackendCtx()) gamesBeforeSync, err = repo.FindAllGames(BackendCtx())
handleError("FindAllSoundtracks Before", err, "") handleError("FindAllGames Before", err, "")
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync))) logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
allGames, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx()) allGames, err = repo.GetAllGamesIncludingDeleted(BackendCtx())
handleError("GetAllSoundtracksIncludingDeleted", err, "") handleError("GetAllGamesIncludingDeleted", err, "")
err = repo.SetSoundtrackDeletionDate(BackendCtx()) err = repo.SetGameDeletionDate(BackendCtx())
handleError("SetSoundtrackDeletionDate", err, "") handleError("SetGameDeletionDate", err, "")
directories, err := os.ReadDir(musicPath) directories, err := os.ReadDir(musicPath)
if err != nil { if err != nil {
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error())) logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
} }
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true)) pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true)) poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
defer pool.Release() defer pool.Release()
@@ -239,8 +236,8 @@ func syncGamesNew(full bool) {
syncWg.Wait() syncWg.Wait()
checkBrokenSongsNew() checkBrokenSongsNew()
gamesAfterSync, err = repo.FindAllSoundtracks(BackendCtx()) gamesAfterSync, err = repo.FindAllGames(BackendCtx())
handleError("FindAllSoundtracks After", err, "") handleError("FindAllGames After", err, "")
finished := time.Now() finished := time.Now()
totalTime = finished.Sub(start) totalTime = finished.Sub(start)
@@ -265,10 +262,8 @@ func checkBrokenSongsNew() {
}) })
} }
brokenWg.Wait() brokenWg.Wait()
for _, bs := range brokenSongs { err = repo.RemoveBrokenSongs(BackendCtx(), brokenSongs)
err = repo.RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: bs.SoundtrackID, Path: bs.Path}) handleError("RemoveBrokenSongs", err, "")
handleError("RemoveBrokenSong", err, "")
}
} }
func checkBrokenSongNew(song repository.Song) { func checkBrokenSongNew(song repository.Song) {
@@ -276,7 +271,7 @@ func checkBrokenSongNew(song repository.Song) {
openFile, err := os.Open(song.Path) openFile, err := os.Open(song.Path)
if err != nil { if err != nil {
//File not found //File not found
brokenSongs = append(brokenSongs, brokenSong{SoundtrackID: song.SoundtrackID, Path: song.Path}) brokenSongs = append(brokenSongs, song.Path)
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path)) logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
} else { } else {
err = openFile.Close() err = openFile.Close()
@@ -293,33 +288,33 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
dirHash := getHashForDir(gameDir) dirHash := getHashForDir(gameDir)
var status GameStatus = NewGame var status GameStatus = NewGame
var oldGame repository.Soundtrack var oldGame repository.Game
var id int32 = -1 var id int32 = -1
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync)) //fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
for _, currentGame := range allGames { for _, currentGame := range allGames {
oldGame = currentGame oldGame = currentGame
//fmt.Printf("%s | %s\n", oldGame.SoundtrackName, oldGame.Hash) //fmt.Printf("%s | %s\n", oldGame.GameName, oldGame.Hash)
if oldGame.SoundtrackName == file.Name() && oldGame.Hash == dirHash { if oldGame.GameName == file.Name() && oldGame.Hash == dirHash {
status = NotChanged status = NotChanged
id = oldGame.ID id = oldGame.ID
//fmt.Printf("Game not changed\n") //fmt.Printf("Game not changed\n")
break break
} else if oldGame.SoundtrackName == file.Name() && oldGame.Hash != dirHash { } else if oldGame.GameName == file.Name() && oldGame.Hash != dirHash {
status = GameChanged status = GameChanged
id = oldGame.ID id = oldGame.ID
//fmt.Printf("Game changed\n") //fmt.Printf("Game changed\n")
break break
} else if oldGame.SoundtrackName != file.Name() && oldGame.Hash == dirHash { } else if oldGame.GameName != file.Name() && oldGame.Hash == dirHash {
status = TitleChanged status = TitleChanged
id = oldGame.ID id = oldGame.ID
//fmt.Printf("SoundtrackName changed\n") //fmt.Printf("GameName changed\n")
break break
} }
} }
if full && status != NewGame { if full {
status = TitleChanged status = TitleChanged
} }
entries, err := os.ReadDir(gameDir) entries, err := os.ReadDir(gameDir)
@@ -340,8 +335,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
break break
} }
} }
err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash}) err = repo.InsertGameWithExistingId(BackendCtx(), repository.InsertGameWithExistingIdParams{ID: id, GameName: file.Name(), Path: gameDir, Hash: dirHash})
handleError("InsertSoundtrackWithExistingId", err, "") handleError("InsertGameWithExistingId", err, "")
if err != nil { if err != nil {
logging.GetLogger().Debug("Game already exists, removing old ID file", logging.GetLogger().Debug("Game already exists, removing old ID file",
zap.Int32("id", id), zap.Int32("id", id),
@@ -374,24 +369,24 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
zap.String("game", file.Name()), zap.String("game", file.Name()),
zap.String("hash", dirHash), zap.String("hash", dirHash),
zap.String("status", status.String())) zap.String("status", status.String()))
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id}) err = repo.UpdateGameHash(BackendCtx(), repository.UpdateGameHashParams{Hash: dirHash, ID: id})
handleError("UpdateSoundtrackHash", err, "") handleError("UpdateGameHash", err, "")
gamesChangedContent = append(gamesChangedContent, file.Name()) gamesChangedContent = append(gamesChangedContent, file.Name())
newCheckSongs(entries, gameDir, id) newCheckSongs(entries, gameDir, id)
case TitleChanged: case TitleChanged:
logging.GetLogger().Debug("Game title changed", logging.GetLogger().Debug("Game title changed",
zap.Int32("id", id), zap.Int32("id", id),
zap.String("oldName", oldGame.SoundtrackName), zap.String("oldName", oldGame.GameName),
zap.String("newName", file.Name()), zap.String("newName", file.Name()),
zap.String("hash", dirHash), zap.String("hash", dirHash),
zap.String("status", status.String())) zap.String("status", status.String()))
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: gameDir, ID: id}) err = repo.UpdateGameName(BackendCtx(), repository.UpdateGameNameParams{Name: file.Name(), Path: gameDir, ID: id})
handleError("UpdateSoundtrackName", err, "") handleError("UpdateGameName", err, "")
newCheckSongs(entries, gameDir, id) newCheckSongs(entries, gameDir, id)
if gamesChangedTitle == nil { if gamesChangedTitle == nil {
gamesChangedTitle = make(map[string]string) gamesChangedTitle = make(map[string]string)
} }
gamesChangedTitle[oldGame.SoundtrackName] = file.Name() gamesChangedTitle[oldGame.GameName] = file.Name()
case NotChanged: case NotChanged:
var found bool = false var found bool = false
for _, beforeGame := range gamesBeforeSync { for _, beforeGame := range gamesBeforeSync {
@@ -420,8 +415,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
zap.String("game", file.Name()), zap.String("game", file.Name()),
zap.String("hash", dirHash), zap.String("hash", dirHash),
zap.String("status", status.String())) zap.String("status", status.String()))
err = repo.RemoveSoundtrackDeletionDate(BackendCtx(), id) err = repo.RemoveDeletionDate(BackendCtx(), id)
handleError("RemoveSoundtrackDeletionDate", err, "") handleError("RemoveDeletionDate", err, "")
} }
foldersSynced++ foldersSynced++
logging.GetLogger().Debug("Sync progress", logging.GetLogger().Debug("Sync progress",
@@ -432,14 +427,14 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
func insertGameNew(name string, path string, hash string) int32 { func insertGameNew(name string, path string, hash string) int32 {
var duplicateError = errors.New("ERROR: duplicate key value violates unique") var duplicateError = errors.New("ERROR: duplicate key value violates unique")
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash}) id, err := repo.InsertGame(BackendCtx(), repository.InsertGameParams{GameName: name, Path: path, Hash: hash})
handleError("InsertSoundtrack", err, "") handleError("InsertGame", err, "")
if err != nil { if err != nil {
logging.GetLogger().Warn("ID collision detected, resetting sequence") logging.GetLogger().Warn("ID collision detected, resetting sequence")
if strings.HasPrefix(err.Error(), duplicateError.Error()) { if strings.HasPrefix(err.Error(), duplicateError.Error()) {
logging.GetLogger().Debug("Resetting game ID sequence") logging.GetLogger().Debug("Resetting game ID sequence")
_, err = repo.ResetSoundtrackIdSeq(BackendCtx()) _, err = repo.ResetGameIdSeq(BackendCtx())
handleError("ResetSoundtrackIdSeq", err, "") handleError("ResetGameIdSeq", err, "")
id = insertGameNew(name, path, hash) id = insertGameNew(name, path, hash)
} }
} }
@@ -483,7 +478,7 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
songName, _ := strings.CutSuffix(fileName, ".mp3") songName, _ := strings.CutSuffix(fileName, ".mp3")
song, err := repo.GetSongWithHash(BackendCtx(), songHash) song, err := repo.GetSongWithHash(BackendCtx(), songHash)
handleError("GetSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
if err == nil { if err == nil {
if song.SongName == songName && song.Path == path { if song.SongName == songName && song.Path == path {
return false return false
@@ -496,31 +491,31 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
zap.String("song_hash", songHash)) zap.String("song_hash", songHash))
count, err := repo.CheckSongWithHash(BackendCtx(), songHash) count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
handleError("CheckSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash)) handleError("CheckSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if err != nil { if err != nil {
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path}) count2, err := repo.CheckSong(BackendCtx(), path)
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash)) handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if count2 > 0 { if count2 > 0 {
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path}) err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
count, err = repo.CheckSongWithHash(BackendCtx(), songHash) count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
handleError("CheckSongWithHash 2", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("CheckSongWithHash 2", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
} }
} }
//count, _ := repo.CheckSong(ctx, path) //count, _ := repo.CheckSong(ctx, path)
if count > 0 { if count > 0 {
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash}) err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
handleError("UpdateSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("UpdateSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
} else { } else {
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path}) count2, err := repo.CheckSong(BackendCtx(), path)
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
if count2 > 0 { if count2 > 0 {
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path}) err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
} else { } else {
err = repo.AddSong(BackendCtx(), repository.AddSongParams{SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash}) err = repo.AddSong(BackendCtx(), repository.AddSongParams{GameID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
handleError("AddSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) handleError("AddSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
} }
} }
-111
View File
@@ -1,111 +0,0 @@
package backend
type VersionData struct {
Version string `json:"version" example:"1.0.0"`
Changelog []string `json:"changelog" example:"[\"Initial release\",\"Bug fixes\"]"`
}
var data = []VersionData{
{
Version: "5.0.0-Beta",
Changelog: []string{
"#16 - Upgrade Echo framework from v4 to v5",
"#17 - Add Zap structured logging framework",
"#18 - Add OpenAPI/Swagger documentation",
"#19 - Replace Tailwind CSS with pure CSS",
"#20 - Change domain from sanplex.tech to sanplex.xyz",
"#21 - Refactor handlers into domain-specific files",
"#22 - Change VersionData Changelog from string to string array",
"#23 - Update all dependencies to latest versions",
},
},
{
Version: "4.5.0",
Changelog: []string{
"#1 - Created request to check newest version of the app",
"#2 - Added request to download the newest version of the app",
"#3 - Added request to check progress during sync",
"#4 - Now blocking all request while sync is in progress",
"#5 - Implemented ants for thread pooling",
"#6 - Changed the sync request to now only start the sync",
},
},
{
Version: "4.0.0",
Changelog: []string{
"Changed framework from gin to Echo",
"Reorganized the code",
"Implemented sqlc",
"Added support to send character images from the server",
"Added function to create a new database of no one exists",
},
},
{
Version: "3.2",
Changelog: []string{"Upgraded Go version and the version of all dependencies. Fixed som more bugs."},
},
{
Version: "3.1",
Changelog: []string{"Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend."},
},
{
Version: "3.0",
Changelog: []string{"Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application."},
},
{
Version: "2.3.0",
Changelog: []string{"Images should not be included in the database, removes songs where the path doesn't work."},
},
{
Version: "2.2.0",
Changelog: []string{"Changed the structure of the whole application, should be no changes to functionality."},
},
{
Version: "2.1.4",
Changelog: []string{"Game list should now be sorted, a new endpoint with the game list in random order have been added."},
},
{
Version: "2.1.3",
Changelog: []string{"Added a check to see if song exists before returning it, if not a new song will be picked up."},
},
{
Version: "2.1.2",
Changelog: []string{"Added test server to swagger file."},
},
{
Version: "2.1.1",
Changelog: []string{"Fixed bug where wrong song was showed as currently played."},
},
{
Version: "2.1.0",
Changelog: []string{
"Added /addQue to add the last received song to the songQue.",
"Changed /rand and /rand/low to not add song to the que.",
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
},
},
{
Version: "2.0.3",
Changelog: []string{"Another small change that should fix the caching problem."},
},
{
Version: "2.0.2",
Changelog: []string{"Hopefully fixed the caching problem with random."},
},
{
Version: "2.0.1",
Changelog: []string{"Fixed CORS"},
},
{
Version: "2.0.0",
Changelog: []string{"Rebuilt the application in Go."},
},
}
func GetLatestVersion() VersionData {
return data[0]
}
func GetVersionHistory() []VersionData {
return data
}
-21
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"time"
"music-server/internal/logging" "music-server/internal/logging"
@@ -60,26 +59,6 @@ func (db *Database) Close() {
} }
} }
// Health checks the health of the database connection by pinging the database.
// It returns a map with keys indicating various health statistics.
func (db *Database) Health() map[string]string {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
stats := make(map[string]string)
// Ping the database
err := db.Pool.Ping(ctx)
if err != nil {
stats["status"] = "down"
stats["error"] = err.Error()
return stats
}
stats["status"] = "up"
return stats
}
// RunMigrations runs all pending database migrations to the latest version. // RunMigrations runs all pending database migrations to the latest version.
// Uses the existing pool to extract connection details. // Uses the existing pool to extract connection details.
func (db *Database) RunMigrations() error { func (db *Database) RunMigrations() error {
+16 -3
View File
@@ -20,9 +20,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// TODO: DEPRECATED - Remove these global variables once all code is migrated to use Database struct // TODO: Remove these global variables once test_helpers.go is fully migrated to use Database struct
// Use database.go's Database struct instead. These globals remain for backward compatibility
// with legacy code paths. New code should use the Database struct from database.go.
var Dbpool *pgxpool.Pool var Dbpool *pgxpool.Pool
var Ctx = context.Background() var Ctx = context.Background()
@@ -56,6 +54,21 @@ func CloseDb() {
Dbpool.Close() Dbpool.Close()
} }
func Testf() {
rows, dbErr := Dbpool.Query(Ctx, "select game_name from game")
if dbErr != nil {
logging.GetLogger().Fatal("Query failed", zap.String("error", dbErr.Error()))
}
for rows.Next() {
var gameName string
dbErr = rows.Scan(&gameName)
if dbErr != nil {
logging.GetLogger().Error("Row scan failed", zap.String("error", dbErr.Error()))
}
logging.GetLogger().Debug("Game found", zap.String("name", gameName))
}
}
func ResetGameIdSeq() { func ResetGameIdSeq() {
_, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);") _, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);")
if err != nil { if err != nil {
-251
View File
@@ -1,251 +0,0 @@
package db
import (
"database/sql"
"fmt"
"os"
"testing"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
"github.com/stretchr/testify/require"
)
// TestMigrationsStepByStep tests applying migrations incrementally
// Then adding data manually, then completing migrations
func TestMigrationsStepByStep(t *testing.T) {
host := os.Getenv("DB_HOST")
port := os.Getenv("DB_PORT")
user := os.Getenv("DB_USERNAME")
password := os.Getenv("DB_PASSWORD")
// Use a unique database name for this test
dbname := "music_server_migration_test"
if host == "" || port == "" || user == "" || password == "" {
t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)")
}
// Clean up: drop database if it exists
cleanupDB(t, host, port, user, password, dbname)
defer cleanupDB(t, host, port, user, password, dbname)
// Create the database
createTestDB(t, host, port, user, password, dbname)
// Step 1: Apply first 4 migrations (before soundtrack rename)
// This creates: game, song, vgmq, song_list tables
// And sessions table with indexes
t.Run("ApplyFirst4Migrations", func(t *testing.T) {
applyMigrations(t, host, port, user, password, dbname, 4)
})
// Step 2: Add data manually to game and song tables
t.Run("AddManualData", func(t *testing.T) {
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", connStr)
require.NoError(t, err)
defer db.Close()
// Insert 5 games manually
for i := 1; i <= 5; i++ {
gameName := fmt.Sprintf("Manual Game %d", i)
path := fmt.Sprintf("/manual/path/game%d", i)
hash := fmt.Sprintf("hash-%d", i)
_, err := db.Exec(`INSERT INTO game (game_name, path, hash, added)
VALUES ($1, $2, $3, NOW())`,
gameName, path, hash)
require.NoError(t, err, "Failed to insert game %d", i)
}
// Insert songs for each game
songs := []struct {
gameID int
name string
path string
}{
{1, "Song A", "/path/a.mp3"},
{1, "Song B", "/path/b.mp3"},
{2, "Song C", "/path/c.mp3"},
{2, "Song D", "/path/d.mp3"},
{3, "Song E", "/path/e.mp3"},
{4, "Song F", "/path/f.mp3"},
{4, "Song G", "/path/g.mp3"},
{4, "Song H", "/path/h.mp3"},
{5, "Song I", "/path/i.mp3"},
}
for _, s := range songs {
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
VALUES ($1, $2, $3, $4)`,
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
require.NoError(t, err, "Failed to insert song %s", s.name)
}
// Verify data was inserted
var gameCount int
err = db.QueryRow("SELECT COUNT(*) FROM game").Scan(&gameCount)
require.NoError(t, err)
require.Equal(t, 5, gameCount, "Expected 5 games")
var songCount int
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
require.NoError(t, err)
require.Equal(t, 9, songCount, "Expected 9 songs")
t.Log("✓ Manually inserted 5 games with 9 songs")
})
// Step 3: Apply migration 5 (rename game→soundtrack)
t.Run("ApplyMigration5", func(t *testing.T) {
// Apply the remaining migrations (just migration 5)
applyMigrations(t, host, port, user, password, dbname, 1)
// Verify tables were renamed
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", connStr)
require.NoError(t, err)
defer db.Close()
// Check that soundtrack table exists
var soundtrackCount int
err = db.QueryRow("SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount)
require.NoError(t, err)
require.Equal(t, 5, soundtrackCount, "Expected 5 soundtracks after migration")
// Check that game table no longer exists
_, err = db.Exec("SELECT 1 FROM game LIMIT 1")
require.Error(t, err, "game table should not exist after migration")
// Check that song table has soundtrack_id column
var songCount int
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
require.NoError(t, err)
require.Equal(t, 9, songCount, "Expected 9 songs after migration")
// Verify data integrity: soundtrack_name values
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
require.NoError(t, err)
defer rows.Close()
expectedNames := []string{"Manual Game 1", "Manual Game 2", "Manual Game 3", "Manual Game 4", "Manual Game 5"}
actualNames := make([]string, 0)
for rows.Next() {
var name string
err := rows.Scan(&name)
require.NoError(t, err)
actualNames = append(actualNames, name)
}
require.Equal(t, expectedNames, actualNames, "Soundtrack names should match original game names")
t.Log("✓ Migration 5 applied successfully, data preserved")
})
}
// cleanupDB drops the test database
func cleanupDB(t *testing.T, host, port, user, password, dbname string) {
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
host, port, user, password)
db, err := sql.Open("postgres", connStr)
if err != nil {
t.Logf("Warning: could not connect to cleanup DB: %v", err)
return
}
defer db.Close()
// Check if database exists before dropping
var exists int
err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists)
if err != nil && err != sql.ErrNoRows {
t.Logf("Warning: could not check if DB exists: %v", err)
return
}
if exists == 1 {
_, err = db.Exec("DROP DATABASE " + dbname + " WITH (FORCE)")
if err != nil {
t.Logf("Warning: could not drop DB: %v", err)
}
}
}
// createTestDB creates a fresh test database
func createTestDB(t *testing.T, host, port, user, password, dbname string) {
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
host, port, user, password)
db, err := sql.Open("postgres", connStr)
require.NoError(t, err)
defer db.Close()
// Drop if exists
cleanupDB(t, host, port, user, password, dbname)
// Create database
_, err = db.Exec("CREATE DATABASE " + dbname)
require.NoError(t, err)
// Enable UUID extension if needed
connStrDB := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db2, err := sql.Open("postgres", connStrDB)
require.NoError(t, err)
defer db2.Close()
_, err = db2.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
if err != nil {
t.Logf("Note: uuid-ossp extension may not be available: %v", err)
}
}
// applyMigrations applies n migrations to the database using Go migrate library
func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) {
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
user, password, host, port, dbname)
db, err := sql.Open("postgres", migrationURL)
require.NoError(t, err)
defer db.Close()
driver, err := postgres.WithInstance(db, &postgres.Config{})
require.NoError(t, err)
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"postgres", driver)
require.NoError(t, err)
// Get current version
version, _, err := m.Version()
if err != nil && err != migrate.ErrNilVersion {
require.NoError(t, err)
}
if err == migrate.ErrNilVersion {
version = 0
}
t.Logf("Current migration version: %d", version)
// Apply exactly 'steps' migrations
if steps > 0 {
err = m.Steps(steps)
if err != nil && err != migrate.ErrNoChange {
require.NoError(t, err)
}
} else if steps < 0 {
err = m.Steps(steps)
require.NoError(t, err)
}
// Get new version
newVersion, _, err := m.Version()
if err != nil && err != migrate.ErrNilVersion {
require.NoError(t, err)
}
if err == migrate.ErrNilVersion {
newVersion = 0
}
t.Logf("Migration version after applying %d steps: %d", steps, newVersion)
}
@@ -1,33 +0,0 @@
-- Revert: Rename soundtrack table back to game
ALTER TABLE soundtrack RENAME TO game;
-- Revert primary key sequence
ALTER SEQUENCE soundtrack_id_seq RENAME TO game_id_seq;
-- Revert columns in game table
ALTER TABLE game RENAME COLUMN soundtrack_name TO game_name;
-- Revert song table: rename soundtrack_id back to game_id
ALTER TABLE song RENAME COLUMN soundtrack_id TO game_id;
-- Revert song primary key
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
ALTER TABLE song ADD PRIMARY KEY (game_id, path);
ALTER TABLE song RENAME CONSTRAINT song_pkey_soundtrack TO song_pkey;
-- Revert song_list table references
ALTER TABLE song_list RENAME COLUMN soundtrack_name TO game_name;
-- Revert foreign key constraint
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
ALTER TABLE song ADD CONSTRAINT song_game_id_fkey
FOREIGN KEY (game_id) REFERENCES game(id);
-- Revert indexes
ALTER INDEX IF EXISTS idx_soundtrack_deleted RENAME TO idx_game_deleted;
ALTER INDEX IF EXISTS idx_soundtrack_hash RENAME TO idx_game_hash;
ALTER INDEX IF EXISTS idx_soundtrack_path RENAME TO idx_game_path;
ALTER INDEX IF EXISTS idx_soundtrack_name RENAME TO idx_game_name;
ALTER INDEX IF EXISTS idx_song_soundtrack_id RENAME TO idx_song_game_id;
ALTER INDEX IF EXISTS idx_song_soundtrack_id_song_name RENAME TO idx_song_game_id_song_name;
ALTER INDEX IF EXISTS song_list_soundtrack_name_idx RENAME TO song_list_game_name_idx;
@@ -1,32 +0,0 @@
-- Rename game table to soundtrack
ALTER TABLE game RENAME TO soundtrack;
-- Rename primary key sequence
ALTER SEQUENCE game_id_seq RENAME TO soundtrack_id_seq;
-- Rename columns in soundtrack table
ALTER TABLE soundtrack RENAME COLUMN game_name TO soundtrack_name;
-- Update song table: rename game_id to soundtrack_id
ALTER TABLE song RENAME COLUMN game_id TO soundtrack_id;
-- Update song primary key
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
ALTER TABLE song ADD PRIMARY KEY (soundtrack_id, path);
-- Update song_list table references
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
-- Rename foreign key constraint
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_game_id_fkey;
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
-- Rename indexes
ALTER INDEX IF EXISTS idx_game_deleted RENAME TO idx_soundtrack_deleted;
ALTER INDEX IF EXISTS idx_game_hash RENAME TO idx_soundtrack_hash;
ALTER INDEX IF EXISTS idx_game_path RENAME TO idx_soundtrack_path;
ALTER INDEX IF EXISTS idx_game_name RENAME TO idx_soundtrack_name;
ALTER INDEX IF EXISTS idx_song_game_id RENAME TO idx_song_soundtrack_id;
ALTER INDEX IF EXISTS idx_song_game_id_song_name RENAME TO idx_song_soundtrack_id_song_name;
ALTER INDEX IF EXISTS song_list_game_name_idx RENAME TO song_list_soundtrack_name_idx;
@@ -1,24 +0,0 @@
-- Rollback: Remove id column and restore composite PK
-- Step 1: Drop indexes created in up migration
DROP INDEX IF EXISTS idx_song_soundtrack_id;
DROP INDEX IF EXISTS idx_song_path;
-- Step 2: Drop foreign key constraint
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
-- Step 3: Drop new primary key
ALTER TABLE song DROP CONSTRAINT song_pkey;
-- Step 4: Drop unique constraint on id
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_id_unique;
-- Step 5: Restore composite primary key
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (soundtrack_id, path);
-- Step 6: Drop the id column
ALTER TABLE song DROP COLUMN id;
-- Step 7: Recreate original foreign key (soundtrack_id references soundtrack.id)
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
@@ -1,36 +0,0 @@
-- Migration: Add id column to song table and change PK from composite to single column
-- This prepares the song table for eventual UUID migration
-- Step 1: Add new id column (nullable initially)
ALTER TABLE song ADD COLUMN id serial4;
-- Step 2: Create unique constraint on id (allows backfilling)
ALTER TABLE song ADD CONSTRAINT song_id_unique UNIQUE (id);
-- Step 3: Backfill existing rows with sequential IDs
-- Use DEFAULT which pulls from the sequence
UPDATE song SET id = DEFAULT WHERE id IS NULL;
-- Step 4: Verify all rows have an id
-- If this returns 0, backfill worked
-- SELECT COUNT(*) FROM song WHERE id IS NULL;
-- Step 5: Drop the composite primary key (soundtrack_id, path)
ALTER TABLE song DROP CONSTRAINT song_pkey;
-- Step 6: Add new primary key on id column
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (id);
-- Step 7: Ensure soundtrack_id remains a foreign key to soundtrack
-- First drop existing FK if it exists (from the rename migration)
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
-- Then recreate it
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
-- Step 8: Create index on soundtrack_id for query performance
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_id ON song(soundtrack_id);
-- Step 9: Create index on path for lookups (previously part of PK)
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
+49
View File
@@ -0,0 +1,49 @@
-- name: ResetGameIdSeq :one
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);
-- name: GetGameNameById :one
SELECT game_name FROM game WHERE id = $1;
-- name: GetGameById :one
SELECT *
FROM game
WHERE id = $1
AND deleted IS NULL;
-- name: SetGameDeletionDate :exec
UPDATE game SET deleted=now() WHERE deleted IS NULL;
-- name: ClearGames :exec
DELETE FROM game;
-- name: UpdateGameName :exec
UPDATE game SET game_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
-- name: UpdateGameHash :exec
UPDATE game SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
-- name: RemoveDeletionDate :exec
UPDATE game SET deleted=NULL WHERE id=$1;
-- name: GetIdByGameName :one
SELECT id FROM game WHERE game_name = $1;
-- name: InsertGame :one
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
-- name: InsertGameWithExistingId :exec
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
-- name: FindAllGames :many
SELECT *
FROM game
WHERE deleted IS NULL
ORDER BY game_name;
-- name: GetAllGamesIncludingDeleted :many
SELECT *
FROM game
ORDER BY game_name;
-- name: AddGamePlayed :exec
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1;
+10 -13
View File
@@ -1,14 +1,14 @@
-- name: ClearSongs :exec -- name: ClearSongs :exec
DELETE FROM song; DELETE FROM song;
-- name: ClearSongsBySoundtrackId :exec -- name: ClearSongsByGameId :exec
DELETE FROM song WHERE soundtrack_id = $1; DELETE FROM song WHERE game_id = $1;
-- name: AddSong :exec -- name: AddSong :exec
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5); INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
-- name: CheckSong :one -- name: CheckSong :one
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2; SELECT COUNT(*) FROM song WHERE path = $1;
-- name: CheckSongWithHash :one -- name: CheckSongWithHash :one
SELECT COUNT(*) FROM song WHERE hash = $1; SELECT COUNT(*) FROM song WHERE hash = $1;
@@ -20,25 +20,22 @@ SELECT * FROM song WHERE hash = $1;
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4; UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
-- name: AddHashToSong :exec -- name: AddHashToSong :exec
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3; UPDATE song SET hash=$1 where path=$2;
-- name: FindSongsFromSoundtrack :many -- name: FindSongsFromGame :many
SELECT * SELECT *
FROM song FROM song
WHERE soundtrack_id = $1; WHERE game_id = $1;
-- name: AddSongPlayed :exec -- name: AddSongPlayed :exec
UPDATE song SET times_played = times_played + 1 UPDATE song SET times_played = times_played + 1
WHERE soundtrack_id = $1 AND song_name = $2; WHERE game_id = $1 AND song_name = $2;
-- name: FetchAllSongs :many -- name: FetchAllSongs :many
SELECT * FROM song; SELECT * FROM song;
-- name: GetSongById :one
SELECT * FROM song WHERE id = $1;
-- name: RemoveBrokenSong :exec -- name: RemoveBrokenSong :exec
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2; DELETE FROM song WHERE path = $1;
-- name: RemoveBrokenSongs :exec -- name: RemoveBrokenSongs :exec
DELETE FROM song WHERE id = ANY($1); DELETE FROM song where path = any (sqlc.slice('paths'));
+1 -1
View File
@@ -1,5 +1,5 @@
-- name: InsertSongInList :exec -- name: InsertSongInList :exec
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name) INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
VALUES ($1, $2, $3, $4, $5); VALUES ($1, $2, $3, $4, $5);
-- name: GetSongList :many -- name: GetSongList :many
-49
View File
@@ -1,49 +0,0 @@
-- name: ResetSoundtrackIdSeq :one
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);
-- name: GetSoundtrackNameById :one
SELECT soundtrack_name FROM soundtrack WHERE id = $1;
-- name: GetSoundtrackById :one
SELECT *
FROM soundtrack
WHERE id = $1
AND deleted IS NULL;
-- name: SetSoundtrackDeletionDate :exec
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL;
-- name: ClearSoundtracks :exec
DELETE FROM soundtrack;
-- name: UpdateSoundtrackName :exec
UPDATE soundtrack SET soundtrack_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
-- name: UpdateSoundtrackHash :exec
UPDATE soundtrack SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
-- name: RemoveSoundtrackDeletionDate :exec
UPDATE soundtrack SET deleted=NULL WHERE id=$1;
-- name: GetIdBySoundtrackName :one
SELECT id FROM soundtrack WHERE soundtrack_name = $1;
-- name: InsertSoundtrack :one
INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
-- name: InsertSoundtrackWithExistingId :exec
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
-- name: FindAllSoundtracks :many
SELECT *
FROM soundtrack
WHERE deleted IS NULL
ORDER BY soundtrack_name;
-- name: GetAllSoundtracksIncludingDeleted :many
SELECT *
FROM soundtrack
ORDER BY soundtrack_name;
-- name: AddSoundtrackPlayed :exec
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1;
+57 -57
View File
@@ -1,10 +1,10 @@
-- Most played soundtracks with their songs -- Most played games with their songs
-- name: GetMostPlayedGamesWithSongs :many -- name: GetMostPlayedGamesWithSongs :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -13,20 +13,20 @@ SELECT
'file_name', s.file_name 'file_name', s.file_name
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played DESC, g.soundtrack_name ORDER BY g.times_played DESC, g.game_name
LIMIT $1; LIMIT $1;
-- Least played soundtracks with their songs -- Least played games with their songs
-- name: GetLeastPlayedGamesWithSongs :many -- name: GetLeastPlayedGamesWithSongs :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -35,39 +35,39 @@ SELECT
'file_name', s.file_name 'file_name', s.file_name
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played ASC, g.soundtrack_name ORDER BY g.times_played ASC, g.game_name
LIMIT $1; LIMIT $1;
-- Most played songs with their soundtrack info -- Most played songs with their game info
-- name: GetMostPlayedSongsWithGame :many -- name: GetMostPlayedSongsWithGame :many
SELECT SELECT
s.soundtrack_id as soundtrack_id, s.game_id as game_id,
g.soundtrack_name, g.game_name,
s.song_name, s.song_name,
s.path, s.path,
s.times_played, s.times_played,
s.file_name s.file_name
FROM song s FROM song s
JOIN soundtrack g ON s.soundtrack_id = g.id JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
ORDER BY s.times_played DESC, s.song_name ORDER BY s.times_played DESC, s.song_name
LIMIT $1; LIMIT $1;
-- Least played songs with their soundtrack info -- Least played songs with their game info
-- name: GetLeastPlayedSongsWithGame :many -- name: GetLeastPlayedSongsWithGame :many
SELECT SELECT
s.soundtrack_id as soundtrack_id, s.game_id as game_id,
g.soundtrack_name, g.game_name,
s.song_name, s.song_name,
s.path, s.path,
s.times_played, s.times_played,
s.file_name s.file_name
FROM song s FROM song s
JOIN soundtrack g ON s.soundtrack_id = g.id JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
ORDER BY s.times_played ASC, s.song_name ORDER BY s.times_played ASC, s.song_name
LIMIT $1; LIMIT $1;
@@ -75,9 +75,9 @@ LIMIT $1;
-- Games that have never been played (times_played = 0) -- Games that have never been played (times_played = 0)
-- name: GetNeverPlayedGames :many -- name: GetNeverPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.added, g.added,
json_agg( json_agg(
json_build_object( json_build_object(
@@ -86,19 +86,19 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.times_played = 0 WHERE g.deleted IS NULL AND g.times_played = 0
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added GROUP BY g.id, g.game_name, g.times_played, g.added
ORDER BY g.soundtrack_name; ORDER BY g.game_name;
-- Last played soundtracks (most recently played) -- Last played games (most recently played)
-- name: GetLastPlayedGames :many -- name: GetLastPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -106,20 +106,20 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played DESC ORDER BY g.last_played DESC
LIMIT $1; LIMIT $1;
-- Oldest played soundtracks (least recently played, but has been played at least once) -- Oldest played games (least recently played, but has been played at least once)
-- name: GetOldestPlayedGames :many -- name: GetOldestPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -127,22 +127,22 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played ASC ORDER BY g.last_played ASC
LIMIT $1; LIMIT $1;
-- Get statistics summary -- Get statistics summary
-- name: GetStatisticsSummary :one -- name: GetStatisticsSummary :one
SELECT SELECT
COUNT(*) as total_soundtracks, COUNT(*) as total_games,
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks, SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_games,
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks, SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_games,
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays, COALESCE(SUM(times_played), 0)::bigint as total_game_plays,
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays, COALESCE(AVG(times_played), 0)::float as avg_game_plays,
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays, COALESCE(MAX(times_played), 0)::bigint as max_game_plays,
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays COALESCE(MIN(times_played), 0)::bigint as min_game_plays
FROM soundtrack FROM game
WHERE deleted IS NULL; WHERE deleted IS NULL;
+246
View File
@@ -0,0 +1,246 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: game.sql
package repository
import (
"context"
)
const addGamePlayed = `-- name: AddGamePlayed :exec
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1
`
func (q *Queries) AddGamePlayed(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, addGamePlayed, id)
return err
}
const clearGames = `-- name: ClearGames :exec
DELETE FROM game
`
func (q *Queries) ClearGames(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearGames)
return err
}
const findAllGames = `-- name: FindAllGames :many
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM game
WHERE deleted IS NULL
ORDER BY game_name
`
func (q *Queries) FindAllGames(ctx context.Context) ([]Game, error) {
rows, err := q.db.Query(ctx, findAllGames)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Game
for rows.Next() {
var i Game
if err := rows.Scan(
&i.ID,
&i.GameName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllGamesIncludingDeleted = `-- name: GetAllGamesIncludingDeleted :many
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM game
ORDER BY game_name
`
func (q *Queries) GetAllGamesIncludingDeleted(ctx context.Context) ([]Game, error) {
rows, err := q.db.Query(ctx, getAllGamesIncludingDeleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Game
for rows.Next() {
var i Game
if err := rows.Scan(
&i.ID,
&i.GameName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGameById = `-- name: GetGameById :one
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM game
WHERE id = $1
AND deleted IS NULL
`
func (q *Queries) GetGameById(ctx context.Context, id int32) (Game, error) {
row := q.db.QueryRow(ctx, getGameById, id)
var i Game
err := row.Scan(
&i.ID,
&i.GameName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
)
return i, err
}
const getGameNameById = `-- name: GetGameNameById :one
SELECT game_name FROM game WHERE id = $1
`
func (q *Queries) GetGameNameById(ctx context.Context, id int32) (string, error) {
row := q.db.QueryRow(ctx, getGameNameById, id)
var game_name string
err := row.Scan(&game_name)
return game_name, err
}
const getIdByGameName = `-- name: GetIdByGameName :one
SELECT id FROM game WHERE game_name = $1
`
func (q *Queries) GetIdByGameName(ctx context.Context, gameName string) (int32, error) {
row := q.db.QueryRow(ctx, getIdByGameName, gameName)
var id int32
err := row.Scan(&id)
return id, err
}
const insertGame = `-- name: InsertGame :one
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
`
type InsertGameParams struct {
GameName string `json:"game_name"`
Path string `json:"path"`
Hash string `json:"hash"`
}
func (q *Queries) InsertGame(ctx context.Context, arg InsertGameParams) (int32, error) {
row := q.db.QueryRow(ctx, insertGame, arg.GameName, arg.Path, arg.Hash)
var id int32
err := row.Scan(&id)
return id, err
}
const insertGameWithExistingId = `-- name: InsertGameWithExistingId :exec
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
`
type InsertGameWithExistingIdParams struct {
ID int32 `json:"id"`
GameName string `json:"game_name"`
Path string `json:"path"`
Hash string `json:"hash"`
}
func (q *Queries) InsertGameWithExistingId(ctx context.Context, arg InsertGameWithExistingIdParams) error {
_, err := q.db.Exec(ctx, insertGameWithExistingId,
arg.ID,
arg.GameName,
arg.Path,
arg.Hash,
)
return err
}
const removeDeletionDate = `-- name: RemoveDeletionDate :exec
UPDATE game SET deleted=NULL WHERE id=$1
`
func (q *Queries) RemoveDeletionDate(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, removeDeletionDate, id)
return err
}
const resetGameIdSeq = `-- name: ResetGameIdSeq :one
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1)
`
func (q *Queries) ResetGameIdSeq(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, resetGameIdSeq)
var setval int64
err := row.Scan(&setval)
return setval, err
}
const setGameDeletionDate = `-- name: SetGameDeletionDate :exec
UPDATE game SET deleted=now() WHERE deleted IS NULL
`
func (q *Queries) SetGameDeletionDate(ctx context.Context) error {
_, err := q.db.Exec(ctx, setGameDeletionDate)
return err
}
const updateGameHash = `-- name: UpdateGameHash :exec
UPDATE game SET hash=$1, last_changed=now() WHERE id=$2
`
type UpdateGameHashParams struct {
Hash string `json:"hash"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateGameHash(ctx context.Context, arg UpdateGameHashParams) error {
_, err := q.db.Exec(ctx, updateGameHash, arg.Hash, arg.ID)
return err
}
const updateGameName = `-- name: UpdateGameName :exec
UPDATE game SET game_name=$1, path=$2, last_changed=now() WHERE id=$3
`
type UpdateGameNameParams struct {
Name string `json:"name"`
Path string `json:"path"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateGameName(ctx context.Context, arg UpdateGameNameParams) error {
_, err := q.db.Exec(ctx, updateGameName, arg.Name, arg.Path, arg.ID)
return err
}
+24 -25
View File
@@ -10,6 +10,19 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type Game struct {
ID int32 `json:"id"`
GameName string `json:"game_name"`
Added time.Time `json:"added"`
Deleted *time.Time `json:"deleted"`
LastChanged *time.Time `json:"last_changed"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
LastPlayed *time.Time `json:"last_played"`
NumberOfSongs int32 `json:"number_of_songs"`
Hash string `json:"hash"`
}
type Session struct { type Session struct {
Token string `json:"token"` Token string `json:"token"`
IpAddress string `json:"ip_address"` IpAddress string `json:"ip_address"`
@@ -20,34 +33,20 @@ type Session struct {
} }
type Song struct { type Song struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
Path string `json:"path"` Path string `json:"path"`
TimesPlayed int32 `json:"times_played"` TimesPlayed int32 `json:"times_played"`
Hash string `json:"hash"` Hash string `json:"hash"`
FileName *string `json:"file_name"` FileName *string `json:"file_name"`
ID pgtype.Int4 `json:"id"`
} }
type SongList struct { type SongList struct {
MatchDate time.Time `json:"match_date"` MatchDate time.Time `json:"match_date"`
MatchID int32 `json:"match_id"` MatchID int32 `json:"match_id"`
SongNo int32 `json:"song_no"` SongNo int32 `json:"song_no"`
SoundtrackName *string `json:"soundtrack_name"` GameName *string `json:"game_name"`
SongName *string `json:"song_name"` SongName *string `json:"song_name"`
}
type Soundtrack struct {
ID int32 `json:"id"`
SoundtrackName string `json:"soundtrack_name"`
Added time.Time `json:"added"`
Deleted *time.Time `json:"deleted"`
LastChanged *time.Time `json:"last_changed"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
LastPlayed *time.Time `json:"last_played"`
NumberOfSongs int32 `json:"number_of_songs"`
Hash string `json:"hash"`
} }
type Vgmq struct { type Vgmq struct {
+38 -73
View File
@@ -7,40 +7,37 @@ package repository
import ( import (
"context" "context"
"github.com/jackc/pgx/v5/pgtype"
) )
const addHashToSong = `-- name: AddHashToSong :exec const addHashToSong = `-- name: AddHashToSong :exec
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3 UPDATE song SET hash=$1 where path=$2
` `
type AddHashToSongParams struct { type AddHashToSongParams struct {
Hash string `json:"hash"` Hash string `json:"hash"`
SoundtrackID int32 `json:"soundtrack_id"` Path string `json:"path"`
Path string `json:"path"`
} }
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error { func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.SoundtrackID, arg.Path) _, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
return err return err
} }
const addSong = `-- name: AddSong :exec const addSong = `-- name: AddSong :exec
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5) INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
` `
type AddSongParams struct { type AddSongParams struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
Path string `json:"path"` Path string `json:"path"`
FileName *string `json:"file_name"` FileName *string `json:"file_name"`
Hash string `json:"hash"` Hash string `json:"hash"`
} }
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error { func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
_, err := q.db.Exec(ctx, addSong, _, err := q.db.Exec(ctx, addSong,
arg.SoundtrackID, arg.GameID,
arg.SongName, arg.SongName,
arg.Path, arg.Path,
arg.FileName, arg.FileName,
@@ -51,30 +48,25 @@ func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
const addSongPlayed = `-- name: AddSongPlayed :exec const addSongPlayed = `-- name: AddSongPlayed :exec
UPDATE song SET times_played = times_played + 1 UPDATE song SET times_played = times_played + 1
WHERE soundtrack_id = $1 AND song_name = $2 WHERE game_id = $1 AND song_name = $2
` `
type AddSongPlayedParams struct { type AddSongPlayedParams struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
} }
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error { func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
_, err := q.db.Exec(ctx, addSongPlayed, arg.SoundtrackID, arg.SongName) _, err := q.db.Exec(ctx, addSongPlayed, arg.GameID, arg.SongName)
return err return err
} }
const checkSong = `-- name: CheckSong :one const checkSong = `-- name: CheckSong :one
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2 SELECT COUNT(*) FROM song WHERE path = $1
` `
type CheckSongParams struct { func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
SoundtrackID int32 `json:"soundtrack_id"` row := q.db.QueryRow(ctx, checkSong, path)
Path string `json:"path"`
}
func (q *Queries) CheckSong(ctx context.Context, arg CheckSongParams) (int64, error) {
row := q.db.QueryRow(ctx, checkSong, arg.SoundtrackID, arg.Path)
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err
@@ -100,17 +92,17 @@ func (q *Queries) ClearSongs(ctx context.Context) error {
return err return err
} }
const clearSongsBySoundtrackId = `-- name: ClearSongsBySoundtrackId :exec const clearSongsByGameId = `-- name: ClearSongsByGameId :exec
DELETE FROM song WHERE soundtrack_id = $1 DELETE FROM song WHERE game_id = $1
` `
func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int32) error { func (q *Queries) ClearSongsByGameId(ctx context.Context, gameID int32) error {
_, err := q.db.Exec(ctx, clearSongsBySoundtrackId, soundtrackID) _, err := q.db.Exec(ctx, clearSongsByGameId, gameID)
return err return err
} }
const fetchAllSongs = `-- name: FetchAllSongs :many const fetchAllSongs = `-- name: FetchAllSongs :many
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song SELECT game_id, song_name, path, times_played, hash, file_name FROM song
` `
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) { func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
@@ -123,13 +115,12 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
for rows.Next() { for rows.Next() {
var i Song var i Song
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SongName, &i.SongName,
&i.Path, &i.Path,
&i.TimesPlayed, &i.TimesPlayed,
&i.Hash, &i.Hash,
&i.FileName, &i.FileName,
&i.ID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -141,14 +132,14 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
return items, nil return items, nil
} }
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many const findSongsFromGame = `-- name: FindSongsFromGame :many
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id SELECT game_id, song_name, path, times_played, hash, file_name
FROM song FROM song
WHERE soundtrack_id = $1 WHERE game_id = $1
` `
func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int32) ([]Song, error) { func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song, error) {
rows, err := q.db.Query(ctx, findSongsFromSoundtrack, soundtrackID) rows, err := q.db.Query(ctx, findSongsFromGame, gameID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -157,13 +148,12 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
for rows.Next() { for rows.Next() {
var i Song var i Song
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SongName, &i.SongName,
&i.Path, &i.Path,
&i.TimesPlayed, &i.TimesPlayed,
&i.Hash, &i.Hash,
&i.FileName, &i.FileName,
&i.ID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -175,64 +165,39 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
return items, nil return items, nil
} }
const getSongById = `-- name: GetSongById :one
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE id = $1
`
func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error) {
row := q.db.QueryRow(ctx, getSongById, id)
var i Song
err := row.Scan(
&i.SoundtrackID,
&i.SongName,
&i.Path,
&i.TimesPlayed,
&i.Hash,
&i.FileName,
&i.ID,
)
return i, err
}
const getSongWithHash = `-- name: GetSongWithHash :one const getSongWithHash = `-- name: GetSongWithHash :one
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE hash = $1 SELECT game_id, song_name, path, times_played, hash, file_name FROM song WHERE hash = $1
` `
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) { func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
row := q.db.QueryRow(ctx, getSongWithHash, hash) row := q.db.QueryRow(ctx, getSongWithHash, hash)
var i Song var i Song
err := row.Scan( err := row.Scan(
&i.SoundtrackID, &i.GameID,
&i.SongName, &i.SongName,
&i.Path, &i.Path,
&i.TimesPlayed, &i.TimesPlayed,
&i.Hash, &i.Hash,
&i.FileName, &i.FileName,
&i.ID,
) )
return i, err return i, err
} }
const removeBrokenSong = `-- name: RemoveBrokenSong :exec const removeBrokenSong = `-- name: RemoveBrokenSong :exec
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2 DELETE FROM song WHERE path = $1
` `
type RemoveBrokenSongParams struct { func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
SoundtrackID int32 `json:"soundtrack_id"` _, err := q.db.Exec(ctx, removeBrokenSong, path)
Path string `json:"path"`
}
func (q *Queries) RemoveBrokenSong(ctx context.Context, arg RemoveBrokenSongParams) error {
_, err := q.db.Exec(ctx, removeBrokenSong, arg.SoundtrackID, arg.Path)
return err return err
} }
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
DELETE FROM song WHERE id = ANY($1) DELETE FROM song where path = any ($1)
` `
func (q *Queries) RemoveBrokenSongs(ctx context.Context, id pgtype.Int4) error { func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
_, err := q.db.Exec(ctx, removeBrokenSongs, id) _, err := q.db.Exec(ctx, removeBrokenSongs, paths)
return err return err
} }
+9 -9
View File
@@ -11,7 +11,7 @@ import (
) )
const getSongList = `-- name: GetSongList :many const getSongList = `-- name: GetSongList :many
SELECT match_date, match_id, song_no, soundtrack_name, song_name SELECT match_date, match_id, song_no, game_name, song_name
FROM song_list FROM song_list
WHERE match_date = $1 WHERE match_date = $1
ORDER BY song_no DESC ORDER BY song_no DESC
@@ -30,7 +30,7 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
&i.MatchDate, &i.MatchDate,
&i.MatchID, &i.MatchID,
&i.SongNo, &i.SongNo,
&i.SoundtrackName, &i.GameName,
&i.SongName, &i.SongName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -44,16 +44,16 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
} }
const insertSongInList = `-- name: InsertSongInList :exec const insertSongInList = `-- name: InsertSongInList :exec
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name) INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
` `
type InsertSongInListParams struct { type InsertSongInListParams struct {
MatchDate time.Time `json:"match_date"` MatchDate time.Time `json:"match_date"`
MatchID int32 `json:"match_id"` MatchID int32 `json:"match_id"`
SongNo int32 `json:"song_no"` SongNo int32 `json:"song_no"`
SoundtrackName *string `json:"soundtrack_name"` GameName *string `json:"game_name"`
SongName *string `json:"song_name"` SongName *string `json:"song_name"`
} }
func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListParams) error { func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListParams) error {
@@ -61,7 +61,7 @@ func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListPara
arg.MatchDate, arg.MatchDate,
arg.MatchID, arg.MatchID,
arg.SongNo, arg.SongNo,
arg.SoundtrackName, arg.GameName,
arg.SongName, arg.SongName,
) )
return err return err
-246
View File
@@ -1,246 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: soundtrack.sql
package repository
import (
"context"
)
const addSoundtrackPlayed = `-- name: AddSoundtrackPlayed :exec
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1
`
func (q *Queries) AddSoundtrackPlayed(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, addSoundtrackPlayed, id)
return err
}
const clearSoundtracks = `-- name: ClearSoundtracks :exec
DELETE FROM soundtrack
`
func (q *Queries) ClearSoundtracks(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearSoundtracks)
return err
}
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM soundtrack
WHERE deleted IS NULL
ORDER BY soundtrack_name
`
func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error) {
rows, err := q.db.Query(ctx, findAllSoundtracks)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Soundtrack
for rows.Next() {
var i Soundtrack
if err := rows.Scan(
&i.ID,
&i.SoundtrackName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM soundtrack
ORDER BY soundtrack_name
`
func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soundtrack, error) {
rows, err := q.db.Query(ctx, getAllSoundtracksIncludingDeleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Soundtrack
for rows.Next() {
var i Soundtrack
if err := rows.Scan(
&i.ID,
&i.SoundtrackName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getIdBySoundtrackName = `-- name: GetIdBySoundtrackName :one
SELECT id FROM soundtrack WHERE soundtrack_name = $1
`
func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName string) (int32, error) {
row := q.db.QueryRow(ctx, getIdBySoundtrackName, soundtrackName)
var id int32
err := row.Scan(&id)
return id, err
}
const getSoundtrackById = `-- name: GetSoundtrackById :one
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
FROM soundtrack
WHERE id = $1
AND deleted IS NULL
`
func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack, error) {
row := q.db.QueryRow(ctx, getSoundtrackById, id)
var i Soundtrack
err := row.Scan(
&i.ID,
&i.SoundtrackName,
&i.Added,
&i.Deleted,
&i.LastChanged,
&i.Path,
&i.TimesPlayed,
&i.LastPlayed,
&i.NumberOfSongs,
&i.Hash,
)
return i, err
}
const getSoundtrackNameById = `-- name: GetSoundtrackNameById :one
SELECT soundtrack_name FROM soundtrack WHERE id = $1
`
func (q *Queries) GetSoundtrackNameById(ctx context.Context, id int32) (string, error) {
row := q.db.QueryRow(ctx, getSoundtrackNameById, id)
var soundtrack_name string
err := row.Scan(&soundtrack_name)
return soundtrack_name, err
}
const insertSoundtrack = `-- name: InsertSoundtrack :one
INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
`
type InsertSoundtrackParams struct {
SoundtrackName string `json:"soundtrack_name"`
Path string `json:"path"`
Hash string `json:"hash"`
}
func (q *Queries) InsertSoundtrack(ctx context.Context, arg InsertSoundtrackParams) (int32, error) {
row := q.db.QueryRow(ctx, insertSoundtrack, arg.SoundtrackName, arg.Path, arg.Hash)
var id int32
err := row.Scan(&id)
return id, err
}
const insertSoundtrackWithExistingId = `-- name: InsertSoundtrackWithExistingId :exec
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
`
type InsertSoundtrackWithExistingIdParams struct {
ID int32 `json:"id"`
SoundtrackName string `json:"soundtrack_name"`
Path string `json:"path"`
Hash string `json:"hash"`
}
func (q *Queries) InsertSoundtrackWithExistingId(ctx context.Context, arg InsertSoundtrackWithExistingIdParams) error {
_, err := q.db.Exec(ctx, insertSoundtrackWithExistingId,
arg.ID,
arg.SoundtrackName,
arg.Path,
arg.Hash,
)
return err
}
const removeSoundtrackDeletionDate = `-- name: RemoveSoundtrackDeletionDate :exec
UPDATE soundtrack SET deleted=NULL WHERE id=$1
`
func (q *Queries) RemoveSoundtrackDeletionDate(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, removeSoundtrackDeletionDate, id)
return err
}
const resetSoundtrackIdSeq = `-- name: ResetSoundtrackIdSeq :one
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1)
`
func (q *Queries) ResetSoundtrackIdSeq(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, resetSoundtrackIdSeq)
var setval int64
err := row.Scan(&setval)
return setval, err
}
const setSoundtrackDeletionDate = `-- name: SetSoundtrackDeletionDate :exec
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL
`
func (q *Queries) SetSoundtrackDeletionDate(ctx context.Context) error {
_, err := q.db.Exec(ctx, setSoundtrackDeletionDate)
return err
}
const updateSoundtrackHash = `-- name: UpdateSoundtrackHash :exec
UPDATE soundtrack SET hash=$1, last_changed=now() WHERE id=$2
`
type UpdateSoundtrackHashParams struct {
Hash string `json:"hash"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateSoundtrackHash(ctx context.Context, arg UpdateSoundtrackHashParams) error {
_, err := q.db.Exec(ctx, updateSoundtrackHash, arg.Hash, arg.ID)
return err
}
const updateSoundtrackName = `-- name: UpdateSoundtrackName :exec
UPDATE soundtrack SET soundtrack_name=$1, path=$2, last_changed=now() WHERE id=$3
`
type UpdateSoundtrackNameParams struct {
Name string `json:"name"`
Path string `json:"path"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateSoundtrackName(ctx context.Context, arg UpdateSoundtrackNameParams) error {
_, err := q.db.Exec(ctx, updateSoundtrackName, arg.Name, arg.Path, arg.ID)
return err
}
+131 -131
View File
@@ -12,10 +12,10 @@ import (
const getLastPlayedGames = `-- name: GetLastPlayedGames :many const getLastPlayedGames = `-- name: GetLastPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -23,23 +23,23 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played DESC ORDER BY g.last_played DESC
LIMIT $1 LIMIT $1
` `
type GetLastPlayedGamesRow struct { type GetLastPlayedGamesRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"` GamePlayed int32 `json:"game_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"` GameLastPlayed *time.Time `json:"game_last_played"`
Songs []byte `json:"songs"` Songs []byte `json:"songs"`
} }
// Last played soundtracks (most recently played) // Last played games (most recently played)
func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) { func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) {
rows, err := q.db.Query(ctx, getLastPlayedGames, limit) rows, err := q.db.Query(ctx, getLastPlayedGames, limit)
if err != nil { if err != nil {
@@ -50,10 +50,10 @@ func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLas
for rows.Next() { for rows.Next() {
var i GetLastPlayedGamesRow var i GetLastPlayedGamesRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SoundtrackPlayed, &i.GamePlayed,
&i.SoundtrackLastPlayed, &i.GameLastPlayed,
&i.Songs, &i.Songs,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -68,10 +68,10 @@ func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLas
const getLeastPlayedGamesWithSongs = `-- name: GetLeastPlayedGamesWithSongs :many const getLeastPlayedGamesWithSongs = `-- name: GetLeastPlayedGamesWithSongs :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -80,23 +80,23 @@ SELECT
'file_name', s.file_name 'file_name', s.file_name
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played ASC, g.soundtrack_name ORDER BY g.times_played ASC, g.game_name
LIMIT $1 LIMIT $1
` `
type GetLeastPlayedGamesWithSongsRow struct { type GetLeastPlayedGamesWithSongsRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"` GamePlayed int32 `json:"game_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"` GameLastPlayed *time.Time `json:"game_last_played"`
Songs []byte `json:"songs"` Songs []byte `json:"songs"`
} }
// Least played soundtracks with their songs // Least played games with their songs
func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) { func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) {
rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit) rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit)
if err != nil { if err != nil {
@@ -107,10 +107,10 @@ func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32)
for rows.Next() { for rows.Next() {
var i GetLeastPlayedGamesWithSongsRow var i GetLeastPlayedGamesWithSongsRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SoundtrackPlayed, &i.GamePlayed,
&i.SoundtrackLastPlayed, &i.GameLastPlayed,
&i.Songs, &i.Songs,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -125,29 +125,29 @@ func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32)
const getLeastPlayedSongsWithGame = `-- name: GetLeastPlayedSongsWithGame :many const getLeastPlayedSongsWithGame = `-- name: GetLeastPlayedSongsWithGame :many
SELECT SELECT
s.soundtrack_id as soundtrack_id, s.game_id as game_id,
g.soundtrack_name, g.game_name,
s.song_name, s.song_name,
s.path, s.path,
s.times_played, s.times_played,
s.file_name s.file_name
FROM song s FROM song s
JOIN soundtrack g ON s.soundtrack_id = g.id JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
ORDER BY s.times_played ASC, s.song_name ORDER BY s.times_played ASC, s.song_name
LIMIT $1 LIMIT $1
` `
type GetLeastPlayedSongsWithGameRow struct { type GetLeastPlayedSongsWithGameRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
Path string `json:"path"` Path string `json:"path"`
TimesPlayed int32 `json:"times_played"` TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"` FileName *string `json:"file_name"`
} }
// Least played songs with their soundtrack info // Least played songs with their game info
func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) { func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) {
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit) rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit)
if err != nil { if err != nil {
@@ -158,8 +158,8 @@ func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32)
for rows.Next() { for rows.Next() {
var i GetLeastPlayedSongsWithGameRow var i GetLeastPlayedSongsWithGameRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SongName, &i.SongName,
&i.Path, &i.Path,
&i.TimesPlayed, &i.TimesPlayed,
@@ -177,10 +177,10 @@ func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32)
const getMostPlayedGamesWithSongs = `-- name: GetMostPlayedGamesWithSongs :many const getMostPlayedGamesWithSongs = `-- name: GetMostPlayedGamesWithSongs :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -189,23 +189,23 @@ SELECT
'file_name', s.file_name 'file_name', s.file_name
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played DESC, g.soundtrack_name ORDER BY g.times_played DESC, g.game_name
LIMIT $1 LIMIT $1
` `
type GetMostPlayedGamesWithSongsRow struct { type GetMostPlayedGamesWithSongsRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"` GamePlayed int32 `json:"game_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"` GameLastPlayed *time.Time `json:"game_last_played"`
Songs []byte `json:"songs"` Songs []byte `json:"songs"`
} }
// Most played soundtracks with their songs // Most played games with their songs
func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) { func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) {
rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit) rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit)
if err != nil { if err != nil {
@@ -216,10 +216,10 @@ func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32)
for rows.Next() { for rows.Next() {
var i GetMostPlayedGamesWithSongsRow var i GetMostPlayedGamesWithSongsRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SoundtrackPlayed, &i.GamePlayed,
&i.SoundtrackLastPlayed, &i.GameLastPlayed,
&i.Songs, &i.Songs,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -234,29 +234,29 @@ func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32)
const getMostPlayedSongsWithGame = `-- name: GetMostPlayedSongsWithGame :many const getMostPlayedSongsWithGame = `-- name: GetMostPlayedSongsWithGame :many
SELECT SELECT
s.soundtrack_id as soundtrack_id, s.game_id as game_id,
g.soundtrack_name, g.game_name,
s.song_name, s.song_name,
s.path, s.path,
s.times_played, s.times_played,
s.file_name s.file_name
FROM song s FROM song s
JOIN soundtrack g ON s.soundtrack_id = g.id JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL WHERE g.deleted IS NULL
ORDER BY s.times_played DESC, s.song_name ORDER BY s.times_played DESC, s.song_name
LIMIT $1 LIMIT $1
` `
type GetMostPlayedSongsWithGameRow struct { type GetMostPlayedSongsWithGameRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SongName string `json:"song_name"` SongName string `json:"song_name"`
Path string `json:"path"` Path string `json:"path"`
TimesPlayed int32 `json:"times_played"` TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"` FileName *string `json:"file_name"`
} }
// Most played songs with their soundtrack info // Most played songs with their game info
func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) { func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) {
rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit) rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit)
if err != nil { if err != nil {
@@ -267,8 +267,8 @@ func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) (
for rows.Next() { for rows.Next() {
var i GetMostPlayedSongsWithGameRow var i GetMostPlayedSongsWithGameRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SongName, &i.SongName,
&i.Path, &i.Path,
&i.TimesPlayed, &i.TimesPlayed,
@@ -286,9 +286,9 @@ func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) (
const getNeverPlayedGames = `-- name: GetNeverPlayedGames :many const getNeverPlayedGames = `-- name: GetNeverPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.added, g.added,
json_agg( json_agg(
json_build_object( json_build_object(
@@ -297,19 +297,19 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.times_played = 0 WHERE g.deleted IS NULL AND g.times_played = 0
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added GROUP BY g.id, g.game_name, g.times_played, g.added
ORDER BY g.soundtrack_name ORDER BY g.game_name
` `
type GetNeverPlayedGamesRow struct { type GetNeverPlayedGamesRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"` GamePlayed int32 `json:"game_played"`
Added time.Time `json:"added"` Added time.Time `json:"added"`
Songs []byte `json:"songs"` Songs []byte `json:"songs"`
} }
// Games that have never been played (times_played = 0) // Games that have never been played (times_played = 0)
@@ -323,9 +323,9 @@ func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGame
for rows.Next() { for rows.Next() {
var i GetNeverPlayedGamesRow var i GetNeverPlayedGamesRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SoundtrackPlayed, &i.GamePlayed,
&i.Added, &i.Added,
&i.Songs, &i.Songs,
); err != nil { ); err != nil {
@@ -341,10 +341,10 @@ func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGame
const getOldestPlayedGames = `-- name: GetOldestPlayedGames :many const getOldestPlayedGames = `-- name: GetOldestPlayedGames :many
SELECT SELECT
g.id as soundtrack_id, g.id as game_id,
g.soundtrack_name, g.game_name,
g.times_played as soundtrack_played, g.times_played as game_played,
g.last_played as soundtrack_last_played, g.last_played as game_last_played,
json_agg( json_agg(
json_build_object( json_build_object(
'song_name', s.song_name, 'song_name', s.song_name,
@@ -352,23 +352,23 @@ SELECT
'times_played', s.times_played 'times_played', s.times_played
) )
) as songs ) as songs
FROM soundtrack g FROM game g
LEFT JOIN song s ON g.id = s.soundtrack_id LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played ASC ORDER BY g.last_played ASC
LIMIT $1 LIMIT $1
` `
type GetOldestPlayedGamesRow struct { type GetOldestPlayedGamesRow struct {
SoundtrackID int32 `json:"soundtrack_id"` GameID int32 `json:"game_id"`
SoundtrackName string `json:"soundtrack_name"` GameName string `json:"game_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"` GamePlayed int32 `json:"game_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"` GameLastPlayed *time.Time `json:"game_last_played"`
Songs []byte `json:"songs"` Songs []byte `json:"songs"`
} }
// Oldest played soundtracks (least recently played, but has been played at least once) // Oldest played games (least recently played, but has been played at least once)
func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) { func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) {
rows, err := q.db.Query(ctx, getOldestPlayedGames, limit) rows, err := q.db.Query(ctx, getOldestPlayedGames, limit)
if err != nil { if err != nil {
@@ -379,10 +379,10 @@ func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetO
for rows.Next() { for rows.Next() {
var i GetOldestPlayedGamesRow var i GetOldestPlayedGamesRow
if err := rows.Scan( if err := rows.Scan(
&i.SoundtrackID, &i.GameID,
&i.SoundtrackName, &i.GameName,
&i.SoundtrackPlayed, &i.GamePlayed,
&i.SoundtrackLastPlayed, &i.GameLastPlayed,
&i.Songs, &i.Songs,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -397,25 +397,25 @@ func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetO
const getStatisticsSummary = `-- name: GetStatisticsSummary :one const getStatisticsSummary = `-- name: GetStatisticsSummary :one
SELECT SELECT
COUNT(*) as total_soundtracks, COUNT(*) as total_games,
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks, SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_games,
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks, SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_games,
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays, COALESCE(SUM(times_played), 0)::bigint as total_game_plays,
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays, COALESCE(AVG(times_played), 0)::float as avg_game_plays,
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays, COALESCE(MAX(times_played), 0)::bigint as max_game_plays,
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays COALESCE(MIN(times_played), 0)::bigint as min_game_plays
FROM soundtrack FROM game
WHERE deleted IS NULL WHERE deleted IS NULL
` `
type GetStatisticsSummaryRow struct { type GetStatisticsSummaryRow struct {
TotalSoundtracks int64 `json:"total_soundtracks"` TotalGames int64 `json:"total_games"`
PlayedSoundtracks int64 `json:"played_soundtracks"` PlayedGames int64 `json:"played_games"`
NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"` NeverPlayedGames int64 `json:"never_played_games"`
TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"` TotalGamePlays int64 `json:"total_game_plays"`
AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"` AvgGamePlays float64 `json:"avg_game_plays"`
MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"` MaxGamePlays int64 `json:"max_game_plays"`
MinSoundtrackPlays int64 `json:"min_soundtrack_plays"` MinGamePlays int64 `json:"min_game_plays"`
} }
// Get statistics summary // Get statistics summary
@@ -423,13 +423,13 @@ func (q *Queries) GetStatisticsSummary(ctx context.Context) (GetStatisticsSummar
row := q.db.QueryRow(ctx, getStatisticsSummary) row := q.db.QueryRow(ctx, getStatisticsSummary)
var i GetStatisticsSummaryRow var i GetStatisticsSummaryRow
err := row.Scan( err := row.Scan(
&i.TotalSoundtracks, &i.TotalGames,
&i.PlayedSoundtracks, &i.PlayedGames,
&i.NeverPlayedSoundtracks, &i.NeverPlayedGames,
&i.TotalSoundtrackPlays, &i.TotalGamePlays,
&i.AvgSoundtrackPlays, &i.AvgGamePlays,
&i.MaxSoundtrackPlays, &i.MaxGamePlays,
&i.MinSoundtrackPlays, &i.MinGamePlays,
) )
return i, err return i, err
} }
+9 -25
View File
@@ -54,19 +54,8 @@ func TestSetupDB(t *testing.T) {
t.Fatalf("Failed to initialize test database: %v", err) t.Fatalf("Failed to initialize test database: %v", err)
} }
// Clean up any existing schema to ensure clean state
ctx := context.Background()
_, err = TestDatabase.Pool.Exec(ctx, "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;")
if err != nil {
t.Logf("Warning: Could not clean schema: %v", err)
// Continue anyway, migrations might still work
}
// Run migrations // Run migrations
if err := TestDatabase.RunMigrations(); err != nil { if err := TestDatabase.RunMigrations(); err != nil {
// Clean up on failure to prevent nil pointer issues in other tests
TestDatabase.Close()
TestDatabase = nil
t.Fatalf("Failed to run migrations: %v", err) t.Fatalf("Failed to run migrations: %v", err)
} }
}) })
@@ -108,11 +97,10 @@ func createTestDatabase(host, port, dbname, user, password string) {
// "closed pool" errors when tests run sequentially // "closed pool" errors when tests run sequentially
func TestTearDownDB(t *testing.T) { func TestTearDownDB(t *testing.T) {
// CloseDb() // Disabled to prevent pool closure between sequential tests // CloseDb() // Disabled to prevent pool closure between sequential tests
// Note: We also don't nil TestDatabase to allow reuse across tests if TestDatabase != nil {
// if TestDatabase != nil { TestDatabase.Close()
// TestDatabase.Close() TestDatabase = nil
// TestDatabase = nil }
// }
} }
// TestClearDatabase clears all data from the test database // TestClearDatabase clears all data from the test database
@@ -124,13 +112,10 @@ func TestClearDatabase(t *testing.T) {
// Clear all tables in reverse order to respect foreign keys // Clear all tables in reverse order to respect foreign keys
// Note: This assumes the tables exist and have the expected structure // Note: This assumes the tables exist and have the expected structure
// After migration 000005, game table was renamed to soundtrack
tables := []string{ tables := []string{
"song_list", "song_list",
"song", "song",
"soundtrack", "game",
"vgmq",
"sessions",
} }
ctx := context.Background() ctx := context.Background()
@@ -141,10 +126,9 @@ func TestClearDatabase(t *testing.T) {
} }
} }
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005) // Reset sequences
var seqErr error _, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)") if err != nil {
if seqErr != nil { t.Logf("Failed to reset game_id_seq: %v", err)
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
} }
} }
-49
View File
@@ -1,49 +0,0 @@
package server
import (
"net/http"
"os"
"github.com/labstack/echo/v5"
"music-server/internal/backend"
)
type CharacterHandler struct {
}
func NewCharacterHandler() *CharacterHandler {
return &CharacterHandler{}
}
// GetCharacterList godoc
// @Summary Get list of characters
// @Description Returns a list of all available characters
// @Tags characters
// @Accept json
// @Produce json
// @Success 200 {array} string
// @Router /characters [get]
func (c *CharacterHandler) GetCharacterList(ctx *echo.Context) error {
characters := backend.GetCharacterList()
return ctx.JSON(http.StatusOK, characters)
}
// GetCharacter godoc
// @Summary Get character image
// @Description Returns the image for a specific character
// @Tags characters
// @Accept json
// @Produce image/png
// @Param name query string true "Character name"
// @Success 200 {file} file
// @Router /character [get]
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
character := ctx.QueryParam("name")
characterPath := backend.GetCharacter(character)
file, err := os.Open(characterPath)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
defer file.Close()
return ctx.Stream(http.StatusOK, "image/png", file)
}
-29
View File
@@ -1,29 +0,0 @@
package server
import (
"net/http"
"github.com/labstack/echo/v5"
"music-server/internal/db"
)
type HealthHandler struct {
db *db.Database
}
func NewHealthHandler(database *db.Database) *HealthHandler {
return &HealthHandler{db: database}
}
// HealthCheck godoc
//
// @Summary Check server health
// @Description Returns the health status of the server
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {string} string "OK"
// @Router /health [get]
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
return ctx.JSON(http.StatusOK, h.db.Health())
}
-24
View File
@@ -1,24 +0,0 @@
package server
import (
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
// TestHealthCheck verifies the health endpoint returns database status
func TestHealthCheck(t *testing.T) {
e := StartTestServer(t)
// No explicit teardown - handled by StartTestServer's sync.Once
resp := MakeTestRequest(t, e, "GET", "/health")
assert.Equal(t, http.StatusOK, resp.Code)
var healthData map[string]string
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
assert.NoError(t, err)
assert.NotEmpty(t, healthData)
assert.Equal(t, "up", healthData["status"])
}
+86
View File
@@ -0,0 +1,86 @@
package server
import (
"music-server/internal/backend"
"music-server/internal/db"
"net/http"
"github.com/labstack/echo/v5"
)
type IndexHandler struct {
}
func NewIndexHandler() *IndexHandler {
return &IndexHandler{}
}
// GetVersion godoc
//
// @Summary Getting the version of the backend
// @Description get string by ID
// @Tags accounts
// @Accept json
// @Produce json
// @Success 200 {object} backend.VersionData
// @Failure 404 {object} string
// @Router /version [get]
func (i *IndexHandler) GetVersion(ctx *echo.Context) error {
versionHistory := backend.GetVersionHistory()
if versionHistory.Version == "" {
return ctx.JSON(http.StatusNotFound, "version not found")
}
return ctx.JSON(http.StatusOK, versionHistory)
}
// GetDBTest godoc
// @Summary Test database connection
// @Description Tests the database connection
// @Tags database
// @Accept json
// @Produce json
// @Success 200 {string} string "TestedDB"
// @Router /dbtest [get]
func (i *IndexHandler) GetDBTest(ctx *echo.Context) error {
backend.TestDB()
return ctx.JSON(http.StatusOK, "TestedDB")
}
// HealthCheck godoc
// @Summary Check server health
// @Description Returns the health status of the server
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {string} string "OK"
// @Router /health [get]
func (i *IndexHandler) HealthCheck(ctx *echo.Context) error {
return ctx.JSON(http.StatusOK, db.Health())
}
// GetCharacterList godoc
// @Summary Get list of characters
// @Description Returns a list of all available characters
// @Tags characters
// @Accept json
// @Produce json
// @Success 200 {array} string
// @Router /characters [get]
func (i *IndexHandler) GetCharacterList(ctx *echo.Context) error {
characters := backend.GetCharacterList()
return ctx.JSON(http.StatusOK, characters)
}
// GetCharacter godoc
// @Summary Get character image
// @Description Returns the image for a specific character
// @Tags characters
// @Accept json
// @Produce image/png
// @Param name query string true "Character name"
// @Success 200 {file} file
// @Router /character [get]
func (i *IndexHandler) GetCharacter(ctx *echo.Context) error {
character := ctx.QueryParam("name")
return ctx.File(backend.GetCharacter(character))
}
@@ -5,9 +5,45 @@ import (
"net/http" "net/http"
"testing" "testing"
"music-server/internal/backend"
"music-server/internal/db"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// TestHealthCheck verifies the health endpoint returns database status
func TestHealthCheck(t *testing.T) {
// Setup database
db.TestSetupDB(t)
defer db.TestTearDownDB(t)
e := StartTestServer(t)
resp := MakeTestRequest(t, e, "GET", "/health")
assert.Equal(t, http.StatusOK, resp.Code)
var healthData map[string]string
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
assert.NoError(t, err)
assert.NotEmpty(t, healthData)
assert.Equal(t, "up", healthData["status"])
}
// TestGetVersion verifies the version endpoint returns version history
func TestGetVersion(t *testing.T) {
e := StartTestServer(t)
resp := MakeTestRequest(t, e, "GET", "/version")
assert.Equal(t, http.StatusOK, resp.Code)
var versionData backend.VersionData
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
assert.NoError(t, err)
assert.NotEmpty(t, versionData.Version)
assert.NotEmpty(t, versionData.Changelog)
assert.NotEmpty(t, versionData.History)
}
// TestGetCharacterList verifies the characters endpoint returns list of characters // TestGetCharacterList verifies the characters endpoint returns list of characters
func TestGetCharacterList(t *testing.T) { func TestGetCharacterList(t *testing.T) {
e := StartTestServer(t) e := StartTestServer(t)
@@ -45,3 +81,16 @@ func TestGetCharacterNotFound(t *testing.T) {
// Should return 404 or similar error // Should return 404 or similar error
assert.NotEqual(t, http.StatusOK, resp.Code) assert.NotEqual(t, http.StatusOK, resp.Code)
} }
// TestDBTest verifies the database test endpoint
func TestDBTest(t *testing.T) {
// Setup database
db.TestSetupDB(t)
defer db.TestTearDownDB(t)
e := StartTestServer(t)
resp := MakeTestRequest(t, e, "GET", "/dbtest")
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "TestedDB")
}
+10 -10
View File
@@ -229,8 +229,8 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
// GetAllSoundtracks godoc // GetAllGames godoc
// @Summary Get all soundtracks // @Summary Get all games
// @Description Returns a list of all games in order // @Description Returns a list of all games in order
// @Tags music // @Tags music
// @Accept json // @Accept json
@@ -238,17 +238,17 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
// @Success 200 {array} map[string]interface{} // @Success 200 {array} map[string]interface{}
// @Failure 423 {string} string "Syncing is in progress" // @Failure 423 {string} string "Syncing is in progress"
// @Router /music/all/order [get] // @Router /music/all/order [get]
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error { func (m *MusicHandler) GetAllGames(ctx *echo.Context) error {
if backend.Syncing { if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress") logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress") return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
soundtrackList := backend.GetAllSoundtracks() gameList := backend.GetAllGames()
return ctx.JSON(http.StatusOK, soundtrackList) return ctx.JSON(http.StatusOK, gameList)
} }
// GetAllSoundtracksRandom godoc // GetAllGamesRandom godoc
// @Summary Get all soundtracks random // @Summary Get all games random
// @Description Returns a list of all games in random order // @Description Returns a list of all games in random order
// @Tags music // @Tags music
// @Accept json // @Accept json
@@ -256,13 +256,13 @@ func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
// @Success 200 {array} map[string]interface{} // @Success 200 {array} map[string]interface{}
// @Failure 423 {string} string "Syncing is in progress" // @Failure 423 {string} string "Syncing is in progress"
// @Router /music/all/random [get] // @Router /music/all/random [get]
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error { func (m *MusicHandler) GetAllGamesRandom(ctx *echo.Context) error {
if backend.Syncing { if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress") logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress") return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
soundtrackList := backend.GetAllSoundtracksRandom() gameList := backend.GetAllGamesRandom()
return ctx.JSON(http.StatusOK, soundtrackList) return ctx.JSON(http.StatusOK, gameList)
} }
// PutPlayed godoc // PutPlayed godoc
+15 -19
View File
@@ -63,16 +63,12 @@ func (s *Server) RegisterRoutes() http.Handler {
// ============================================ // ============================================
deprecatedMiddleware := middleware.DeprecationMiddleware deprecatedMiddleware := middleware.DeprecationMiddleware
health := NewHealthHandler(s.db) index := NewIndexHandler()
e.GET("/health", deprecatedMiddleware(health.HealthCheck)) e.GET("/version", deprecatedMiddleware(index.GetVersion))
e.GET("/dbtest", deprecatedMiddleware(index.GetDBTest))
version := NewVersionHandler() e.GET("/health", deprecatedMiddleware(index.HealthCheck))
e.GET("/version", deprecatedMiddleware(version.GetLatestVersion)) e.GET("/character", deprecatedMiddleware(index.GetCharacter))
e.GET("/version/history", deprecatedMiddleware(version.GetVersionHistory)) e.GET("/characters", deprecatedMiddleware(index.GetCharacterList))
character := NewCharacterHandler()
e.GET("/character", deprecatedMiddleware(character.GetCharacter))
e.GET("/characters", deprecatedMiddleware(character.GetCharacterList))
download := NewDownloadHandler() download := NewDownloadHandler()
e.GET("/download", deprecatedMiddleware(download.checkLatest)) e.GET("/download", deprecatedMiddleware(download.checkLatest))
@@ -82,13 +78,13 @@ func (s *Server) RegisterRoutes() http.Handler {
sync := NewSyncHandler() sync := NewSyncHandler()
syncGroup := e.Group("/sync") syncGroup := e.Group("/sync")
syncGroup.GET("", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges)) syncGroup.GET("", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress)) syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
syncGroup.GET("/new", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges)) syncGroup.GET("/new", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
syncGroup.GET("/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull)) syncGroup.GET("/full", deprecatedMiddleware(sync.SyncGamesNewFull))
syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull)) syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncGamesNewFull))
syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges)) syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetDB)) syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetGames))
music := NewMusicHandler() music := NewMusicHandler()
musicGroup := e.Group("/music") musicGroup := e.Group("/music")
@@ -102,9 +98,9 @@ func (s *Server) RegisterRoutes() http.Handler {
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs)) musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong)) musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong))
musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong)) musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong))
musicGroup.GET("/all", deprecatedMiddleware(music.GetAllSoundtracksRandom)) musicGroup.GET("/all", deprecatedMiddleware(music.GetAllGamesRandom))
musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllSoundtracks)) musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllGames))
musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllSoundtracksRandom)) musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllGamesRandom))
musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed)) musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed))
musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue)) musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue))
musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed)) musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed))
-179
View File
@@ -1,179 +0,0 @@
package server
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"music-server/internal/backend"
"music-server/internal/db"
"music-server/internal/db/repository"
"github.com/labstack/echo/v5"
"github.com/stretchr/testify/require"
)
// TestStatisticsEndpoints tests the statistics API endpoints
func TestStatisticsEndpoints(t *testing.T) {
// Skip if test database not configured
e := StartTestServer(t)
if e == nil {
t.Skip("Test database not configured")
}
// Get token first
token := getTestToken(t, e)
if token == "" {
t.Skip("Could not get test token")
}
// Test /api/v1/statistics/summary
req := httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var summary backend.StatisticsSummary
err := json.Unmarshal(rec.Body.Bytes(), &summary)
require.NoError(t, err)
require.NotNil(t, summary)
}
// TestPartialMigrationThenSyncThenComplete tests migration workflow
// Note: This test requires the database to be in a specific state
// It tests: partial migration → data insert → sync → complete migration
func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
// This test is complex and requires careful setup
// For now, we test the final state: all migrations + sync
e := StartTestServer(t)
if e == nil {
t.Skip("Test database not configured")
}
// Get token
token := getTestToken(t, e)
if token == "" {
t.Skip("Could not get test token")
}
// Insert test data manually (5 soundtracks with songs)
insertTestData(t)
// Run sync to ensure data is properly loaded
req := httptest.NewRequest(http.MethodGet, "/sync/new", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
// Wait for sync to complete
if !waitForSyncCompletion(t, e, 60) {
t.Error("Sync did not complete within timeout")
}
// Verify data via statistics endpoint
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var summary backend.StatisticsSummary
err := json.Unmarshal(rec.Body.Bytes(), &summary)
require.NoError(t, err)
// After sync with /sync/new, only soundtracks matching filesystem remain
// testMusic has 3 games
require.Equal(t, int64(3), summary.TotalGames)
}
// insertTestData inserts 5 test soundtracks with songs into the database
func insertTestData(t *testing.T) {
if db.TestDatabase == nil || db.TestDatabase.Pool == nil {
t.Skip("Test database not initialized")
return
}
ctx := context.Background()
queries := repository.New(db.TestDatabase.Pool)
// Insert 5 soundtracks
soundtracks := []struct {
name string
path string
}{
{"Test Soundtrack 1", "/path/to/soundtrack1"},
{"Test Soundtrack 2", "/path/to/soundtrack2"},
{"Test Soundtrack 3", "/path/to/soundtrack3"},
{"Test Soundtrack 4", "/path/to/soundtrack4"},
{"Test Soundtrack 5", "/path/to/soundtrack5"},
}
for _, st := range soundtracks {
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
SoundtrackName: st.name,
Path: st.path,
Hash: "test-hash-" + st.name,
})
require.NoError(t, err, "Failed to insert soundtrack: %s", st.name)
}
// Get soundtrack IDs
soundtrackIDs, err := queries.FindAllSoundtracks(ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, len(soundtrackIDs), 5)
// Insert songs for each soundtrack
songData := []struct {
soundtrackID int32
songs []string
}{
{soundtrackIDs[0].ID, []string{"Song A", "Song B"}},
{soundtrackIDs[1].ID, []string{"Song C", "Song D"}},
{soundtrackIDs[2].ID, []string{"Song E"}},
{soundtrackIDs[3].ID, []string{"Song F", "Song G", "Song H"}},
{soundtrackIDs[4].ID, []string{"Song I"}},
}
for _, sd := range songData {
for _, songName := range sd.songs {
err := queries.AddSong(ctx, repository.AddSongParams{
SoundtrackID: sd.soundtrackID,
SongName: songName,
Path: "/path/to/" + songName + ".mp3",
})
require.NoError(t, err, "Failed to insert song: %s", songName)
}
}
t.Logf("Inserted %d soundtracks with %d total songs", len(soundtracks), 8)
}
// getTestToken gets a valid token for testing
func getTestToken(t *testing.T, e *echo.Echo) string {
reqBody := `{"client_type": "test"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/token", strings.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Logf("Failed to get token: %s", rec.Body.String())
return ""
}
var resp struct {
Token string `json:"token"`
}
err := json.Unmarshal(rec.Body.Bytes(), &resp)
require.NoError(t, err)
return resp.Token
}
+17 -19
View File
@@ -34,61 +34,59 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
return ctx.JSON(http.StatusOK, response) return ctx.JSON(http.StatusOK, response)
} }
// SyncSoundtracksNewOnlyChanges godoc // SyncGamesNewOnlyChanges godoc
// @Summary Sync soundtracks with only changes // @Summary Sync games with only changes
// @Description Starts syncing games with only new changes // @Description Starts syncing games with only new changes
// @Tags sync // @Tags sync
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {string} string "Start syncing soundtracks" // @Success 200 {string} string "Start syncing games"
// @Failure 423 {string} string "Syncing is in progress" // @Failure 423 {string} string "Syncing is in progress"
// @Router /sync [get] // @Router /sync [get]
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error { func (s *SyncHandler) SyncGamesNewOnlyChanges(ctx *echo.Context) error {
if backend.Syncing { if backend.Syncing {
logging.GetLogger().Warn("Syncing is already in progress") logging.GetLogger().Warn("Syncing is already in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress") return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
logging.GetLogger().Info("Starting sync with only changes") logging.GetLogger().Info("Starting sync with only changes")
backend.Syncing = true go backend.SyncGamesNewOnlyChanges()
go backend.SyncSoundtracksNewOnlyChanges() return ctx.JSON(http.StatusOK, "Start syncing games")
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
} }
// SyncSoundtracksNewFull godoc // SyncGamesNewFull godoc
// @Summary Sync all games fully // @Summary Sync all games fully
// @Description Starts a full sync of all games // @Description Starts a full sync of all games
// @Tags sync // @Tags sync
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {string} string "Start syncing soundtracks full" // @Success 200 {string} string "Start syncing games full"
// @Failure 423 {string} string "Syncing is in progress" // @Failure 423 {string} string "Syncing is in progress"
// @Router /sync/full [get] // @Router /sync/full [get]
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error { func (s *SyncHandler) SyncGamesNewFull(ctx *echo.Context) error {
if backend.Syncing { if backend.Syncing {
logging.GetLogger().Warn("Syncing is already in progress") logging.GetLogger().Warn("Syncing is already in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress") return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
logging.GetLogger().Info("Starting full sync") logging.GetLogger().Info("Starting full sync")
backend.Syncing = true go backend.SyncGamesNewFull()
go backend.SyncSoundtracksNewFull() return ctx.JSON(http.StatusOK, "Start syncing games full")
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
} }
// ResetDB godoc // ResetGames godoc
// @Summary Reset soundtracks database // @Summary Reset games database
// @Description Resets the games database by deleting all games and songs // @Description Resets the games database by deleting all games and songs
// @Tags sync // @Tags sync
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {string} string "Soundtracks and songs are deleted from the database" // @Success 200 {string} string "Games and songs are deleted from the database"
// @Failure 423 {string} string "Syncing is in progress" // @Failure 423 {string} string "Syncing is in progress"
// @Router /sync/reset [get] // @Router /sync/reset [get]
func (s *SyncHandler) ResetDB(ctx *echo.Context) error { func (s *SyncHandler) ResetGames(ctx *echo.Context) error {
if backend.Syncing { if backend.Syncing {
logging.GetLogger().Warn("Cannot reset - syncing is in progress") logging.GetLogger().Warn("Cannot reset - syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress") return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
logging.GetLogger().Info("Resetting soundtracks database") logging.GetLogger().Info("Resetting games database")
backend.ResetDB() backend.ResetDB()
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database") return ctx.JSON(http.StatusOK, "Games and songs are deleted from the database")
} }
+10 -10
View File
@@ -76,7 +76,7 @@ func TestSyncPopulatesDatabase(t *testing.T) {
// Before sync - should have no games // Before sync - should have no games
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx()) gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
beforeCount := len(gamesBefore) beforeCount := len(gamesBefore)
t.Logf("Games before sync: %d", beforeCount) t.Logf("Games before sync: %d", beforeCount)
@@ -92,7 +92,7 @@ func TestSyncPopulatesDatabase(t *testing.T) {
} }
// After sync - should have games // After sync - should have games
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx()) gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
afterCount := len(gamesAfter) afterCount := len(gamesAfter)
t.Logf("Games after sync: %d", afterCount) t.Logf("Games after sync: %d", afterCount)
@@ -113,7 +113,7 @@ func TestSyncMakesDifference(t *testing.T) {
// Before sync - should have no games // Before sync - should have no games
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx()) gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync") assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync")
@@ -127,7 +127,7 @@ func TestSyncMakesDifference(t *testing.T) {
} }
// After sync - should have games // After sync - should have games
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx()) gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, len(gamesAfter) > 0, "Should have games after sync") assert.True(t, len(gamesAfter) > 0, "Should have games after sync")
} }
@@ -200,7 +200,7 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
// Get initial count // Get initial count
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx()) gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
beforeCount := len(gamesBefore) beforeCount := len(gamesBefore)
// Run incremental sync (should not change count if nothing changed) // Run incremental sync (should not change count if nothing changed)
@@ -211,7 +211,7 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Count should be the same // Count should be the same
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx()) gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
afterCount := len(gamesAfter) afterCount := len(gamesAfter)
// Note: This might not be exactly equal due to timing, but should be close // Note: This might not be exactly equal due to timing, but should be close
@@ -228,7 +228,7 @@ func TestResetGames(t *testing.T) {
// First ensure we have data // First ensure we have data
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx()) gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
beforeCount := len(gamesBefore) beforeCount := len(gamesBefore)
if beforeCount == 0 { if beforeCount == 0 {
@@ -238,7 +238,7 @@ func TestResetGames(t *testing.T) {
t.Error("Sync did not complete within timeout") t.Error("Sync did not complete within timeout")
return return
} }
gamesBefore, _ = repo.FindAllSoundtracks(backend.BackendCtx()) gamesBefore, _ = repo.FindAllGames(backend.BackendCtx())
beforeCount = len(gamesBefore) beforeCount = len(gamesBefore)
} }
@@ -253,7 +253,7 @@ func TestResetGames(t *testing.T) {
// Note: reset might take a moment to propagate // Note: reset might take a moment to propagate
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx()) gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
afterCount := len(gamesAfter) afterCount := len(gamesAfter)
t.Logf("Games after reset: %d", afterCount) t.Logf("Games after reset: %d", afterCount)
@@ -282,7 +282,7 @@ func TestSyncGamesNewFull(t *testing.T) {
// Verify database is populated // Verify database is populated
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
games, err := repo.FindAllSoundtracks(backend.BackendCtx()) games, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, len(games) > 0, "Database should be populated after full sync") assert.True(t, len(games) > 0, "Database should be populated after full sync")
t.Logf("Full sync populated %d games", len(games)) t.Logf("Full sync populated %d games", len(games))
+2 -3
View File
@@ -59,9 +59,8 @@ func StartTestServer(t *testing.T) *echo.Echo {
// Create a Server instance and get its routes // Create a Server instance and get its routes
s := &Server{ s := &Server{
db: db.TestDatabase, db: db.TestDatabase,
tokenHandler: NewTokenHandler(db.TestDatabase.Pool), tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
statisticsHandler: NewStatisticsHandler(),
} }
handler := s.RegisterRoutes() handler := s.RegisterRoutes()
-51
View File
@@ -1,51 +0,0 @@
package server
import (
"net/http"
"github.com/labstack/echo/v5"
"music-server/internal/backend"
)
type VersionHandler struct {
}
func NewVersionHandler() *VersionHandler {
return &VersionHandler{}
}
// GetVersionHistory godoc
//
// @Summary Getting the version history of the backend
// @Description get version history
// @Tags version
// @Accept json
// @Produce json
// @Success 200 {array} backend.VersionData
// @Failure 404 {object} string
// @Router /version/history [get]
func (v *VersionHandler) GetVersionHistory(ctx *echo.Context) error {
versionHistory := backend.GetVersionHistory()
if len(versionHistory) == 0 {
return ctx.JSON(http.StatusNotFound, "version not found")
}
return ctx.JSON(http.StatusOK, versionHistory)
}
// GetLatestVersion godoc
//
// @Summary Getting the latest version of the backend
// @Description get latest version info
// @Tags version
// @Accept json
// @Produce json
// @Success 200 {object} backend.VersionData
// @Failure 404 {object} string
// @Router /version [get]
func (v *VersionHandler) GetLatestVersion(ctx *echo.Context) error {
latestVersion := backend.GetLatestVersion()
if latestVersion.Version == "" {
return ctx.JSON(http.StatusNotFound, "version not found")
}
return ctx.JSON(http.StatusOK, latestVersion)
}
-40
View File
@@ -1,40 +0,0 @@
package server
import (
"encoding/json"
"net/http"
"testing"
"music-server/internal/backend"
"github.com/stretchr/testify/assert"
)
// TestGetLatestVersion verifies the version endpoint returns latest version
func TestGetLatestVersion(t *testing.T) {
e := StartTestServer(t)
resp := MakeTestRequest(t, e, "GET", "/version")
assert.Equal(t, http.StatusOK, resp.Code)
var versionData backend.VersionData
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
assert.NoError(t, err)
assert.NotEmpty(t, versionData.Version)
assert.NotEmpty(t, versionData.Changelog)
}
// TestGetVersionHistory verifies the version history endpoint returns version history
func TestGetVersionHistory(t *testing.T) {
e := StartTestServer(t)
resp := MakeTestRequest(t, e, "GET", "/version/history")
assert.Equal(t, http.StatusOK, resp.Code)
var versionHistory []backend.VersionData
err := json.Unmarshal(resp.Body.Bytes(), &versionHistory)
assert.NoError(t, err)
assert.NotEmpty(t, versionHistory)
assert.NotEmpty(t, versionHistory[0].Version)
assert.NotEmpty(t, versionHistory[0].Changelog)
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// ensureSyncRan ensures that sync has been run before testing music endpoints // ensureSyncRan ensures that sync has been run before testing music endpoints
func ensureSyncRan(t *testing.T, e *echo.Echo) { func ensureSyncRan(t *testing.T, e *echo.Echo) {
repo := repository.New(backend.BackendPool()) repo := repository.New(backend.BackendPool())
games, err := repo.FindAllSoundtracks(backend.BackendCtx()) games, err := repo.FindAllGames(backend.BackendCtx())
assert.NoError(t, err) assert.NoError(t, err)
if len(games) == 0 { if len(games) == 0 {
+3 -13
View File
@@ -80,17 +80,9 @@ run:
@templ generate @templ generate
@go run cmd/main.go @go run cmd/main.go
build-run: build
@go run cmd/main.go
test: build test: build
@echo "Starting test database container..." @echo "Testing..."
@podman-compose -f compose.test.yaml up -d @go test ./... -v
@sleep 10
@echo "Running integration tests..."
@just test-integration
@echo "Stopping test database container..."
@just test-integration-down
# Clean the binary # Clean the binary
clean: clean:
@@ -110,9 +102,7 @@ podman-down:
# Run integration tests with podman # Run integration tests with podman
# Starts a test PostgreSQL container, runs tests, then cleans up # Starts a test PostgreSQL container, runs tests, then cleans up
test-integration: test-integration:
@echo "Cleaning old test database..." @echo "Starting test database container..."
@podman-compose -f compose.test.yaml down -v
@echo "Starting fresh test database container..."
@podman-compose -f compose.test.yaml up -d @podman-compose -f compose.test.yaml up -d
@sleep 10 @sleep 10
@echo "Running integration tests..." @echo "Running integration tests..."