Compare commits
7 Commits
b0418b4f38
...
4e5bdc4ee2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e5bdc4ee2 | |||
| 0894d65ec5 | |||
| 4033899a68 | |||
| c6a07e69e7 | |||
| 6d4a034753 | |||
| 24a9111333 | |||
| 6cc014ffa3 |
+17
-11
@@ -1,26 +1,35 @@
|
||||
# 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
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
|
||||
RUN templ generate
|
||||
|
||||
RUN go build -o main cmd/main.go
|
||||
|
||||
# Stage 2, distribution container
|
||||
# Stage 3: Final image
|
||||
FROM golang:1.25-alpine
|
||||
EXPOSE 8080
|
||||
VOLUME /sorted
|
||||
VOLUME /frontend
|
||||
VOLUME /characters
|
||||
|
||||
COPY --from=build_go /app/main .
|
||||
COPY --from=frontend-builder /app/MusicFrontend/dist /frontend
|
||||
COPY ./songs/ ./songs/
|
||||
|
||||
ENV PORT 8080
|
||||
ENV DB_HOST ""
|
||||
ENV DB_PORT ""
|
||||
@@ -30,7 +39,4 @@ ENV DB_NAME ""
|
||||
ENV MUSIC_PATH ""
|
||||
ENV CHARACTERS_PATH ""
|
||||
|
||||
COPY --from=build_go /app/main .
|
||||
COPY ./songs/ ./songs/
|
||||
|
||||
CMD ./main
|
||||
|
||||
+676
-39
@@ -23,6 +23,539 @@ var doc = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Returns the image for a specific character",
|
||||
@@ -81,29 +614,6 @@ 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": {
|
||||
"get": {
|
||||
"description": "Checks for the latest version of the application",
|
||||
@@ -324,7 +834,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -357,7 +867,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -697,10 +1207,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -729,7 +1239,7 @@ var doc = `{
|
||||
"summary": "Sync all games fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -779,10 +1289,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -798,7 +1308,7 @@ var doc = `{
|
||||
},
|
||||
"/version": {
|
||||
"get": {
|
||||
"description": "get string by ID",
|
||||
"description": "get latest version info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -806,9 +1316,9 @@ var doc = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"accounts"
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version of the backend",
|
||||
"summary": "Getting the latest version of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -824,27 +1334,154 @@ 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": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "string",
|
||||
"example": "account name"
|
||||
},
|
||||
"history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.VersionData"
|
||||
}
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[\"Initial release\"",
|
||||
"\"Bug fixes\"]"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
+676
-39
@@ -4,6 +4,539 @@
|
||||
"contact": {}
|
||||
},
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Returns the image for a specific character",
|
||||
@@ -62,29 +595,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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": {
|
||||
"get": {
|
||||
"description": "Checks for the latest version of the application",
|
||||
@@ -305,7 +815,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -338,7 +848,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -678,10 +1188,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -710,7 +1220,7 @@
|
||||
"summary": "Sync all games fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -760,10 +1270,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -779,7 +1289,7 @@
|
||||
},
|
||||
"/version": {
|
||||
"get": {
|
||||
"description": "get string by ID",
|
||||
"description": "get latest version info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -787,9 +1297,9 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"accounts"
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version of the backend",
|
||||
"summary": "Getting the latest version of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -805,27 +1315,154 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "string",
|
||||
"example": "account name"
|
||||
},
|
||||
"history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.VersionData"
|
||||
}
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[\"Initial release\"",
|
||||
"\"Bug fixes\"]"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+448
-29
@@ -1,20 +1,433 @@
|
||||
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:
|
||||
properties:
|
||||
changelog:
|
||||
example: account name
|
||||
type: string
|
||||
history:
|
||||
example:
|
||||
- '["Initial release"'
|
||||
- '"Bug fixes"]'
|
||||
items:
|
||||
$ref: '#/definitions/backend.VersionData'
|
||||
type: string
|
||||
type: array
|
||||
version:
|
||||
example: 1.0.0
|
||||
type: string
|
||||
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:
|
||||
contact: {}
|
||||
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:
|
||||
get:
|
||||
consumes:
|
||||
@@ -53,21 +466,6 @@ paths:
|
||||
summary: Get list of characters
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
@@ -223,7 +621,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games
|
||||
summary: Get all soundtracks
|
||||
tags:
|
||||
- music
|
||||
/music/all/random:
|
||||
@@ -245,7 +643,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games random
|
||||
summary: Get all soundtracks random
|
||||
tags:
|
||||
- music
|
||||
/music/info:
|
||||
@@ -459,14 +857,14 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games
|
||||
description: Start syncing soundtracks
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Sync games with only changes
|
||||
summary: Sync soundtracks with only changes
|
||||
tags:
|
||||
- sync
|
||||
/sync/full:
|
||||
@@ -478,7 +876,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games full
|
||||
description: Start syncing soundtracks full
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
@@ -513,21 +911,21 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Games and songs are deleted from the database
|
||||
description: Soundtracks and songs are deleted from the database
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Reset games database
|
||||
summary: Reset soundtracks database
|
||||
tags:
|
||||
- sync
|
||||
/version:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get string by ID
|
||||
description: get latest version info
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -539,7 +937,28 @@ paths:
|
||||
description: Not Found
|
||||
schema:
|
||||
type: string
|
||||
summary: Getting the version of the backend
|
||||
summary: Getting the latest version of the backend
|
||||
tags:
|
||||
- accounts
|
||||
- version
|
||||
/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"
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
/* 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;
|
||||
margin: 0;
|
||||
@@ -10,7 +38,9 @@ html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.5;
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -29,15 +59,15 @@ main {
|
||||
max-width: 600px;
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #9ca3af;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 0.5rem;
|
||||
background-color: #e5e7eb;
|
||||
color: #000;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#search_term:focus {
|
||||
outline: none;
|
||||
border-color: #6b7280;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
#clear {
|
||||
@@ -45,23 +75,48 @@ main {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f97316;
|
||||
color: #fff;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#clear:hover {
|
||||
background-color: #ea580c;
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
#games-container {
|
||||
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 */
|
||||
.bg-green-100 {
|
||||
background-color: #dcfce7;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
@@ -69,7 +124,7 @@ main {
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color);
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
|
||||
+23
-1
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
templ HelloForm() {
|
||||
@Base() {
|
||||
<button id="dark-mode-toggle">🌙</button>
|
||||
<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"/>
|
||||
<button type="button" id="clear" name="clear">Clear</button>
|
||||
@@ -12,8 +13,29 @@ templ HelloForm() {
|
||||
if (document.readyState == 'complete') {
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
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("search_term").value = "";
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
@@ -26,7 +48,7 @@ templ HelloForm() {
|
||||
templ FoundGames(games []string) {
|
||||
for _, game := range games {
|
||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||
<p>{ game }</p>
|
||||
<p class="game-text">{ game }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
|
||||
// Test the average calculation logic directly without database access
|
||||
func TestCalculateAverage(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
@@ -28,7 +28,7 @@ func TestCalculateAverage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageEmpty(t *testing.T) {
|
||||
games := []repository.Game{}
|
||||
games := []repository.Soundtrack{}
|
||||
|
||||
if len(games) == 0 {
|
||||
result := int32(0)
|
||||
@@ -52,8 +52,8 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageSingle(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 42},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 42},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
@@ -69,10 +69,10 @@ func TestCalculateAverageSingle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRandomGame(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
// Set seed for reproducible tests
|
||||
@@ -80,93 +80,93 @@ func TestGetRandomGame(t *testing.T) {
|
||||
|
||||
result := games[rand.Intn(len(games))]
|
||||
|
||||
if result.GameName == "" {
|
||||
if result.SoundtrackName == "" {
|
||||
t.Error("random game selection returned empty game")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, g := range games {
|
||||
if g.GameName == result.GameName {
|
||||
if g.SoundtrackName == result.SoundtrackName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("random game selection returned game not in list: %v", result.GameName)
|
||||
t.Errorf("random game selection returned game not in list: %v", result.SoundtrackName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindGameByID(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
||||
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
games []repository.Game
|
||||
games []repository.Soundtrack
|
||||
gameID int32
|
||||
expected repository.Game
|
||||
expected repository.Soundtrack
|
||||
}{
|
||||
{
|
||||
name: "existing game",
|
||||
games: games,
|
||||
gameID: 2,
|
||||
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
},
|
||||
{
|
||||
name: "non-existing game",
|
||||
games: games,
|
||||
gameID: 99,
|
||||
expected: repository.Game{},
|
||||
expected: repository.Soundtrack{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result repository.Game
|
||||
var result repository.Soundtrack
|
||||
for _, game := range tt.games {
|
||||
if game.ID == tt.gameID {
|
||||
result = game
|
||||
break
|
||||
}
|
||||
}
|
||||
if result.ID != tt.expected.ID || result.GameName != tt.expected.GameName {
|
||||
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
||||
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGameNames(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
func TestExtractSoundtrackNames(t *testing.T) {
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, game := range games {
|
||||
result = append(result, game.GameName)
|
||||
result = append(result, game.SoundtrackName)
|
||||
}
|
||||
|
||||
expected := []string{"Game1", "Game2", "Game3"}
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("extractGameNames() length = %d, want %d", len(result), len(expected))
|
||||
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected))
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != expected[i] {
|
||||
t.Errorf("extractGameNames()[%d] = %v, want %v", i, v, expected[i])
|
||||
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShuffleGameNames(t *testing.T) {
|
||||
func TestShuffleSoundtrackNames(t *testing.T) {
|
||||
games := []string{"Game1", "Game2", "Game3"}
|
||||
|
||||
// Test that shuffle doesn't lose any elements
|
||||
@@ -181,7 +181,7 @@ func TestShuffleGameNames(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(games) != len(original) {
|
||||
t.Errorf("shuffleGameNames() changed length from %d to %d", len(original), len(games))
|
||||
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(games))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -195,9 +195,7 @@ func TestShuffleGameNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("shuffleGameNames() lost element: %v", orig)
|
||||
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -186,8 +186,6 @@ func SyncSoundtracksNewOnlyChanges() {
|
||||
}
|
||||
|
||||
func syncGamesNew(full bool) {
|
||||
Syncing = true
|
||||
|
||||
musicPath := os.Getenv("MUSIC_PATH")
|
||||
fmt.Printf("dir: %s\n", musicPath)
|
||||
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
||||
@@ -199,7 +197,7 @@ func syncGamesNew(full bool) {
|
||||
|
||||
initRepo()
|
||||
start = time.Now()
|
||||
foldersToSkip := []string{".sync", "dist", "old", "characters"}
|
||||
foldersToSkip := []string{".sync", "characters", "dist", "old"}
|
||||
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
||||
|
||||
var err error
|
||||
@@ -224,7 +222,6 @@ func syncGamesNew(full bool) {
|
||||
if err != nil {
|
||||
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||
defer pool.Release()
|
||||
@@ -322,7 +319,7 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
||||
}
|
||||
}
|
||||
|
||||
if full {
|
||||
if full && status != NewGame {
|
||||
status = TitleChanged
|
||||
}
|
||||
entries, err := os.ReadDir(gameDir)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"music-server/internal/logging"
|
||||
|
||||
@@ -59,6 +60,26 @@ 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.
|
||||
// Uses the existing pool to extract connection details.
|
||||
func (db *Database) RunMigrations() error {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -80,9 +79,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range songs {
|
||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path)
|
||||
VALUES ($1, $2, $3)`,
|
||||
s.gameID, s.name, s.path)
|
||||
_, 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)
|
||||
}
|
||||
|
||||
@@ -95,9 +94,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs")
|
||||
require.Equal(t, 9, songCount, "Expected 9 songs")
|
||||
|
||||
t.Log("✓ Manually inserted 5 games with 8 songs")
|
||||
t.Log("✓ Manually inserted 5 games with 9 songs")
|
||||
})
|
||||
|
||||
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||
@@ -126,7 +125,7 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs after migration")
|
||||
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")
|
||||
@@ -215,13 +214,18 @@ func applyMigrations(t *testing.T, host, port, user, password, dbname string, st
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://internal/db/migrations",
|
||||
"file://migrations",
|
||||
"postgres", driver)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get current version
|
||||
version, _, err := m.Version()
|
||||
require.NoError(t, err)
|
||||
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
|
||||
@@ -237,6 +241,11 @@ func applyMigrations(t *testing.T, host, port, user, password, dbname string, st
|
||||
|
||||
// Get new version
|
||||
newVersion, _, err := m.Version()
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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);
|
||||
ALTER TABLE song RENAME CONSTRAINT song_pkey TO song_pkey_soundtrack;
|
||||
|
||||
-- Update song_list table references
|
||||
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||
|
||||
@@ -138,8 +138,8 @@ LIMIT $1;
|
||||
-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||
|
||||
@@ -10,14 +10,6 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type IDMigrationStatus struct {
|
||||
TableName string `json:"table_name"`
|
||||
TotalRows int32 `json:"total_rows"`
|
||||
MigratedRows int32 `json:"migrated_rows"`
|
||||
Completed bool `json:"completed"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string `json:"token"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
@@ -35,7 +27,6 @@ type Song struct {
|
||||
Hash string `json:"hash"`
|
||||
FileName *string `json:"file_name"`
|
||||
ID pgtype.Int4 `json:"id"`
|
||||
Uuid pgtype.UUID `json:"uuid"`
|
||||
}
|
||||
|
||||
type SongList struct {
|
||||
@@ -47,17 +38,16 @@ type SongList struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
Uuid pgtype.UUID `json:"uuid"`
|
||||
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 {
|
||||
|
||||
@@ -110,7 +110,7 @@ func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int
|
||||
}
|
||||
|
||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid FROM song
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song
|
||||
`
|
||||
|
||||
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
@@ -130,7 +130,6 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -143,7 +142,7 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
}
|
||||
|
||||
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id
|
||||
FROM song
|
||||
WHERE soundtrack_id = $1
|
||||
`
|
||||
@@ -165,7 +164,6 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -178,7 +176,7 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
}
|
||||
|
||||
const getSongById = `-- name: GetSongById :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid FROM song WHERE id = $1
|
||||
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) {
|
||||
@@ -192,13 +190,12 @@ func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error)
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid FROM song WHERE hash = $1
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE hash = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
||||
@@ -212,7 +209,6 @@ func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (q *Queries) ClearSoundtracks(ctx context.Context) error {
|
||||
}
|
||||
|
||||
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
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
|
||||
@@ -54,7 +54,6 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,7 +66,7 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
||||
}
|
||||
|
||||
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||
FROM soundtrack
|
||||
ORDER BY soundtrack_name
|
||||
`
|
||||
@@ -92,7 +91,6 @@ func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soun
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -116,7 +114,7 @@ func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName stri
|
||||
}
|
||||
|
||||
const getSoundtrackById = `-- name: GetSoundtrackById :one
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
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
|
||||
@@ -136,7 +134,6 @@ func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack,
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -398,8 +398,8 @@ func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetO
|
||||
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||
|
||||
@@ -54,8 +54,19 @@ func TestSetupDB(t *testing.T) {
|
||||
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
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -97,10 +108,11 @@ func createTestDatabase(host, port, dbname, user, password string) {
|
||||
// "closed pool" errors when tests run sequentially
|
||||
func TestTearDownDB(t *testing.T) {
|
||||
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
||||
if TestDatabase != nil {
|
||||
TestDatabase.Close()
|
||||
TestDatabase = nil
|
||||
}
|
||||
// Note: We also don't nil TestDatabase to allow reuse across tests
|
||||
// if TestDatabase != nil {
|
||||
// TestDatabase.Close()
|
||||
// TestDatabase = nil
|
||||
// }
|
||||
}
|
||||
|
||||
// TestClearDatabase clears all data from the test database
|
||||
@@ -112,10 +124,13 @@ func TestClearDatabase(t *testing.T) {
|
||||
|
||||
// Clear all tables in reverse order to respect foreign keys
|
||||
// Note: This assumes the tables exist and have the expected structure
|
||||
// After migration 000005, game table was renamed to soundtrack
|
||||
tables := []string{
|
||||
"song_list",
|
||||
"song",
|
||||
"game",
|
||||
"soundtrack",
|
||||
"vgmq",
|
||||
"sessions",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -126,9 +141,10 @@ func TestClearDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset sequences
|
||||
_, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
|
||||
if err != nil {
|
||||
t.Logf("Failed to reset game_id_seq: %v", err)
|
||||
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
||||
var seqErr error
|
||||
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
||||
if seqErr != nil {
|
||||
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"music-server/internal/backend"
|
||||
@@ -38,5 +39,11 @@ func (c *CharacterHandler) GetCharacterList(ctx *echo.Context) error {
|
||||
// @Router /character [get]
|
||||
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
|
||||
character := ctx.QueryParam("name")
|
||||
return ctx.File(backend.GetCharacter(character))
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{}
|
||||
func NewHealthHandler(database *db.Database) *HealthHandler {
|
||||
return &HealthHandler{db: database}
|
||||
}
|
||||
|
||||
// HealthCheck godoc
|
||||
@@ -24,5 +25,5 @@ func NewHealthHandler() *HealthHandler {
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Router /health [get]
|
||||
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, db.Health())
|
||||
return ctx.JSON(http.StatusOK, h.db.Health())
|
||||
}
|
||||
|
||||
@@ -5,18 +5,13 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"music-server/internal/db"
|
||||
|
||||
"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)
|
||||
// No explicit teardown - handled by StartTestServer's sync.Once
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
@@ -63,7 +63,7 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
// ============================================
|
||||
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||
|
||||
health := NewHealthHandler()
|
||||
health := NewHealthHandler(s.db)
|
||||
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
||||
|
||||
version := NewVersionHandler()
|
||||
|
||||
@@ -73,6 +73,11 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
|
||||
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)
|
||||
@@ -85,9 +90,9 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We inserted 5 soundtracks, so total should be at least 5
|
||||
// (there might be existing data)
|
||||
require.GreaterOrEqual(t, summary.TotalGames, int64(5))
|
||||
// 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
|
||||
@@ -115,8 +120,8 @@ func insertTestData(t *testing.T) {
|
||||
for _, st := range soundtracks {
|
||||
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
||||
SoundtrackName: st.name,
|
||||
Path: st.path,
|
||||
Hash: "test-hash-" + st.name,
|
||||
Path: st.path,
|
||||
Hash: "test-hash-" + st.name,
|
||||
})
|
||||
require.NoError(t, err, "Failed to insert soundtrack: %s", st.name)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
logging.GetLogger().Info("Starting sync with only changes")
|
||||
backend.Syncing = true
|
||||
go backend.SyncSoundtracksNewOnlyChanges()
|
||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
||||
}
|
||||
@@ -68,6 +69,7 @@ func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
logging.GetLogger().Info("Starting full sync")
|
||||
backend.Syncing = true
|
||||
go backend.SyncSoundtracksNewFull()
|
||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
||||
|
||||
// Create a Server instance and get its routes
|
||||
s := &Server{
|
||||
db: db.TestDatabase,
|
||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||
db: db.TestDatabase,
|
||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||
statisticsHandler: NewStatisticsHandler(),
|
||||
}
|
||||
handler := s.RegisterRoutes()
|
||||
|
||||
|
||||
@@ -84,8 +84,13 @@ build-run: build
|
||||
@go run cmd/main.go
|
||||
|
||||
test: build
|
||||
@echo "Testing..."
|
||||
@go test ./... -v
|
||||
@echo "Starting test database container..."
|
||||
@podman-compose -f compose.test.yaml up -d
|
||||
@sleep 10
|
||||
@echo "Running integration tests..."
|
||||
@just test-integration
|
||||
@echo "Stopping test database container..."
|
||||
@just test-integration-down
|
||||
|
||||
# Clean the binary
|
||||
clean:
|
||||
@@ -105,7 +110,9 @@ podman-down:
|
||||
# Run integration tests with podman
|
||||
# Starts a test PostgreSQL container, runs tests, then cleans up
|
||||
test-integration:
|
||||
@echo "Starting test database container..."
|
||||
@echo "Cleaning old test database..."
|
||||
@podman-compose -f compose.test.yaml down -v
|
||||
@echo "Starting fresh test database container..."
|
||||
@podman-compose -f compose.test.yaml up -d
|
||||
@sleep 10
|
||||
@echo "Running integration tests..."
|
||||
|
||||
Reference in New Issue
Block a user