Compare commits
8 Commits
b0418b4f38
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| dbef39b828 | |||
| 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
|
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 2, distribution container
|
# Stage 3: Final image
|
||||||
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 ""
|
||||||
@@ -30,7 +39,4 @@ 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
|
||||||
|
|||||||
+676
-39
@@ -23,6 +23,539 @@ 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",
|
||||||
@@ -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": {
|
"/download": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Checks for the latest version of the application",
|
"description": "Checks for the latest version of the application",
|
||||||
@@ -324,7 +834,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games",
|
"summary": "Get all soundtracks",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -357,7 +867,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games random",
|
"summary": "Get all soundtracks random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -697,10 +1207,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync games with only changes",
|
"summary": "Sync soundtracks with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games",
|
"description": "Start syncing soundtracks",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -729,7 +1239,7 @@ var doc = `{
|
|||||||
"summary": "Sync all games fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games full",
|
"description": "Start syncing soundtracks full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -779,10 +1289,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset games database",
|
"summary": "Reset soundtracks database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Games and songs are deleted from the database",
|
"description": "Soundtracks and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -798,7 +1308,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/version": {
|
"/version": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get string by ID",
|
"description": "get latest version info",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -806,9 +1316,9 @@ var doc = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"accounts"
|
"version"
|
||||||
],
|
],
|
||||||
"summary": "Getting the version of the backend",
|
"summary": "Getting the latest version of the backend",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"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": {
|
"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": {
|
||||||
"$ref": "#/definitions/backend.VersionData"
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|||||||
+676
-39
@@ -4,6 +4,539 @@
|
|||||||
"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",
|
||||||
@@ -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": {
|
"/download": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Checks for the latest version of the application",
|
"description": "Checks for the latest version of the application",
|
||||||
@@ -305,7 +815,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games",
|
"summary": "Get all soundtracks",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -338,7 +848,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games random",
|
"summary": "Get all soundtracks random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -678,10 +1188,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync games with only changes",
|
"summary": "Sync soundtracks with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games",
|
"description": "Start syncing soundtracks",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -710,7 +1220,7 @@
|
|||||||
"summary": "Sync all games fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games full",
|
"description": "Start syncing soundtracks full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -760,10 +1270,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset games database",
|
"summary": "Reset soundtracks database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Games and songs are deleted from the database",
|
"description": "Soundtracks and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -779,7 +1289,7 @@
|
|||||||
},
|
},
|
||||||
"/version": {
|
"/version": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get string by ID",
|
"description": "get latest version info",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -787,9 +1297,9 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"accounts"
|
"version"
|
||||||
],
|
],
|
||||||
"summary": "Getting the version of the backend",
|
"summary": "Getting the latest version of the backend",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"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": {
|
"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": {
|
||||||
"$ref": "#/definitions/backend.VersionData"
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+448
-29
@@ -1,20 +1,433 @@
|
|||||||
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: account name
|
example:
|
||||||
type: string
|
- '["Initial release"'
|
||||||
history:
|
- '"Bug fixes"]'
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/backend.VersionData'
|
type: string
|
||||||
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:
|
||||||
@@ -53,21 +466,6 @@ 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:
|
||||||
@@ -223,7 +621,7 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all games
|
summary: Get all soundtracks
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/all/random:
|
/music/all/random:
|
||||||
@@ -245,7 +643,7 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all games random
|
summary: Get all soundtracks random
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/info:
|
/music/info:
|
||||||
@@ -459,14 +857,14 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing games
|
description: Start syncing soundtracks
|
||||||
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 games with only changes
|
summary: Sync soundtracks with only changes
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/sync/full:
|
/sync/full:
|
||||||
@@ -478,7 +876,7 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing games full
|
description: Start syncing soundtracks full
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
@@ -513,21 +911,21 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Games and songs are deleted from the database
|
description: Soundtracks 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 games database
|
summary: Reset soundtracks database
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/version:
|
/version:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: get string by ID
|
description: get latest version info
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -539,7 +937,28 @@ paths:
|
|||||||
description: Not Found
|
description: Not Found
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Getting the version of the backend
|
summary: Getting the latest version of the backend
|
||||||
tags:
|
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"
|
swagger: "2.0"
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
/* 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;
|
||||||
@@ -10,7 +38,9 @@ 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: #f3f4f6;
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -29,15 +59,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 #9ca3af;
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #e5e7eb;
|
background-color: var(--bg-secondary);
|
||||||
color: #000;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_term:focus {
|
#search_term:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #6b7280;
|
border-color: var(--border-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear {
|
#clear {
|
||||||
@@ -45,23 +75,48 @@ main {
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #f97316;
|
background-color: var(--accent-primary);
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear:hover {
|
#clear:hover {
|
||||||
background-color: #ea580c;
|
background-color: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
#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: #dcfce7;
|
background-color: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-4 {
|
.p-4 {
|
||||||
@@ -69,7 +124,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-md {
|
.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 {
|
.rounded-lg {
|
||||||
|
|||||||
+23
-1
@@ -2,6 +2,7 @@ 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>
|
||||||
@@ -12,8 +13,29 @@ 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');
|
||||||
@@ -26,7 +48,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>{ game }</p>
|
<p class="game-text">{ game }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "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.Game{}
|
games := []repository.Soundtrack{}
|
||||||
|
|
||||||
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.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 42},
|
{SoundtrackName: "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.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "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.GameName == "" {
|
if result.SoundtrackName == "" {
|
||||||
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.GameName == result.GameName {
|
if g.SoundtrackName == result.SoundtrackName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
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) {
|
func TestFindGameByID(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
games []repository.Game
|
games []repository.Soundtrack
|
||||||
gameID int32
|
gameID int32
|
||||||
expected repository.Game
|
expected repository.Soundtrack
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "existing game",
|
name: "existing game",
|
||||||
games: games,
|
games: games,
|
||||||
gameID: 2,
|
gameID: 2,
|
||||||
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-existing game",
|
name: "non-existing game",
|
||||||
games: games,
|
games: games,
|
||||||
gameID: 99,
|
gameID: 99,
|
||||||
expected: repository.Game{},
|
expected: repository.Soundtrack{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Game
|
var result repository.Soundtrack
|
||||||
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.GameName != tt.expected.GameName {
|
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
||||||
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractGameNames(t *testing.T) {
|
func TestExtractSoundtrackNames(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
result = append(result, game.GameName)
|
result = append(result, game.SoundtrackName)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{"Game1", "Game2", "Game3"}
|
expected := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
if len(result) != len(expected) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, v := range result {
|
for i, v := range result {
|
||||||
if v != expected[i] {
|
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"}
|
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 TestShuffleGameNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(games) != len(original) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,9 +195,7 @@ func TestShuffleGameNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
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) {
|
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))
|
||||||
@@ -199,7 +197,7 @@ func syncGamesNew(full bool) {
|
|||||||
|
|
||||||
initRepo()
|
initRepo()
|
||||||
start = time.Now()
|
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))
|
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -224,7 +222,6 @@ func syncGamesNew(full bool) {
|
|||||||
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()
|
||||||
@@ -322,7 +319,7 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if full {
|
if full && status != NewGame {
|
||||||
status = TitleChanged
|
status = TitleChanged
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(gameDir)
|
entries, err := os.ReadDir(gameDir)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"music-server/internal/logging"
|
"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.
|
// 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 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -80,9 +79,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range songs {
|
for _, s := range songs {
|
||||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path)
|
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
|
||||||
VALUES ($1, $2, $3)`,
|
VALUES ($1, $2, $3, $4)`,
|
||||||
s.gameID, s.name, s.path)
|
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
|
||||||
require.NoError(t, err, "Failed to insert song %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
|
var songCount int
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
require.NoError(t, err)
|
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)
|
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||||
@@ -126,7 +125,7 @@ func TestMigrationsStepByStep(t *testing.T) {
|
|||||||
var songCount int
|
var songCount int
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
require.NoError(t, err)
|
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
|
// Verify data integrity: soundtrack_name values
|
||||||
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
m, err := migrate.NewWithDatabaseInstance(
|
m, err := migrate.NewWithDatabaseInstance(
|
||||||
"file://internal/db/migrations",
|
"file://migrations",
|
||||||
"postgres", driver)
|
"postgres", driver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get current version
|
// Get current version
|
||||||
version, _, err := m.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)
|
t.Logf("Current migration version: %d", version)
|
||||||
|
|
||||||
// Apply exactly 'steps' migrations
|
// Apply exactly 'steps' migrations
|
||||||
@@ -237,6 +241,11 @@ func applyMigrations(t *testing.T, host, port, user, password, dbname string, st
|
|||||||
|
|
||||||
// Get new version
|
// Get new version
|
||||||
newVersion, _, err := m.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)
|
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
|
-- Update song primary key
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||||
ALTER TABLE song ADD PRIMARY KEY (soundtrack_id, path);
|
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
|
-- Update song_list table references
|
||||||
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ LIMIT $1;
|
|||||||
-- name: GetStatisticsSummary :one
|
-- name: GetStatisticsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_soundtracks,
|
COUNT(*) as total_soundtracks,
|
||||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
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 never_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(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"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 {
|
type Session struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
IpAddress string `json:"ip_address"`
|
IpAddress string `json:"ip_address"`
|
||||||
@@ -35,7 +27,6 @@ type Song struct {
|
|||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
ID pgtype.Int4 `json:"id"`
|
ID pgtype.Int4 `json:"id"`
|
||||||
Uuid pgtype.UUID `json:"uuid"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SongList struct {
|
type SongList struct {
|
||||||
@@ -47,17 +38,16 @@ type SongList struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Soundtrack struct {
|
type Soundtrack struct {
|
||||||
ID int32 `json:"id"`
|
ID int32 `json:"id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
Added time.Time `json:"added"`
|
Added time.Time `json:"added"`
|
||||||
Deleted *time.Time `json:"deleted"`
|
Deleted *time.Time `json:"deleted"`
|
||||||
LastChanged *time.Time `json:"last_changed"`
|
LastChanged *time.Time `json:"last_changed"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
LastPlayed *time.Time `json:"last_played"`
|
LastPlayed *time.Time `json:"last_played"`
|
||||||
NumberOfSongs int32 `json:"number_of_songs"`
|
NumberOfSongs int32 `json:"number_of_songs"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Uuid pgtype.UUID `json:"uuid"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Vgmq struct {
|
type Vgmq struct {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
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) {
|
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.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -143,7 +142,7 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
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
|
FROM song
|
||||||
WHERE soundtrack_id = $1
|
WHERE soundtrack_id = $1
|
||||||
`
|
`
|
||||||
@@ -165,7 +164,6 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
|||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -178,7 +176,7 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSongById = `-- name: GetSongById :one
|
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) {
|
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.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Uuid,
|
|
||||||
)
|
)
|
||||||
return i, err
|
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, 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) {
|
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.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Uuid,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (q *Queries) ClearSoundtracks(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
|
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
|
FROM soundtrack
|
||||||
WHERE deleted IS NULL
|
WHERE deleted IS NULL
|
||||||
ORDER BY soundtrack_name
|
ORDER BY soundtrack_name
|
||||||
@@ -54,7 +54,6 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
|||||||
&i.LastPlayed,
|
&i.LastPlayed,
|
||||||
&i.NumberOfSongs,
|
&i.NumberOfSongs,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -67,7 +66,7 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
|
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
|
FROM soundtrack
|
||||||
ORDER BY soundtrack_name
|
ORDER BY soundtrack_name
|
||||||
`
|
`
|
||||||
@@ -92,7 +91,6 @@ func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soun
|
|||||||
&i.LastPlayed,
|
&i.LastPlayed,
|
||||||
&i.NumberOfSongs,
|
&i.NumberOfSongs,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -116,7 +114,7 @@ func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSoundtrackById = `-- name: GetSoundtrackById :one
|
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
|
FROM soundtrack
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND deleted IS NULL
|
AND deleted IS NULL
|
||||||
@@ -136,7 +134,6 @@ func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack,
|
|||||||
&i.LastPlayed,
|
&i.LastPlayed,
|
||||||
&i.NumberOfSongs,
|
&i.NumberOfSongs,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.Uuid,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,8 +398,8 @@ 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_soundtracks,
|
||||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
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 never_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(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||||
COALESCE(MAX(times_played), 0)::bigint as max_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)
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -97,10 +108,11 @@ 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
|
||||||
if TestDatabase != nil {
|
// Note: We also don't nil TestDatabase to allow reuse across tests
|
||||||
TestDatabase.Close()
|
// if TestDatabase != nil {
|
||||||
TestDatabase = nil
|
// TestDatabase.Close()
|
||||||
}
|
// TestDatabase = nil
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestClearDatabase clears all data from the test database
|
// 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
|
// 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",
|
||||||
"game",
|
"soundtrack",
|
||||||
|
"vgmq",
|
||||||
|
"sessions",
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -126,9 +141,10 @@ func TestClearDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset sequences
|
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
||||||
_, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
|
var seqErr error
|
||||||
if err != nil {
|
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
||||||
t.Logf("Failed to reset game_id_seq: %v", err)
|
if seqErr != nil {
|
||||||
|
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"music-server/internal/backend"
|
"music-server/internal/backend"
|
||||||
@@ -38,5 +39,11 @@ func (c *CharacterHandler) GetCharacterList(ctx *echo.Context) error {
|
|||||||
// @Router /character [get]
|
// @Router /character [get]
|
||||||
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
|
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
|
||||||
character := ctx.QueryParam("name")
|
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 {
|
type HealthHandler struct {
|
||||||
|
db *db.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthHandler() *HealthHandler {
|
func NewHealthHandler(database *db.Database) *HealthHandler {
|
||||||
return &HealthHandler{}
|
return &HealthHandler{db: database}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheck godoc
|
// HealthCheck godoc
|
||||||
@@ -24,5 +25,5 @@ func NewHealthHandler() *HealthHandler {
|
|||||||
// @Success 200 {string} string "OK"
|
// @Success 200 {string} string "OK"
|
||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
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"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"music-server/internal/db"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHealthCheck verifies the health endpoint returns database status
|
// TestHealthCheck verifies the health endpoint returns database status
|
||||||
func TestHealthCheck(t *testing.T) {
|
func TestHealthCheck(t *testing.T) {
|
||||||
// Setup database
|
|
||||||
db.TestSetupDB(t)
|
|
||||||
defer db.TestTearDownDB(t)
|
|
||||||
|
|
||||||
e := StartTestServer(t)
|
e := StartTestServer(t)
|
||||||
|
// No explicit teardown - handled by StartTestServer's sync.Once
|
||||||
|
|
||||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// ============================================
|
// ============================================
|
||||||
deprecatedMiddleware := middleware.DeprecationMiddleware
|
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||||
|
|
||||||
health := NewHealthHandler()
|
health := NewHealthHandler(s.db)
|
||||||
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
||||||
|
|
||||||
version := NewVersionHandler()
|
version := NewVersionHandler()
|
||||||
@@ -112,10 +112,10 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// ============================================
|
// ============================================
|
||||||
// API v1 Routes with Token Authentication
|
// API v1 Routes with Token Authentication
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Create /api/v1 group
|
// Create /api/v1 group
|
||||||
apiV1 := e.Group("/api/v1")
|
apiV1 := e.Group("/api/v1")
|
||||||
|
|
||||||
// Public endpoints - no token required
|
// Public endpoints - no token required
|
||||||
apiV1.POST("/token", func(c *echo.Context) error {
|
apiV1.POST("/token", func(c *echo.Context) error {
|
||||||
return s.tokenHandler.CreateTokenHandler(c)
|
return s.tokenHandler.CreateTokenHandler(c)
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
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
|
// Verify data via statistics endpoint
|
||||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
@@ -85,9 +90,9 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
|||||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// We inserted 5 soundtracks, so total should be at least 5
|
// After sync with /sync/new, only soundtracks matching filesystem remain
|
||||||
// (there might be existing data)
|
// testMusic has 3 games
|
||||||
require.GreaterOrEqual(t, summary.TotalGames, int64(5))
|
require.Equal(t, int64(3), summary.TotalGames)
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertTestData inserts 5 test soundtracks with songs into the database
|
// insertTestData inserts 5 test soundtracks with songs into the database
|
||||||
@@ -115,8 +120,8 @@ func insertTestData(t *testing.T) {
|
|||||||
for _, st := range soundtracks {
|
for _, st := range soundtracks {
|
||||||
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
||||||
SoundtrackName: st.name,
|
SoundtrackName: st.name,
|
||||||
Path: st.path,
|
Path: st.path,
|
||||||
Hash: "test-hash-" + st.name,
|
Hash: "test-hash-" + st.name,
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Failed to insert soundtrack: %s", 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")
|
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.SyncSoundtracksNewOnlyChanges()
|
go backend.SyncSoundtracksNewOnlyChanges()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
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")
|
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.SyncSoundtracksNewFull()
|
go backend.SyncSoundtracksNewFull()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
|||||||
|
|
||||||
// Initialize database for tests
|
// Initialize database for tests
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
|
|
||||||
// Initialize backend with test database pool
|
// Initialize backend with test database pool
|
||||||
// This ensures BackendRepo() and BackendCtx() are available
|
// This ensures BackendRepo() and BackendCtx() are available
|
||||||
if db.TestDatabase != nil && db.TestDatabase.Pool != nil {
|
if db.TestDatabase != nil && db.TestDatabase.Pool != nil {
|
||||||
@@ -59,8 +59,9 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,13 @@ build-run: build
|
|||||||
@go run cmd/main.go
|
@go run cmd/main.go
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
@echo "Testing..."
|
@echo "Starting test database container..."
|
||||||
@go test ./... -v
|
@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 the binary
|
||||||
clean:
|
clean:
|
||||||
@@ -105,7 +110,9 @@ 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 "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
|
@podman-compose -f compose.test.yaml up -d
|
||||||
@sleep 10
|
@sleep 10
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
|
|||||||
Reference in New Issue
Block a user