Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda60e0c07 | ||
|
|
a8df738108 | ||
|
|
dbef39b828 | ||
|
|
4e5bdc4ee2 | ||
|
|
0894d65ec5 | ||
|
|
4033899a68 | ||
|
|
c6a07e69e7 | ||
|
|
6d4a034753 | ||
|
|
b0418b4f38 | ||
|
|
176848bb6d | ||
|
|
fb387901cf | ||
|
|
0f29c33b1a | ||
|
|
cec408187d | ||
|
|
c60f40d7e3 | ||
|
|
2f407f6eef | ||
|
|
4c2db11cc5 | ||
|
|
06cbad708d | ||
|
|
89e884fae9 | ||
|
|
24a9111333 | ||
|
|
6cc014ffa3 | ||
|
|
8f8b555ea5 |
@@ -0,0 +1,31 @@
|
||||
# Directories
|
||||
.gitea
|
||||
git
|
||||
.idea
|
||||
testMusic
|
||||
testCharacters
|
||||
|
||||
# Files
|
||||
Dockerfile
|
||||
docker-compose*
|
||||
compose.yaml
|
||||
.dockerignore
|
||||
.gitignore
|
||||
*.pprof
|
||||
main
|
||||
conf.yaml
|
||||
output.css
|
||||
tailwindcss
|
||||
.env
|
||||
|
||||
# Node.js (frontend build artifacts)
|
||||
node_modules
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
# Test files
|
||||
*_test.go
|
||||
integration_test.go
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
+13
-12
@@ -1,26 +1,30 @@
|
||||
# Stage 1: Build backend
|
||||
FROM golang:1.25-alpine as build_go
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY *.go ./
|
||||
RUN find . -name "*_test.go" -delete && \
|
||||
find . -name "integration_test.go" -delete
|
||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
|
||||
RUN templ generate
|
||||
|
||||
RUN go build -o main cmd/main.go
|
||||
|
||||
# Stage 2, distribution container
|
||||
# Stage 2: Final image
|
||||
FROM golang:1.25-alpine
|
||||
EXPOSE 8080
|
||||
VOLUME /sorted
|
||||
VOLUME /frontend
|
||||
VOLUME /characters
|
||||
|
||||
COPY --from=build_go /app/main .
|
||||
COPY --from=gitea.sanplex.xyz/sansan/musicfrontend:latest /usr/share/nginx/html /frontend
|
||||
# Generate config.js with empty API_HOSTNAME (relative paths)
|
||||
RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > /frontend/config.js
|
||||
COPY ./songs/ ./songs/
|
||||
|
||||
ENV PORT 8080
|
||||
ENV DB_HOST ""
|
||||
ENV DB_PORT ""
|
||||
@@ -30,7 +34,4 @@ ENV DB_NAME ""
|
||||
ENV MUSIC_PATH ""
|
||||
ENV CHARACTERS_PATH ""
|
||||
|
||||
COPY --from=build_go /app/main .
|
||||
COPY ./songs/ ./songs/
|
||||
|
||||
CMD ./main
|
||||
|
||||
+686
-49
@@ -23,6 +23,539 @@ var doc = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/v1/statistics/soundtracks/last-played": {
|
||||
"get": {
|
||||
"description": "Returns the most recently played soundtracks",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get last played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/least-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N least played soundtracks with their songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get least played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/most-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N most played soundtracks with their songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get most played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/never-played": {
|
||||
"get": {
|
||||
"description": "Returns all soundtracks that have never been played (times_played = 0)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get never played soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/oldest-played": {
|
||||
"get": {
|
||||
"description": "Returns the least recently played soundtracks (that have been played at least once)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get oldest played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/songs/least-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N least played songs with their game info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get least played songs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/songs/most-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N most played songs with their game info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get most played songs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/summary": {
|
||||
"get": {
|
||||
"description": "Returns overall statistics about the music library",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get statistics summary",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/backend.StatisticsSummary"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/token": {
|
||||
"post": {
|
||||
"description": "Returns a new session token for API access",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Create session token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Client type",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.TokenRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.TokenResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletes the current session token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Invalidate session token",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bearer token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/token/cleanup": {
|
||||
"post": {
|
||||
"description": "Removes all expired session tokens from the database",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Cleanup expired sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bearer token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/character": {
|
||||
"get": {
|
||||
"description": "Returns the image for a specific character",
|
||||
@@ -81,29 +614,6 @@ var doc = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/dbtest": {
|
||||
"get": {
|
||||
"description": "Tests the database connection",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"database"
|
||||
],
|
||||
"summary": "Test database connection",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "TestedDB",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/download": {
|
||||
"get": {
|
||||
"description": "Checks for the latest version of the application",
|
||||
@@ -314,7 +824,7 @@ var doc = `{
|
||||
},
|
||||
"/music/all/order": {
|
||||
"get": {
|
||||
"description": "Returns a list of all games in order",
|
||||
"description": "Returns a list of all soundtracks in order",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -324,7 +834,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -347,7 +857,7 @@ var doc = `{
|
||||
},
|
||||
"/music/all/random": {
|
||||
"get": {
|
||||
"description": "Returns a list of all games in random order",
|
||||
"description": "Returns a list of all soundtracks in random order",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -357,7 +867,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -687,7 +1197,7 @@ var doc = `{
|
||||
},
|
||||
"/sync": {
|
||||
"get": {
|
||||
"description": "Starts syncing games with only new changes",
|
||||
"description": "Starts syncing soundtracks with only new changes",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -697,10 +1207,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -716,7 +1226,7 @@ var doc = `{
|
||||
},
|
||||
"/sync/full": {
|
||||
"get": {
|
||||
"description": "Starts a full sync of all games",
|
||||
"description": "Starts a full sync of all soundtracks",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -726,10 +1236,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync all games fully",
|
||||
"summary": "Sync all soundtracks fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -769,7 +1279,7 @@ var doc = `{
|
||||
},
|
||||
"/sync/reset": {
|
||||
"get": {
|
||||
"description": "Resets the games database by deleting all games and songs",
|
||||
"description": "Resets the soundtracks database by deleting all soundtracks and songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -779,10 +1289,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -798,7 +1308,7 @@ var doc = `{
|
||||
},
|
||||
"/version": {
|
||||
"get": {
|
||||
"description": "get string by ID",
|
||||
"description": "get latest version info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -806,9 +1316,9 @@ var doc = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"accounts"
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version of the backend",
|
||||
"summary": "Getting the latest version of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -824,27 +1334,154 @@ var doc = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"backend.VersionData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "string",
|
||||
"example": "account name"
|
||||
},
|
||||
"history": {
|
||||
"/version/history": {
|
||||
"get": {
|
||||
"description": "get version history",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version history of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.VersionData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"backend.SoundtrackWithSongs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"soundtrack_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soundtrack_last_played": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_played": {
|
||||
"type": "integer"
|
||||
},
|
||||
"songs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.SongInfoForStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soundtrack_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"song_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"times_played": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.StatisticsSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avg_soundtrack_plays": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"never_played_soundtracks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"played_soundtracks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_soundtracks": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.VersionData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[\"Initial release\"",
|
||||
"\"Bug fixes\"]"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server.TokenRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_type": {
|
||||
"description": "Optional: \"web\", \"mobile\", \"api\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
+686
-49
@@ -4,6 +4,539 @@
|
||||
"contact": {}
|
||||
},
|
||||
"paths": {
|
||||
"/api/v1/statistics/soundtracks/last-played": {
|
||||
"get": {
|
||||
"description": "Returns the most recently played soundtracks",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get last played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/least-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N least played soundtracks with their songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get least played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/most-played": {
|
||||
"get": {
|
||||
"description": "Returns the top N most played soundtracks with their songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get most played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/never-played": {
|
||||
"get": {
|
||||
"description": "Returns all soundtracks that have never been played (times_played = 0)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get never played soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/soundtracks/oldest-played": {
|
||||
"get": {
|
||||
"description": "Returns the least recently played soundtracks (that have been played at least once)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get oldest played soundtracks",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SoundtrackWithSongs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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 soundtrack 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 soundtrack info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get most played songs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of results (default: 10)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/statistics/summary": {
|
||||
"get": {
|
||||
"description": "Returns overall statistics about the music library",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"statistics"
|
||||
],
|
||||
"summary": "Get statistics summary",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/backend.StatisticsSummary"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/token": {
|
||||
"post": {
|
||||
"description": "Returns a new session token for API access",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Create session token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Client type",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.TokenRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.TokenResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletes the current session token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Invalidate session token",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bearer token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/token/cleanup": {
|
||||
"post": {
|
||||
"description": "Removes all expired session tokens from the database",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Cleanup expired sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bearer token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/character": {
|
||||
"get": {
|
||||
"description": "Returns the image for a specific character",
|
||||
@@ -62,29 +595,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/dbtest": {
|
||||
"get": {
|
||||
"description": "Tests the database connection",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"database"
|
||||
],
|
||||
"summary": "Test database connection",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "TestedDB",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/download": {
|
||||
"get": {
|
||||
"description": "Checks for the latest version of the application",
|
||||
@@ -295,7 +805,7 @@
|
||||
},
|
||||
"/music/all/order": {
|
||||
"get": {
|
||||
"description": "Returns a list of all games in order",
|
||||
"description": "Returns a list of all soundtracks in order",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -305,7 +815,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -328,7 +838,7 @@
|
||||
},
|
||||
"/music/all/random": {
|
||||
"get": {
|
||||
"description": "Returns a list of all games in random order",
|
||||
"description": "Returns a list of all soundtracks in random order",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -338,7 +848,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -668,7 +1178,7 @@
|
||||
},
|
||||
"/sync": {
|
||||
"get": {
|
||||
"description": "Starts syncing games with only new changes",
|
||||
"description": "Starts syncing soundtracks with only new changes",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -678,10 +1188,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -697,7 +1207,7 @@
|
||||
},
|
||||
"/sync/full": {
|
||||
"get": {
|
||||
"description": "Starts a full sync of all games",
|
||||
"description": "Starts a full sync of all soundtracks",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -707,10 +1217,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync all games fully",
|
||||
"summary": "Sync all soundtracks fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -750,7 +1260,7 @@
|
||||
},
|
||||
"/sync/reset": {
|
||||
"get": {
|
||||
"description": "Resets the games database by deleting all games and songs",
|
||||
"description": "Resets the soundtracks database by deleting all soundtracks and songs",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -760,10 +1270,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -779,7 +1289,7 @@
|
||||
},
|
||||
"/version": {
|
||||
"get": {
|
||||
"description": "get string by ID",
|
||||
"description": "get latest version info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -787,9 +1297,9 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"accounts"
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version of the backend",
|
||||
"summary": "Getting the latest version of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -805,27 +1315,154 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"backend.VersionData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "string",
|
||||
"example": "account name"
|
||||
},
|
||||
"history": {
|
||||
"/version/history": {
|
||||
"get": {
|
||||
"description": "get version history",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"version"
|
||||
],
|
||||
"summary": "Getting the version history of the backend",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.VersionData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"backend.SoundtrackWithSongs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"soundtrack_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soundtrack_last_played": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_played": {
|
||||
"type": "integer"
|
||||
},
|
||||
"songs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.SongInfoForStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"soundtrack_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soundtrack_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"song_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"times_played": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.StatisticsSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avg_soundtrack_plays": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"never_played_soundtracks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"played_soundtracks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_soundtrack_plays": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_soundtracks": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend.VersionData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changelog": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[\"Initial release\"",
|
||||
"\"Bug fixes\"]"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server.TokenRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_type": {
|
||||
"description": "Optional: \"web\", \"mobile\", \"api\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+448
-29
@@ -1,20 +1,433 @@
|
||||
definitions:
|
||||
backend.GameWithSongs:
|
||||
properties:
|
||||
game_id:
|
||||
type: integer
|
||||
game_last_played:
|
||||
type: string
|
||||
game_name:
|
||||
type: string
|
||||
game_played:
|
||||
type: integer
|
||||
songs:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
type: object
|
||||
backend.SongInfoForStats:
|
||||
properties:
|
||||
file_name:
|
||||
type: string
|
||||
game_id:
|
||||
type: integer
|
||||
game_name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
song_name:
|
||||
type: string
|
||||
times_played:
|
||||
type: integer
|
||||
type: object
|
||||
backend.StatisticsSummary:
|
||||
properties:
|
||||
avg_game_plays:
|
||||
type: number
|
||||
max_game_plays:
|
||||
type: integer
|
||||
min_game_plays:
|
||||
type: integer
|
||||
never_played_games:
|
||||
type: integer
|
||||
played_games:
|
||||
type: integer
|
||||
total_game_plays:
|
||||
type: integer
|
||||
total_games:
|
||||
type: integer
|
||||
type: object
|
||||
backend.VersionData:
|
||||
properties:
|
||||
changelog:
|
||||
example: account name
|
||||
type: string
|
||||
history:
|
||||
example:
|
||||
- '["Initial release"'
|
||||
- '"Bug fixes"]'
|
||||
items:
|
||||
$ref: '#/definitions/backend.VersionData'
|
||||
type: string
|
||||
type: array
|
||||
version:
|
||||
example: 1.0.0
|
||||
type: string
|
||||
type: object
|
||||
server.TokenRequest:
|
||||
properties:
|
||||
client_type:
|
||||
description: 'Optional: "web", "mobile", "api"'
|
||||
type: string
|
||||
type: object
|
||||
server.TokenResponse:
|
||||
properties:
|
||||
client_type:
|
||||
type: string
|
||||
expires_at:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
/api/v1/statistics/games/last-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the most recently played games
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get last played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/least-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N least played games with their songs
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get least played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/most-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N most played games with their songs
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get most played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/never-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns all games that have never been played (times_played = 0)
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get never played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/oldest-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the least recently played games (that have been played
|
||||
at least once)
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get oldest played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/songs/least-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N least played songs with their game info
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get least played songs
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/songs/most-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N most played songs with their game info
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get most played songs
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/summary:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns overall statistics about the music library
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/backend.StatisticsSummary'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get statistics summary
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/token:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Deletes the current session token
|
||||
parameters:
|
||||
- description: Bearer token
|
||||
in: header
|
||||
name: Authorization
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Invalidate session token
|
||||
tags:
|
||||
- auth
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a new session token for API access
|
||||
parameters:
|
||||
- description: Client type
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/server.TokenRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/server.TokenResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create session token
|
||||
tags:
|
||||
- auth
|
||||
/api/v1/token/cleanup:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Removes all expired session tokens from the database
|
||||
parameters:
|
||||
- description: Bearer token
|
||||
in: header
|
||||
name: Authorization
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Cleanup expired sessions
|
||||
tags:
|
||||
- auth
|
||||
/character:
|
||||
get:
|
||||
consumes:
|
||||
@@ -53,21 +466,6 @@ paths:
|
||||
summary: Get list of characters
|
||||
tags:
|
||||
- characters
|
||||
/dbtest:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Tests the database connection
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: TestedDB
|
||||
schema:
|
||||
type: string
|
||||
summary: Test database connection
|
||||
tags:
|
||||
- database
|
||||
/download:
|
||||
get:
|
||||
consumes:
|
||||
@@ -223,7 +621,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games
|
||||
summary: Get all soundtracks
|
||||
tags:
|
||||
- music
|
||||
/music/all/random:
|
||||
@@ -245,7 +643,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games random
|
||||
summary: Get all soundtracks random
|
||||
tags:
|
||||
- music
|
||||
/music/info:
|
||||
@@ -459,14 +857,14 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games
|
||||
description: Start syncing soundtracks
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Sync games with only changes
|
||||
summary: Sync soundtracks with only changes
|
||||
tags:
|
||||
- sync
|
||||
/sync/full:
|
||||
@@ -478,7 +876,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games full
|
||||
description: Start syncing soundtracks full
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
@@ -513,21 +911,21 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Games and songs are deleted from the database
|
||||
description: Soundtracks and songs are deleted from the database
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Reset games database
|
||||
summary: Reset soundtracks database
|
||||
tags:
|
||||
- sync
|
||||
/version:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get string by ID
|
||||
description: get latest version info
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -539,7 +937,28 @@ paths:
|
||||
description: Not Found
|
||||
schema:
|
||||
type: string
|
||||
summary: Getting the version of the backend
|
||||
summary: Getting the latest version of the backend
|
||||
tags:
|
||||
- accounts
|
||||
- version
|
||||
/version/history:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get version history
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.VersionData'
|
||||
type: array
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
type: string
|
||||
summary: Getting the version history of the backend
|
||||
tags:
|
||||
- version
|
||||
swagger: "2.0"
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
/* Pure CSS styles for Music Search */
|
||||
|
||||
:root {
|
||||
/* Light mode colors (default) */
|
||||
--bg-primary: #f3f4f6;
|
||||
--bg-secondary: #e5e7eb;
|
||||
--bg-tertiary: #dcfce7;
|
||||
--text-primary: #000;
|
||||
--text-secondary: #374151;
|
||||
--border-primary: #9ca3af;
|
||||
--border-focus: #6b7280;
|
||||
--accent-primary: #f97316;
|
||||
--accent-hover: #ea580c;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark mode colors matching frontend */
|
||||
--bg-primary: #555;
|
||||
--bg-secondary: #333;
|
||||
--bg-tertiary: #2a2a2a;
|
||||
--text-primary: #fff;
|
||||
--text-secondary: #ff9c00;
|
||||
--border-primary: #666;
|
||||
--border-focus: #ff9c00;
|
||||
--accent-primary: #ff9c00;
|
||||
--accent-hover: #e68a00;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -10,7 +38,9 @@ html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.5;
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -29,15 +59,15 @@ main {
|
||||
max-width: 600px;
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #9ca3af;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 0.5rem;
|
||||
background-color: #e5e7eb;
|
||||
color: #000;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#search_term:focus {
|
||||
outline: none;
|
||||
border-color: #6b7280;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
#clear {
|
||||
@@ -45,23 +75,48 @@ main {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f97316;
|
||||
color: #fff;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#clear:hover {
|
||||
background-color: #ea580c;
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
#games-container {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.game-text {
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Dark mode toggle */
|
||||
#dark-mode-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#dark-mode-toggle:hover {
|
||||
background-color: var(--border-primary);
|
||||
}
|
||||
|
||||
/* Game result cards */
|
||||
.bg-green-100 {
|
||||
background-color: #dcfce7;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
@@ -69,7 +124,7 @@ main {
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color);
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"music-server/internal/backend"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var games_added []string
|
||||
|
||||
func FindGameWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
search_term := r.FormValue("search_term")
|
||||
|
||||
search(search_term)
|
||||
|
||||
component := FoundGames(games_added)
|
||||
err = component.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
log.Fatalf("Error rendering in FindGameWebHandler: %e", err)
|
||||
}
|
||||
}
|
||||
|
||||
func search(searchText string) {
|
||||
games_added = nil
|
||||
games := backend.GetAllSoundtracks()
|
||||
for _, game := range games {
|
||||
if is_match_exact(searchText, game) {
|
||||
add_game(game)
|
||||
}
|
||||
}
|
||||
for _, game := range games {
|
||||
if is_match_contains(clean_term(searchText), clean_term(game)) {
|
||||
add_game(game)
|
||||
}
|
||||
}
|
||||
for _, game := range games {
|
||||
if is_match_regex(clean_term(searchText), clean_term(game)) {
|
||||
add_game(game)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_exact(search_term string, game_name string) bool {
|
||||
search_term = strings.ToLower(search_term)
|
||||
game_name = strings.ToLower(game_name)
|
||||
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if strings.Contains(game_name, search_term) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_contains(search_term string, game_name string) bool {
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if strings.Contains(game_name, search_term) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_regex(search_term string, game_name string) bool {
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if compile_regex(search_term).MatchString(game_name) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func add_game(game string) {
|
||||
if !check_if_game_exists(game) {
|
||||
games_added = append(games_added, game)
|
||||
}
|
||||
}
|
||||
|
||||
func check_if_game_exists(gameName string) bool {
|
||||
game_exists := false
|
||||
for _, child := range games_added {
|
||||
if child == gameName {
|
||||
game_exists = true
|
||||
}
|
||||
}
|
||||
return game_exists
|
||||
}
|
||||
|
||||
func compile_regex(search_term string) *regexp.Regexp {
|
||||
regText := ".*"
|
||||
for _, letter := range search_term {
|
||||
regText += string(letter) + ".*"
|
||||
}
|
||||
r, _ := regexp.Compile(regText)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func clean_term(term string) string {
|
||||
term = strings.ReplaceAll(term, " ", "")
|
||||
term = strings.ReplaceAll(term, "é", "e")
|
||||
term = strings.ReplaceAll(term, "+", "plus")
|
||||
term = strings.ReplaceAll(term, "&", "and")
|
||||
term = strings.ReplaceAll(term, "'n", "and")
|
||||
return strings.ToLower(term)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package web
|
||||
|
||||
templ HelloForm() {
|
||||
@Base() {
|
||||
<div id="search-container">
|
||||
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
|
||||
<button type="button" id="clear" name="clear">Clear</button>
|
||||
</div>
|
||||
<div id="games-container"></div>
|
||||
<script>
|
||||
document.addEventListener('readystatechange', () => {
|
||||
if (document.readyState == 'complete') {
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
document.getElementById("search_term").focus();
|
||||
}
|
||||
});
|
||||
document.getElementById("clear").addEventListener("click", function (event) {
|
||||
document.getElementById("search_term").value = "";
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
document.getElementById("search_term").focus();
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
templ FoundGames(games []string) {
|
||||
for _, game := range games {
|
||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||
<p>{ game }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"music-server/internal/backend"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var soundtracks_added []string
|
||||
|
||||
// Precomputed data for optimization
|
||||
type SoundtrackData struct {
|
||||
Original string
|
||||
Cleaned string
|
||||
Abbreviation string
|
||||
}
|
||||
|
||||
var (
|
||||
precomputedSoundtracks []SoundtrackData
|
||||
precomputedOnce sync.Once
|
||||
regexCache = make(map[string]*regexp.Regexp)
|
||||
regexCacheMutex sync.Mutex
|
||||
)
|
||||
|
||||
func FindSoundtrackWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
search_term := r.FormValue("search_term")
|
||||
|
||||
search(search_term)
|
||||
|
||||
component := FoundSoundtracks(soundtracks_added)
|
||||
err = component.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
log.Fatalf("Error rendering in FindSoundtrackWebHandler: %e", err)
|
||||
}
|
||||
}
|
||||
|
||||
func search(searchText string) {
|
||||
soundtracks_added = nil
|
||||
|
||||
// Precompute cleaned search term once
|
||||
cleanedSearchTerm := clean_term(searchText)
|
||||
|
||||
// Use precomputed data for efficiency
|
||||
soundtrackData := getPrecomputedSoundtracks()
|
||||
seen := make(map[string]bool) // O(1) duplicate checking
|
||||
|
||||
for _, data := range soundtrackData {
|
||||
// Check exact match (case-insensitive contains)
|
||||
if is_match_exact(searchText, data.Original) && !seen[data.Original] {
|
||||
soundtracks_added = append(soundtracks_added, data.Original)
|
||||
seen[data.Original] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, data := range soundtrackData {
|
||||
// Check contains match with cleaned terms
|
||||
if !seen[data.Original] && is_match_contains(cleanedSearchTerm, data.Cleaned) {
|
||||
soundtracks_added = append(soundtracks_added, data.Original)
|
||||
seen[data.Original] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, data := range soundtrackData {
|
||||
// Check regex match with cleaned terms
|
||||
if !seen[data.Original] && is_match_regex_cached(cleanedSearchTerm, data.Cleaned) {
|
||||
soundtracks_added = append(soundtracks_added, data.Original)
|
||||
seen[data.Original] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_exact(search_term string, soundtrack_name string) bool {
|
||||
search_term = strings.ToLower(search_term)
|
||||
soundtrack_name = strings.ToLower(soundtrack_name)
|
||||
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if strings.Contains(soundtrack_name, search_term) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_contains(search_term string, soundtrack_name string) bool {
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if strings.Contains(soundtrack_name, search_term) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func is_match_regex(search_term string, soundtrack_name string) bool {
|
||||
if search_term == "" {
|
||||
return true
|
||||
} else if compile_regex(search_term).MatchString(soundtrack_name) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func compile_regex(search_term string) *regexp.Regexp {
|
||||
regText := ".*"
|
||||
for _, letter := range search_term {
|
||||
regText += string(letter) + ".*"
|
||||
}
|
||||
r, _ := regexp.Compile(regText)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// is_match_regex_cached uses cached compiled regex patterns for better performance
|
||||
func is_match_regex_cached(search_term string, soundtrack_name string) bool {
|
||||
if search_term == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
regexCacheMutex.Lock()
|
||||
re, exists := regexCache[search_term]
|
||||
if !exists {
|
||||
re = compile_regex(search_term)
|
||||
regexCache[search_term] = re
|
||||
}
|
||||
regexCacheMutex.Unlock()
|
||||
|
||||
return re.MatchString(soundtrack_name)
|
||||
}
|
||||
|
||||
func clean_term(term string) string {
|
||||
term = strings.ReplaceAll(term, " ", "")
|
||||
term = strings.ReplaceAll(term, "é", "e")
|
||||
term = strings.ReplaceAll(term, "+", "plus")
|
||||
term = strings.ReplaceAll(term, "&", "and")
|
||||
term = strings.ReplaceAll(term, "'n", "and")
|
||||
return strings.ToLower(term)
|
||||
}
|
||||
|
||||
// toLower converts a string to lowercase.
|
||||
func toLower(s string) string {
|
||||
return strings.Map(unicode.ToLower, s)
|
||||
}
|
||||
|
||||
// levenshteinWithThreshold calculates Levenshtein distance with early termination and space optimization.
|
||||
// Uses O(min(n,m)) space instead of O(n*m) and stops early if threshold is exceeded.
|
||||
func levenshteinWithThreshold(s, t string, threshold int) int {
|
||||
if threshold < 0 {
|
||||
threshold = 2 // default threshold
|
||||
}
|
||||
|
||||
m, n := len(s), len(t)
|
||||
|
||||
// Quick checks for early termination
|
||||
if m == 0 {
|
||||
return n
|
||||
}
|
||||
if n == 0 {
|
||||
return m
|
||||
}
|
||||
if abs(m-n) > threshold {
|
||||
return threshold + 1 // Can't be within threshold
|
||||
}
|
||||
|
||||
// Use the shorter string for the row to minimize space
|
||||
if m < n {
|
||||
s, t = t, s
|
||||
m, n = n, m
|
||||
}
|
||||
|
||||
// Space optimization: only store two rows
|
||||
prevRow := make([]int, n+1)
|
||||
currRow := make([]int, n+1)
|
||||
|
||||
// Initialize first row
|
||||
for j := 0; j <= n; j++ {
|
||||
prevRow[j] = j
|
||||
}
|
||||
|
||||
for i := 1; i <= m; i++ {
|
||||
currRow[0] = i
|
||||
minInRow := currRow[0]
|
||||
|
||||
for j := 1; j <= n; j++ {
|
||||
if s[i-1] == t[j-1] {
|
||||
currRow[j] = prevRow[j-1]
|
||||
} else {
|
||||
currRow[j] = min3(prevRow[j], currRow[j-1], prevRow[j-1]) + 1
|
||||
}
|
||||
if currRow[j] < minInRow {
|
||||
minInRow = currRow[j]
|
||||
}
|
||||
}
|
||||
|
||||
// Early termination: if minimum in current row exceeds threshold
|
||||
if minInRow > threshold {
|
||||
return threshold + 1
|
||||
}
|
||||
|
||||
// Swap rows for next iteration
|
||||
prevRow, currRow = currRow, prevRow
|
||||
}
|
||||
|
||||
return prevRow[n]
|
||||
}
|
||||
|
||||
// Helper functions for optimized Levenshtein
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func min3(a, b, c int) int {
|
||||
if a < b {
|
||||
if a < c {
|
||||
return a
|
||||
}
|
||||
return c
|
||||
}
|
||||
if b < c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// precomputeSoundtrackData initializes cleaned terms and abbreviations for all soundtracks
|
||||
func precomputeSoundtrackData() {
|
||||
precomputedOnce.Do(func() {
|
||||
soundtracks := backend.GetAllSoundtracks()
|
||||
precomputedSoundtracks = make([]SoundtrackData, len(soundtracks))
|
||||
|
||||
for i, soundtrack := range soundtracks {
|
||||
cleaned := clean_term(soundtrack)
|
||||
precomputedSoundtracks[i] = SoundtrackData{
|
||||
Original: soundtrack,
|
||||
Cleaned: cleaned,
|
||||
Abbreviation: extractAbbreviation(soundtrack),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getPrecomputedSoundtracks ensures precomputed data is available and returns it
|
||||
func getPrecomputedSoundtracks() []SoundtrackData {
|
||||
precomputeSoundtrackData()
|
||||
return precomputedSoundtracks
|
||||
}
|
||||
|
||||
// levenshtein maintains backward compatibility by calling the optimized version
|
||||
func levenshtein(s, t string) int {
|
||||
return levenshteinWithThreshold(s, t, -1) // -1 uses default threshold
|
||||
}
|
||||
|
||||
// extractAbbreviation extracts the first letters of each word in a string.
|
||||
func extractAbbreviation(s string) string {
|
||||
words := strings.Fields(s)
|
||||
abbr := ""
|
||||
for _, word := range words {
|
||||
if len(word) > 0 {
|
||||
abbr += string(word[0])
|
||||
}
|
||||
}
|
||||
return toLower(abbr)
|
||||
}
|
||||
|
||||
// fuzzyFindSubstring checks if the query fuzzy-matches any substring of the item.
|
||||
func fuzzyFindSubstring(query, item string, threshold int) (bool, int) {
|
||||
query = toLower(query)
|
||||
item = toLower(item)
|
||||
queryLen := len(query)
|
||||
itemLen := len(item)
|
||||
if queryLen > itemLen {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Check for exact substring match first (weight: 100)
|
||||
if strings.Contains(item, query) {
|
||||
return true, 100
|
||||
}
|
||||
|
||||
// Check for fuzzy substring match using optimized Levenshtein
|
||||
for i := 0; i <= itemLen-queryLen; i++ {
|
||||
substring := item[i : i+queryLen]
|
||||
distance := levenshteinWithThreshold(query, substring, threshold)
|
||||
if distance <= threshold {
|
||||
// Weight: higher for matches at the start of the string
|
||||
weight := 50 - i // Higher weight for earlier matches
|
||||
return true, weight
|
||||
}
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// fuzzyFind checks if the query matches the item (substring or abbreviation).
|
||||
func fuzzyFind(query string, item string, threshold int) (bool, int) {
|
||||
query = toLower(query)
|
||||
item = toLower(item)
|
||||
|
||||
// Check for substring fuzzy match
|
||||
if matched, weight := fuzzyFindSubstring(query, item, threshold); matched {
|
||||
return true, weight
|
||||
}
|
||||
|
||||
// Check for abbreviation match using optimized Levenshtein
|
||||
abbr := extractAbbreviation(item)
|
||||
distance := levenshteinWithThreshold(query, abbr, threshold)
|
||||
if distance <= threshold {
|
||||
// Weight: higher for exact abbreviation matches
|
||||
weight := 100 - distance*10 // Higher weight for exact matches
|
||||
return true, weight
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// getAdaptiveThreshold returns a threshold based on query length
|
||||
func getAdaptiveThreshold(query string) int {
|
||||
threshold := 2 // Base threshold for minor typos
|
||||
queryLen := len(query)
|
||||
if queryLen > 6 {
|
||||
// For longer queries, allow more tolerance
|
||||
threshold = queryLen / 3
|
||||
if threshold < 2 {
|
||||
threshold = 2
|
||||
} else if threshold > 4 {
|
||||
threshold = 4 // Cap at 4 for very long queries
|
||||
}
|
||||
}
|
||||
return threshold
|
||||
}
|
||||
|
||||
// fuzzySearch performs fuzzy search on soundtracks using the cleaned terms for consistency
|
||||
func fuzzySearch(searchText string) []string {
|
||||
query := clean_term(searchText)
|
||||
threshold := getAdaptiveThreshold(query)
|
||||
|
||||
// Use precomputed data for efficiency
|
||||
soundtrackData := getPrecomputedSoundtracks()
|
||||
|
||||
type match struct {
|
||||
item string
|
||||
weight int
|
||||
}
|
||||
var matches []match
|
||||
seen := make(map[string]bool) // O(1) duplicate checking
|
||||
|
||||
for _, data := range soundtrackData {
|
||||
if matched, weight := fuzzyFind(query, data.Cleaned, threshold); matched {
|
||||
if !seen[data.Original] {
|
||||
matches = append(matches, match{data.Original, weight})
|
||||
seen[data.Original] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by weight (descending)
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
return matches[i].weight > matches[j].weight
|
||||
})
|
||||
|
||||
// Extract just the soundtrack names in order
|
||||
result := make([]string, len(matches))
|
||||
for i, m := range matches {
|
||||
result[i] = m.item
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindSoundtrackFuzzyWebHandler handles fuzzy search requests
|
||||
func FindSoundtrackFuzzyWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
search_term := r.FormValue("search_term")
|
||||
|
||||
results := fuzzySearch(search_term)
|
||||
|
||||
component := FoundSoundtracks(results)
|
||||
err = component.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
log.Fatalf("Error rendering in FindSoundtrackFuzzyWebHandler: %e", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package web
|
||||
|
||||
templ SearchForm() {
|
||||
@Base() {
|
||||
<button id="dark-mode-toggle">🌙</button>
|
||||
<div id="search-container">
|
||||
<input id="search_term" name="search_term" type="text" hx-post="/findfuzzy" hx-trigger="keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
|
||||
<div class="radio-group" style="display: inline-block; margin-left: 10px;">
|
||||
<label><input type="radio" name="search_type" value="fuzzy" checked> Fuzzy</label>
|
||||
<label><input type="radio" name="search_type" value="normal"> Normal</label>
|
||||
</div>
|
||||
<button type="button" id="clear" name="clear">Clear</button>
|
||||
</div>
|
||||
<div id="soundtracks-container"></div>
|
||||
<script>
|
||||
// Get current search type from radio buttons
|
||||
function getSearchType() {
|
||||
return document.querySelector('input[name="search_type"]:checked').value;
|
||||
}
|
||||
|
||||
// Get endpoint based on search type
|
||||
function getSearchEndpoint() {
|
||||
return getSearchType() === 'fuzzy' ? '/findfuzzy' : '/find';
|
||||
}
|
||||
|
||||
// Update search input endpoint
|
||||
function updateSearchEndpoint() {
|
||||
const endpoint = getSearchEndpoint();
|
||||
document.getElementById('search_term').setAttribute('hx-post', endpoint);
|
||||
}
|
||||
|
||||
document.addEventListener('readystatechange', () => {
|
||||
if (document.readyState == 'complete') {
|
||||
// Initialize with fuzzy search (default)
|
||||
htmlx.ajax('POST', '/findfuzzy', '#soundtracks-container');
|
||||
document.getElementById("search_term").focus();
|
||||
|
||||
// Add event listeners for radio buttons
|
||||
document.querySelectorAll('input[name="search_type"]').forEach(radio => {
|
||||
radio.addEventListener('change', updateSearchEndpoint);
|
||||
});
|
||||
|
||||
// Initialize dark mode from localStorage (default to dark)
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
if (savedTheme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.getElementById('dark-mode-toggle').textContent = '☀️';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Dark mode toggle functionality
|
||||
document.getElementById("dark-mode-toggle").addEventListener("click", function() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Update toggle button text
|
||||
this.textContent = newTheme === 'dark' ? '☀️' : '🌙';
|
||||
});
|
||||
|
||||
document.getElementById("clear").addEventListener("click", function (event) {
|
||||
document.getElementById("search_term").value = "";
|
||||
// Use current search type endpoint
|
||||
htmlx.ajax('POST', getSearchEndpoint(), '#soundtracks-container');
|
||||
document.getElementById("search_term").focus();
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
templ FoundSoundtracks(soundtracks []string) {
|
||||
for _, soundtrack := range soundtracks {
|
||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||
<p class="soundtrack-text">{ soundtrack }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"music-server/internal/logging"
|
||||
"go.uber.org/zap"
|
||||
"music-server/internal/logging"
|
||||
)
|
||||
|
||||
func GetCharacterList() []string {
|
||||
@@ -30,10 +30,10 @@ func GetCharacterList() []string {
|
||||
|
||||
func GetCharacter(character string) string {
|
||||
charactersPath := os.Getenv("CHARACTERS_PATH")
|
||||
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath))
|
||||
// Clean the path - remove trailing slashes and then add one for consistency
|
||||
charactersPath = strings.TrimSuffix(charactersPath, "/")
|
||||
charactersPath += "/"
|
||||
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath+character))
|
||||
return charactersPath + character
|
||||
}
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"music-server/internal/db"
|
||||
)
|
||||
|
||||
func TestDB() {
|
||||
db.Testf()
|
||||
}
|
||||
|
||||
type VersionData struct {
|
||||
Version string `json:"version" example:"1.0.0"`
|
||||
Changelog string `json:"changelog" example:"account name"`
|
||||
History []VersionData `json:"history"`
|
||||
}
|
||||
|
||||
func GetVersionHistory() VersionData {
|
||||
data := VersionData{Version: "4.5.0",
|
||||
Changelog: "#1 - Created request to check newest version of the app\n" +
|
||||
"#2 - Added request to download the newest version of the app\n" +
|
||||
"#3 - Added request to check progress during sync\n" +
|
||||
"#4 - Now blocking all request while sync is in progress\n" +
|
||||
"#5 - Implemented ants for thread pooling\n" +
|
||||
"#6 - Changed the sync request to now only start the sync",
|
||||
History: []VersionData{
|
||||
{
|
||||
Version: "4.0.0",
|
||||
Changelog: "Changed framework from gin to Echo\n" +
|
||||
"Reorganized the code\n" +
|
||||
"Implemented sqlc\n" +
|
||||
"Added support to send character images from the server\n" +
|
||||
"Added function to create a new database of no one exists",
|
||||
},
|
||||
{
|
||||
Version: "3.2",
|
||||
Changelog: "Upgraded Go version and the version of all dependencies. Fixed som more bugs.",
|
||||
},
|
||||
{
|
||||
Version: "3.1",
|
||||
Changelog: "Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend.",
|
||||
},
|
||||
{
|
||||
Version: "3.0",
|
||||
Changelog: "Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application.",
|
||||
},
|
||||
{
|
||||
Version: "2.3.0",
|
||||
Changelog: "Images should not be included in the database, removes songs where the path doesn't work.",
|
||||
},
|
||||
{
|
||||
Version: "2.2.0",
|
||||
Changelog: "Changed the structure of the whole application, should be no changes to functionality.",
|
||||
},
|
||||
{
|
||||
Version: "2.1.4",
|
||||
Changelog: "Game list should now be sorted, a new endpoint with the game list in random order have been added.",
|
||||
},
|
||||
{
|
||||
Version: "2.1.3",
|
||||
Changelog: "Added a check to see if song exists before returning it, if not a new song will be picked up.",
|
||||
},
|
||||
{
|
||||
Version: "2.1.2",
|
||||
Changelog: "Added test server to swagger file.",
|
||||
},
|
||||
{
|
||||
Version: "2.1.1",
|
||||
Changelog: "Fixed bug where wrong song was showed as currently played.",
|
||||
},
|
||||
{
|
||||
Version: "2.1.0",
|
||||
Changelog: "Added /addQue to add the last received song to the songQue. " +
|
||||
"Changed /rand and /rand/low to not add song to the que. " +
|
||||
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
|
||||
},
|
||||
{
|
||||
Version: "2.0.3",
|
||||
Changelog: "Another small change that should fix the caching problem.",
|
||||
},
|
||||
{
|
||||
Version: "2.0.2",
|
||||
Changelog: "Hopefully fixed the caching problem with random.",
|
||||
},
|
||||
{
|
||||
Version: "2.0.1",
|
||||
Changelog: "Fixed CORS",
|
||||
},
|
||||
{
|
||||
Version: "2.0.0",
|
||||
Changelog: "Rebuilt the application in Go.",
|
||||
},
|
||||
},
|
||||
}
|
||||
return data
|
||||
}
|
||||
+53
-53
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
type SongInfo struct {
|
||||
Game string `json:"Game"`
|
||||
GamePlayed int32 `json:"GamePlayed"`
|
||||
Soundtrack string `json:"Soundtrack"`
|
||||
SoundtrackPlayed int32 `json:"SoundtrackPlayed"`
|
||||
Song string `json:"Song"`
|
||||
SongPlayed int32 `json:"SongPlayed"`
|
||||
CurrentlyPlaying bool `json:"CurrentlyPlaying"`
|
||||
@@ -22,7 +22,7 @@ type SongInfo struct {
|
||||
|
||||
var currentSong = -1
|
||||
|
||||
var gamesNew []repository.Soundtrack
|
||||
var soundtracksNew []repository.Soundtrack
|
||||
|
||||
var songQueNew []repository.Song
|
||||
|
||||
@@ -37,12 +37,12 @@ func initRepo() {
|
||||
}
|
||||
}
|
||||
|
||||
func getAllGames() []repository.Soundtrack {
|
||||
if len(gamesNew) == 0 {
|
||||
func getAllSoundtracks() []repository.Soundtrack {
|
||||
if len(soundtracksNew) == 0 {
|
||||
initRepo()
|
||||
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||
soundtracksNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||
}
|
||||
return gamesNew
|
||||
return soundtracksNew
|
||||
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func Reset() {
|
||||
songQueNew = nil
|
||||
currentSong = -1
|
||||
initRepo()
|
||||
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||
soundtracksNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||
}
|
||||
|
||||
func AddLatestToQue() {
|
||||
@@ -92,34 +92,34 @@ func SetPlayed(songNumber int) {
|
||||
}
|
||||
|
||||
func GetRandomSong() string {
|
||||
getAllGames()
|
||||
if len(gamesNew) == 0 {
|
||||
getAllSoundtracks()
|
||||
if len(soundtracksNew) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
song := getSongFromList(gamesNew)
|
||||
song := getSongFromList(soundtracksNew)
|
||||
lastFetchedNew = song
|
||||
return song.Path
|
||||
}
|
||||
|
||||
func GetRandomSongLowChance() string {
|
||||
getAllGames()
|
||||
getAllSoundtracks()
|
||||
|
||||
var listOfGames []repository.Soundtrack
|
||||
var listOfSoundtracks []repository.Soundtrack
|
||||
|
||||
var averagePlayed = getAveragePlayed()
|
||||
|
||||
for _, data := range gamesNew {
|
||||
for _, data := range soundtracksNew {
|
||||
timesToAdd := averagePlayed - data.TimesPlayed
|
||||
if timesToAdd <= 0 {
|
||||
listOfGames = append(listOfGames, data)
|
||||
listOfSoundtracks = append(listOfSoundtracks, data)
|
||||
} else {
|
||||
for i := int32(0); i < timesToAdd; i++ {
|
||||
listOfGames = append(listOfGames, data)
|
||||
listOfSoundtracks = append(listOfSoundtracks, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
song := getSongFromList(listOfGames)
|
||||
song := getSongFromList(listOfSoundtracks)
|
||||
|
||||
lastFetchedNew = song
|
||||
return song.Path
|
||||
@@ -127,11 +127,11 @@ func GetRandomSongLowChance() string {
|
||||
}
|
||||
|
||||
func GetRandomSongClassic() string {
|
||||
getAllGames()
|
||||
getAllSoundtracks()
|
||||
|
||||
var listOfAllSongs []repository.Song
|
||||
for _, game := range gamesNew {
|
||||
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||
for _, soundtrack := range soundtracksNew {
|
||||
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), soundtrack.ID)
|
||||
listOfAllSongs = append(listOfAllSongs, songList...)
|
||||
}
|
||||
|
||||
@@ -139,25 +139,25 @@ func GetRandomSongClassic() string {
|
||||
var song repository.Song
|
||||
for !songFound {
|
||||
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
||||
gameData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID)
|
||||
soundtrackData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID)
|
||||
|
||||
if err != nil {
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||
logging.GetLogger().Warn("Song not found, removed from database",
|
||||
zap.String("song", song.SongName),
|
||||
zap.String("game", gameData.SoundtrackName),
|
||||
zap.String("soundtrack", soundtrackData.SoundtrackName),
|
||||
zap.String("filename", *song.FileName))
|
||||
continue
|
||||
}
|
||||
|
||||
//Check if file exists and open
|
||||
openFile, err := os.Open(song.Path)
|
||||
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
|
||||
if err != nil || (song.FileName != nil && soundtrackData.Path+*song.FileName != song.Path) {
|
||||
//File not found
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||
logging.GetLogger().Warn("Song not found, removed from database",
|
||||
zap.String("song", song.SongName),
|
||||
zap.String("game", gameData.SoundtrackName),
|
||||
zap.String("soundtrack", soundtrackData.SoundtrackName),
|
||||
zap.String("filename", *song.FileName))
|
||||
} else {
|
||||
songFound = true
|
||||
@@ -177,11 +177,11 @@ func GetSongInfo() SongInfo {
|
||||
}
|
||||
var currentSongData = songQueNew[currentSong]
|
||||
|
||||
currentGameData := getCurrentGame(currentSongData)
|
||||
currentSoundtrackData := getCurrentSoundtrack(currentSongData)
|
||||
|
||||
return SongInfo{
|
||||
Game: currentGameData.SoundtrackName,
|
||||
GamePlayed: currentGameData.TimesPlayed,
|
||||
Soundtrack: currentSoundtrackData.SoundtrackName,
|
||||
SoundtrackPlayed: currentSoundtrackData.TimesPlayed,
|
||||
Song: currentSongData.SongName,
|
||||
SongPlayed: currentSongData.TimesPlayed,
|
||||
CurrentlyPlaying: true,
|
||||
@@ -193,10 +193,10 @@ func GetPlayedSongs() []SongInfo {
|
||||
var songList []SongInfo
|
||||
|
||||
for i, song := range songQueNew {
|
||||
gameData := getCurrentGame(song)
|
||||
soundtrackData := getCurrentSoundtrack(song)
|
||||
songList = append(songList, SongInfo{
|
||||
Game: gameData.SoundtrackName,
|
||||
GamePlayed: gameData.TimesPlayed,
|
||||
Soundtrack: soundtrackData.SoundtrackName,
|
||||
SoundtrackPlayed: soundtrackData.TimesPlayed,
|
||||
Song: song.SongName,
|
||||
SongPlayed: song.TimesPlayed,
|
||||
CurrentlyPlaying: i == currentSong,
|
||||
@@ -218,21 +218,21 @@ func GetSong(song string) string {
|
||||
}
|
||||
|
||||
func GetAllSoundtracks() []string {
|
||||
getAllGames()
|
||||
getAllSoundtracks()
|
||||
|
||||
var jsonArray []string
|
||||
for _, game := range gamesNew {
|
||||
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||
for _, soundtrack := range soundtracksNew {
|
||||
jsonArray = append(jsonArray, soundtrack.SoundtrackName)
|
||||
}
|
||||
return jsonArray
|
||||
}
|
||||
|
||||
func GetAllSoundtracksRandom() []string {
|
||||
getAllGames()
|
||||
getAllSoundtracks()
|
||||
|
||||
var jsonArray []string
|
||||
for _, game := range gamesNew {
|
||||
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||
for _, soundtrack := range soundtracksNew {
|
||||
jsonArray = append(jsonArray, soundtrack.SoundtrackName)
|
||||
}
|
||||
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
||||
return jsonArray
|
||||
@@ -266,12 +266,12 @@ func GetPreviousSong() string {
|
||||
}
|
||||
}
|
||||
|
||||
func getSongFromList(games []repository.Soundtrack) repository.Song {
|
||||
func getSongFromList(soundtracks []repository.Soundtrack) repository.Song {
|
||||
songFound := false
|
||||
var song repository.Song
|
||||
for !songFound {
|
||||
game := getRandomGame(games)
|
||||
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||
soundtrack := getRandomSoundtrack(soundtracks)
|
||||
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), soundtrack.ID)
|
||||
if len(songs) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -280,12 +280,12 @@ func getSongFromList(games []repository.Soundtrack) repository.Song {
|
||||
|
||||
//Check if file exists and open
|
||||
openFile, err := os.Open(song.Path)
|
||||
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
||||
if err != nil || (song.FileName != nil && soundtrack.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
||||
//File not found
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||
logging.GetLogger().Warn("Song not found, removed from database",
|
||||
zap.String("song", song.SongName),
|
||||
zap.String("game", game.SoundtrackName),
|
||||
zap.String("soundtrack", soundtrack.SoundtrackName),
|
||||
zap.Any("filename", song.FileName))
|
||||
} else {
|
||||
songFound = true
|
||||
@@ -299,24 +299,24 @@ func getSongFromList(games []repository.Soundtrack) repository.Song {
|
||||
return song
|
||||
}
|
||||
|
||||
func getCurrentGame(currentSongData repository.Song) repository.Soundtrack {
|
||||
for _, game := range gamesNew {
|
||||
if game.ID == currentSongData.SoundtrackID {
|
||||
return game
|
||||
func getCurrentSoundtrack(currentSongData repository.Song) repository.Soundtrack {
|
||||
for _, soundtrack := range soundtracksNew {
|
||||
if soundtrack.ID == currentSongData.SoundtrackID {
|
||||
return soundtrack
|
||||
}
|
||||
}
|
||||
return repository.Soundtrack{}
|
||||
}
|
||||
|
||||
func getAveragePlayed() int32 {
|
||||
getAllGames()
|
||||
getAllSoundtracks()
|
||||
var sum int32
|
||||
for _, data := range gamesNew {
|
||||
for _, data := range soundtracksNew {
|
||||
sum += data.TimesPlayed
|
||||
}
|
||||
return sum / int32(len(gamesNew))
|
||||
return sum / int32(len(soundtracksNew))
|
||||
}
|
||||
|
||||
func getRandomGame(listOfGames []repository.Soundtrack) repository.Soundtrack {
|
||||
return listOfGames[rand.Intn(len(listOfGames))]
|
||||
func getRandomSoundtrack(listOfSoundtracks []repository.Soundtrack) repository.Soundtrack {
|
||||
return listOfSoundtracks[rand.Intn(len(listOfSoundtracks))]
|
||||
}
|
||||
|
||||
@@ -9,17 +9,17 @@ import (
|
||||
|
||||
// Test the average calculation logic directly without database access
|
||||
func TestCalculateAverage(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
soundtracks := []repository.Soundtrack{
|
||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
for _, data := range games {
|
||||
for _, data := range soundtracks {
|
||||
sum += data.TimesPlayed
|
||||
}
|
||||
result := sum / int32(len(games))
|
||||
result := sum / int32(len(soundtracks))
|
||||
expected := int32(20)
|
||||
|
||||
if result != expected {
|
||||
@@ -28,9 +28,9 @@ func TestCalculateAverage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageEmpty(t *testing.T) {
|
||||
games := []repository.Game{}
|
||||
soundtracks := []repository.Soundtrack{}
|
||||
|
||||
if len(games) == 0 {
|
||||
if len(soundtracks) == 0 {
|
||||
result := int32(0)
|
||||
expected := int32(0)
|
||||
if result != expected {
|
||||
@@ -40,10 +40,10 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
var sum int32
|
||||
for _, data := range games {
|
||||
for _, data := range soundtracks {
|
||||
sum += data.TimesPlayed
|
||||
}
|
||||
result := sum / int32(len(games))
|
||||
result := sum / int32(len(soundtracks))
|
||||
expected := int32(0)
|
||||
|
||||
if result != expected {
|
||||
@@ -52,152 +52,150 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageSingle(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 42},
|
||||
soundtracks := []repository.Soundtrack{
|
||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 42},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
for _, data := range games {
|
||||
for _, data := range soundtracks {
|
||||
sum += data.TimesPlayed
|
||||
}
|
||||
result := sum / int32(len(games))
|
||||
result := sum / int32(len(soundtracks))
|
||||
expected := int32(42)
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Average calculation with single game = %v, want %v", result, expected)
|
||||
t.Errorf("Average calculation with single soundtrack = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRandomGame(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
func TestGetRandomSoundtrack(t *testing.T) {
|
||||
soundtracks := []repository.Soundtrack{
|
||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
// Set seed for reproducible tests
|
||||
rand.Seed(42)
|
||||
|
||||
result := games[rand.Intn(len(games))]
|
||||
result := soundtracks[rand.Intn(len(soundtracks))]
|
||||
|
||||
if result.GameName == "" {
|
||||
t.Error("random game selection returned empty game")
|
||||
if result.SoundtrackName == "" {
|
||||
t.Error("random soundtrack selection returned empty soundtrack")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, g := range games {
|
||||
if g.GameName == result.GameName {
|
||||
for _, s := range soundtracks {
|
||||
if s.SoundtrackName == result.SoundtrackName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("random game selection returned game not in list: %v", result.GameName)
|
||||
t.Errorf("random soundtrack selection returned soundtrack not in list: %v", result.SoundtrackName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindGameByID(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
||||
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
||||
func TestFindSoundtrackByID(t *testing.T) {
|
||||
soundtracks := []repository.Soundtrack{
|
||||
{ID: 1, SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
||||
{ID: 2, SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
||||
{ID: 3, SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
games []repository.Game
|
||||
gameID int32
|
||||
expected repository.Game
|
||||
soundtracks []repository.Soundtrack
|
||||
soundtrackID int32
|
||||
expected repository.Soundtrack
|
||||
}{
|
||||
{
|
||||
name: "existing game",
|
||||
games: games,
|
||||
gameID: 2,
|
||||
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
name: "existing soundtrack",
|
||||
soundtracks: soundtracks,
|
||||
soundtrackID: 2,
|
||||
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
||||
},
|
||||
{
|
||||
name: "non-existing game",
|
||||
games: games,
|
||||
gameID: 99,
|
||||
expected: repository.Game{},
|
||||
name: "non-existing soundtrack",
|
||||
soundtracks: soundtracks,
|
||||
soundtrackID: 99,
|
||||
expected: repository.Soundtrack{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result repository.Game
|
||||
for _, game := range tt.games {
|
||||
if game.ID == tt.gameID {
|
||||
result = game
|
||||
var result repository.Soundtrack
|
||||
for _, s := range tt.soundtracks {
|
||||
if s.ID == tt.soundtrackID {
|
||||
result = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if result.ID != tt.expected.ID || result.GameName != tt.expected.GameName {
|
||||
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
||||
t.Errorf("findSoundtrackByID() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGameNames(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
func TestExtractSoundtrackNames(t *testing.T) {
|
||||
soundtracks := []repository.Soundtrack{
|
||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, game := range games {
|
||||
result = append(result, game.GameName)
|
||||
for _, s := range soundtracks {
|
||||
result = append(result, s.SoundtrackName)
|
||||
}
|
||||
|
||||
expected := []string{"Game1", "Game2", "Game3"}
|
||||
expected := []string{"Soundtrack1", "Soundtrack2", "Soundtrack3"}
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("extractGameNames() length = %d, want %d", len(result), len(expected))
|
||||
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected))
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != expected[i] {
|
||||
t.Errorf("extractGameNames()[%d] = %v, want %v", i, v, expected[i])
|
||||
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShuffleGameNames(t *testing.T) {
|
||||
games := []string{"Game1", "Game2", "Game3"}
|
||||
func TestShuffleSoundtrackNames(t *testing.T) {
|
||||
soundtracks := []string{"Soundtrack1", "Soundtrack2", "Soundtrack3"}
|
||||
|
||||
// Test that shuffle doesn't lose any elements
|
||||
// We can't test the order since it's random, but we can test length and contents
|
||||
original := make([]string, len(games))
|
||||
copy(original, games)
|
||||
original := make([]string, len(soundtracks))
|
||||
copy(original, soundtracks)
|
||||
|
||||
// Simple shuffle implementation for testing
|
||||
for i := range games {
|
||||
for i := range soundtracks {
|
||||
j := i // In real code this would be random
|
||||
games[i], games[j] = games[j], games[i]
|
||||
soundtracks[i], soundtracks[j] = soundtracks[j], soundtracks[i]
|
||||
}
|
||||
|
||||
if len(games) != len(original) {
|
||||
t.Errorf("shuffleGameNames() changed length from %d to %d", len(original), len(games))
|
||||
if len(soundtracks) != len(original) {
|
||||
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(soundtracks))
|
||||
return
|
||||
}
|
||||
|
||||
// Check all original elements are still present
|
||||
for _, orig := range original {
|
||||
found := false
|
||||
for _, g := range games {
|
||||
if g == orig {
|
||||
for _, s := range soundtracks {
|
||||
if s == orig {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("shuffleGameNames() lost element: %v", orig)
|
||||
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GameWithSongs represents a game with its songs for statistics
|
||||
type GameWithSongs struct {
|
||||
SoundtrackID int32 `json:"game_id"`
|
||||
SoundtrackName string `json:"game_name"`
|
||||
SoundtrackPlayed int32 `json:"game_played"`
|
||||
SoundtrackLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
||||
// SoundtrackWithSongs represents a soundtrack with its songs for statistics
|
||||
type SoundtrackWithSongs struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played,omitempty"`
|
||||
Songs []SongInfoForStats `json:"songs"`
|
||||
}
|
||||
|
||||
// SongInfoForStats represents a song with game info for statistics
|
||||
// SongInfoForStats represents a song with soundtrack info for statistics
|
||||
type SongInfoForStats struct {
|
||||
SoundtrackID int32 `json:"game_id"`
|
||||
SoundtrackName string `json:"game_name"`
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SongName string `json:"song_name"`
|
||||
Path string `json:"path"`
|
||||
TimesPlayed int32 `json:"times_played"`
|
||||
@@ -30,13 +30,13 @@ type SongInfoForStats struct {
|
||||
|
||||
// StatisticsSummary holds overall statistics
|
||||
type StatisticsSummary struct {
|
||||
TotalGames int64 `json:"total_games"`
|
||||
PlayedGames int64 `json:"played_games"`
|
||||
NeverPlayedGames int64 `json:"never_played_games"`
|
||||
TotalGamePlays int64 `json:"total_game_plays"`
|
||||
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||
MaxGamePlays int64 `json:"max_game_plays"`
|
||||
MinGamePlays int64 `json:"min_game_plays"`
|
||||
TotalSoundtracks int64 `json:"total_soundtracks"`
|
||||
PlayedSoundtracks int64 `json:"played_soundtracks"`
|
||||
NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"`
|
||||
TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"`
|
||||
AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"`
|
||||
MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"`
|
||||
MinSoundtrackPlays int64 `json:"min_soundtrack_plays"`
|
||||
}
|
||||
|
||||
// StatisticsHandler manages statistics operations
|
||||
@@ -49,19 +49,19 @@ func NewStatisticsHandler() *StatisticsHandler {
|
||||
return &StatisticsHandler{}
|
||||
}
|
||||
|
||||
// GetMostPlayedGamesWithSongs returns the top N most played games with their songs
|
||||
func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||
// GetMostPlayedSoundtracksWithSongs returns the top N most played soundtracks with their songs
|
||||
func (h *StatisticsHandler) GetMostPlayedSoundtracksWithSongs(limit int32) ([]SoundtrackWithSongs, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
// Get raw results
|
||||
rows, err := queries.GetMostPlayedGamesWithSongs(ctx, limit)
|
||||
rows, err := queries.GetMostPlayedSoundtracksWithSongs(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to GameWithSongs
|
||||
var result []GameWithSongs
|
||||
// Convert to SoundtrackWithSongs
|
||||
var result []SoundtrackWithSongs
|
||||
for _, row := range rows {
|
||||
var songs []SongInfoForStats
|
||||
if row.Songs != nil {
|
||||
@@ -71,7 +71,7 @@ func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWith
|
||||
songs = make([]SongInfoForStats, 0)
|
||||
}
|
||||
}
|
||||
result = append(result, GameWithSongs{
|
||||
result = append(result, SoundtrackWithSongs{
|
||||
SoundtrackID: row.SoundtrackID,
|
||||
SoundtrackName: row.SoundtrackName,
|
||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||
@@ -82,17 +82,17 @@ func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWith
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLeastPlayedGamesWithSongs returns the top N least played games with their songs
|
||||
func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||
// GetLeastPlayedSoundtracksWithSongs returns the top N least played soundtracks with their songs
|
||||
func (h *StatisticsHandler) GetLeastPlayedSoundtracksWithSongs(limit int32) ([]SoundtrackWithSongs, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetLeastPlayedGamesWithSongs(ctx, limit)
|
||||
rows, err := queries.GetLeastPlayedSoundtracksWithSongs(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []GameWithSongs
|
||||
var result []SoundtrackWithSongs
|
||||
for _, row := range rows {
|
||||
var songs []SongInfoForStats
|
||||
if row.Songs != nil {
|
||||
@@ -100,7 +100,7 @@ func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWit
|
||||
songs = make([]SongInfoForStats, 0)
|
||||
}
|
||||
}
|
||||
result = append(result, GameWithSongs{
|
||||
result = append(result, SoundtrackWithSongs{
|
||||
SoundtrackID: row.SoundtrackID,
|
||||
SoundtrackName: row.SoundtrackName,
|
||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||
@@ -111,12 +111,12 @@ func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWit
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetMostPlayedSongsWithGame returns the top N most played songs with their game info
|
||||
func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||
// GetMostPlayedSongsWithSoundtrack returns the top N most played songs with their soundtrack info
|
||||
func (h *StatisticsHandler) GetMostPlayedSongsWithSoundtrack(limit int32) ([]SongInfoForStats, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetMostPlayedSongsWithGame(ctx, limit)
|
||||
rows, err := queries.GetMostPlayedSongsWithSoundtrack(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,12 +135,12 @@ func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoF
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLeastPlayedSongsWithGame returns the top N least played songs with their game info
|
||||
func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||
// GetLeastPlayedSongsWithSoundtrack returns the top N least played songs with their soundtrack info
|
||||
func (h *StatisticsHandler) GetLeastPlayedSongsWithSoundtrack(limit int32) ([]SongInfoForStats, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetLeastPlayedSongsWithGame(ctx, limit)
|
||||
rows, err := queries.GetLeastPlayedSongsWithSoundtrack(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -159,17 +159,17 @@ func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfo
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetNeverPlayedGames returns games that have never been played
|
||||
func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||
// GetNeverPlayedSoundtracks returns soundtracks that have never been played
|
||||
func (h *StatisticsHandler) GetNeverPlayedSoundtracks() ([]SoundtrackWithSongs, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetNeverPlayedGames(ctx)
|
||||
rows, err := queries.GetNeverPlayedSoundtracks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []GameWithSongs
|
||||
var result []SoundtrackWithSongs
|
||||
for _, row := range rows {
|
||||
var songs []SongInfoForStats
|
||||
if row.Songs != nil {
|
||||
@@ -177,7 +177,7 @@ func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||
songs = make([]SongInfoForStats, 0)
|
||||
}
|
||||
}
|
||||
result = append(result, GameWithSongs{
|
||||
result = append(result, SoundtrackWithSongs{
|
||||
SoundtrackID: row.SoundtrackID,
|
||||
SoundtrackName: row.SoundtrackName,
|
||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||
@@ -188,17 +188,17 @@ func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLastPlayedGames returns the most recently played games
|
||||
func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||
// GetLastPlayedSoundtracks returns the most recently played soundtracks
|
||||
func (h *StatisticsHandler) GetLastPlayedSoundtracks(limit int32) ([]SoundtrackWithSongs, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetLastPlayedGames(ctx, limit)
|
||||
rows, err := queries.GetLastPlayedSoundtracks(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []GameWithSongs
|
||||
var result []SoundtrackWithSongs
|
||||
for _, row := range rows {
|
||||
var songs []SongInfoForStats
|
||||
if row.Songs != nil {
|
||||
@@ -206,7 +206,7 @@ func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, er
|
||||
songs = make([]SongInfoForStats, 0)
|
||||
}
|
||||
}
|
||||
result = append(result, GameWithSongs{
|
||||
result = append(result, SoundtrackWithSongs{
|
||||
SoundtrackID: row.SoundtrackID,
|
||||
SoundtrackName: row.SoundtrackName,
|
||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||
@@ -217,17 +217,17 @@ func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetOldestPlayedGames returns the least recently played games
|
||||
func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||
// GetOldestPlayedSoundtracks returns the least recently played soundtracks
|
||||
func (h *StatisticsHandler) GetOldestPlayedSoundtracks(limit int32) ([]SoundtrackWithSongs, error) {
|
||||
queries := BackendRepo()
|
||||
ctx := BackendCtx()
|
||||
|
||||
rows, err := queries.GetOldestPlayedGames(ctx, limit)
|
||||
rows, err := queries.GetOldestPlayedSoundtracks(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []GameWithSongs
|
||||
var result []SoundtrackWithSongs
|
||||
for _, row := range rows {
|
||||
var songs []SongInfoForStats
|
||||
if row.Songs != nil {
|
||||
@@ -235,7 +235,7 @@ func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs,
|
||||
songs = make([]SongInfoForStats, 0)
|
||||
}
|
||||
}
|
||||
result = append(result, GameWithSongs{
|
||||
result = append(result, SoundtrackWithSongs{
|
||||
SoundtrackID: row.SoundtrackID,
|
||||
SoundtrackName: row.SoundtrackName,
|
||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||
@@ -257,13 +257,13 @@ func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
|
||||
}
|
||||
|
||||
return &StatisticsSummary{
|
||||
TotalGames: int64(row.TotalSoundtracks),
|
||||
PlayedGames: int64(row.PlayedSoundtracks),
|
||||
NeverPlayedGames: int64(row.NeverPlayedSoundtracks),
|
||||
TotalGamePlays: int64(row.TotalSoundtrackPlays),
|
||||
AvgGamePlays: float64(row.AvgSoundtrackPlays),
|
||||
MaxGamePlays: int64(row.MaxSoundtrackPlays),
|
||||
MinGamePlays: int64(row.MinSoundtrackPlays),
|
||||
TotalSoundtracks: int64(row.TotalSoundtracks),
|
||||
PlayedSoundtracks: int64(row.PlayedSoundtracks),
|
||||
NeverPlayedSoundtracks: int64(row.NeverPlayedSoundtracks),
|
||||
TotalSoundtrackPlays: int64(row.TotalSoundtrackPlays),
|
||||
AvgSoundtrackPlays: float64(row.AvgSoundtrackPlays),
|
||||
MaxSoundtrackPlays: int64(row.MaxSoundtrackPlays),
|
||||
MinSoundtrackPlays: int64(row.MinSoundtrackPlays),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+143
-170
@@ -30,25 +30,31 @@ var start time.Time
|
||||
var totalTime time.Duration
|
||||
var timeSpent time.Duration
|
||||
|
||||
var allGames []repository.Soundtrack
|
||||
var gamesBeforeSync []repository.Soundtrack
|
||||
var gamesAfterSync []repository.Soundtrack
|
||||
var gamesAdded []string
|
||||
var gamesReAdded []string
|
||||
var gamesChangedTitle map[string]string
|
||||
var gamesChangedContent []string
|
||||
var gamesRemoved []string
|
||||
var allSoundtracks []repository.Soundtrack
|
||||
var soundtracksBeforeSync []repository.Soundtrack
|
||||
var soundtracksAfterSync []repository.Soundtrack
|
||||
var soundtracksAdded []string
|
||||
var soundtracksReAdded []string
|
||||
var soundtracksChangedTitle map[string]string
|
||||
var soundtracksChangedContent []string
|
||||
var soundtracksRemoved []string
|
||||
var catchedErrors []string
|
||||
var brokenSongs []string
|
||||
|
||||
type brokenSong struct {
|
||||
SoundtrackID int32
|
||||
Path string
|
||||
}
|
||||
|
||||
var brokenSongs []brokenSong
|
||||
var pool *ants.Pool
|
||||
var poolSong *ants.Pool
|
||||
|
||||
type SyncResponse struct {
|
||||
GamesAdded []string `json:"games_added"`
|
||||
GamesReAdded []string `json:"games_re_added"`
|
||||
GamesChangedTitle map[string]string `json:"games_changed_title"`
|
||||
GamesChangedContent []string `json:"games_changed_content"`
|
||||
GamesRemoved []string `json:"games_removed"`
|
||||
SoundtracksAdded []string `json:"soundtracks_added"`
|
||||
SoundtracksReAdded []string `json:"soundtracks_re_added"`
|
||||
SoundtracksChangedTitle map[string]string `json:"soundtracks_changed_title"`
|
||||
SoundtracksChangedContent []string `json:"soundtracks_changed_content"`
|
||||
SoundtracksRemoved []string `json:"soundtracks_removed"`
|
||||
CatchedErrors []string `json:"catched_errors"`
|
||||
TotalTime string `json:"total_time"`
|
||||
}
|
||||
@@ -58,24 +64,24 @@ type ProgressResponse struct {
|
||||
TimeSpent string `json:"time_spent"`
|
||||
}
|
||||
|
||||
type GameStatus int
|
||||
type SoundtrackStatus int
|
||||
|
||||
const (
|
||||
NotChanged GameStatus = iota
|
||||
NotChanged SoundtrackStatus = iota
|
||||
TitleChanged
|
||||
GameChanged
|
||||
NewGame
|
||||
SoundtrackChanged
|
||||
NewSoundtrack
|
||||
)
|
||||
|
||||
var statusName = map[GameStatus]string{
|
||||
var statusName = map[SoundtrackStatus]string{
|
||||
NotChanged: "Not changed",
|
||||
TitleChanged: "Title changed",
|
||||
GameChanged: "Game changed",
|
||||
NewGame: "New game",
|
||||
SoundtrackChanged: "Soundtrack changed",
|
||||
NewSoundtrack: "New soundtrack",
|
||||
}
|
||||
|
||||
func (gs GameStatus) String() string {
|
||||
return statusName[gs]
|
||||
func (ss SoundtrackStatus) String() string {
|
||||
return statusName[ss]
|
||||
}
|
||||
|
||||
func ResetDB() {
|
||||
@@ -101,54 +107,54 @@ func SyncProgress() ProgressResponse {
|
||||
|
||||
func SyncResult() SyncResponse {
|
||||
logging.GetLogger().Info("Sync completed",
|
||||
zap.Int("games_before", len(gamesBeforeSync)),
|
||||
zap.Int("games_after", len(gamesAfterSync)))
|
||||
zap.Int("soundtracks_before", len(soundtracksBeforeSync)),
|
||||
zap.Int("soundtracks_after", len(soundtracksAfterSync)))
|
||||
|
||||
if len(gamesAdded) > 0 {
|
||||
logging.GetLogger().Debug("Games added", zap.Strings("games", gamesAdded))
|
||||
if len(soundtracksAdded) > 0 {
|
||||
logging.GetLogger().Debug("Soundtracks added", zap.Strings("soundtracks", soundtracksAdded))
|
||||
}
|
||||
|
||||
if len(gamesReAdded) > 0 {
|
||||
logging.GetLogger().Debug("Games readded", zap.Strings("games", gamesReAdded))
|
||||
if len(soundtracksReAdded) > 0 {
|
||||
logging.GetLogger().Debug("Soundtracks readded", zap.Strings("soundtracks", soundtracksReAdded))
|
||||
}
|
||||
|
||||
if len(gamesChangedTitle) > 0 {
|
||||
logging.GetLogger().Debug("Games with changed title", zap.Any("changes", gamesChangedTitle))
|
||||
if len(soundtracksChangedTitle) > 0 {
|
||||
logging.GetLogger().Debug("Soundtracks with changed title", zap.Any("changes", soundtracksChangedTitle))
|
||||
}
|
||||
|
||||
if len(gamesChangedContent) > 0 {
|
||||
logging.GetLogger().Debug("Games with changed content", zap.Strings("games", gamesChangedContent))
|
||||
if len(soundtracksChangedContent) > 0 {
|
||||
logging.GetLogger().Debug("Soundtracks with changed content", zap.Strings("soundtracks", soundtracksChangedContent))
|
||||
}
|
||||
|
||||
var gamesRemovedTemp []string
|
||||
for _, beforeGame := range gamesBeforeSync {
|
||||
var soundtracksRemovedTemp []string
|
||||
for _, beforeSoundtrack := range soundtracksBeforeSync {
|
||||
var found = false
|
||||
for _, afterGame := range gamesAfterSync {
|
||||
if beforeGame.SoundtrackName == afterGame.SoundtrackName {
|
||||
for _, afterSoundtrack := range soundtracksAfterSync {
|
||||
if beforeSoundtrack.SoundtrackName == afterSoundtrack.SoundtrackName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.SoundtrackName)
|
||||
soundtracksRemovedTemp = append(soundtracksRemovedTemp, beforeSoundtrack.SoundtrackName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, game := range gamesRemovedTemp {
|
||||
for _, soundtrack := range soundtracksRemovedTemp {
|
||||
var found bool = false
|
||||
for key := range gamesChangedTitle {
|
||||
if game == key {
|
||||
for key := range soundtracksChangedTitle {
|
||||
if soundtrack == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
gamesRemoved = append(gamesRemoved, game)
|
||||
soundtracksRemoved = append(soundtracksRemoved, soundtrack)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gamesRemoved) > 0 {
|
||||
logging.GetLogger().Debug("Games removed", zap.Strings("games", gamesRemoved))
|
||||
if len(soundtracksRemoved) > 0 {
|
||||
logging.GetLogger().Debug("Soundtracks removed", zap.Strings("soundtracks", soundtracksRemoved))
|
||||
}
|
||||
|
||||
if len(catchedErrors) > 0 {
|
||||
@@ -159,29 +165,27 @@ func SyncResult() SyncResponse {
|
||||
logging.GetLogger().Info("Sync completed", zap.String("total_time", out.Format("15:04:05.00000")))
|
||||
|
||||
return SyncResponse{
|
||||
GamesAdded: gamesAdded,
|
||||
GamesReAdded: gamesReAdded,
|
||||
GamesChangedTitle: gamesChangedTitle,
|
||||
GamesChangedContent: gamesChangedContent,
|
||||
GamesRemoved: gamesRemoved,
|
||||
SoundtracksAdded: soundtracksAdded,
|
||||
SoundtracksReAdded: soundtracksReAdded,
|
||||
SoundtracksChangedTitle: soundtracksChangedTitle,
|
||||
SoundtracksChangedContent: soundtracksChangedContent,
|
||||
SoundtracksRemoved: soundtracksRemoved,
|
||||
CatchedErrors: catchedErrors,
|
||||
TotalTime: out.Format("15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
func SyncSoundtracksNewFull() {
|
||||
syncGamesNew(true)
|
||||
func SyncSoundtracksFull() {
|
||||
syncSoundtracks(true)
|
||||
Reset()
|
||||
}
|
||||
|
||||
func SyncSoundtracksNewOnlyChanges() {
|
||||
syncGamesNew(false)
|
||||
func SyncSoundtracksOnlyChanges() {
|
||||
syncSoundtracks(false)
|
||||
Reset()
|
||||
}
|
||||
|
||||
func syncGamesNew(full bool) {
|
||||
Syncing = true
|
||||
|
||||
func syncSoundtracks(full bool) {
|
||||
musicPath := os.Getenv("MUSIC_PATH")
|
||||
fmt.Printf("dir: %s\n", musicPath)
|
||||
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
||||
@@ -193,23 +197,23 @@ func syncGamesNew(full bool) {
|
||||
|
||||
initRepo()
|
||||
start = time.Now()
|
||||
foldersToSkip := []string{".sync", "dist", "old", "characters"}
|
||||
foldersToSkip := []string{".sync", "characters", "dist", "old"}
|
||||
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
||||
|
||||
var err error
|
||||
gamesAdded = nil
|
||||
gamesReAdded = nil
|
||||
gamesChangedTitle = nil
|
||||
gamesChangedContent = nil
|
||||
gamesRemoved = nil
|
||||
soundtracksAdded = nil
|
||||
soundtracksReAdded = nil
|
||||
soundtracksChangedTitle = nil
|
||||
soundtracksChangedContent = nil
|
||||
soundtracksRemoved = nil
|
||||
catchedErrors = nil
|
||||
brokenSongs = nil
|
||||
|
||||
gamesBeforeSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||
soundtracksBeforeSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||
handleError("FindAllSoundtracks Before", err, "")
|
||||
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
|
||||
logging.GetLogger().Info("Starting sync", zap.Int("soundtracks_before", len(soundtracksBeforeSync)))
|
||||
|
||||
allGames, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx())
|
||||
allSoundtracks, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx())
|
||||
handleError("GetAllSoundtracksIncludingDeleted", err, "")
|
||||
err = repo.SetSoundtrackDeletionDate(BackendCtx())
|
||||
handleError("SetSoundtrackDeletionDate", err, "")
|
||||
@@ -218,7 +222,6 @@ func syncGamesNew(full bool) {
|
||||
if err != nil {
|
||||
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||
defer pool.Release()
|
||||
@@ -230,13 +233,13 @@ func syncGamesNew(full bool) {
|
||||
for _, dir := range directories {
|
||||
pool.Submit(func() {
|
||||
defer syncWg.Done()
|
||||
syncGameNew(dir, foldersToSkip, musicPath, full)
|
||||
syncSoundtrack(dir, foldersToSkip, musicPath, full)
|
||||
})
|
||||
}
|
||||
syncWg.Wait()
|
||||
checkBrokenSongsNew()
|
||||
checkBrokenSongs()
|
||||
|
||||
gamesAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||
soundtracksAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||
handleError("FindAllSoundtracks After", err, "")
|
||||
|
||||
finished := time.Now()
|
||||
@@ -247,7 +250,7 @@ func syncGamesNew(full bool) {
|
||||
Syncing = false
|
||||
}
|
||||
|
||||
func checkBrokenSongsNew() {
|
||||
func checkBrokenSongs() {
|
||||
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
||||
handleError("FetchAllSongs", err, "")
|
||||
var brokenWg sync.WaitGroup
|
||||
@@ -258,20 +261,22 @@ func checkBrokenSongsNew() {
|
||||
for _, song := range allSongs {
|
||||
poolBroken.Submit(func() {
|
||||
defer brokenWg.Done()
|
||||
checkBrokenSongNew(song)
|
||||
checkBrokenSong(song)
|
||||
})
|
||||
}
|
||||
brokenWg.Wait()
|
||||
err = repo.RemoveBrokenSongs(BackendCtx(), brokenSongs)
|
||||
handleError("RemoveBrokenSongs", err, "")
|
||||
for _, bs := range brokenSongs {
|
||||
err = repo.RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: bs.SoundtrackID, Path: bs.Path})
|
||||
handleError("RemoveBrokenSong", err, "")
|
||||
}
|
||||
}
|
||||
|
||||
func checkBrokenSongNew(song repository.Song) {
|
||||
func checkBrokenSong(song repository.Song) {
|
||||
//Check if file exists and open
|
||||
openFile, err := os.Open(song.Path)
|
||||
if err != nil {
|
||||
//File not found
|
||||
brokenSongs = append(brokenSongs, song.Path)
|
||||
brokenSongs = append(brokenSongs, brokenSong{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
||||
} else {
|
||||
err = openFile.Close()
|
||||
@@ -281,118 +286,86 @@ func checkBrokenSongNew(song repository.Song) {
|
||||
}
|
||||
}
|
||||
|
||||
func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
||||
func syncSoundtrack(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
||||
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
||||
logging.GetLogger().Debug("Syncing game", zap.String("game", file.Name()))
|
||||
gameDir := baseDir + file.Name() + "/"
|
||||
dirHash := getHashForDir(gameDir)
|
||||
logging.GetLogger().Debug("Syncing soundtrack", zap.String("soundtrack", file.Name()))
|
||||
soundtrackDir := baseDir + file.Name() + "/"
|
||||
dirHash := getHashForDir(soundtrackDir)
|
||||
|
||||
var status GameStatus = NewGame
|
||||
var oldGame repository.Soundtrack
|
||||
var status SoundtrackStatus = NewSoundtrack
|
||||
var oldSoundtrack repository.Soundtrack
|
||||
var id int32 = -1
|
||||
|
||||
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
|
||||
//fmt.Printf("Soundtracks before: %d\n", len(soundtracksBeforeSync))
|
||||
|
||||
for _, currentGame := range allGames {
|
||||
oldGame = currentGame
|
||||
//fmt.Printf("%s | %s\n", oldGame.SoundtrackName, oldGame.Hash)
|
||||
if oldGame.SoundtrackName == file.Name() && oldGame.Hash == dirHash {
|
||||
for _, currentSoundtrack := range allSoundtracks {
|
||||
oldSoundtrack = currentSoundtrack
|
||||
//fmt.Printf("%s | %s\n", oldSoundtrack.SoundtrackName, oldSoundtrack.Hash)
|
||||
if oldSoundtrack.SoundtrackName == file.Name() && oldSoundtrack.Hash == dirHash {
|
||||
status = NotChanged
|
||||
id = oldGame.ID
|
||||
//fmt.Printf("Game not changed\n")
|
||||
id = oldSoundtrack.ID
|
||||
//fmt.Printf("Soundtrack not changed\n")
|
||||
break
|
||||
} else if oldGame.SoundtrackName == file.Name() && oldGame.Hash != dirHash {
|
||||
status = GameChanged
|
||||
id = oldGame.ID
|
||||
//fmt.Printf("Game changed\n")
|
||||
} else if oldSoundtrack.SoundtrackName == file.Name() && oldSoundtrack.Hash != dirHash {
|
||||
status = SoundtrackChanged
|
||||
id = oldSoundtrack.ID
|
||||
//fmt.Printf("Soundtrack changed\n")
|
||||
break
|
||||
} else if oldGame.SoundtrackName != file.Name() && oldGame.Hash == dirHash {
|
||||
} else if oldSoundtrack.SoundtrackName != file.Name() && oldSoundtrack.Hash == dirHash {
|
||||
status = TitleChanged
|
||||
id = oldGame.ID
|
||||
id = oldSoundtrack.ID
|
||||
//fmt.Printf("SoundtrackName changed\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if full {
|
||||
if full && status != NewSoundtrack {
|
||||
status = TitleChanged
|
||||
}
|
||||
entries, err := os.ReadDir(gameDir)
|
||||
entries, err := os.ReadDir(soundtrackDir)
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to read game directory", zap.String("path", gameDir), zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to read soundtrack directory", zap.String("path", soundtrackDir), zap.String("error", err.Error()))
|
||||
}
|
||||
switch status {
|
||||
case NewGame:
|
||||
if id != -1 {
|
||||
for _, entry := range entries {
|
||||
fileInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get file info", zap.String("error", err.Error()))
|
||||
continue
|
||||
}
|
||||
id = getIdFromFileNew(fileInfo)
|
||||
if id != -1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash})
|
||||
handleError("InsertSoundtrackWithExistingId", err, "")
|
||||
if err != nil {
|
||||
logging.GetLogger().Debug("Game already exists, removing old ID file",
|
||||
case NewSoundtrack:
|
||||
id = insertSoundtrack(file.Name(), soundtrackDir, dirHash)
|
||||
logging.GetLogger().Debug("New soundtrack detected",
|
||||
zap.Int32("id", id),
|
||||
zap.String("game_dir", gameDir))
|
||||
fileName := gameDir + "/." + strconv.Itoa(int(id)) + ".id"
|
||||
logging.GetLogger().Debug("Removing ID file", zap.String("filename", fileName))
|
||||
|
||||
err := os.Remove(fileName)
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to remove ID file", zap.String("filename", fileName), zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
newDirHash := getHashForDir(gameDir)
|
||||
|
||||
id = insertGameNew(file.Name(), gameDir, newDirHash)
|
||||
}
|
||||
} else {
|
||||
id = insertGameNew(file.Name(), gameDir, dirHash)
|
||||
}
|
||||
logging.GetLogger().Debug("New game detected",
|
||||
zap.Int32("id", id),
|
||||
zap.String("game", file.Name()),
|
||||
zap.String("soundtrack", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
zap.String("status", status.String()))
|
||||
gamesAdded = append(gamesAdded, file.Name())
|
||||
newCheckSongs(entries, gameDir, id)
|
||||
case GameChanged:
|
||||
logging.GetLogger().Debug("Game changed",
|
||||
soundtracksAdded = append(soundtracksAdded, file.Name())
|
||||
checkSongs(entries, soundtrackDir, id)
|
||||
case SoundtrackChanged:
|
||||
logging.GetLogger().Debug("Soundtrack changed",
|
||||
zap.Int32("id", id),
|
||||
zap.String("game", file.Name()),
|
||||
zap.String("soundtrack", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
zap.String("status", status.String()))
|
||||
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
||||
handleError("UpdateSoundtrackHash", err, "")
|
||||
gamesChangedContent = append(gamesChangedContent, file.Name())
|
||||
newCheckSongs(entries, gameDir, id)
|
||||
soundtracksChangedContent = append(soundtracksChangedContent, file.Name())
|
||||
checkSongs(entries, soundtrackDir, id)
|
||||
case TitleChanged:
|
||||
logging.GetLogger().Debug("Game title changed",
|
||||
logging.GetLogger().Debug("Soundtrack title changed",
|
||||
zap.Int32("id", id),
|
||||
zap.String("oldName", oldGame.SoundtrackName),
|
||||
zap.String("oldName", oldSoundtrack.SoundtrackName),
|
||||
zap.String("newName", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
zap.String("status", status.String()))
|
||||
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: gameDir, ID: id})
|
||||
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: soundtrackDir, ID: id})
|
||||
handleError("UpdateSoundtrackName", err, "")
|
||||
newCheckSongs(entries, gameDir, id)
|
||||
if gamesChangedTitle == nil {
|
||||
gamesChangedTitle = make(map[string]string)
|
||||
checkSongs(entries, soundtrackDir, id)
|
||||
if soundtracksChangedTitle == nil {
|
||||
soundtracksChangedTitle = make(map[string]string)
|
||||
}
|
||||
gamesChangedTitle[oldGame.SoundtrackName] = file.Name()
|
||||
soundtracksChangedTitle[oldSoundtrack.SoundtrackName] = file.Name()
|
||||
case NotChanged:
|
||||
var found bool = false
|
||||
for _, beforeGame := range gamesBeforeSync {
|
||||
if dirHash == beforeGame.Hash {
|
||||
for _, beforeSoundtrack := range soundtracksBeforeSync {
|
||||
if dirHash == beforeSoundtrack.Hash {
|
||||
found = true
|
||||
logging.GetLogger().Debug("Game not changed",
|
||||
logging.GetLogger().Debug("Soundtrack not changed",
|
||||
zap.Int32("id", id),
|
||||
zap.String("newName", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
@@ -400,9 +373,9 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
newCheckSongs(entries, gameDir, id)
|
||||
gamesReAdded = append(gamesReAdded, file.Name())
|
||||
logging.GetLogger().Debug("Game added again",
|
||||
checkSongs(entries, soundtrackDir, id)
|
||||
soundtracksReAdded = append(soundtracksReAdded, file.Name())
|
||||
logging.GetLogger().Debug("Soundtrack added again",
|
||||
zap.Int32("id", id),
|
||||
zap.String("newName", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
@@ -410,9 +383,9 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
||||
|
||||
}
|
||||
}
|
||||
logging.GetLogger().Debug("Game sync status",
|
||||
logging.GetLogger().Debug("Soundtrack sync status",
|
||||
zap.Int32("id", id),
|
||||
zap.String("game", file.Name()),
|
||||
zap.String("soundtrack", file.Name()),
|
||||
zap.String("hash", dirHash),
|
||||
zap.String("status", status.String()))
|
||||
err = repo.RemoveSoundtrackDeletionDate(BackendCtx(), id)
|
||||
@@ -425,24 +398,24 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
||||
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
||||
}
|
||||
|
||||
func insertGameNew(name string, path string, hash string) int32 {
|
||||
func insertSoundtrack(name string, path string, hash string) int32 {
|
||||
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
||||
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash})
|
||||
handleError("InsertSoundtrack", err, "")
|
||||
if err != nil {
|
||||
logging.GetLogger().Warn("ID collision detected, resetting sequence")
|
||||
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
||||
logging.GetLogger().Debug("Resetting game ID sequence")
|
||||
logging.GetLogger().Debug("Resetting soundtrack ID sequence")
|
||||
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
||||
handleError("ResetSoundtrackIdSeq", err, "")
|
||||
id = insertGameNew(name, path, hash)
|
||||
id = insertSoundtrack(name, path, hash)
|
||||
}
|
||||
}
|
||||
return id
|
||||
|
||||
}
|
||||
|
||||
func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
|
||||
func checkSongs(entries []os.DirEntry, soundtrackDir string, id int32) int32 {
|
||||
//hasher := md5.New()
|
||||
var numberOfSongs int32
|
||||
numberOfFiles := len(entries)
|
||||
@@ -452,7 +425,7 @@ func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
|
||||
for _, entry := range entries {
|
||||
poolSong.Submit(func() {
|
||||
defer songWg.Done()
|
||||
if newCheckSong(entry, gameDir, id) {
|
||||
if checkSong(entry, soundtrackDir, id) {
|
||||
numberOfSongs++
|
||||
}
|
||||
})
|
||||
@@ -461,7 +434,7 @@ func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
|
||||
return numberOfSongs
|
||||
}
|
||||
|
||||
func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||
func checkSong(entry os.DirEntry, soundtrackDir string, id int32) bool {
|
||||
fileInfo, err := entry.Info()
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
||||
@@ -469,7 +442,7 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||
}
|
||||
|
||||
if isSong(fileInfo) {
|
||||
path := gameDir + entry.Name()
|
||||
path := soundtrackDir + entry.Name()
|
||||
|
||||
songHash := getHashForFile(path)
|
||||
//numberOfSongs++
|
||||
@@ -485,7 +458,7 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||
}
|
||||
}
|
||||
logging.GetLogger().Debug("Song changed",
|
||||
zap.Int32("game_id", id),
|
||||
zap.Int32("soundtrack_id", id),
|
||||
zap.String("path", path),
|
||||
zap.String("song_name", songName),
|
||||
zap.String("song_hash", songHash))
|
||||
@@ -493,10 +466,10 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||
handleError("CheckSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||
if err != nil {
|
||||
count2, err := repo.CheckSong(BackendCtx(), path)
|
||||
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||
if count2 > 0 {
|
||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||
handleError("CheckSongWithHash 2", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||
@@ -508,10 +481,10 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
||||
handleError("UpdateSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||
} else {
|
||||
count2, err := repo.CheckSong(BackendCtx(), path)
|
||||
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||
if count2 > 0 {
|
||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||
} else {
|
||||
err = repo.AddSong(BackendCtx(), repository.AddSongParams{SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
|
||||
@@ -543,8 +516,8 @@ func handleError(funcName string, err error, msg string) {
|
||||
}
|
||||
}
|
||||
|
||||
func getHashForDir(gameDir string) string {
|
||||
directory, _ := directory_checksum.ScanDirectory(gameDir, afero.NewOsFs())
|
||||
func getHashForDir(soundtrackDir string) string {
|
||||
directory, _ := directory_checksum.ScanDirectory(soundtrackDir, afero.NewOsFs())
|
||||
hash, _ := directory.ComputeDirectoryChecksums()
|
||||
|
||||
return hash
|
||||
@@ -565,7 +538,7 @@ func getHashForFile(path string) string {
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func getIdFromFileNew(file os.FileInfo) int32 {
|
||||
func getIdFromFile(file os.FileInfo) int32 {
|
||||
name := file.Name()
|
||||
if !file.IsDir() && strings.HasSuffix(name, ".id") {
|
||||
name = strings.Replace(name, ".id", "", 1)
|
||||
|
||||
@@ -155,9 +155,9 @@ func TestGetIdFromFileNew(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getIdFromFileNew(tt.fileInfo)
|
||||
result := getIdFromFile(tt.fileInfo)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getIdFromFileNew() = %v, want %v", result, tt.expected)
|
||||
t.Errorf("getIdFromFile() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package backend
|
||||
|
||||
type VersionData struct {
|
||||
Version string `json:"version" example:"1.0.0"`
|
||||
Changelog []string `json:"changelog" example:"[\"Initial release\",\"Bug fixes\"]"`
|
||||
}
|
||||
|
||||
var data = []VersionData{
|
||||
{
|
||||
Version: "5.0.0-Beta",
|
||||
Changelog: []string{
|
||||
"#16 - Upgrade Echo framework from v4 to v5",
|
||||
"#17 - Add Zap structured logging framework",
|
||||
"#18 - Add OpenAPI/Swagger documentation",
|
||||
"#19 - Replace Tailwind CSS with pure CSS",
|
||||
"#20 - Change domain from sanplex.tech to sanplex.xyz",
|
||||
"#21 - Refactor handlers into domain-specific files",
|
||||
"#22 - Change VersionData Changelog from string to string array",
|
||||
"#23 - Update all dependencies to latest versions",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "4.5.0",
|
||||
Changelog: []string{
|
||||
"#1 - Created request to check newest version of the app",
|
||||
"#2 - Added request to download the newest version of the app",
|
||||
"#3 - Added request to check progress during sync",
|
||||
"#4 - Now blocking all request while sync is in progress",
|
||||
"#5 - Implemented ants for thread pooling",
|
||||
"#6 - Changed the sync request to now only start the sync",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "4.0.0",
|
||||
Changelog: []string{
|
||||
"Changed framework from gin to Echo",
|
||||
"Reorganized the code",
|
||||
"Implemented sqlc",
|
||||
"Added support to send character images from the server",
|
||||
"Added function to create a new database of no one exists",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "3.2",
|
||||
Changelog: []string{"Upgraded Go version and the version of all dependencies. Fixed som more bugs."},
|
||||
},
|
||||
{
|
||||
Version: "3.1",
|
||||
Changelog: []string{"Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend."},
|
||||
},
|
||||
{
|
||||
Version: "3.0",
|
||||
Changelog: []string{"Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application."},
|
||||
},
|
||||
{
|
||||
Version: "2.3.0",
|
||||
Changelog: []string{"Images should not be included in the database, removes songs where the path doesn't work."},
|
||||
},
|
||||
{
|
||||
Version: "2.2.0",
|
||||
Changelog: []string{"Changed the structure of the whole application, should be no changes to functionality."},
|
||||
},
|
||||
{
|
||||
Version: "2.1.4",
|
||||
Changelog: []string{"Game list should now be sorted, a new endpoint with the game list in random order have been added."},
|
||||
},
|
||||
{
|
||||
Version: "2.1.3",
|
||||
Changelog: []string{"Added a check to see if song exists before returning it, if not a new song will be picked up."},
|
||||
},
|
||||
{
|
||||
Version: "2.1.2",
|
||||
Changelog: []string{"Added test server to swagger file."},
|
||||
},
|
||||
{
|
||||
Version: "2.1.1",
|
||||
Changelog: []string{"Fixed bug where wrong song was showed as currently played."},
|
||||
},
|
||||
{
|
||||
Version: "2.1.0",
|
||||
Changelog: []string{
|
||||
"Added /addQue to add the last received song to the songQue.",
|
||||
"Changed /rand and /rand/low to not add song to the que.",
|
||||
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "2.0.3",
|
||||
Changelog: []string{"Another small change that should fix the caching problem."},
|
||||
},
|
||||
{
|
||||
Version: "2.0.2",
|
||||
Changelog: []string{"Hopefully fixed the caching problem with random."},
|
||||
},
|
||||
{
|
||||
Version: "2.0.1",
|
||||
Changelog: []string{"Fixed CORS"},
|
||||
},
|
||||
{
|
||||
Version: "2.0.0",
|
||||
Changelog: []string{"Rebuilt the application in Go."},
|
||||
},
|
||||
}
|
||||
|
||||
func GetLatestVersion() VersionData {
|
||||
return data[0]
|
||||
}
|
||||
|
||||
func GetVersionHistory() []VersionData {
|
||||
return data
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"music-server/internal/logging"
|
||||
|
||||
@@ -59,6 +60,26 @@ func (db *Database) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// Health checks the health of the database connection by pinging the database.
|
||||
// It returns a map with keys indicating various health statistics.
|
||||
func (db *Database) Health() map[string]string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats := make(map[string]string)
|
||||
|
||||
// Ping the database
|
||||
err := db.Pool.Ping(ctx)
|
||||
if err != nil {
|
||||
stats["status"] = "down"
|
||||
stats["error"] = err.Error()
|
||||
return stats
|
||||
}
|
||||
|
||||
stats["status"] = "up"
|
||||
return stats
|
||||
}
|
||||
|
||||
// RunMigrations runs all pending database migrations to the latest version.
|
||||
// Uses the existing pool to extract connection details.
|
||||
func (db *Database) RunMigrations() error {
|
||||
|
||||
+6
-19
@@ -20,7 +20,9 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TODO: Remove these global variables once test_helpers.go is fully migrated to use Database struct
|
||||
// TODO: DEPRECATED - Remove these global variables once all code is migrated to use Database struct
|
||||
// Use database.go's Database struct instead. These globals remain for backward compatibility
|
||||
// with legacy code paths. New code should use the Database struct from database.go.
|
||||
var Dbpool *pgxpool.Pool
|
||||
var Ctx = context.Background()
|
||||
|
||||
@@ -54,25 +56,10 @@ func CloseDb() {
|
||||
Dbpool.Close()
|
||||
}
|
||||
|
||||
func Testf() {
|
||||
rows, dbErr := Dbpool.Query(Ctx, "select game_name from game")
|
||||
if dbErr != nil {
|
||||
logging.GetLogger().Fatal("Query failed", zap.String("error", dbErr.Error()))
|
||||
}
|
||||
for rows.Next() {
|
||||
var gameName string
|
||||
dbErr = rows.Scan(&gameName)
|
||||
if dbErr != nil {
|
||||
logging.GetLogger().Error("Row scan failed", zap.String("error", dbErr.Error()))
|
||||
}
|
||||
logging.GetLogger().Debug("Game found", zap.String("name", gameName))
|
||||
}
|
||||
}
|
||||
|
||||
func ResetGameIdSeq() {
|
||||
_, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);")
|
||||
func ResetSoundtrackIdSeq() {
|
||||
_, err := Dbpool.Query(Ctx, "SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);")
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to reset game ID sequence", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to reset soundtrack ID sequence", zap.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -12,13 +16,17 @@ import (
|
||||
// TestMigrationsStepByStep tests applying migrations incrementally
|
||||
// Then adding data manually, then completing migrations
|
||||
func TestMigrationsStepByStep(t *testing.T) {
|
||||
host := "localhost"
|
||||
port := "5432"
|
||||
user := "postgres"
|
||||
password := "postgres"
|
||||
host := os.Getenv("DB_HOST")
|
||||
port := os.Getenv("DB_PORT")
|
||||
user := os.Getenv("DB_USERNAME")
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
// Use a unique database name for this test
|
||||
dbname := "music_server_migration_test"
|
||||
|
||||
if host == "" || port == "" || user == "" || password == "" {
|
||||
t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)")
|
||||
}
|
||||
|
||||
// Clean up: drop database if it exists
|
||||
cleanupDB(t, host, port, user, password, dbname)
|
||||
defer cleanupDB(t, host, port, user, password, dbname)
|
||||
@@ -71,9 +79,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range songs {
|
||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path)
|
||||
VALUES ($1, $2, $3)`,
|
||||
s.gameID, s.name, s.path)
|
||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
|
||||
require.NoError(t, err, "Failed to insert song %s", s.name)
|
||||
}
|
||||
|
||||
@@ -86,9 +94,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs")
|
||||
require.Equal(t, 9, songCount, "Expected 9 songs")
|
||||
|
||||
t.Log("✓ Manually inserted 5 games with 8 songs")
|
||||
t.Log("✓ Manually inserted 5 games with 9 songs")
|
||||
})
|
||||
|
||||
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||
@@ -117,7 +125,7 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs after migration")
|
||||
require.Equal(t, 9, songCount, "Expected 9 songs after migration")
|
||||
|
||||
// Verify data integrity: soundtrack_name values
|
||||
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
|
||||
@@ -193,17 +201,51 @@ func createTestDB(t *testing.T, host, port, user, password, dbname string) {
|
||||
}
|
||||
}
|
||||
|
||||
// applyMigrations applies n migrations to the database
|
||||
// applyMigrations applies n migrations to the database using Go migrate library
|
||||
func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) {
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, user, password, dbname)
|
||||
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
||||
user, password, host, port, dbname)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
db, err := sql.Open("postgres", migrationURL)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
// This is a simplified version - in a real test you'd use the migrate library
|
||||
// For now, we'll just log that migrations should be applied
|
||||
t.Logf("Note: To fully test migrations, configure test DB and use migrate library")
|
||||
t.Logf("Would apply %d migration(s) to database: %s", steps, dbname)
|
||||
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://migrations",
|
||||
"postgres", driver)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get current version
|
||||
version, _, err := m.Version()
|
||||
if err != nil && err != migrate.ErrNilVersion {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if err == migrate.ErrNilVersion {
|
||||
version = 0
|
||||
}
|
||||
t.Logf("Current migration version: %d", version)
|
||||
|
||||
// Apply exactly 'steps' migrations
|
||||
if steps > 0 {
|
||||
err = m.Steps(steps)
|
||||
if err != nil && err != migrate.ErrNoChange {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
} else if steps < 0 {
|
||||
err = m.Steps(steps)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Get new version
|
||||
newVersion, _, err := m.Version()
|
||||
if err != nil && err != migrate.ErrNilVersion {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if err == migrate.ErrNilVersion {
|
||||
newVersion = 0
|
||||
}
|
||||
t.Logf("Migration version after applying %d steps: %d", steps, newVersion)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ ALTER TABLE song RENAME COLUMN game_id TO soundtrack_id;
|
||||
-- Update song primary key
|
||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||
ALTER TABLE song ADD PRIMARY KEY (soundtrack_id, path);
|
||||
ALTER TABLE song RENAME CONSTRAINT song_pkey TO song_pkey_soundtrack;
|
||||
|
||||
-- Update song_list table references
|
||||
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Rollback: Remove id column and restore composite PK
|
||||
|
||||
-- Step 1: Drop indexes created in up migration
|
||||
DROP INDEX IF EXISTS idx_song_soundtrack_id;
|
||||
DROP INDEX IF EXISTS idx_song_path;
|
||||
|
||||
-- Step 2: Drop foreign key constraint
|
||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||
|
||||
-- Step 3: Drop new primary key
|
||||
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||
|
||||
-- Step 4: Drop unique constraint on id
|
||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_id_unique;
|
||||
|
||||
-- Step 5: Restore composite primary key
|
||||
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (soundtrack_id, path);
|
||||
|
||||
-- Step 6: Drop the id column
|
||||
ALTER TABLE song DROP COLUMN id;
|
||||
|
||||
-- Step 7: Recreate original foreign key (soundtrack_id references soundtrack.id)
|
||||
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||
@@ -0,0 +1,36 @@
|
||||
-- Migration: Add id column to song table and change PK from composite to single column
|
||||
-- This prepares the song table for eventual UUID migration
|
||||
|
||||
-- Step 1: Add new id column (nullable initially)
|
||||
ALTER TABLE song ADD COLUMN id serial4;
|
||||
|
||||
-- Step 2: Create unique constraint on id (allows backfilling)
|
||||
ALTER TABLE song ADD CONSTRAINT song_id_unique UNIQUE (id);
|
||||
|
||||
-- Step 3: Backfill existing rows with sequential IDs
|
||||
-- Use DEFAULT which pulls from the sequence
|
||||
UPDATE song SET id = DEFAULT WHERE id IS NULL;
|
||||
|
||||
-- Step 4: Verify all rows have an id
|
||||
-- If this returns 0, backfill worked
|
||||
-- SELECT COUNT(*) FROM song WHERE id IS NULL;
|
||||
|
||||
-- Step 5: Drop the composite primary key (soundtrack_id, path)
|
||||
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||
|
||||
-- Step 6: Add new primary key on id column
|
||||
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (id);
|
||||
|
||||
-- Step 7: Ensure soundtrack_id remains a foreign key to soundtrack
|
||||
-- First drop existing FK if it exists (from the rename migration)
|
||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||
|
||||
-- Then recreate it
|
||||
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||
|
||||
-- Step 8: Create index on soundtrack_id for query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_id ON song(soundtrack_id);
|
||||
|
||||
-- Step 9: Create index on path for lookups (previously part of PK)
|
||||
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
||||
@@ -8,7 +8,7 @@ DELETE FROM song WHERE soundtrack_id = $1;
|
||||
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
||||
|
||||
-- name: CheckSong :one
|
||||
SELECT COUNT(*) FROM song WHERE path = $1;
|
||||
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||
|
||||
-- name: CheckSongWithHash :one
|
||||
SELECT COUNT(*) FROM song WHERE hash = $1;
|
||||
@@ -20,7 +20,7 @@ SELECT * FROM song WHERE hash = $1;
|
||||
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
||||
|
||||
-- name: AddHashToSong :exec
|
||||
UPDATE song SET hash=$1 where path=$2;
|
||||
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3;
|
||||
|
||||
-- name: FindSongsFromSoundtrack :many
|
||||
SELECT *
|
||||
@@ -34,8 +34,11 @@ WHERE soundtrack_id = $1 AND song_name = $2;
|
||||
-- name: FetchAllSongs :many
|
||||
SELECT * FROM song;
|
||||
|
||||
-- name: GetSongById :one
|
||||
SELECT * FROM song WHERE id = $1;
|
||||
|
||||
-- name: RemoveBrokenSong :exec
|
||||
DELETE FROM song WHERE path = $1;
|
||||
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||
|
||||
-- name: RemoveBrokenSongs :exec
|
||||
DELETE FROM song where path = any (sqlc.slice('paths'));
|
||||
DELETE FROM song WHERE id = ANY($1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Most played soundtracks with their songs
|
||||
-- name: GetMostPlayedGamesWithSongs :many
|
||||
-- name: GetMostPlayedSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -21,7 +21,7 @@ ORDER BY g.times_played DESC, g.soundtrack_name
|
||||
LIMIT $1;
|
||||
|
||||
-- Least played soundtracks with their songs
|
||||
-- name: GetLeastPlayedGamesWithSongs :many
|
||||
-- name: GetLeastPlayedSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -43,7 +43,7 @@ ORDER BY g.times_played ASC, g.soundtrack_name
|
||||
LIMIT $1;
|
||||
|
||||
-- Most played songs with their soundtrack info
|
||||
-- name: GetMostPlayedSongsWithGame :many
|
||||
-- name: GetMostPlayedSongsWithSoundtrack :many
|
||||
SELECT
|
||||
s.soundtrack_id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -58,7 +58,7 @@ ORDER BY s.times_played DESC, s.song_name
|
||||
LIMIT $1;
|
||||
|
||||
-- Least played songs with their soundtrack info
|
||||
-- name: GetLeastPlayedSongsWithGame :many
|
||||
-- name: GetLeastPlayedSongsWithSoundtrack :many
|
||||
SELECT
|
||||
s.soundtrack_id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -72,8 +72,8 @@ WHERE g.deleted IS NULL
|
||||
ORDER BY s.times_played ASC, s.song_name
|
||||
LIMIT $1;
|
||||
|
||||
-- Games that have never been played (times_played = 0)
|
||||
-- name: GetNeverPlayedGames :many
|
||||
-- Soundtracks that have never been played (times_played = 0)
|
||||
-- name: GetNeverPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -93,7 +93,7 @@ GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
||||
ORDER BY g.soundtrack_name;
|
||||
|
||||
-- Last played soundtracks (most recently played)
|
||||
-- name: GetLastPlayedGames :many
|
||||
-- name: GetLastPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -114,7 +114,7 @@ ORDER BY g.last_played DESC
|
||||
LIMIT $1;
|
||||
|
||||
-- Oldest played soundtracks (least recently played, but has been played at least once)
|
||||
-- name: GetOldestPlayedGames :many
|
||||
-- name: GetOldestPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -138,8 +138,8 @@ LIMIT $1;
|
||||
-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||
|
||||
@@ -26,6 +26,7 @@ type Song struct {
|
||||
TimesPlayed int32 `json:"times_played"`
|
||||
Hash string `json:"hash"`
|
||||
FileName *string `json:"file_name"`
|
||||
ID pgtype.Int4 `json:"id"`
|
||||
}
|
||||
|
||||
type SongList struct {
|
||||
|
||||
@@ -7,19 +7,22 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const addHashToSong = `-- name: AddHashToSong :exec
|
||||
UPDATE song SET hash=$1 where path=$2
|
||||
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3
|
||||
`
|
||||
|
||||
type AddHashToSongParams struct {
|
||||
Hash string `json:"hash"`
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
||||
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
|
||||
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.SoundtrackID, arg.Path)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -62,11 +65,16 @@ func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) er
|
||||
}
|
||||
|
||||
const checkSong = `-- name: CheckSong :one
|
||||
SELECT COUNT(*) FROM song WHERE path = $1
|
||||
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||
`
|
||||
|
||||
func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, checkSong, path)
|
||||
type CheckSongParams struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (q *Queries) CheckSong(ctx context.Context, arg CheckSongParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, checkSong, arg.SoundtrackID, arg.Path)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
@@ -102,7 +110,7 @@ func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int
|
||||
}
|
||||
|
||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name 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) {
|
||||
@@ -121,6 +129,7 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
&i.TimesPlayed,
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,7 +142,7 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
}
|
||||
|
||||
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id
|
||||
FROM song
|
||||
WHERE soundtrack_id = $1
|
||||
`
|
||||
@@ -154,6 +163,7 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
&i.TimesPlayed,
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -165,8 +175,27 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getSongById = `-- name: GetSongById :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error) {
|
||||
row := q.db.QueryRow(ctx, getSongById, id)
|
||||
var i Song
|
||||
err := row.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SongName,
|
||||
&i.Path,
|
||||
&i.TimesPlayed,
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name 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) {
|
||||
@@ -179,25 +208,31 @@ func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error
|
||||
&i.TimesPlayed,
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
||||
DELETE FROM song WHERE path = $1
|
||||
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
|
||||
_, err := q.db.Exec(ctx, removeBrokenSong, path)
|
||||
type RemoveBrokenSongParams struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveBrokenSong(ctx context.Context, arg RemoveBrokenSongParams) error {
|
||||
_, err := q.db.Exec(ctx, removeBrokenSong, arg.SoundtrackID, arg.Path)
|
||||
return err
|
||||
}
|
||||
|
||||
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
||||
DELETE FROM song where path = any ($1)
|
||||
DELETE FROM song WHERE id = ANY($1)
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
|
||||
_, err := q.db.Exec(ctx, removeBrokenSongs, paths)
|
||||
func (q *Queries) RemoveBrokenSongs(ctx context.Context, id pgtype.Int4) error {
|
||||
_, err := q.db.Exec(ctx, removeBrokenSongs, id)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const getLastPlayedGames = `-- name: GetLastPlayedGames :many
|
||||
const getLastPlayedSoundtracks = `-- name: GetLastPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -31,7 +31,7 @@ ORDER BY g.last_played DESC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetLastPlayedGamesRow struct {
|
||||
type GetLastPlayedSoundtracksRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
@@ -40,15 +40,15 @@ type GetLastPlayedGamesRow struct {
|
||||
}
|
||||
|
||||
// Last played soundtracks (most recently played)
|
||||
func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLastPlayedGames, limit)
|
||||
func (q *Queries) GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]GetLastPlayedSoundtracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLastPlayedSoundtracks, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLastPlayedGamesRow
|
||||
var items []GetLastPlayedSoundtracksRow
|
||||
for rows.Next() {
|
||||
var i GetLastPlayedGamesRow
|
||||
var i GetLastPlayedSoundtracksRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -66,7 +66,7 @@ func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLas
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLeastPlayedGamesWithSongs = `-- name: GetLeastPlayedGamesWithSongs :many
|
||||
const getLeastPlayedSoundtracksWithSongs = `-- name: GetLeastPlayedSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -88,7 +88,7 @@ ORDER BY g.times_played ASC, g.soundtrack_name
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetLeastPlayedGamesWithSongsRow struct {
|
||||
type GetLeastPlayedSoundtracksWithSongsRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
@@ -97,15 +97,15 @@ type GetLeastPlayedGamesWithSongsRow struct {
|
||||
}
|
||||
|
||||
// Least played soundtracks with their songs
|
||||
func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit)
|
||||
func (q *Queries) GetLeastPlayedSoundtracksWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedSoundtracksWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedSoundtracksWithSongs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLeastPlayedGamesWithSongsRow
|
||||
var items []GetLeastPlayedSoundtracksWithSongsRow
|
||||
for rows.Next() {
|
||||
var i GetLeastPlayedGamesWithSongsRow
|
||||
var i GetLeastPlayedSoundtracksWithSongsRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -123,7 +123,7 @@ func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLeastPlayedSongsWithGame = `-- name: GetLeastPlayedSongsWithGame :many
|
||||
const getLeastPlayedSongsWithSoundtrack = `-- name: GetLeastPlayedSongsWithSoundtrack :many
|
||||
SELECT
|
||||
s.soundtrack_id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -138,7 +138,7 @@ ORDER BY s.times_played ASC, s.song_name
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetLeastPlayedSongsWithGameRow struct {
|
||||
type GetLeastPlayedSongsWithSoundtrackRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SongName string `json:"song_name"`
|
||||
@@ -148,15 +148,15 @@ type GetLeastPlayedSongsWithGameRow struct {
|
||||
}
|
||||
|
||||
// Least played songs with their soundtrack info
|
||||
func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit)
|
||||
func (q *Queries) GetLeastPlayedSongsWithSoundtrack(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithSoundtrackRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithSoundtrack, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLeastPlayedSongsWithGameRow
|
||||
var items []GetLeastPlayedSongsWithSoundtrackRow
|
||||
for rows.Next() {
|
||||
var i GetLeastPlayedSongsWithGameRow
|
||||
var i GetLeastPlayedSongsWithSoundtrackRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -175,7 +175,7 @@ func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMostPlayedGamesWithSongs = `-- name: GetMostPlayedGamesWithSongs :many
|
||||
const getMostPlayedSoundtracksWithSongs = `-- name: GetMostPlayedSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -197,7 +197,7 @@ ORDER BY g.times_played DESC, g.soundtrack_name
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetMostPlayedGamesWithSongsRow struct {
|
||||
type GetMostPlayedSoundtracksWithSongsRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
@@ -206,15 +206,15 @@ type GetMostPlayedGamesWithSongsRow struct {
|
||||
}
|
||||
|
||||
// Most played soundtracks with their songs
|
||||
func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit)
|
||||
func (q *Queries) GetMostPlayedSoundtracksWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedSoundtracksWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedSoundtracksWithSongs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMostPlayedGamesWithSongsRow
|
||||
var items []GetMostPlayedSoundtracksWithSongsRow
|
||||
for rows.Next() {
|
||||
var i GetMostPlayedGamesWithSongsRow
|
||||
var i GetMostPlayedSoundtracksWithSongsRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -232,7 +232,7 @@ func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMostPlayedSongsWithGame = `-- name: GetMostPlayedSongsWithGame :many
|
||||
const getMostPlayedSongsWithSoundtrack = `-- name: GetMostPlayedSongsWithSoundtrack :many
|
||||
SELECT
|
||||
s.soundtrack_id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -247,7 +247,7 @@ ORDER BY s.times_played DESC, s.song_name
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetMostPlayedSongsWithGameRow struct {
|
||||
type GetMostPlayedSongsWithSoundtrackRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SongName string `json:"song_name"`
|
||||
@@ -257,15 +257,15 @@ type GetMostPlayedSongsWithGameRow struct {
|
||||
}
|
||||
|
||||
// Most played songs with their soundtrack info
|
||||
func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit)
|
||||
func (q *Queries) GetMostPlayedSongsWithSoundtrack(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithSoundtrackRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedSongsWithSoundtrack, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMostPlayedSongsWithGameRow
|
||||
var items []GetMostPlayedSongsWithSoundtrackRow
|
||||
for rows.Next() {
|
||||
var i GetMostPlayedSongsWithGameRow
|
||||
var i GetMostPlayedSongsWithSoundtrackRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -284,7 +284,7 @@ func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) (
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNeverPlayedGames = `-- name: GetNeverPlayedGames :many
|
||||
const getNeverPlayedSoundtracks = `-- name: GetNeverPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -304,7 +304,7 @@ GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
||||
ORDER BY g.soundtrack_name
|
||||
`
|
||||
|
||||
type GetNeverPlayedGamesRow struct {
|
||||
type GetNeverPlayedSoundtracksRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
@@ -312,16 +312,16 @@ type GetNeverPlayedGamesRow struct {
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Games that have never been played (times_played = 0)
|
||||
func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getNeverPlayedGames)
|
||||
// Soundtracks that have never been played (times_played = 0)
|
||||
func (q *Queries) GetNeverPlayedSoundtracks(ctx context.Context) ([]GetNeverPlayedSoundtracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, getNeverPlayedSoundtracks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetNeverPlayedGamesRow
|
||||
var items []GetNeverPlayedSoundtracksRow
|
||||
for rows.Next() {
|
||||
var i GetNeverPlayedGamesRow
|
||||
var i GetNeverPlayedSoundtracksRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -339,7 +339,7 @@ func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGame
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getOldestPlayedGames = `-- name: GetOldestPlayedGames :many
|
||||
const getOldestPlayedSoundtracks = `-- name: GetOldestPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.soundtrack_name,
|
||||
@@ -360,7 +360,7 @@ ORDER BY g.last_played ASC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetOldestPlayedGamesRow struct {
|
||||
type GetOldestPlayedSoundtracksRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
@@ -369,15 +369,15 @@ type GetOldestPlayedGamesRow struct {
|
||||
}
|
||||
|
||||
// Oldest played soundtracks (least recently played, but has been played at least once)
|
||||
func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getOldestPlayedGames, limit)
|
||||
func (q *Queries) GetOldestPlayedSoundtracks(ctx context.Context, limit int32) ([]GetOldestPlayedSoundtracksRow, error) {
|
||||
rows, err := q.db.Query(ctx, getOldestPlayedSoundtracks, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetOldestPlayedGamesRow
|
||||
var items []GetOldestPlayedSoundtracksRow
|
||||
for rows.Next() {
|
||||
var i GetOldestPlayedGamesRow
|
||||
var i GetOldestPlayedSoundtracksRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackName,
|
||||
@@ -398,8 +398,8 @@ func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetO
|
||||
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||
|
||||
@@ -54,8 +54,19 @@ func TestSetupDB(t *testing.T) {
|
||||
t.Fatalf("Failed to initialize test database: %v", err)
|
||||
}
|
||||
|
||||
// Clean up any existing schema to ensure clean state
|
||||
ctx := context.Background()
|
||||
_, err = TestDatabase.Pool.Exec(ctx, "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;")
|
||||
if err != nil {
|
||||
t.Logf("Warning: Could not clean schema: %v", err)
|
||||
// Continue anyway, migrations might still work
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
if err := TestDatabase.RunMigrations(); err != nil {
|
||||
// Clean up on failure to prevent nil pointer issues in other tests
|
||||
TestDatabase.Close()
|
||||
TestDatabase = nil
|
||||
t.Fatalf("Failed to run migrations: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -97,10 +108,11 @@ func createTestDatabase(host, port, dbname, user, password string) {
|
||||
// "closed pool" errors when tests run sequentially
|
||||
func TestTearDownDB(t *testing.T) {
|
||||
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
||||
if TestDatabase != nil {
|
||||
TestDatabase.Close()
|
||||
TestDatabase = nil
|
||||
}
|
||||
// Note: We also don't nil TestDatabase to allow reuse across tests
|
||||
// if TestDatabase != nil {
|
||||
// TestDatabase.Close()
|
||||
// TestDatabase = nil
|
||||
// }
|
||||
}
|
||||
|
||||
// TestClearDatabase clears all data from the test database
|
||||
@@ -112,10 +124,13 @@ func TestClearDatabase(t *testing.T) {
|
||||
|
||||
// Clear all tables in reverse order to respect foreign keys
|
||||
// Note: This assumes the tables exist and have the expected structure
|
||||
// After migration 000005, game table was renamed to soundtrack
|
||||
tables := []string{
|
||||
"song_list",
|
||||
"song",
|
||||
"game",
|
||||
"soundtrack",
|
||||
"vgmq",
|
||||
"sessions",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -126,9 +141,10 @@ func TestClearDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset sequences
|
||||
_, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
|
||||
if err != nil {
|
||||
t.Logf("Failed to reset game_id_seq: %v", err)
|
||||
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
||||
var seqErr error
|
||||
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
||||
if seqErr != nil {
|
||||
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"music-server/internal/backend"
|
||||
)
|
||||
|
||||
type CharacterHandler struct {
|
||||
}
|
||||
|
||||
func NewCharacterHandler() *CharacterHandler {
|
||||
return &CharacterHandler{}
|
||||
}
|
||||
|
||||
// GetCharacterList godoc
|
||||
// @Summary Get list of characters
|
||||
// @Description Returns a list of all available characters
|
||||
// @Tags characters
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Router /characters [get]
|
||||
func (c *CharacterHandler) GetCharacterList(ctx *echo.Context) error {
|
||||
characters := backend.GetCharacterList()
|
||||
return ctx.JSON(http.StatusOK, characters)
|
||||
}
|
||||
|
||||
// GetCharacter godoc
|
||||
// @Summary Get character image
|
||||
// @Description Returns the image for a specific character
|
||||
// @Tags characters
|
||||
// @Accept json
|
||||
// @Produce image/png
|
||||
// @Param name query string true "Character name"
|
||||
// @Success 200 {file} file
|
||||
// @Router /character [get]
|
||||
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
|
||||
character := ctx.QueryParam("name")
|
||||
characterPath := backend.GetCharacter(character)
|
||||
file, err := os.Open(characterPath)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
return ctx.Stream(http.StatusOK, "image/png", file)
|
||||
}
|
||||
@@ -5,45 +5,9 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"music-server/internal/backend"
|
||||
"music-server/internal/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestHealthCheck verifies the health endpoint returns database status
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
// Setup database
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
e := StartTestServer(t)
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var healthData map[string]string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, healthData)
|
||||
assert.Equal(t, "up", healthData["status"])
|
||||
}
|
||||
|
||||
// TestGetVersion verifies the version endpoint returns version history
|
||||
func TestGetVersion(t *testing.T) {
|
||||
e := StartTestServer(t)
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/version")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var versionData backend.VersionData
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, versionData.Version)
|
||||
assert.NotEmpty(t, versionData.Changelog)
|
||||
assert.NotEmpty(t, versionData.History)
|
||||
}
|
||||
|
||||
// TestGetCharacterList verifies the characters endpoint returns list of characters
|
||||
func TestGetCharacterList(t *testing.T) {
|
||||
e := StartTestServer(t)
|
||||
@@ -81,16 +45,3 @@ func TestGetCharacterNotFound(t *testing.T) {
|
||||
// Should return 404 or similar error
|
||||
assert.NotEqual(t, http.StatusOK, resp.Code)
|
||||
}
|
||||
|
||||
// TestDBTest verifies the database test endpoint
|
||||
func TestDBTest(t *testing.T) {
|
||||
// Setup database
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
e := StartTestServer(t)
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/dbtest")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
assert.Contains(t, resp.Body.String(), "TestedDB")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"music-server/internal/db"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func NewHealthHandler(database *db.Database) *HealthHandler {
|
||||
return &HealthHandler{db: database}
|
||||
}
|
||||
|
||||
// HealthCheck godoc
|
||||
//
|
||||
// @Summary Check server health
|
||||
// @Description Returns the health status of the server
|
||||
// @Tags health
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Router /health [get]
|
||||
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, h.db.Health())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestHealthCheck verifies the health endpoint returns database status
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
e := StartTestServer(t)
|
||||
// No explicit teardown - handled by StartTestServer's sync.Once
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var healthData map[string]string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, healthData)
|
||||
assert.Equal(t, "up", healthData["status"])
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"music-server/internal/backend"
|
||||
"music-server/internal/db"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
type IndexHandler struct {
|
||||
}
|
||||
|
||||
func NewIndexHandler() *IndexHandler {
|
||||
return &IndexHandler{}
|
||||
}
|
||||
|
||||
// GetVersion godoc
|
||||
//
|
||||
// @Summary Getting the version of the backend
|
||||
// @Description get string by ID
|
||||
// @Tags accounts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} backend.VersionData
|
||||
// @Failure 404 {object} string
|
||||
// @Router /version [get]
|
||||
func (i *IndexHandler) GetVersion(ctx *echo.Context) error {
|
||||
versionHistory := backend.GetVersionHistory()
|
||||
if versionHistory.Version == "" {
|
||||
return ctx.JSON(http.StatusNotFound, "version not found")
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, versionHistory)
|
||||
}
|
||||
|
||||
// GetDBTest godoc
|
||||
// @Summary Test database connection
|
||||
// @Description Tests the database connection
|
||||
// @Tags database
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "TestedDB"
|
||||
// @Router /dbtest [get]
|
||||
func (i *IndexHandler) GetDBTest(ctx *echo.Context) error {
|
||||
backend.TestDB()
|
||||
return ctx.JSON(http.StatusOK, "TestedDB")
|
||||
}
|
||||
|
||||
// HealthCheck godoc
|
||||
// @Summary Check server health
|
||||
// @Description Returns the health status of the server
|
||||
// @Tags health
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Router /health [get]
|
||||
func (i *IndexHandler) HealthCheck(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, db.Health())
|
||||
}
|
||||
|
||||
// GetCharacterList godoc
|
||||
// @Summary Get list of characters
|
||||
// @Description Returns a list of all available characters
|
||||
// @Tags characters
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Router /characters [get]
|
||||
func (i *IndexHandler) GetCharacterList(ctx *echo.Context) error {
|
||||
characters := backend.GetCharacterList()
|
||||
return ctx.JSON(http.StatusOK, characters)
|
||||
}
|
||||
|
||||
// GetCharacter godoc
|
||||
// @Summary Get character image
|
||||
// @Description Returns the image for a specific character
|
||||
// @Tags characters
|
||||
// @Accept json
|
||||
// @Produce image/png
|
||||
// @Param name query string true "Character name"
|
||||
// @Success 200 {file} file
|
||||
// @Router /character [get]
|
||||
func (i *IndexHandler) GetCharacter(ctx *echo.Context) error {
|
||||
character := ctx.QueryParam("name")
|
||||
return ctx.File(backend.GetCharacter(character))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"music-server/internal/backend"
|
||||
"music-server/internal/logging"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
// SyncCheckMiddleware blocks requests when syncing is in progress.
|
||||
// It returns HTTP 423 Locked with a standard message.
|
||||
// This middleware should be applied to handlers that cannot execute during sync.
|
||||
func SyncCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress - request blocked")
|
||||
return c.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,6 @@ func NewMusicHandler() *MusicHandler {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music [get]
|
||||
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
song := ctx.QueryParam("song")
|
||||
if song == "" {
|
||||
return ctx.String(http.StatusBadRequest, "song can't be empty")
|
||||
@@ -58,10 +54,6 @@ func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/soundTest [get]
|
||||
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetSoundCheckSong()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -80,10 +72,6 @@ func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/reset [get]
|
||||
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
backend.Reset()
|
||||
return ctx.NoContent(http.StatusOK)
|
||||
}
|
||||
@@ -98,10 +86,6 @@ func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/rand [get]
|
||||
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetRandomSong()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -121,10 +105,6 @@ func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/rand/low [get]
|
||||
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetRandomSongLowChance()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -144,10 +124,6 @@ func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/rand/classic [get]
|
||||
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetRandomSongClassic()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -193,10 +169,6 @@ func (m *MusicHandler) GetPlayedSongs(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/next [get]
|
||||
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetNextSong()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -216,10 +188,6 @@ func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/previous [get]
|
||||
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
songPath := backend.GetPreviousSong()
|
||||
file, err := os.Open(songPath)
|
||||
if err != nil {
|
||||
@@ -231,7 +199,7 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
||||
|
||||
// GetAllSoundtracks godoc
|
||||
// @Summary Get all soundtracks
|
||||
// @Description Returns a list of all games in order
|
||||
// @Description Returns a list of all soundtracks in order
|
||||
// @Tags music
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -239,17 +207,13 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/all/order [get]
|
||||
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
soundtrackList := backend.GetAllSoundtracks()
|
||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||
}
|
||||
|
||||
// GetAllSoundtracksRandom godoc
|
||||
// @Summary Get all soundtracks random
|
||||
// @Description Returns a list of all games in random order
|
||||
// @Description Returns a list of all soundtracks in random order
|
||||
// @Tags music
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -257,10 +221,6 @@ func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/all/random [get]
|
||||
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
soundtrackList := backend.GetAllSoundtracksRandom()
|
||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||
}
|
||||
@@ -277,10 +237,6 @@ func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/played [put]
|
||||
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
@@ -299,10 +255,6 @@ func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/addQue [get]
|
||||
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
backend.AddLatestToQue()
|
||||
return ctx.NoContent(http.StatusOK)
|
||||
}
|
||||
@@ -316,10 +268,6 @@ func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /music/addPlayed [get]
|
||||
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Info("Syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
backend.AddLatestPlayed()
|
||||
return ctx.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
+43
-38
@@ -50,8 +50,9 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
fileServer := http.FileServer(http.FS(web.Assets))
|
||||
e.GET("/assets/*", echo.WrapHandler(fileServer))
|
||||
|
||||
e.GET("/search", echo.WrapHandler(templ.Handler(web.HelloForm())))
|
||||
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindGameWebHandler)))
|
||||
e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm())))
|
||||
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackWebHandler)))
|
||||
e.POST("/findfuzzy", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackFuzzyWebHandler)))
|
||||
|
||||
e.Static("/", "/frontend")
|
||||
|
||||
@@ -63,12 +64,16 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
// ============================================
|
||||
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||
|
||||
index := NewIndexHandler()
|
||||
e.GET("/version", deprecatedMiddleware(index.GetVersion))
|
||||
e.GET("/dbtest", deprecatedMiddleware(index.GetDBTest))
|
||||
e.GET("/health", deprecatedMiddleware(index.HealthCheck))
|
||||
e.GET("/character", deprecatedMiddleware(index.GetCharacter))
|
||||
e.GET("/characters", deprecatedMiddleware(index.GetCharacterList))
|
||||
health := NewHealthHandler(s.db)
|
||||
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
||||
|
||||
version := NewVersionHandler()
|
||||
e.GET("/version", deprecatedMiddleware(version.GetLatestVersion))
|
||||
e.GET("/version/history", deprecatedMiddleware(version.GetVersionHistory))
|
||||
|
||||
character := NewCharacterHandler()
|
||||
e.GET("/character", deprecatedMiddleware(character.GetCharacter))
|
||||
e.GET("/characters", deprecatedMiddleware(character.GetCharacterList))
|
||||
|
||||
download := NewDownloadHandler()
|
||||
e.GET("/download", deprecatedMiddleware(download.checkLatest))
|
||||
@@ -78,32 +83,32 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
|
||||
sync := NewSyncHandler()
|
||||
syncGroup := e.Group("/sync")
|
||||
syncGroup.GET("", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||
syncGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
||||
syncGroup.GET("/new", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||
syncGroup.GET("/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
||||
syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
||||
syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||
syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetDB))
|
||||
syncGroup.GET("/new", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||
syncGroup.GET("/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
||||
syncGroup.GET("/new/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
||||
syncGroup.GET("/quick", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||
syncGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.ResetDB)))
|
||||
|
||||
music := NewMusicHandler()
|
||||
musicGroup := e.Group("/music")
|
||||
musicGroup.GET("", deprecatedMiddleware(music.GetSong))
|
||||
musicGroup.GET("/soundTest", deprecatedMiddleware(music.GetSoundCheckSong))
|
||||
musicGroup.GET("/reset", deprecatedMiddleware(music.ResetMusic))
|
||||
musicGroup.GET("/rand", deprecatedMiddleware(music.GetRandomSong))
|
||||
musicGroup.GET("/rand/low", deprecatedMiddleware(music.GetRandomSongLowChance))
|
||||
musicGroup.GET("/rand/classic", deprecatedMiddleware(music.GetRandomSongClassic))
|
||||
musicGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSong)))
|
||||
musicGroup.GET("/soundTest", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSoundCheckSong)))
|
||||
musicGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.ResetMusic)))
|
||||
musicGroup.GET("/rand", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSong)))
|
||||
musicGroup.GET("/rand/low", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongLowChance)))
|
||||
musicGroup.GET("/rand/classic", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongClassic)))
|
||||
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
||||
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
||||
musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong))
|
||||
musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong))
|
||||
musicGroup.GET("/all", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
||||
musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllSoundtracks))
|
||||
musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
||||
musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed))
|
||||
musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue))
|
||||
musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed))
|
||||
musicGroup.GET("/next", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetNextSong)))
|
||||
musicGroup.GET("/previous", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetPreviousSong)))
|
||||
musicGroup.GET("/all", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
||||
musicGroup.GET("/all/order", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracks)))
|
||||
musicGroup.GET("/all/random", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
||||
musicGroup.PUT("/played", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.PutPlayed)))
|
||||
musicGroup.GET("/addQue", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestToQue)))
|
||||
musicGroup.GET("/addPlayed", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestPlayed)))
|
||||
|
||||
// ============================================
|
||||
// API v1 Routes with Token Authentication
|
||||
@@ -132,20 +137,20 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
|
||||
// Statistics API endpoints (protected by token auth)
|
||||
statistics := s.statisticsHandler
|
||||
protectedV1.GET("/statistics/games/most-played", func(c *echo.Context) error {
|
||||
return statistics.GetMostPlayedGames(c)
|
||||
protectedV1.GET("/statistics/soundtracks/most-played", func(c *echo.Context) error {
|
||||
return statistics.GetMostPlayedSoundtracks(c)
|
||||
})
|
||||
protectedV1.GET("/statistics/games/least-played", func(c *echo.Context) error {
|
||||
return statistics.GetLeastPlayedGames(c)
|
||||
protectedV1.GET("/statistics/soundtracks/least-played", func(c *echo.Context) error {
|
||||
return statistics.GetLeastPlayedSoundtracks(c)
|
||||
})
|
||||
protectedV1.GET("/statistics/games/never-played", func(c *echo.Context) error {
|
||||
return statistics.GetNeverPlayedGames(c)
|
||||
protectedV1.GET("/statistics/soundtracks/never-played", func(c *echo.Context) error {
|
||||
return statistics.GetNeverPlayedSoundtracks(c)
|
||||
})
|
||||
protectedV1.GET("/statistics/games/last-played", func(c *echo.Context) error {
|
||||
return statistics.GetLastPlayedGames(c)
|
||||
protectedV1.GET("/statistics/soundtracks/last-played", func(c *echo.Context) error {
|
||||
return statistics.GetLastPlayedSoundtracks(c)
|
||||
})
|
||||
protectedV1.GET("/statistics/games/oldest-played", func(c *echo.Context) error {
|
||||
return statistics.GetOldestPlayedGames(c)
|
||||
protectedV1.GET("/statistics/soundtracks/oldest-played", func(c *echo.Context) error {
|
||||
return statistics.GetOldestPlayedSoundtracks(c)
|
||||
})
|
||||
protectedV1.GET("/statistics/songs/most-played", func(c *echo.Context) error {
|
||||
return statistics.GetMostPlayedSongs(c)
|
||||
|
||||
@@ -23,20 +23,20 @@ func NewStatisticsHandler() *StatisticsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// GetMostPlayedGames returns top N most played games with songs
|
||||
// GET /api/v1/statistics/games/most-played
|
||||
// GetMostPlayedSoundtracks returns top N most played soundtracks with songs
|
||||
// GET /api/v1/statistics/soundtracks/most-played
|
||||
//
|
||||
// @Summary Get most played games
|
||||
// @Description Returns the top N most played games with their songs
|
||||
// @Summary Get most played soundtracks
|
||||
// @Description Returns the top N most played soundtracks with their songs
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of results (default: 10)"
|
||||
// @Success 200 {array} backend.GameWithSongs
|
||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/statistics/games/most-played [get]
|
||||
func (h *StatisticsHandler) GetMostPlayedGames(ctx *echo.Context) error {
|
||||
// @Router /api/v1/statistics/soundtracks/most-played [get]
|
||||
func (h *StatisticsHandler) GetMostPlayedSoundtracks(ctx *echo.Context) error {
|
||||
limit := 10 // default
|
||||
limitStr := ctx.QueryParam("limit")
|
||||
if limitStr != "" {
|
||||
@@ -51,28 +51,28 @@ func (h *StatisticsHandler) GetMostPlayedGames(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
games, err := h.statsBackend.GetMostPlayedGamesWithSongs(int32(limit))
|
||||
soundtracks, err := h.statsBackend.GetMostPlayedSoundtracksWithSongs(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get most played games", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to get most played soundtracks", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, games)
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
|
||||
// GetLeastPlayedGames returns top N least played games with songs
|
||||
// GET /api/v1/statistics/games/least-played
|
||||
// GetLeastPlayedSoundtracks returns top N least played soundtracks with songs
|
||||
// GET /api/v1/statistics/soundtracks/least-played
|
||||
//
|
||||
// @Summary Get least played games
|
||||
// @Description Returns the top N least played games with their songs
|
||||
// @Summary Get least played soundtracks
|
||||
// @Description Returns the top N least played soundtracks with their songs
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of results (default: 10)"
|
||||
// @Success 200 {array} backend.GameWithSongs
|
||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/statistics/games/least-played [get]
|
||||
func (h *StatisticsHandler) GetLeastPlayedGames(ctx *echo.Context) error {
|
||||
// @Router /api/v1/statistics/soundtracks/least-played [get]
|
||||
func (h *StatisticsHandler) GetLeastPlayedSoundtracks(ctx *echo.Context) error {
|
||||
limit := 10
|
||||
limitStr := ctx.QueryParam("limit")
|
||||
if limitStr != "" {
|
||||
@@ -86,19 +86,19 @@ func (h *StatisticsHandler) GetLeastPlayedGames(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
games, err := h.statsBackend.GetLeastPlayedGamesWithSongs(int32(limit))
|
||||
soundtracks, err := h.statsBackend.GetLeastPlayedSoundtracksWithSongs(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get least played games", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to get least played soundtracks", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, games)
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
|
||||
// GetMostPlayedSongs returns top N most played songs with game info
|
||||
// GetMostPlayedSongs returns top N most played songs with soundtrack info
|
||||
// GET /api/v1/statistics/songs/most-played
|
||||
//
|
||||
// @Summary Get most played songs
|
||||
// @Description Returns the top N most played songs with their game info
|
||||
// @Description Returns the top N most played songs with their soundtrack info
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -121,7 +121,7 @@ func (h *StatisticsHandler) GetMostPlayedSongs(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
songs, err := h.statsBackend.GetMostPlayedSongsWithGame(int32(limit))
|
||||
songs, err := h.statsBackend.GetMostPlayedSongsWithSoundtrack(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get most played songs", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
@@ -129,11 +129,11 @@ func (h *StatisticsHandler) GetMostPlayedSongs(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, songs)
|
||||
}
|
||||
|
||||
// GetLeastPlayedSongs returns top N least played songs with game info
|
||||
// GetLeastPlayedSongs returns top N least played songs with soundtrack info
|
||||
// GET /api/v1/statistics/songs/least-played
|
||||
//
|
||||
// @Summary Get least played songs
|
||||
// @Description Returns the top N least played songs with their game info
|
||||
// @Description Returns the top N least played songs with their soundtrack info
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -156,7 +156,7 @@ func (h *StatisticsHandler) GetLeastPlayedSongs(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
songs, err := h.statsBackend.GetLeastPlayedSongsWithGame(int32(limit))
|
||||
songs, err := h.statsBackend.GetLeastPlayedSongsWithSoundtrack(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get least played songs", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
@@ -164,40 +164,40 @@ func (h *StatisticsHandler) GetLeastPlayedSongs(ctx *echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, songs)
|
||||
}
|
||||
|
||||
// GetNeverPlayedGames returns games that have never been played
|
||||
// GET /api/v1/statistics/games/never-played
|
||||
// GetNeverPlayedSoundtracks returns soundtracks that have never been played
|
||||
// GET /api/v1/statistics/soundtracks/never-played
|
||||
//
|
||||
// @Summary Get never played games
|
||||
// @Description Returns all games that have never been played (times_played = 0)
|
||||
// @Summary Get never played soundtracks
|
||||
// @Description Returns all soundtracks that have never been played (times_played = 0)
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} backend.GameWithSongs
|
||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/statistics/games/never-played [get]
|
||||
func (h *StatisticsHandler) GetNeverPlayedGames(ctx *echo.Context) error {
|
||||
games, err := h.statsBackend.GetNeverPlayedGames()
|
||||
// @Router /api/v1/statistics/soundtracks/never-played [get]
|
||||
func (h *StatisticsHandler) GetNeverPlayedSoundtracks(ctx *echo.Context) error {
|
||||
soundtracks, err := h.statsBackend.GetNeverPlayedSoundtracks()
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get never played games", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to get never played soundtracks", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, games)
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
|
||||
// GetLastPlayedGames returns most recently played games
|
||||
// GET /api/v1/statistics/games/last-played
|
||||
// GetLastPlayedSoundtracks returns most recently played soundtracks
|
||||
// GET /api/v1/statistics/soundtracks/last-played
|
||||
//
|
||||
// @Summary Get last played games
|
||||
// @Description Returns the most recently played games
|
||||
// @Summary Get last played soundtracks
|
||||
// @Description Returns the most recently played soundtracks
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of results (default: 10)"
|
||||
// @Success 200 {array} backend.GameWithSongs
|
||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/statistics/games/last-played [get]
|
||||
func (h *StatisticsHandler) GetLastPlayedGames(ctx *echo.Context) error {
|
||||
// @Router /api/v1/statistics/soundtracks/last-played [get]
|
||||
func (h *StatisticsHandler) GetLastPlayedSoundtracks(ctx *echo.Context) error {
|
||||
limit := 10
|
||||
limitStr := ctx.QueryParam("limit")
|
||||
if limitStr != "" {
|
||||
@@ -211,28 +211,28 @@ func (h *StatisticsHandler) GetLastPlayedGames(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
games, err := h.statsBackend.GetLastPlayedGames(int32(limit))
|
||||
soundtracks, err := h.statsBackend.GetLastPlayedSoundtracks(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get last played games", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to get last played soundtracks", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, games)
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
|
||||
// GetOldestPlayedGames returns least recently played games
|
||||
// GET /api/v1/statistics/games/oldest-played
|
||||
// GetOldestPlayedSoundtracks returns least recently played soundtracks
|
||||
// GET /api/v1/statistics/soundtracks/oldest-played
|
||||
//
|
||||
// @Summary Get oldest played games
|
||||
// @Description Returns the least recently played games (that have been played at least once)
|
||||
// @Summary Get oldest played soundtracks
|
||||
// @Description Returns the least recently played soundtracks (that have been played at least once)
|
||||
// @Tags statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of results (default: 10)"
|
||||
// @Success 200 {array} backend.GameWithSongs
|
||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/statistics/games/oldest-played [get]
|
||||
func (h *StatisticsHandler) GetOldestPlayedGames(ctx *echo.Context) error {
|
||||
// @Router /api/v1/statistics/soundtracks/oldest-played [get]
|
||||
func (h *StatisticsHandler) GetOldestPlayedSoundtracks(ctx *echo.Context) error {
|
||||
limit := 10
|
||||
limitStr := ctx.QueryParam("limit")
|
||||
if limitStr != "" {
|
||||
@@ -246,12 +246,12 @@ func (h *StatisticsHandler) GetOldestPlayedGames(ctx *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
games, err := h.statsBackend.GetOldestPlayedGames(int32(limit))
|
||||
soundtracks, err := h.statsBackend.GetOldestPlayedSoundtracks(int32(limit))
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get oldest played games", zap.String("error", err.Error()))
|
||||
logging.GetLogger().Error("Failed to get oldest played soundtracks", zap.String("error", err.Error()))
|
||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, games)
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
|
||||
// GetStatisticsSummary returns overall statistics
|
||||
|
||||
@@ -73,6 +73,11 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
// Wait for sync to complete
|
||||
if !waitForSyncCompletion(t, e, 60) {
|
||||
t.Error("Sync did not complete within timeout")
|
||||
}
|
||||
|
||||
// Verify data via statistics endpoint
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
@@ -85,9 +90,9 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We inserted 5 soundtracks, so total should be at least 5
|
||||
// (there might be existing data)
|
||||
require.GreaterOrEqual(t, summary.TotalGames, int64(5))
|
||||
// After sync with /sync/new, only soundtracks matching filesystem remain
|
||||
// testMusic has 3 soundtracks
|
||||
require.Equal(t, int64(3), summary.TotalSoundtracks)
|
||||
}
|
||||
|
||||
// insertTestData inserts 5 test soundtracks with songs into the database
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
|
||||
|
||||
// SyncSoundtracksNewOnlyChanges godoc
|
||||
// @Summary Sync soundtracks with only changes
|
||||
// @Description Starts syncing games with only new changes
|
||||
// @Description Starts syncing soundtracks with only new changes
|
||||
// @Tags sync
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -44,18 +44,15 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /sync [get]
|
||||
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Warn("Syncing is already in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
logging.GetLogger().Info("Starting sync with only changes")
|
||||
go backend.SyncSoundtracksNewOnlyChanges()
|
||||
backend.Syncing = true
|
||||
go backend.SyncSoundtracksOnlyChanges()
|
||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
||||
}
|
||||
|
||||
// SyncSoundtracksNewFull godoc
|
||||
// @Summary Sync all games fully
|
||||
// @Description Starts a full sync of all games
|
||||
// @Summary Sync all soundtracks fully
|
||||
// @Description Starts a full sync of all soundtracks
|
||||
// @Tags sync
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -63,18 +60,15 @@ func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /sync/full [get]
|
||||
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Warn("Syncing is already in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
logging.GetLogger().Info("Starting full sync")
|
||||
go backend.SyncSoundtracksNewFull()
|
||||
backend.Syncing = true
|
||||
go backend.SyncSoundtracksFull()
|
||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
||||
}
|
||||
|
||||
// ResetDB godoc
|
||||
// @Summary Reset soundtracks database
|
||||
// @Description Resets the games database by deleting all games and songs
|
||||
// @Description Resets the soundtracks database by deleting all soundtracks and songs
|
||||
// @Tags sync
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -82,10 +76,6 @@ func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /sync/reset [get]
|
||||
func (s *SyncHandler) ResetDB(ctx *echo.Context) error {
|
||||
if backend.Syncing {
|
||||
logging.GetLogger().Warn("Cannot reset - syncing is in progress")
|
||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||
}
|
||||
logging.GetLogger().Info("Resetting soundtracks database")
|
||||
backend.ResetDB()
|
||||
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database")
|
||||
|
||||
@@ -61,7 +61,7 @@ func waitForSyncCompletion(t *testing.T, e *echo.Echo, maxAttempts int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestSyncPopulatesDatabase verifies that sync populates the database with games
|
||||
// TestSyncPopulatesDatabase verifies that sync populates the database with soundtracks
|
||||
func TestSyncPopulatesDatabase(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
@@ -74,12 +74,12 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
||||
// Clear any existing data first
|
||||
db.TestClearDatabase(t)
|
||||
|
||||
// Before sync - should have no games
|
||||
// Before sync - should have no soundtracks
|
||||
repo := repository.New(backend.BackendPool())
|
||||
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
soundtracksBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
beforeCount := len(gamesBefore)
|
||||
t.Logf("Games before sync: %d", beforeCount)
|
||||
beforeCount := len(soundtracksBefore)
|
||||
t.Logf("Soundtracks before sync: %d", beforeCount)
|
||||
assert.Equal(t, 0, beforeCount, "Database should be empty after clear")
|
||||
|
||||
// Run sync
|
||||
@@ -91,14 +91,14 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
||||
t.Error("Sync did not complete within timeout")
|
||||
}
|
||||
|
||||
// After sync - should have games
|
||||
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
// After sync - should have soundtracks
|
||||
soundtracksAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
afterCount := len(gamesAfter)
|
||||
t.Logf("Games after sync: %d", afterCount)
|
||||
afterCount := len(soundtracksAfter)
|
||||
t.Logf("Soundtracks after sync: %d", afterCount)
|
||||
|
||||
// Should have more games than before (unless database was already populated)
|
||||
assert.True(t, afterCount > 0, "Database should have games after sync")
|
||||
// Should have more soundtracks than before (unless database was already populated)
|
||||
assert.True(t, afterCount > 0, "Database should have soundtracks after sync")
|
||||
}
|
||||
|
||||
// TestSyncMakesDifference verifies that sync actually changes the database state
|
||||
@@ -111,11 +111,11 @@ func TestSyncMakesDifference(t *testing.T) {
|
||||
// Clear any existing data first
|
||||
db.TestClearDatabase(t)
|
||||
|
||||
// Before sync - should have no games
|
||||
// Before sync - should have no soundtracks
|
||||
repo := repository.New(backend.BackendPool())
|
||||
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
soundtracksBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync")
|
||||
assert.Equal(t, 0, len(soundtracksBefore), "Should have no soundtracks before sync")
|
||||
|
||||
// Run sync
|
||||
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
||||
@@ -126,10 +126,10 @@ func TestSyncMakesDifference(t *testing.T) {
|
||||
t.Error("Sync did not complete within timeout")
|
||||
}
|
||||
|
||||
// After sync - should have games
|
||||
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
// After sync - should have soundtracks
|
||||
soundtracksAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(gamesAfter) > 0, "Should have games after sync")
|
||||
assert.True(t, len(soundtracksAfter) > 0, "Should have soundtracks after sync")
|
||||
}
|
||||
|
||||
// TestSyncProgress verifies the sync progress endpoint
|
||||
@@ -183,8 +183,8 @@ func TestSyncProgress(t *testing.T) {
|
||||
assert.True(t, foundComplete, "Should have seen completion")
|
||||
}
|
||||
|
||||
// TestSyncGamesNewOnlyChanges verifies the incremental sync endpoint
|
||||
func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
||||
// TestSyncSoundtracksNewOnlyChanges verifies the incremental sync endpoint
|
||||
func TestSyncSoundtracksNewOnlyChanges(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
@@ -200,8 +200,8 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
||||
|
||||
// Get initial count
|
||||
repo := repository.New(backend.BackendPool())
|
||||
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount := len(gamesBefore)
|
||||
soundtracksBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount := len(soundtracksBefore)
|
||||
|
||||
// Run incremental sync (should not change count if nothing changed)
|
||||
resp := MakeTestRequest(t, e, "GET", "/sync/new")
|
||||
@@ -211,16 +211,16 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Count should be the same
|
||||
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
afterCount := len(gamesAfter)
|
||||
soundtracksAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
afterCount := len(soundtracksAfter)
|
||||
|
||||
// Note: This might not be exactly equal due to timing, but should be close
|
||||
t.Logf("Games before incremental sync: %d, after: %d", beforeCount, afterCount)
|
||||
t.Logf("Soundtracks before incremental sync: %d, after: %d", beforeCount, afterCount)
|
||||
}
|
||||
|
||||
// TestResetGames verifies the reset endpoint clears the database
|
||||
// TestResetSoundtracks verifies the reset endpoint clears the database
|
||||
// RUN THIS LAST
|
||||
func TestResetGames(t *testing.T) {
|
||||
func TestResetSoundtracks(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
@@ -228,8 +228,8 @@ func TestResetGames(t *testing.T) {
|
||||
|
||||
// First ensure we have data
|
||||
repo := repository.New(backend.BackendPool())
|
||||
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount := len(gamesBefore)
|
||||
soundtracksBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount := len(soundtracksBefore)
|
||||
|
||||
if beforeCount == 0 {
|
||||
// Run sync to populate
|
||||
@@ -238,12 +238,12 @@ func TestResetGames(t *testing.T) {
|
||||
t.Error("Sync did not complete within timeout")
|
||||
return
|
||||
}
|
||||
gamesBefore, _ = repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount = len(gamesBefore)
|
||||
soundtracksBefore, _ = repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
beforeCount = len(soundtracksBefore)
|
||||
}
|
||||
|
||||
t.Logf("Games before reset: %d", beforeCount)
|
||||
assert.True(t, beforeCount > 0, "Should have games to reset")
|
||||
t.Logf("Soundtracks before reset: %d", beforeCount)
|
||||
assert.True(t, beforeCount > 0, "Should have soundtracks to reset")
|
||||
|
||||
// Call reset
|
||||
resp := MakeTestRequest(t, e, "GET", "/sync/reset")
|
||||
@@ -253,16 +253,16 @@ func TestResetGames(t *testing.T) {
|
||||
// Note: reset might take a moment to propagate
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
afterCount := len(gamesAfter)
|
||||
soundtracksAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
afterCount := len(soundtracksAfter)
|
||||
|
||||
t.Logf("Games after reset: %d", afterCount)
|
||||
t.Logf("Soundtracks after reset: %d", afterCount)
|
||||
assert.Equal(t, 0, afterCount, "Database should be empty after reset")
|
||||
}
|
||||
|
||||
// TestSyncGamesNewFull verifies the full sync endpoint
|
||||
// RUN THIS LAST (before TestResetGames)
|
||||
func TestSyncGamesNewFull(t *testing.T) {
|
||||
// TestSyncSoundtracksNewFull verifies the full sync endpoint
|
||||
// RUN THIS LAST (before TestResetSoundtracks)
|
||||
func TestSyncSoundtracksNewFull(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
@@ -282,8 +282,8 @@ func TestSyncGamesNewFull(t *testing.T) {
|
||||
|
||||
// Verify database is populated
|
||||
repo := repository.New(backend.BackendPool())
|
||||
games, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
soundtracks, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(games) > 0, "Database should be populated after full sync")
|
||||
t.Logf("Full sync populated %d games", len(games))
|
||||
assert.True(t, len(soundtracks) > 0, "Database should be populated after full sync")
|
||||
t.Logf("Full sync populated %d soundtracks", len(soundtracks))
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
||||
s := &Server{
|
||||
db: db.TestDatabase,
|
||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||
statisticsHandler: NewStatisticsHandler(),
|
||||
}
|
||||
handler := s.RegisterRoutes()
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"music-server/internal/backend"
|
||||
)
|
||||
|
||||
type VersionHandler struct {
|
||||
}
|
||||
|
||||
func NewVersionHandler() *VersionHandler {
|
||||
return &VersionHandler{}
|
||||
}
|
||||
|
||||
// GetVersionHistory godoc
|
||||
//
|
||||
// @Summary Getting the version history of the backend
|
||||
// @Description get version history
|
||||
// @Tags version
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} backend.VersionData
|
||||
// @Failure 404 {object} string
|
||||
// @Router /version/history [get]
|
||||
func (v *VersionHandler) GetVersionHistory(ctx *echo.Context) error {
|
||||
versionHistory := backend.GetVersionHistory()
|
||||
if len(versionHistory) == 0 {
|
||||
return ctx.JSON(http.StatusNotFound, "version not found")
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, versionHistory)
|
||||
}
|
||||
|
||||
// GetLatestVersion godoc
|
||||
//
|
||||
// @Summary Getting the latest version of the backend
|
||||
// @Description get latest version info
|
||||
// @Tags version
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} backend.VersionData
|
||||
// @Failure 404 {object} string
|
||||
// @Router /version [get]
|
||||
func (v *VersionHandler) GetLatestVersion(ctx *echo.Context) error {
|
||||
latestVersion := backend.GetLatestVersion()
|
||||
if latestVersion.Version == "" {
|
||||
return ctx.JSON(http.StatusNotFound, "version not found")
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, latestVersion)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"music-server/internal/backend"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGetLatestVersion verifies the version endpoint returns latest version
|
||||
func TestGetLatestVersion(t *testing.T) {
|
||||
e := StartTestServer(t)
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/version")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var versionData backend.VersionData
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, versionData.Version)
|
||||
assert.NotEmpty(t, versionData.Changelog)
|
||||
}
|
||||
|
||||
// TestGetVersionHistory verifies the version history endpoint returns version history
|
||||
func TestGetVersionHistory(t *testing.T) {
|
||||
e := StartTestServer(t)
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/version/history")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var versionHistory []backend.VersionData
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &versionHistory)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, versionHistory)
|
||||
assert.NotEmpty(t, versionHistory[0].Version)
|
||||
assert.NotEmpty(t, versionHistory[0].Changelog)
|
||||
}
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
// ensureSyncRan ensures that sync has been run before testing music endpoints
|
||||
func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
||||
repo := repository.New(backend.BackendPool())
|
||||
games, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
soundtracks, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||
assert.NoError(t, err)
|
||||
|
||||
if len(games) == 0 {
|
||||
if len(soundtracks) == 0 {
|
||||
// Run sync
|
||||
t.Log("No games found, running sync first...")
|
||||
t.Log("No soundtracks found, running sync first...")
|
||||
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
@@ -32,8 +32,8 @@ func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAllGames verifies the /music/all/order endpoint
|
||||
func TestZGetAllGames(t *testing.T) {
|
||||
// TestGetAllSoundtracks verifies the /music/all/order endpoint
|
||||
func TestZGetAllSoundtracks(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
@@ -45,15 +45,15 @@ func TestZGetAllGames(t *testing.T) {
|
||||
resp := MakeTestRequest(t, e, "GET", "/music/all/order")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var games []string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &games)
|
||||
var soundtracks []string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &soundtracks)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, games, "Should have games after sync")
|
||||
t.Logf("Found %d games", len(games))
|
||||
assert.NotEmpty(t, soundtracks, "Should have soundtracks after sync")
|
||||
t.Logf("Found %d soundtracks", len(soundtracks))
|
||||
}
|
||||
|
||||
// TestGetAllGamesRandom verifies the /music/all/random endpoint
|
||||
func TestZGetAllGamesRandom(t *testing.T) {
|
||||
// TestGetAllSoundtracksRandom verifies the /music/all/random endpoint
|
||||
func TestZGetAllSoundtracksRandom(t *testing.T) {
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
@@ -65,17 +65,17 @@ func TestZGetAllGamesRandom(t *testing.T) {
|
||||
resp := MakeTestRequest(t, e, "GET", "/music/all/random")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var games []string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &games)
|
||||
var soundtracks []string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &soundtracks)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, games, "Should have games after sync")
|
||||
assert.NotEmpty(t, soundtracks, "Should have soundtracks after sync")
|
||||
|
||||
// Verify it's shuffled (not in original order)
|
||||
// We can't easily verify randomness, but we can check it's the same length
|
||||
resp2 := MakeTestRequest(t, e, "GET", "/music/all/order")
|
||||
var gamesOrdered []string
|
||||
json.Unmarshal(resp2.Body.Bytes(), &gamesOrdered)
|
||||
assert.Equal(t, len(games), len(gamesOrdered), "Random and ordered should have same count")
|
||||
var soundtracksOrdered []string
|
||||
json.Unmarshal(resp2.Body.Bytes(), &soundtracksOrdered)
|
||||
assert.Equal(t, len(soundtracks), len(soundtracksOrdered), "Random and ordered should have same count")
|
||||
}
|
||||
|
||||
// TestGetRandomSong verifies the /music/rand endpoint
|
||||
@@ -153,7 +153,7 @@ func TestZGetSongInfo(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
// Note: CurrentlyPlaying might be false if no song is currently set
|
||||
// Just verify we got a valid response
|
||||
t.Logf("Song info: Game=%s, Song=%s", info.Game, info.Song)
|
||||
t.Logf("Song info: Soundtrack=%s, Song=%s", info.Soundtrack, info.Song)
|
||||
}
|
||||
|
||||
// TestGetPlayedSongs verifies the /music/list endpoint
|
||||
|
||||
@@ -80,9 +80,17 @@ run:
|
||||
@templ generate
|
||||
@go run cmd/main.go
|
||||
|
||||
build-run: build
|
||||
@go run cmd/main.go
|
||||
|
||||
test: build
|
||||
@echo "Testing..."
|
||||
@go test ./... -v
|
||||
@echo "Starting test database container..."
|
||||
@podman-compose -f compose.test.yaml up -d
|
||||
@sleep 10
|
||||
@echo "Running integration tests..."
|
||||
@just test-integration
|
||||
@echo "Stopping test database container..."
|
||||
@just test-integration-down
|
||||
|
||||
# Clean the binary
|
||||
clean:
|
||||
@@ -102,7 +110,9 @@ podman-down:
|
||||
# Run integration tests with podman
|
||||
# Starts a test PostgreSQL container, runs tests, then cleans up
|
||||
test-integration:
|
||||
@echo "Starting test database container..."
|
||||
@echo "Cleaning old test database..."
|
||||
@podman-compose -f compose.test.yaml down -v
|
||||
@echo "Starting fresh test database container..."
|
||||
@podman-compose -f compose.test.yaml up -d
|
||||
@sleep 10
|
||||
@echo "Running integration tests..."
|
||||
|
||||
Reference in New Issue
Block a user