Compare commits
5
Commits
main
..
c63202242b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63202242b | ||
|
|
3418f492f5 | ||
|
|
f4d1c3cf28 | ||
|
|
98c1948eff | ||
|
|
3e37303979 |
No files matched your search
@@ -1,31 +0,0 @@
|
|||||||
# 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,7 +13,3 @@ CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters
|
|||||||
PORT=8081
|
PORT=8081
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
LOG_JSON=false
|
LOG_JSON=false
|
||||||
|
|
||||||
# Session Authentication (for testing)
|
|
||||||
# Set to disable session auth during development
|
|
||||||
DISABLE_SESSION_AUTH=true
|
|
||||||
@@ -12,6 +12,3 @@ package.json
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
cpu.pprof
|
cpu.pprof
|
||||||
main
|
main
|
||||||
|
|
||||||
# Database dumps
|
|
||||||
*_dump.sql
|
|
||||||
+12
-12
@@ -1,29 +1,26 @@
|
|||||||
# Stage 1: Build backend
|
|
||||||
FROM golang:1.25-alpine as build_go
|
FROM golang:1.25-alpine as build_go
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY cmd/ ./cmd/
|
|
||||||
COPY internal/ ./internal/
|
COPY . .
|
||||||
RUN find . -name "*_test.go" -delete && \
|
|
||||||
find . -name "integration_test.go" -delete
|
|
||||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
|
||||||
RUN templ generate
|
RUN templ generate
|
||||||
|
|
||||||
RUN go build -o main cmd/main.go
|
RUN go build -o main cmd/main.go
|
||||||
|
|
||||||
# Stage 2: Final image
|
# Stage 2, distribution container
|
||||||
FROM golang:1.25-alpine
|
FROM golang:1.25-alpine
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME /sorted
|
VOLUME /sorted
|
||||||
|
VOLUME /frontend
|
||||||
VOLUME /characters
|
VOLUME /characters
|
||||||
|
|
||||||
COPY --from=build_go /app/main .
|
|
||||||
COPY --from=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 PORT 8080
|
||||||
ENV DB_HOST ""
|
ENV DB_HOST ""
|
||||||
ENV DB_PORT ""
|
ENV DB_PORT ""
|
||||||
@@ -33,4 +30,7 @@ ENV DB_NAME ""
|
|||||||
ENV MUSIC_PATH ""
|
ENV MUSIC_PATH ""
|
||||||
ENV CHARACTERS_PATH ""
|
ENV CHARACTERS_PATH ""
|
||||||
|
|
||||||
|
COPY --from=build_go /app/main .
|
||||||
|
COPY ./songs/ ./songs/
|
||||||
|
|
||||||
CMD ./main
|
CMD ./main
|
||||||
+45
-695
@@ -23,548 +23,6 @@ var doc = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/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/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/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. Requires valid API password.",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Create session token",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Password and 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"description": "Deletes the current session token",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Invalidate session token",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Bearer token",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/token/cleanup": {
|
|
||||||
"post": {
|
|
||||||
"description": "Removes all expired session tokens from the database",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Cleanup expired sessions",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Bearer token",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/character": {
|
"/character": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the image for a specific character",
|
"description": "Returns the image for a specific character",
|
||||||
@@ -623,6 +81,29 @@ var doc = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/dbtest": {
|
||||||
|
"get": {
|
||||||
|
"description": "Tests the database connection",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"database"
|
||||||
|
],
|
||||||
|
"summary": "Test database connection",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "TestedDB",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/download": {
|
"/download": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Checks for the latest version of the application",
|
"description": "Checks for the latest version of the application",
|
||||||
@@ -833,7 +314,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/music/all/order": {
|
"/music/all/order": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of all soundtracks in order",
|
"description": "Returns a list of all games in order",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -843,7 +324,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all soundtracks",
|
"summary": "Get all games",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -866,7 +347,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/music/all/random": {
|
"/music/all/random": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of all soundtracks in random order",
|
"description": "Returns a list of all games in random order",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -876,7 +357,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all soundtracks random",
|
"summary": "Get all games random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1206,7 +687,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/sync": {
|
"/sync": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Starts syncing soundtracks with only new changes",
|
"description": "Starts syncing games with only new changes",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1216,10 +697,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync soundtracks with only changes",
|
"summary": "Sync games with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing soundtracks",
|
"description": "Start syncing games",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1235,7 +716,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/sync/full": {
|
"/sync/full": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Starts a full sync of all soundtracks",
|
"description": "Starts a full sync of all games",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1245,10 +726,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync all soundtracks fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing soundtracks full",
|
"description": "Start syncing games full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1288,7 +769,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/sync/reset": {
|
"/sync/reset": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Resets the soundtracks database by deleting all soundtracks and songs",
|
"description": "Resets the games database by deleting all games and songs",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1298,10 +779,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset soundtracks database",
|
"summary": "Reset games database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Soundtracks and songs are deleted from the database",
|
"description": "Games and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1317,7 +798,7 @@ var doc = `{
|
|||||||
},
|
},
|
||||||
"/version": {
|
"/version": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get latest version info",
|
"description": "get string by ID",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1325,9 +806,9 @@ var doc = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"version"
|
"accounts"
|
||||||
],
|
],
|
||||||
"summary": "Getting the latest version of the backend",
|
"summary": "Getting the version of the backend",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1343,158 +824,27 @@ var doc = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"/version/history": {
|
|
||||||
"get": {
|
|
||||||
"description": "get version history",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"version"
|
|
||||||
],
|
|
||||||
"summary": "Getting the version history of the backend",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/backend.VersionData"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"backend.SongInfoForStats": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"file_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"song_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"soundtrack_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"times_played": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"backend.SoundtrackWithSongs": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"songs": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"soundtrack_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"soundtrack_last_played": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_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": {
|
"backend.VersionData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"changelog": {
|
"changelog": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "account name"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/backend.VersionData"
|
||||||
},
|
}
|
||||||
"example": [
|
|
||||||
"[\"Initial release\"",
|
|
||||||
"\"Bug fixes\"]"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "1.0.0"
|
"example": "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"server.TokenRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"client_type": {
|
|
||||||
"description": "Optional: \"web\", \"mobile\", \"api\"",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"description": "Required: API password",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"server.TokenResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"client_type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|||||||
+45
-695
@@ -4,548 +4,6 @@
|
|||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/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/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/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. Requires valid API password.",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Create session token",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Password and 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"description": "Deletes the current session token",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Invalidate session token",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Bearer token",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/token/cleanup": {
|
|
||||||
"post": {
|
|
||||||
"description": "Removes all expired session tokens from the database",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"auth"
|
|
||||||
],
|
|
||||||
"summary": "Cleanup expired sessions",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Bearer token",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/character": {
|
"/character": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the image for a specific character",
|
"description": "Returns the image for a specific character",
|
||||||
@@ -604,6 +62,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/dbtest": {
|
||||||
|
"get": {
|
||||||
|
"description": "Tests the database connection",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"database"
|
||||||
|
],
|
||||||
|
"summary": "Test database connection",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "TestedDB",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/download": {
|
"/download": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Checks for the latest version of the application",
|
"description": "Checks for the latest version of the application",
|
||||||
@@ -814,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"/music/all/order": {
|
"/music/all/order": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of all soundtracks in order",
|
"description": "Returns a list of all games in order",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -824,7 +305,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all soundtracks",
|
"summary": "Get all games",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -847,7 +328,7 @@
|
|||||||
},
|
},
|
||||||
"/music/all/random": {
|
"/music/all/random": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of all soundtracks in random order",
|
"description": "Returns a list of all games in random order",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -857,7 +338,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all soundtracks random",
|
"summary": "Get all games random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1187,7 +668,7 @@
|
|||||||
},
|
},
|
||||||
"/sync": {
|
"/sync": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Starts syncing soundtracks with only new changes",
|
"description": "Starts syncing games with only new changes",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1197,10 +678,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync soundtracks with only changes",
|
"summary": "Sync games with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing soundtracks",
|
"description": "Start syncing games",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1216,7 +697,7 @@
|
|||||||
},
|
},
|
||||||
"/sync/full": {
|
"/sync/full": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Starts a full sync of all soundtracks",
|
"description": "Starts a full sync of all games",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1226,10 +707,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync all soundtracks fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing soundtracks full",
|
"description": "Start syncing games full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1269,7 +750,7 @@
|
|||||||
},
|
},
|
||||||
"/sync/reset": {
|
"/sync/reset": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Resets the soundtracks database by deleting all soundtracks and songs",
|
"description": "Resets the games database by deleting all games and songs",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1279,10 +760,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset soundtracks database",
|
"summary": "Reset games database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Soundtracks and songs are deleted from the database",
|
"description": "Games and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -1298,7 +779,7 @@
|
|||||||
},
|
},
|
||||||
"/version": {
|
"/version": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get latest version info",
|
"description": "get string by ID",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1306,9 +787,9 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"version"
|
"accounts"
|
||||||
],
|
],
|
||||||
"summary": "Getting the latest version of the backend",
|
"summary": "Getting the version of the backend",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1324,158 +805,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"/version/history": {
|
|
||||||
"get": {
|
|
||||||
"description": "get version history",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"version"
|
|
||||||
],
|
|
||||||
"summary": "Getting the version history of the backend",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/backend.VersionData"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"backend.SongInfoForStats": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"file_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"song_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"soundtrack_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"times_played": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"backend.SoundtrackWithSongs": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"songs": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/backend.SongInfoForStats"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"soundtrack_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"soundtrack_last_played": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"soundtrack_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": {
|
"backend.VersionData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"changelog": {
|
"changelog": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "account name"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/backend.VersionData"
|
||||||
},
|
}
|
||||||
"example": [
|
|
||||||
"[\"Initial release\"",
|
|
||||||
"\"Bug fixes\"]"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "1.0.0"
|
"example": "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"server.TokenRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"client_type": {
|
|
||||||
"description": "Optional: \"web\", \"mobile\", \"api\"",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"description": "Required: API password",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"server.TokenResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"client_type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+35
-466
@@ -1,444 +1,20 @@
|
|||||||
definitions:
|
definitions:
|
||||||
backend.SongInfoForStats:
|
|
||||||
properties:
|
|
||||||
file_name:
|
|
||||||
type: string
|
|
||||||
path:
|
|
||||||
type: string
|
|
||||||
song_name:
|
|
||||||
type: string
|
|
||||||
soundtrack_id:
|
|
||||||
type: integer
|
|
||||||
soundtrack_name:
|
|
||||||
type: string
|
|
||||||
times_played:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
backend.SoundtrackWithSongs:
|
|
||||||
properties:
|
|
||||||
songs:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/backend.SongInfoForStats'
|
|
||||||
type: array
|
|
||||||
soundtrack_id:
|
|
||||||
type: integer
|
|
||||||
soundtrack_last_played:
|
|
||||||
type: string
|
|
||||||
soundtrack_name:
|
|
||||||
type: string
|
|
||||||
soundtrack_played:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
backend.StatisticsSummary:
|
|
||||||
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
|
|
||||||
type: object
|
|
||||||
backend.VersionData:
|
backend.VersionData:
|
||||||
properties:
|
properties:
|
||||||
changelog:
|
changelog:
|
||||||
example:
|
example: account name
|
||||||
- '["Initial release"'
|
|
||||||
- '"Bug fixes"]'
|
|
||||||
items:
|
|
||||||
type: string
|
type: string
|
||||||
|
history:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.VersionData'
|
||||||
type: array
|
type: array
|
||||||
version:
|
version:
|
||||||
example: 1.0.0
|
example: 1.0.0
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
server.TokenRequest:
|
|
||||||
properties:
|
|
||||||
client_type:
|
|
||||||
description: 'Optional: "web", "mobile", "api"'
|
|
||||||
type: string
|
|
||||||
password:
|
|
||||||
description: 'Required: API password'
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
server.TokenResponse:
|
|
||||||
properties:
|
|
||||||
client_type:
|
|
||||||
type: string
|
|
||||||
expires_at:
|
|
||||||
type: string
|
|
||||||
token:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
paths:
|
paths:
|
||||||
/api/v1/statistics/songs/least-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns the top N least played songs with their soundtrack 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 soundtrack 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/soundtracks/last-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns the most recently played soundtracks
|
|
||||||
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.SoundtrackWithSongs'
|
|
||||||
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 soundtracks
|
|
||||||
tags:
|
|
||||||
- statistics
|
|
||||||
/api/v1/statistics/soundtracks/least-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns the top N least played soundtracks 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.SoundtrackWithSongs'
|
|
||||||
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 soundtracks
|
|
||||||
tags:
|
|
||||||
- statistics
|
|
||||||
/api/v1/statistics/soundtracks/most-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns the top N most played soundtracks 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.SoundtrackWithSongs'
|
|
||||||
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 soundtracks
|
|
||||||
tags:
|
|
||||||
- statistics
|
|
||||||
/api/v1/statistics/soundtracks/never-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns all soundtracks that have never been played (times_played
|
|
||||||
= 0)
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/backend.SoundtrackWithSongs'
|
|
||||||
type: array
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Get never played soundtracks
|
|
||||||
tags:
|
|
||||||
- statistics
|
|
||||||
/api/v1/statistics/soundtracks/oldest-played:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns the least recently played soundtracks (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.SoundtrackWithSongs'
|
|
||||||
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 soundtracks
|
|
||||||
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. Requires valid API
|
|
||||||
password.
|
|
||||||
parameters:
|
|
||||||
- description: Password and 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
|
|
||||||
"401":
|
|
||||||
description: Unauthorized
|
|
||||||
schema:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Create session token
|
|
||||||
tags:
|
|
||||||
- auth
|
|
||||||
/api/v1/token/cleanup:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Removes all expired session tokens from the database
|
|
||||||
parameters:
|
|
||||||
- description: Bearer token
|
|
||||||
in: header
|
|
||||||
name: Authorization
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
|
||||||
"401":
|
|
||||||
description: Unauthorized
|
|
||||||
schema:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Cleanup expired sessions
|
|
||||||
tags:
|
|
||||||
- auth
|
|
||||||
/character:
|
/character:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -477,6 +53,21 @@ paths:
|
|||||||
summary: Get list of characters
|
summary: Get list of characters
|
||||||
tags:
|
tags:
|
||||||
- characters
|
- characters
|
||||||
|
/dbtest:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Tests the database connection
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: TestedDB
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Test database connection
|
||||||
|
tags:
|
||||||
|
- database
|
||||||
/download:
|
/download:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -617,7 +208,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Returns a list of all soundtracks in order
|
description: Returns a list of all games in order
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -632,14 +223,14 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all soundtracks
|
summary: Get all games
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/all/random:
|
/music/all/random:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Returns a list of all soundtracks in random order
|
description: Returns a list of all games in random order
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -654,7 +245,7 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all soundtracks random
|
summary: Get all games random
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/info:
|
/music/info:
|
||||||
@@ -863,38 +454,38 @@ paths:
|
|||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Starts syncing soundtracks with only new changes
|
description: Starts syncing games with only new changes
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing soundtracks
|
description: Start syncing games
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Sync soundtracks with only changes
|
summary: Sync games with only changes
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/sync/full:
|
/sync/full:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Starts a full sync of all soundtracks
|
description: Starts a full sync of all games
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing soundtracks full
|
description: Start syncing games full
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Sync all soundtracks fully
|
summary: Sync all games fully
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/sync/progress:
|
/sync/progress:
|
||||||
@@ -917,27 +508,26 @@ paths:
|
|||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Resets the soundtracks database by deleting all soundtracks and
|
description: Resets the games database by deleting all games and songs
|
||||||
songs
|
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Soundtracks and songs are deleted from the database
|
description: Games and songs are deleted from the database
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Reset soundtracks database
|
summary: Reset games database
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/version:
|
/version:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: get latest version info
|
description: get string by ID
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -949,28 +539,7 @@ paths:
|
|||||||
description: Not Found
|
description: Not Found
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Getting the latest version of the backend
|
summary: Getting the version of the backend
|
||||||
tags:
|
tags:
|
||||||
- version
|
- accounts
|
||||||
/version/history:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: get version history
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/backend.VersionData'
|
|
||||||
type: array
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
summary: Getting the version history of the backend
|
|
||||||
tags:
|
|
||||||
- version
|
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
@@ -1,33 +1,5 @@
|
|||||||
/* Pure CSS styles for Music Search */
|
/* Pure CSS styles for Music Search */
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Light mode colors (default) */
|
|
||||||
--bg-primary: #f3f4f6;
|
|
||||||
--bg-secondary: #e5e7eb;
|
|
||||||
--bg-tertiary: #dcfce7;
|
|
||||||
--text-primary: #000;
|
|
||||||
--text-secondary: #374151;
|
|
||||||
--border-primary: #9ca3af;
|
|
||||||
--border-focus: #6b7280;
|
|
||||||
--accent-primary: #f97316;
|
|
||||||
--accent-hover: #ea580c;
|
|
||||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
/* Dark mode colors matching frontend */
|
|
||||||
--bg-primary: #555;
|
|
||||||
--bg-secondary: #333;
|
|
||||||
--bg-tertiary: #2a2a2a;
|
|
||||||
--text-primary: #fff;
|
|
||||||
--text-secondary: #ff9c00;
|
|
||||||
--border-primary: #666;
|
|
||||||
--border-focus: #ff9c00;
|
|
||||||
--accent-primary: #ff9c00;
|
|
||||||
--accent-hover: #e68a00;
|
|
||||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -38,9 +10,7 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: var(--bg-primary);
|
background-color: #f3f4f6;
|
||||||
color: var(--text-primary);
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -59,15 +29,15 @@ main {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid #9ca3af;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: var(--bg-secondary);
|
background-color: #e5e7eb;
|
||||||
color: var(--text-primary);
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_term:focus {
|
#search_term:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--border-focus);
|
border-color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear {
|
#clear {
|
||||||
@@ -75,48 +45,23 @@ main {
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: var(--accent-primary);
|
background-color: #f97316;
|
||||||
color: var(--text-primary);
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear:hover {
|
#clear:hover {
|
||||||
background-color: var(--accent-hover);
|
background-color: #ea580c;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games-container {
|
#games-container {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-text {
|
|
||||||
color: var(--text-primary);
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode toggle */
|
|
||||||
#dark-mode-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1000;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dark-mode-toggle:hover {
|
|
||||||
background-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Game result cards */
|
/* Game result cards */
|
||||||
.bg-green-100 {
|
.bg-green-100 {
|
||||||
background-color: var(--bg-tertiary);
|
background-color: #dcfce7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-4 {
|
.p-4 {
|
||||||
@@ -124,7 +69,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-md {
|
.shadow-md {
|
||||||
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color);
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ package web
|
|||||||
|
|
||||||
templ Base() {
|
templ Base() {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>Music Search</title>
|
<title>Music Search</title>
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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.GetAllGames()
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"music-server/internal/backend"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var soundtracks_added []string
|
|
||||||
|
|
||||||
func FindSoundtrackWebHandler(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")
|
|
||||||
search(search_term)
|
|
||||||
results := soundtracks_added
|
|
||||||
|
|
||||||
component := FoundSoundtracks(results)
|
|
||||||
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
|
|
||||||
soundtrackData := backend.GetSearchCache()
|
|
||||||
results := searchWithData(searchText, soundtrackData)
|
|
||||||
soundtracks_added = results
|
|
||||||
}
|
|
||||||
|
|
||||||
// searchWithData is a testable version of search that takes soundtrack data as a parameter
|
|
||||||
func searchWithData(searchText string, soundtrackData []backend.SoundtrackData) []string {
|
|
||||||
var results []string
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
// Precompute cleaned and lowercase search terms once
|
|
||||||
cleanedSearchTerm := strings.ToLower(searchText)
|
|
||||||
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, " ", "")
|
|
||||||
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "é", "e")
|
|
||||||
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "+", "plus")
|
|
||||||
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "&", "and")
|
|
||||||
cleanedSearchTerm = strings.ReplaceAll(cleanedSearchTerm, "'n", "and")
|
|
||||||
lowercaseSearchText := strings.ToLower(searchText)
|
|
||||||
|
|
||||||
// Pass 1: Exact match (case-insensitive contains)
|
|
||||||
for _, data := range soundtrackData {
|
|
||||||
if !seen[data.Original] && (searchText == "" || strings.Contains(data.Lowercase, lowercaseSearchText)) {
|
|
||||||
results = append(results, data.Original)
|
|
||||||
seen[data.Original] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: Contains match with cleaned terms
|
|
||||||
for _, data := range soundtrackData {
|
|
||||||
if !seen[data.Original] && (cleanedSearchTerm == "" || strings.Contains(data.Cleaned, cleanedSearchTerm)) {
|
|
||||||
results = append(results, data.Original)
|
|
||||||
seen[data.Original] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 3: Regex match with cleaned terms
|
|
||||||
for _, data := range soundtrackData {
|
|
||||||
if !seen[data.Original] && backend.MatchRegexCached(cleanedSearchTerm, data.Cleaned) {
|
|
||||||
results = append(results, data.Original)
|
|
||||||
seen[data.Original] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
templ SearchForm() {
|
|
||||||
@Base() {
|
|
||||||
<button id="dark-mode-toggle">🌙</button>
|
|
||||||
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
|
|
||||||
<button type="button" id="clear" name="clear">Clear</button>
|
|
||||||
<div id="soundtracks-container"></div>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
if (document.readyState == 'complete') {
|
|
||||||
// Initialize search
|
|
||||||
htmlx.ajax('POST', '/find', '#soundtracks-container');
|
|
||||||
document.getElementById("search_term").focus();
|
|
||||||
|
|
||||||
// Initialize theme from localStorage (default to dark)
|
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
||||||
document.getElementById('dark-mode-toggle').textContent = savedTheme === 'dark' ? '☀️' : '🌙';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 = "";
|
|
||||||
htmlx.ajax('POST', '/find', '#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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"music-server/internal/backend"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestCleanTerm tests that search terms are cleaned correctly
|
|
||||||
func TestCleanTerm(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
// CleanSearchTermForMatch removes spaces, replaces specific chars, and lowercases
|
|
||||||
{"MGS4", "mgs4"},
|
|
||||||
{"RE2", "re2"},
|
|
||||||
{"FFVII", "ffvii"},
|
|
||||||
{"Ys", "ys"},
|
|
||||||
{" test ", "test"},
|
|
||||||
{"Test 123", "test123"},
|
|
||||||
{"A+B", "aplusb"},
|
|
||||||
{"é", "e"},
|
|
||||||
// Note: CleanSearchTermForMatch does NOT remove apostrophes, it only replaces "'n" with "and"
|
|
||||||
{"It's", "it's"},
|
|
||||||
// But it does replace "'n" with "and"
|
|
||||||
{"I'n", "iand"},
|
|
||||||
{"", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := backend.CleanSearchTermForMatch(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("CleanSearchTermForMatch(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// searchWithDataHelper is a helper to create backend.SoundtrackData from original names
|
|
||||||
func searchWithDataHelper(originals []string) []backend.SoundtrackData {
|
|
||||||
data := make([]backend.SoundtrackData, len(originals))
|
|
||||||
for i, orig := range originals {
|
|
||||||
data[i] = backend.SoundtrackData{
|
|
||||||
Original: orig,
|
|
||||||
Lowercase: strings.ToLower(orig),
|
|
||||||
Cleaned: backend.CleanSearchTermForMatch(orig),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNormalSearch tests the normal search function
|
|
||||||
func TestNormalSearch(t *testing.T) {
|
|
||||||
// Set up comprehensive test data with all user-provided soundtracks
|
|
||||||
testData := searchWithDataHelper([]string{
|
|
||||||
"A Bird Story",
|
|
||||||
"Actraiser",
|
|
||||||
"Actraiser 2",
|
|
||||||
"Bangai-O Spirits",
|
|
||||||
"Ciconia When they Cry",
|
|
||||||
"Civilization IV",
|
|
||||||
"Civilization VI",
|
|
||||||
"Clair Obscur - Expedition 33",
|
|
||||||
"Club House Games (42 All-Time Classics)",
|
|
||||||
"Final Fantasy Dissidia 012 Duodecim (Sista Fantasin Meningsskiljaktigheter 012 Tolv)",
|
|
||||||
"Final Fantasy VII Crisis Core",
|
|
||||||
"Final Fantasy VII Rebirth",
|
|
||||||
"Final Fantasy VII Remake",
|
|
||||||
"Final Fantasy VII",
|
|
||||||
"Joe & Mac",
|
|
||||||
"Judgment",
|
|
||||||
"Kingdom Hearts",
|
|
||||||
"Kingdom Hearts Birth By Sleep",
|
|
||||||
"Kingdom Hearts II",
|
|
||||||
"Kingdom Hearts III",
|
|
||||||
"Lemmings (SNES + Mega Drive)",
|
|
||||||
"Lemmings 3D",
|
|
||||||
"Mega Man X4",
|
|
||||||
"Metal Gear Rising - Revengeance",
|
|
||||||
"Metal Gear Solid (+Twin Snakes)",
|
|
||||||
"Metal Gear Solid 2 - Sons of Liberty",
|
|
||||||
"Metal Gear Solid 3 - Snake Eater",
|
|
||||||
"Metal Gear Solid 4 - Guns of the Patriots",
|
|
||||||
"Metroid Prime",
|
|
||||||
"Octopath Traveler",
|
|
||||||
"Octopath Traveler 2",
|
|
||||||
"Ori and the Blind Forest",
|
|
||||||
"Ori and the Will of the Wisps",
|
|
||||||
"OutRun 2006 - Coast 2 Coast",
|
|
||||||
"Paper Mario - The Thousand Year Door",
|
|
||||||
"Pokémon HeartGold & SoulSilver",
|
|
||||||
"Resident Evil 2 (Remake)",
|
|
||||||
"Shenmue",
|
|
||||||
"Super Mario Galaxy",
|
|
||||||
"Trails in the Sky FC (Sora no Kiseki FC)",
|
|
||||||
"Trails in the Sky SC (Sora no Kiseki SC)",
|
|
||||||
"Turrican II - The Final Fight",
|
|
||||||
"Umineko no Naku Koro ni",
|
|
||||||
"Umineko no Naku Koro ni Chiru",
|
|
||||||
"VVVVVV",
|
|
||||||
"YS VIII - Lacrimosa of DANA",
|
|
||||||
"Yakuza 0",
|
|
||||||
"Ys (Chronicles)",
|
|
||||||
"Ys - Memories of Celceta",
|
|
||||||
"Ys - The Ark of Napishtim",
|
|
||||||
"Ys II (Chronicles)",
|
|
||||||
"Ys III - Wanderers from Ys (+The Oath in Felghana)",
|
|
||||||
"Ys IV - The Dawn of Ys",
|
|
||||||
"Ys Origin",
|
|
||||||
"Ys Seven",
|
|
||||||
"Ys X Nordics",
|
|
||||||
"Call of Duty: Modern Warfare",
|
|
||||||
"Half-Life 3",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test cases covering all match types
|
|
||||||
tests := []struct {
|
|
||||||
query string
|
|
||||||
wantFound string
|
|
||||||
}{
|
|
||||||
// Exact substring matches
|
|
||||||
{"Metal", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
{"Resident", "Resident Evil 2 (Remake)"},
|
|
||||||
{"Final", "Final Fantasy VII"},
|
|
||||||
{"Ys", "Ys III - Wanderers from Ys (+The Oath in Felghana)"},
|
|
||||||
|
|
||||||
// Contains matches (on cleaned terms)
|
|
||||||
{"gotp", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
{"mgs", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
{"kh", "Kingdom Hearts"},
|
|
||||||
{"mg", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
|
|
||||||
// Regex matches
|
|
||||||
{"mgs4go", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
{"re2r", "Resident Evil 2 (Remake)"},
|
|
||||||
{"ffv", "Final Fantasy VII"},
|
|
||||||
{"yiwfyt", "Ys III - Wanderers from Ys (+The Oath in Felghana)"},
|
|
||||||
{"codmw", "Call of Duty: Modern Warfare"},
|
|
||||||
{"mgs4gop", "Metal Gear Solid 4 - Guns of the Patriots"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.query, func(t *testing.T) {
|
|
||||||
results := searchWithData(tt.query, testData)
|
|
||||||
|
|
||||||
// Check that the expected item is in the results
|
|
||||||
found := false
|
|
||||||
for _, result := range results {
|
|
||||||
if result == tt.wantFound {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("searchWithData(%q, ...) did not find expected item %q, got %v", tt.query, tt.wantFound, results)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSearchOrdering tests that Pass 1 matches come before Pass 2 matches
|
|
||||||
func TestSearchOrdering(t *testing.T) {
|
|
||||||
// "MG" matches Pass 1 (exact substring on original)
|
|
||||||
// "Metal Gear" matches Pass 2 (contains on cleaned) but NOT Pass 1 (no "mg" substring in "metal gear")
|
|
||||||
testData := searchWithDataHelper([]string{
|
|
||||||
"Metal Gear",
|
|
||||||
"MG",
|
|
||||||
})
|
|
||||||
|
|
||||||
results := searchWithData("mg", testData)
|
|
||||||
|
|
||||||
// Find indices
|
|
||||||
indexPass1 := -1
|
|
||||||
indexPass2 := -1
|
|
||||||
for i, result := range results {
|
|
||||||
if result == "MG" {
|
|
||||||
indexPass1 = i
|
|
||||||
}
|
|
||||||
if result == "Metal Gear" {
|
|
||||||
indexPass2 = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 1 match (MG) should come before Pass 2 match (Metal Gear)
|
|
||||||
if indexPass1 >= 0 && indexPass2 >= 0 && indexPass1 >= indexPass2 {
|
|
||||||
t.Errorf("Expected Pass 1 match 'MG' to come before Pass 2 match 'Metal Gear', but got order: %v", results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: The tests above verify:
|
|
||||||
// 1. clean_term - the term cleaning logic
|
|
||||||
// 2. search - the complete normal search with exact, contains, and regex matching
|
|
||||||
// 3. ordering - exact matches come before contains matches
|
|
||||||
@@ -5,7 +5,6 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/MShekow/directory-checksum v1.4.18
|
github.com/MShekow/directory-checksum v1.4.18
|
||||||
github.com/a-h/templ v0.3.1020
|
github.com/a-h/templ v0.3.1020
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/labstack/echo/v5 v5.1.1
|
github.com/labstack/echo/v5 v5.1.1
|
||||||
@@ -19,8 +18,6 @@ require (
|
|||||||
go.uber.org/zap v1.28.0
|
go.uber.org/zap v1.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
@@ -35,6 +32,7 @@ require (
|
|||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.10.0 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
@@ -52,6 +50,7 @@ require (
|
|||||||
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
|
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
|
||||||
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
|
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
|
||||||
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
|
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"music-server/internal/logging"
|
"music-server/internal/logging"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCharacterList() []string {
|
func GetCharacterList() []string {
|
||||||
@@ -30,10 +30,10 @@ func GetCharacterList() []string {
|
|||||||
|
|
||||||
func GetCharacter(character string) string {
|
func GetCharacter(character string) string {
|
||||||
charactersPath := os.Getenv("CHARACTERS_PATH")
|
charactersPath := os.Getenv("CHARACTERS_PATH")
|
||||||
|
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath))
|
||||||
// Clean the path - remove trailing slashes and then add one for consistency
|
// Clean the path - remove trailing slashes and then add one for consistency
|
||||||
charactersPath = strings.TrimSuffix(charactersPath, "/")
|
charactersPath = strings.TrimSuffix(charactersPath, "/")
|
||||||
charactersPath += "/"
|
charactersPath += "/"
|
||||||
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath+character))
|
|
||||||
return charactersPath + character
|
return charactersPath + character
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"music-server/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDB() {
|
||||||
|
db.Testf()
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionData struct {
|
||||||
|
Version string `json:"version" example:"1.0.0"`
|
||||||
|
Changelog string `json:"changelog" example:"account name"`
|
||||||
|
History []VersionData `json:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVersionHistory() VersionData {
|
||||||
|
data := VersionData{Version: "4.5.0",
|
||||||
|
Changelog: "#1 - Created request to check newest version of the app\n" +
|
||||||
|
"#2 - Added request to download the newest version of the app\n" +
|
||||||
|
"#3 - Added request to check progress during sync\n" +
|
||||||
|
"#4 - Now blocking all request while sync is in progress\n" +
|
||||||
|
"#5 - Implemented ants for thread pooling\n" +
|
||||||
|
"#6 - Changed the sync request to now only start the sync",
|
||||||
|
History: []VersionData{
|
||||||
|
{
|
||||||
|
Version: "4.0.0",
|
||||||
|
Changelog: "Changed framework from gin to Echo\n" +
|
||||||
|
"Reorganized the code\n" +
|
||||||
|
"Implemented sqlc\n" +
|
||||||
|
"Added support to send character images from the server\n" +
|
||||||
|
"Added function to create a new database of no one exists",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.2",
|
||||||
|
Changelog: "Upgraded Go version and the version of all dependencies. Fixed som more bugs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.1",
|
||||||
|
Changelog: "Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.0",
|
||||||
|
Changelog: "Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.3.0",
|
||||||
|
Changelog: "Images should not be included in the database, removes songs where the path doesn't work.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.2.0",
|
||||||
|
Changelog: "Changed the structure of the whole application, should be no changes to functionality.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.4",
|
||||||
|
Changelog: "Game list should now be sorted, a new endpoint with the game list in random order have been added.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.3",
|
||||||
|
Changelog: "Added a check to see if song exists before returning it, if not a new song will be picked up.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.2",
|
||||||
|
Changelog: "Added test server to swagger file.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.1",
|
||||||
|
Changelog: "Fixed bug where wrong song was showed as currently played.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.0",
|
||||||
|
Changelog: "Added /addQue to add the last received song to the songQue. " +
|
||||||
|
"Changed /rand and /rand/low to not add song to the que. " +
|
||||||
|
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.3",
|
||||||
|
Changelog: "Another small change that should fix the caching problem.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.2",
|
||||||
|
Changelog: "Hopefully fixed the caching problem with random.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.1",
|
||||||
|
Changelog: "Fixed CORS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.0",
|
||||||
|
Changelog: "Rebuilt the application in Go.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
+60
-60
@@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SongInfo struct {
|
type SongInfo struct {
|
||||||
Soundtrack string `json:"Soundtrack"`
|
Game string `json:"Game"`
|
||||||
SoundtrackPlayed int32 `json:"SoundtrackPlayed"`
|
GamePlayed int32 `json:"GamePlayed"`
|
||||||
Song string `json:"Song"`
|
Song string `json:"Song"`
|
||||||
SongPlayed int32 `json:"SongPlayed"`
|
SongPlayed int32 `json:"SongPlayed"`
|
||||||
CurrentlyPlaying bool `json:"CurrentlyPlaying"`
|
CurrentlyPlaying bool `json:"CurrentlyPlaying"`
|
||||||
@@ -22,7 +22,7 @@ type SongInfo struct {
|
|||||||
|
|
||||||
var currentSong = -1
|
var currentSong = -1
|
||||||
|
|
||||||
var soundtracksNew []repository.Soundtrack
|
var gamesNew []repository.Game
|
||||||
|
|
||||||
var songQueNew []repository.Song
|
var songQueNew []repository.Song
|
||||||
|
|
||||||
@@ -37,12 +37,12 @@ func initRepo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllSoundtracks() []repository.Soundtrack {
|
func getAllGames() []repository.Game {
|
||||||
if len(soundtracksNew) == 0 {
|
if len(gamesNew) == 0 {
|
||||||
initRepo()
|
initRepo()
|
||||||
soundtracksNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
|
||||||
}
|
}
|
||||||
return soundtracksNew
|
return gamesNew
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ func Reset() {
|
|||||||
songQueNew = nil
|
songQueNew = nil
|
||||||
currentSong = -1
|
currentSong = -1
|
||||||
initRepo()
|
initRepo()
|
||||||
soundtracksNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddLatestToQue() {
|
func AddLatestToQue() {
|
||||||
@@ -77,8 +77,8 @@ func AddLatestPlayed() {
|
|||||||
currentSongData := songQueNew[currentSong]
|
currentSongData := songQueNew[currentSong]
|
||||||
|
|
||||||
initRepo()
|
initRepo()
|
||||||
BackendRepo().AddSoundtrackPlayed(BackendCtx(), currentSongData.SoundtrackID)
|
BackendRepo().AddGamePlayed(BackendCtx(), currentSongData.GameID)
|
||||||
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: currentSongData.SoundtrackID, SongName: currentSongData.SongName})
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: currentSongData.GameID, SongName: currentSongData.SongName})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetPlayed(songNumber int) {
|
func SetPlayed(songNumber int) {
|
||||||
@@ -87,39 +87,39 @@ func SetPlayed(songNumber int) {
|
|||||||
}
|
}
|
||||||
songData := songQueNew[songNumber]
|
songData := songQueNew[songNumber]
|
||||||
initRepo()
|
initRepo()
|
||||||
BackendRepo().AddSoundtrackPlayed(BackendCtx(), songData.SoundtrackID)
|
BackendRepo().AddGamePlayed(BackendCtx(), songData.GameID)
|
||||||
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: songData.SoundtrackID, SongName: songData.SongName})
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: songData.GameID, SongName: songData.SongName})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomSong() string {
|
func GetRandomSong() string {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
if len(soundtracksNew) == 0 {
|
if len(gamesNew) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
song := getSongFromList(soundtracksNew)
|
song := getSongFromList(gamesNew)
|
||||||
lastFetchedNew = song
|
lastFetchedNew = song
|
||||||
return song.Path
|
return song.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomSongLowChance() string {
|
func GetRandomSongLowChance() string {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
|
|
||||||
var listOfSoundtracks []repository.Soundtrack
|
var listOfGames []repository.Game
|
||||||
|
|
||||||
var averagePlayed = getAveragePlayed()
|
var averagePlayed = getAveragePlayed()
|
||||||
|
|
||||||
for _, data := range soundtracksNew {
|
for _, data := range gamesNew {
|
||||||
timesToAdd := averagePlayed - data.TimesPlayed
|
timesToAdd := averagePlayed - data.TimesPlayed
|
||||||
if timesToAdd <= 0 {
|
if timesToAdd <= 0 {
|
||||||
listOfSoundtracks = append(listOfSoundtracks, data)
|
listOfGames = append(listOfGames, data)
|
||||||
} else {
|
} else {
|
||||||
for i := int32(0); i < timesToAdd; i++ {
|
for i := int32(0); i < timesToAdd; i++ {
|
||||||
listOfSoundtracks = append(listOfSoundtracks, data)
|
listOfGames = append(listOfGames, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
song := getSongFromList(listOfSoundtracks)
|
song := getSongFromList(listOfGames)
|
||||||
|
|
||||||
lastFetchedNew = song
|
lastFetchedNew = song
|
||||||
return song.Path
|
return song.Path
|
||||||
@@ -127,11 +127,11 @@ func GetRandomSongLowChance() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomSongClassic() string {
|
func GetRandomSongClassic() string {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
|
|
||||||
var listOfAllSongs []repository.Song
|
var listOfAllSongs []repository.Song
|
||||||
for _, soundtrack := range soundtracksNew {
|
for _, game := range gamesNew {
|
||||||
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), soundtrack.ID)
|
songList, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
|
||||||
listOfAllSongs = append(listOfAllSongs, songList...)
|
listOfAllSongs = append(listOfAllSongs, songList...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,25 +139,25 @@ func GetRandomSongClassic() string {
|
|||||||
var song repository.Song
|
var song repository.Song
|
||||||
for !songFound {
|
for !songFound {
|
||||||
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
||||||
soundtrackData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID)
|
gameData, err := BackendRepo().GetGameById(BackendCtx(), song.GameID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("soundtrack", soundtrackData.SoundtrackName),
|
zap.String("game", gameData.GameName),
|
||||||
zap.String("filename", *song.FileName))
|
zap.String("filename", *song.FileName))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if file exists and open
|
//Check if file exists and open
|
||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil || (song.FileName != nil && soundtrackData.Path+*song.FileName != song.Path) {
|
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
|
||||||
//File not found
|
//File not found
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("soundtrack", soundtrackData.SoundtrackName),
|
zap.String("game", gameData.GameName),
|
||||||
zap.String("filename", *song.FileName))
|
zap.String("filename", *song.FileName))
|
||||||
} else {
|
} else {
|
||||||
songFound = true
|
songFound = true
|
||||||
@@ -177,11 +177,11 @@ func GetSongInfo() SongInfo {
|
|||||||
}
|
}
|
||||||
var currentSongData = songQueNew[currentSong]
|
var currentSongData = songQueNew[currentSong]
|
||||||
|
|
||||||
currentSoundtrackData := getCurrentSoundtrack(currentSongData)
|
currentGameData := getCurrentGame(currentSongData)
|
||||||
|
|
||||||
return SongInfo{
|
return SongInfo{
|
||||||
Soundtrack: currentSoundtrackData.SoundtrackName,
|
Game: currentGameData.GameName,
|
||||||
SoundtrackPlayed: currentSoundtrackData.TimesPlayed,
|
GamePlayed: currentGameData.TimesPlayed,
|
||||||
Song: currentSongData.SongName,
|
Song: currentSongData.SongName,
|
||||||
SongPlayed: currentSongData.TimesPlayed,
|
SongPlayed: currentSongData.TimesPlayed,
|
||||||
CurrentlyPlaying: true,
|
CurrentlyPlaying: true,
|
||||||
@@ -193,10 +193,10 @@ func GetPlayedSongs() []SongInfo {
|
|||||||
var songList []SongInfo
|
var songList []SongInfo
|
||||||
|
|
||||||
for i, song := range songQueNew {
|
for i, song := range songQueNew {
|
||||||
soundtrackData := getCurrentSoundtrack(song)
|
gameData := getCurrentGame(song)
|
||||||
songList = append(songList, SongInfo{
|
songList = append(songList, SongInfo{
|
||||||
Soundtrack: soundtrackData.SoundtrackName,
|
Game: gameData.GameName,
|
||||||
SoundtrackPlayed: soundtrackData.TimesPlayed,
|
GamePlayed: gameData.TimesPlayed,
|
||||||
Song: song.SongName,
|
Song: song.SongName,
|
||||||
SongPlayed: song.TimesPlayed,
|
SongPlayed: song.TimesPlayed,
|
||||||
CurrentlyPlaying: i == currentSong,
|
CurrentlyPlaying: i == currentSong,
|
||||||
@@ -217,22 +217,22 @@ func GetSong(song string) string {
|
|||||||
return songData.Path
|
return songData.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllSoundtracks() []string {
|
func GetAllGames() []string {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
|
|
||||||
var jsonArray []string
|
var jsonArray []string
|
||||||
for _, soundtrack := range soundtracksNew {
|
for _, game := range gamesNew {
|
||||||
jsonArray = append(jsonArray, soundtrack.SoundtrackName)
|
jsonArray = append(jsonArray, game.GameName)
|
||||||
}
|
}
|
||||||
return jsonArray
|
return jsonArray
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllSoundtracksRandom() []string {
|
func GetAllGamesRandom() []string {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
|
|
||||||
var jsonArray []string
|
var jsonArray []string
|
||||||
for _, soundtrack := range soundtracksNew {
|
for _, game := range gamesNew {
|
||||||
jsonArray = append(jsonArray, soundtrack.SoundtrackName)
|
jsonArray = append(jsonArray, game.GameName)
|
||||||
}
|
}
|
||||||
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
||||||
return jsonArray
|
return jsonArray
|
||||||
@@ -266,12 +266,12 @@ func GetPreviousSong() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSongFromList(soundtracks []repository.Soundtrack) repository.Song {
|
func getSongFromList(games []repository.Game) repository.Song {
|
||||||
songFound := false
|
songFound := false
|
||||||
var song repository.Song
|
var song repository.Song
|
||||||
for !songFound {
|
for !songFound {
|
||||||
soundtrack := getRandomSoundtrack(soundtracks)
|
game := getRandomGame(games)
|
||||||
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), soundtrack.ID)
|
songs, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
|
||||||
if len(songs) == 0 {
|
if len(songs) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -280,12 +280,12 @@ func getSongFromList(soundtracks []repository.Soundtrack) repository.Song {
|
|||||||
|
|
||||||
//Check if file exists and open
|
//Check if file exists and open
|
||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil || (song.FileName != nil && soundtrack.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
||||||
//File not found
|
//File not found
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("soundtrack", soundtrack.SoundtrackName),
|
zap.String("game", game.GameName),
|
||||||
zap.Any("filename", song.FileName))
|
zap.Any("filename", song.FileName))
|
||||||
} else {
|
} else {
|
||||||
songFound = true
|
songFound = true
|
||||||
@@ -299,24 +299,24 @@ func getSongFromList(soundtracks []repository.Soundtrack) repository.Song {
|
|||||||
return song
|
return song
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentSoundtrack(currentSongData repository.Song) repository.Soundtrack {
|
func getCurrentGame(currentSongData repository.Song) repository.Game {
|
||||||
for _, soundtrack := range soundtracksNew {
|
for _, game := range gamesNew {
|
||||||
if soundtrack.ID == currentSongData.SoundtrackID {
|
if game.ID == currentSongData.GameID {
|
||||||
return soundtrack
|
return game
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return repository.Soundtrack{}
|
return repository.Game{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAveragePlayed() int32 {
|
func getAveragePlayed() int32 {
|
||||||
getAllSoundtracks()
|
getAllGames()
|
||||||
var sum int32
|
var sum int32
|
||||||
for _, data := range soundtracksNew {
|
for _, data := range gamesNew {
|
||||||
sum += data.TimesPlayed
|
sum += data.TimesPlayed
|
||||||
}
|
}
|
||||||
return sum / int32(len(soundtracksNew))
|
return sum / int32(len(gamesNew))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRandomSoundtrack(listOfSoundtracks []repository.Soundtrack) repository.Soundtrack {
|
func getRandomGame(listOfGames []repository.Game) repository.Game {
|
||||||
return listOfSoundtracks[rand.Intn(len(listOfSoundtracks))]
|
return listOfGames[rand.Intn(len(listOfGames))]
|
||||||
}
|
}
|
||||||
@@ -9,17 +9,17 @@ import (
|
|||||||
|
|
||||||
// Test the average calculation logic directly without database access
|
// Test the average calculation logic directly without database access
|
||||||
func TestCalculateAverage(t *testing.T) {
|
func TestCalculateAverage(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{
|
games := []repository.Game{
|
||||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
{GameName: "Game1", TimesPlayed: 10},
|
||||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
{GameName: "Game2", TimesPlayed: 20},
|
||||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
{GameName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
var sum int32
|
var sum int32
|
||||||
for _, data := range soundtracks {
|
for _, data := range games {
|
||||||
sum += data.TimesPlayed
|
sum += data.TimesPlayed
|
||||||
}
|
}
|
||||||
result := sum / int32(len(soundtracks))
|
result := sum / int32(len(games))
|
||||||
expected := int32(20)
|
expected := int32(20)
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
@@ -28,9 +28,9 @@ func TestCalculateAverage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateAverageEmpty(t *testing.T) {
|
func TestCalculateAverageEmpty(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{}
|
games := []repository.Game{}
|
||||||
|
|
||||||
if len(soundtracks) == 0 {
|
if len(games) == 0 {
|
||||||
result := int32(0)
|
result := int32(0)
|
||||||
expected := int32(0)
|
expected := int32(0)
|
||||||
if result != expected {
|
if result != expected {
|
||||||
@@ -40,10 +40,10 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sum int32
|
var sum int32
|
||||||
for _, data := range soundtracks {
|
for _, data := range games {
|
||||||
sum += data.TimesPlayed
|
sum += data.TimesPlayed
|
||||||
}
|
}
|
||||||
result := sum / int32(len(soundtracks))
|
result := sum / int32(len(games))
|
||||||
expected := int32(0)
|
expected := int32(0)
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
@@ -52,150 +52,152 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateAverageSingle(t *testing.T) {
|
func TestCalculateAverageSingle(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{
|
games := []repository.Game{
|
||||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 42},
|
{GameName: "Game1", TimesPlayed: 42},
|
||||||
}
|
}
|
||||||
|
|
||||||
var sum int32
|
var sum int32
|
||||||
for _, data := range soundtracks {
|
for _, data := range games {
|
||||||
sum += data.TimesPlayed
|
sum += data.TimesPlayed
|
||||||
}
|
}
|
||||||
result := sum / int32(len(soundtracks))
|
result := sum / int32(len(games))
|
||||||
expected := int32(42)
|
expected := int32(42)
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
t.Errorf("Average calculation with single soundtrack = %v, want %v", result, expected)
|
t.Errorf("Average calculation with single game = %v, want %v", result, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRandomSoundtrack(t *testing.T) {
|
func TestGetRandomGame(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{
|
games := []repository.Game{
|
||||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
{GameName: "Game1", TimesPlayed: 10},
|
||||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
{GameName: "Game2", TimesPlayed: 20},
|
||||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
{GameName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seed for reproducible tests
|
// Set seed for reproducible tests
|
||||||
rand.Seed(42)
|
rand.Seed(42)
|
||||||
|
|
||||||
result := soundtracks[rand.Intn(len(soundtracks))]
|
result := games[rand.Intn(len(games))]
|
||||||
|
|
||||||
if result.SoundtrackName == "" {
|
if result.GameName == "" {
|
||||||
t.Error("random soundtrack selection returned empty soundtrack")
|
t.Error("random game selection returned empty game")
|
||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, s := range soundtracks {
|
for _, g := range games {
|
||||||
if s.SoundtrackName == result.SoundtrackName {
|
if g.GameName == result.GameName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("random soundtrack selection returned soundtrack not in list: %v", result.SoundtrackName)
|
t.Errorf("random game selection returned game not in list: %v", result.GameName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindSoundtrackByID(t *testing.T) {
|
func TestFindGameByID(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{
|
games := []repository.Game{
|
||||||
{ID: 1, SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
||||||
{ID: 2, SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||||
{ID: 3, SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
soundtracks []repository.Soundtrack
|
games []repository.Game
|
||||||
soundtrackID int32
|
gameID int32
|
||||||
expected repository.Soundtrack
|
expected repository.Game
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "existing soundtrack",
|
name: "existing game",
|
||||||
soundtracks: soundtracks,
|
games: games,
|
||||||
soundtrackID: 2,
|
gameID: 2,
|
||||||
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-existing soundtrack",
|
name: "non-existing game",
|
||||||
soundtracks: soundtracks,
|
games: games,
|
||||||
soundtrackID: 99,
|
gameID: 99,
|
||||||
expected: repository.Soundtrack{},
|
expected: repository.Game{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var result repository.Soundtrack
|
var result repository.Game
|
||||||
for _, s := range tt.soundtracks {
|
for _, game := range tt.games {
|
||||||
if s.ID == tt.soundtrackID {
|
if game.ID == tt.gameID {
|
||||||
result = s
|
result = game
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
if result.ID != tt.expected.ID || result.GameName != tt.expected.GameName {
|
||||||
t.Errorf("findSoundtrackByID() = %v, want %v", result, tt.expected)
|
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractSoundtrackNames(t *testing.T) {
|
func TestExtractGameNames(t *testing.T) {
|
||||||
soundtracks := []repository.Soundtrack{
|
games := []repository.Game{
|
||||||
{SoundtrackName: "Soundtrack1", TimesPlayed: 10},
|
{GameName: "Game1", TimesPlayed: 10},
|
||||||
{SoundtrackName: "Soundtrack2", TimesPlayed: 20},
|
{GameName: "Game2", TimesPlayed: 20},
|
||||||
{SoundtrackName: "Soundtrack3", TimesPlayed: 30},
|
{GameName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
for _, s := range soundtracks {
|
for _, game := range games {
|
||||||
result = append(result, s.SoundtrackName)
|
result = append(result, game.GameName)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{"Soundtrack1", "Soundtrack2", "Soundtrack3"}
|
expected := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
if len(result) != len(expected) {
|
if len(result) != len(expected) {
|
||||||
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected))
|
t.Errorf("extractGameNames() length = %d, want %d", len(result), len(expected))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, v := range result {
|
for i, v := range result {
|
||||||
if v != expected[i] {
|
if v != expected[i] {
|
||||||
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i])
|
t.Errorf("extractGameNames()[%d] = %v, want %v", i, v, expected[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShuffleSoundtrackNames(t *testing.T) {
|
func TestShuffleGameNames(t *testing.T) {
|
||||||
soundtracks := []string{"Soundtrack1", "Soundtrack2", "Soundtrack3"}
|
games := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
// Test that shuffle doesn't lose any elements
|
// 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
|
// We can't test the order since it's random, but we can test length and contents
|
||||||
original := make([]string, len(soundtracks))
|
original := make([]string, len(games))
|
||||||
copy(original, soundtracks)
|
copy(original, games)
|
||||||
|
|
||||||
// Simple shuffle implementation for testing
|
// Simple shuffle implementation for testing
|
||||||
for i := range soundtracks {
|
for i := range games {
|
||||||
j := i // In real code this would be random
|
j := i // In real code this would be random
|
||||||
soundtracks[i], soundtracks[j] = soundtracks[j], soundtracks[i]
|
games[i], games[j] = games[j], games[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(soundtracks) != len(original) {
|
if len(games) != len(original) {
|
||||||
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(soundtracks))
|
t.Errorf("shuffleGameNames() changed length from %d to %d", len(original), len(games))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all original elements are still present
|
// Check all original elements are still present
|
||||||
for _, orig := range original {
|
for _, orig := range original {
|
||||||
found := false
|
found := false
|
||||||
for _, s := range soundtracks {
|
for _, g := range games {
|
||||||
if s == orig {
|
if g == orig {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
t.Errorf("shuffleGameNames() lost element: %v", orig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SoundtrackData holds precomputed search data for a soundtrack
|
|
||||||
type SoundtrackData struct {
|
|
||||||
Original string
|
|
||||||
Lowercase string
|
|
||||||
Cleaned string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
searchCache []SoundtrackData
|
|
||||||
searchCacheMutex sync.Mutex
|
|
||||||
searchRegexCache = make(map[string]*regexp.Regexp)
|
|
||||||
searchRegexCacheMutex sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetSearchCache returns the precomputed search cache, initializing it if needed
|
|
||||||
func GetSearchCache() []SoundtrackData {
|
|
||||||
searchCacheMutex.Lock()
|
|
||||||
defer searchCacheMutex.Unlock()
|
|
||||||
|
|
||||||
if len(searchCache) == 0 {
|
|
||||||
loadSearchCache()
|
|
||||||
}
|
|
||||||
return searchCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReloadSearchCache clears and reloads the search cache.
|
|
||||||
// This should be called after a sync operation to ensure search results are up to date.
|
|
||||||
func ReloadSearchCache() {
|
|
||||||
searchCacheMutex.Lock()
|
|
||||||
defer searchCacheMutex.Unlock()
|
|
||||||
searchCache = nil
|
|
||||||
loadSearchCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadSearchCache loads the soundtrack data into the cache
|
|
||||||
func loadSearchCache() {
|
|
||||||
soundtracks := GetAllSoundtracks()
|
|
||||||
searchCache = make([]SoundtrackData, len(soundtracks))
|
|
||||||
|
|
||||||
for i, soundtrack := range soundtracks {
|
|
||||||
cleaned := cleanSearchTerm(soundtrack)
|
|
||||||
lowercase := strings.ToLower(soundtrack)
|
|
||||||
searchCache[i] = SoundtrackData{
|
|
||||||
Original: soundtrack,
|
|
||||||
Lowercase: lowercase,
|
|
||||||
Cleaned: cleaned,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanSearchTermForMatch cleans a search term for better matching
|
|
||||||
func CleanSearchTermForMatch(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanSearchTerm cleans a search term for better matching (internal)
|
|
||||||
func cleanSearchTerm(term string) string {
|
|
||||||
return CleanSearchTermForMatch(term)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchRegexCached checks if a search term matches using cached compiled regex
|
|
||||||
func MatchRegexCached(searchTerm string, soundtrackName string) bool {
|
|
||||||
if searchTerm == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
searchRegexCacheMutex.Lock()
|
|
||||||
re, exists := searchRegexCache[searchTerm]
|
|
||||||
if !exists {
|
|
||||||
re = compileSearchRegex(searchTerm)
|
|
||||||
searchRegexCache[searchTerm] = re
|
|
||||||
}
|
|
||||||
searchRegexCacheMutex.Unlock()
|
|
||||||
|
|
||||||
return re.MatchString(soundtrackName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileSearchRegex compiles a search term into a regex pattern
|
|
||||||
func compileSearchRegex(searchTerm string) *regexp.Regexp {
|
|
||||||
regText := ".*"
|
|
||||||
for _, letter := range searchTerm {
|
|
||||||
regText += string(letter) + ".*"
|
|
||||||
}
|
|
||||||
re, _ := regexp.Compile(regText)
|
|
||||||
return re
|
|
||||||
}
|
|
||||||
@@ -9,19 +9,19 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SoundtrackWithSongs represents a soundtrack with its songs for statistics
|
// GameWithSongs represents a game with its songs for statistics
|
||||||
type SoundtrackWithSongs struct {
|
type GameWithSongs struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
GamePlayed int32 `json:"game_played"`
|
||||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played,omitempty"`
|
GameLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
||||||
Songs []SongInfoForStats `json:"songs"`
|
Songs []SongInfoForStats `json:"songs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SongInfoForStats represents a song with soundtrack info for statistics
|
// SongInfoForStats represents a song with game info for statistics
|
||||||
type SongInfoForStats struct {
|
type SongInfoForStats struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
@@ -30,13 +30,13 @@ type SongInfoForStats struct {
|
|||||||
|
|
||||||
// StatisticsSummary holds overall statistics
|
// StatisticsSummary holds overall statistics
|
||||||
type StatisticsSummary struct {
|
type StatisticsSummary struct {
|
||||||
TotalSoundtracks int64 `json:"total_soundtracks"`
|
TotalGames int64 `json:"total_games"`
|
||||||
PlayedSoundtracks int64 `json:"played_soundtracks"`
|
PlayedGames int64 `json:"played_games"`
|
||||||
NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"`
|
NeverPlayedGames int64 `json:"never_played_games"`
|
||||||
TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"`
|
TotalGamePlays int64 `json:"total_game_plays"`
|
||||||
AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"`
|
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||||
MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"`
|
MaxGamePlays int64 `json:"max_game_plays"`
|
||||||
MinSoundtrackPlays int64 `json:"min_soundtrack_plays"`
|
MinGamePlays int64 `json:"min_game_plays"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatisticsHandler manages statistics operations
|
// StatisticsHandler manages statistics operations
|
||||||
@@ -49,19 +49,19 @@ func NewStatisticsHandler() *StatisticsHandler {
|
|||||||
return &StatisticsHandler{}
|
return &StatisticsHandler{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostPlayedSoundtracksWithSongs returns the top N most played soundtracks with their songs
|
// GetMostPlayedGamesWithSongs returns the top N most played games with their songs
|
||||||
func (h *StatisticsHandler) GetMostPlayedSoundtracksWithSongs(limit int32) ([]SoundtrackWithSongs, error) {
|
func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
// Get raw results
|
// Get raw results
|
||||||
rows, err := queries.GetMostPlayedSoundtracksWithSongs(ctx, limit)
|
rows, err := queries.GetMostPlayedGamesWithSongs(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to SoundtrackWithSongs
|
// Convert to GameWithSongs
|
||||||
var result []SoundtrackWithSongs
|
var result []GameWithSongs
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
var songs []SongInfoForStats
|
var songs []SongInfoForStats
|
||||||
if row.Songs != nil {
|
if row.Songs != nil {
|
||||||
@@ -71,28 +71,28 @@ func (h *StatisticsHandler) GetMostPlayedSoundtracksWithSongs(limit int32) ([]So
|
|||||||
songs = make([]SongInfoForStats, 0)
|
songs = make([]SongInfoForStats, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, SoundtrackWithSongs{
|
result = append(result, GameWithSongs{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
GamePlayed: row.GamePlayed,
|
||||||
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
Songs: songs,
|
Songs: songs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLeastPlayedSoundtracksWithSongs returns the top N least played soundtracks with their songs
|
// GetLeastPlayedGamesWithSongs returns the top N least played games with their songs
|
||||||
func (h *StatisticsHandler) GetLeastPlayedSoundtracksWithSongs(limit int32) ([]SoundtrackWithSongs, error) {
|
func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetLeastPlayedSoundtracksWithSongs(ctx, limit)
|
rows, err := queries.GetLeastPlayedGamesWithSongs(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []SoundtrackWithSongs
|
var result []GameWithSongs
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
var songs []SongInfoForStats
|
var songs []SongInfoForStats
|
||||||
if row.Songs != nil {
|
if row.Songs != nil {
|
||||||
@@ -100,23 +100,23 @@ func (h *StatisticsHandler) GetLeastPlayedSoundtracksWithSongs(limit int32) ([]S
|
|||||||
songs = make([]SongInfoForStats, 0)
|
songs = make([]SongInfoForStats, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, SoundtrackWithSongs{
|
result = append(result, GameWithSongs{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
GamePlayed: row.GamePlayed,
|
||||||
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
Songs: songs,
|
Songs: songs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostPlayedSongsWithSoundtrack returns the top N most played songs with their soundtrack info
|
// GetMostPlayedSongsWithGame returns the top N most played songs with their game info
|
||||||
func (h *StatisticsHandler) GetMostPlayedSongsWithSoundtrack(limit int32) ([]SongInfoForStats, error) {
|
func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetMostPlayedSongsWithSoundtrack(ctx, limit)
|
rows, err := queries.GetMostPlayedSongsWithGame(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -124,8 +124,8 @@ func (h *StatisticsHandler) GetMostPlayedSongsWithSoundtrack(limit int32) ([]Son
|
|||||||
var result []SongInfoForStats
|
var result []SongInfoForStats
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
result = append(result, SongInfoForStats{
|
result = append(result, SongInfoForStats{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SongName: row.SongName,
|
SongName: row.SongName,
|
||||||
Path: row.Path,
|
Path: row.Path,
|
||||||
TimesPlayed: row.TimesPlayed,
|
TimesPlayed: row.TimesPlayed,
|
||||||
@@ -135,12 +135,12 @@ func (h *StatisticsHandler) GetMostPlayedSongsWithSoundtrack(limit int32) ([]Son
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLeastPlayedSongsWithSoundtrack returns the top N least played songs with their soundtrack info
|
// GetLeastPlayedSongsWithGame returns the top N least played songs with their game info
|
||||||
func (h *StatisticsHandler) GetLeastPlayedSongsWithSoundtrack(limit int32) ([]SongInfoForStats, error) {
|
func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetLeastPlayedSongsWithSoundtrack(ctx, limit)
|
rows, err := queries.GetLeastPlayedSongsWithGame(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,8 +148,8 @@ func (h *StatisticsHandler) GetLeastPlayedSongsWithSoundtrack(limit int32) ([]So
|
|||||||
var result []SongInfoForStats
|
var result []SongInfoForStats
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
result = append(result, SongInfoForStats{
|
result = append(result, SongInfoForStats{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SongName: row.SongName,
|
SongName: row.SongName,
|
||||||
Path: row.Path,
|
Path: row.Path,
|
||||||
TimesPlayed: row.TimesPlayed,
|
TimesPlayed: row.TimesPlayed,
|
||||||
@@ -159,17 +159,17 @@ func (h *StatisticsHandler) GetLeastPlayedSongsWithSoundtrack(limit int32) ([]So
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNeverPlayedSoundtracks returns soundtracks that have never been played
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
func (h *StatisticsHandler) GetNeverPlayedSoundtracks() ([]SoundtrackWithSongs, error) {
|
func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetNeverPlayedSoundtracks(ctx)
|
rows, err := queries.GetNeverPlayedGames(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []SoundtrackWithSongs
|
var result []GameWithSongs
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
var songs []SongInfoForStats
|
var songs []SongInfoForStats
|
||||||
if row.Songs != nil {
|
if row.Songs != nil {
|
||||||
@@ -177,28 +177,28 @@ func (h *StatisticsHandler) GetNeverPlayedSoundtracks() ([]SoundtrackWithSongs,
|
|||||||
songs = make([]SongInfoForStats, 0)
|
songs = make([]SongInfoForStats, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, SoundtrackWithSongs{
|
result = append(result, GameWithSongs{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
GamePlayed: row.GamePlayed,
|
||||||
SoundtrackLastPlayed: nil,
|
GameLastPlayed: nil,
|
||||||
Songs: songs,
|
Songs: songs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLastPlayedSoundtracks returns the most recently played soundtracks
|
// GetLastPlayedGames returns the most recently played games
|
||||||
func (h *StatisticsHandler) GetLastPlayedSoundtracks(limit int32) ([]SoundtrackWithSongs, error) {
|
func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetLastPlayedSoundtracks(ctx, limit)
|
rows, err := queries.GetLastPlayedGames(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []SoundtrackWithSongs
|
var result []GameWithSongs
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
var songs []SongInfoForStats
|
var songs []SongInfoForStats
|
||||||
if row.Songs != nil {
|
if row.Songs != nil {
|
||||||
@@ -206,28 +206,28 @@ func (h *StatisticsHandler) GetLastPlayedSoundtracks(limit int32) ([]SoundtrackW
|
|||||||
songs = make([]SongInfoForStats, 0)
|
songs = make([]SongInfoForStats, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, SoundtrackWithSongs{
|
result = append(result, GameWithSongs{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
GamePlayed: row.GamePlayed,
|
||||||
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
Songs: songs,
|
Songs: songs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOldestPlayedSoundtracks returns the least recently played soundtracks
|
// GetOldestPlayedGames returns the least recently played games
|
||||||
func (h *StatisticsHandler) GetOldestPlayedSoundtracks(limit int32) ([]SoundtrackWithSongs, error) {
|
func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
queries := BackendRepo()
|
queries := BackendRepo()
|
||||||
ctx := BackendCtx()
|
ctx := BackendCtx()
|
||||||
|
|
||||||
rows, err := queries.GetOldestPlayedSoundtracks(ctx, limit)
|
rows, err := queries.GetOldestPlayedGames(ctx, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []SoundtrackWithSongs
|
var result []GameWithSongs
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
var songs []SongInfoForStats
|
var songs []SongInfoForStats
|
||||||
if row.Songs != nil {
|
if row.Songs != nil {
|
||||||
@@ -235,11 +235,11 @@ func (h *StatisticsHandler) GetOldestPlayedSoundtracks(limit int32) ([]Soundtrac
|
|||||||
songs = make([]SongInfoForStats, 0)
|
songs = make([]SongInfoForStats, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, SoundtrackWithSongs{
|
result = append(result, GameWithSongs{
|
||||||
SoundtrackID: row.SoundtrackID,
|
GameID: row.GameID,
|
||||||
SoundtrackName: row.SoundtrackName,
|
GameName: row.GameName,
|
||||||
SoundtrackPlayed: row.SoundtrackPlayed,
|
GamePlayed: row.GamePlayed,
|
||||||
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
Songs: songs,
|
Songs: songs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -257,13 +257,13 @@ func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &StatisticsSummary{
|
return &StatisticsSummary{
|
||||||
TotalSoundtracks: int64(row.TotalSoundtracks),
|
TotalGames: int64(row.TotalGames),
|
||||||
PlayedSoundtracks: int64(row.PlayedSoundtracks),
|
PlayedGames: int64(row.PlayedGames),
|
||||||
NeverPlayedSoundtracks: int64(row.NeverPlayedSoundtracks),
|
NeverPlayedGames: int64(row.NeverPlayedGames),
|
||||||
TotalSoundtrackPlays: int64(row.TotalSoundtrackPlays),
|
TotalGamePlays: int64(row.TotalGamePlays),
|
||||||
AvgSoundtrackPlays: float64(row.AvgSoundtrackPlays),
|
AvgGamePlays: float64(row.AvgGamePlays),
|
||||||
MaxSoundtrackPlays: int64(row.MaxSoundtrackPlays),
|
MaxGamePlays: int64(row.MaxGamePlays),
|
||||||
MinSoundtrackPlays: int64(row.MinSoundtrackPlays),
|
MinGamePlays: int64(row.MinGamePlays),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+198
-234
@@ -16,7 +16,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/panjf2000/ants/v2"
|
"github.com/panjf2000/ants/v2"
|
||||||
|
|
||||||
"github.com/MShekow/directory-checksum/directory_checksum"
|
"github.com/MShekow/directory-checksum/directory_checksum"
|
||||||
@@ -31,31 +30,25 @@ var start time.Time
|
|||||||
var totalTime time.Duration
|
var totalTime time.Duration
|
||||||
var timeSpent time.Duration
|
var timeSpent time.Duration
|
||||||
|
|
||||||
var allSoundtracks []repository.Soundtrack
|
var allGames []repository.Game
|
||||||
var soundtracksBeforeSync []repository.Soundtrack
|
var gamesBeforeSync []repository.Game
|
||||||
var soundtracksAfterSync []repository.Soundtrack
|
var gamesAfterSync []repository.Game
|
||||||
var soundtracksAdded []string
|
var gamesAdded []string
|
||||||
var soundtracksReAdded []string
|
var gamesReAdded []string
|
||||||
var soundtracksChangedTitle map[string]string
|
var gamesChangedTitle map[string]string
|
||||||
var soundtracksChangedContent []string
|
var gamesChangedContent []string
|
||||||
var soundtracksRemoved []string
|
var gamesRemoved []string
|
||||||
var catchedErrors []string
|
var catchedErrors []string
|
||||||
|
var brokenSongs []string
|
||||||
type brokenSong struct {
|
|
||||||
SoundtrackID int32
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
var brokenSongs []brokenSong
|
|
||||||
var pool *ants.Pool
|
var pool *ants.Pool
|
||||||
var poolSong *ants.Pool
|
var poolSong *ants.Pool
|
||||||
|
|
||||||
type SyncResponse struct {
|
type SyncResponse struct {
|
||||||
SoundtracksAdded []string `json:"soundtracks_added"`
|
GamesAdded []string `json:"games_added"`
|
||||||
SoundtracksReAdded []string `json:"soundtracks_re_added"`
|
GamesReAdded []string `json:"games_re_added"`
|
||||||
SoundtracksChangedTitle map[string]string `json:"soundtracks_changed_title"`
|
GamesChangedTitle map[string]string `json:"games_changed_title"`
|
||||||
SoundtracksChangedContent []string `json:"soundtracks_changed_content"`
|
GamesChangedContent []string `json:"games_changed_content"`
|
||||||
SoundtracksRemoved []string `json:"soundtracks_removed"`
|
GamesRemoved []string `json:"games_removed"`
|
||||||
CatchedErrors []string `json:"catched_errors"`
|
CatchedErrors []string `json:"catched_errors"`
|
||||||
TotalTime string `json:"total_time"`
|
TotalTime string `json:"total_time"`
|
||||||
}
|
}
|
||||||
@@ -65,29 +58,29 @@ type ProgressResponse struct {
|
|||||||
TimeSpent string `json:"time_spent"`
|
TimeSpent string `json:"time_spent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SoundtrackStatus int
|
type GameStatus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NotChanged SoundtrackStatus = iota
|
NotChanged GameStatus = iota
|
||||||
TitleChanged
|
TitleChanged
|
||||||
SoundtrackChanged
|
GameChanged
|
||||||
NewSoundtrack
|
NewGame
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusName = map[SoundtrackStatus]string{
|
var statusName = map[GameStatus]string{
|
||||||
NotChanged: "Not changed",
|
NotChanged: "Not changed",
|
||||||
TitleChanged: "Title changed",
|
TitleChanged: "Title changed",
|
||||||
SoundtrackChanged: "Soundtrack changed",
|
GameChanged: "Game changed",
|
||||||
NewSoundtrack: "New soundtrack",
|
NewGame: "New game",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss SoundtrackStatus) String() string {
|
func (gs GameStatus) String() string {
|
||||||
return statusName[ss]
|
return statusName[gs]
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetDB() {
|
func ResetDB() {
|
||||||
repo.ClearSongs(BackendCtx())
|
repo.ClearSongs(BackendCtx())
|
||||||
repo.ClearSoundtracks(BackendCtx())
|
repo.ClearGames(BackendCtx())
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncProgress() ProgressResponse {
|
func SyncProgress() ProgressResponse {
|
||||||
@@ -108,54 +101,54 @@ func SyncProgress() ProgressResponse {
|
|||||||
|
|
||||||
func SyncResult() SyncResponse {
|
func SyncResult() SyncResponse {
|
||||||
logging.GetLogger().Info("Sync completed",
|
logging.GetLogger().Info("Sync completed",
|
||||||
zap.Int("soundtracks_before", len(soundtracksBeforeSync)),
|
zap.Int("games_before", len(gamesBeforeSync)),
|
||||||
zap.Int("soundtracks_after", len(soundtracksAfterSync)))
|
zap.Int("games_after", len(gamesAfterSync)))
|
||||||
|
|
||||||
if len(soundtracksAdded) > 0 {
|
if len(gamesAdded) > 0 {
|
||||||
logging.GetLogger().Debug("Soundtracks added", zap.Strings("soundtracks", soundtracksAdded))
|
logging.GetLogger().Debug("Games added", zap.Strings("games", gamesAdded))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(soundtracksReAdded) > 0 {
|
if len(gamesReAdded) > 0 {
|
||||||
logging.GetLogger().Debug("Soundtracks readded", zap.Strings("soundtracks", soundtracksReAdded))
|
logging.GetLogger().Debug("Games readded", zap.Strings("games", gamesReAdded))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(soundtracksChangedTitle) > 0 {
|
if len(gamesChangedTitle) > 0 {
|
||||||
logging.GetLogger().Debug("Soundtracks with changed title", zap.Any("changes", soundtracksChangedTitle))
|
logging.GetLogger().Debug("Games with changed title", zap.Any("changes", gamesChangedTitle))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(soundtracksChangedContent) > 0 {
|
if len(gamesChangedContent) > 0 {
|
||||||
logging.GetLogger().Debug("Soundtracks with changed content", zap.Strings("soundtracks", soundtracksChangedContent))
|
logging.GetLogger().Debug("Games with changed content", zap.Strings("games", gamesChangedContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
var soundtracksRemovedTemp []string
|
var gamesRemovedTemp []string
|
||||||
for _, beforeSoundtrack := range soundtracksBeforeSync {
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
var found = false
|
var found = false
|
||||||
for _, afterSoundtrack := range soundtracksAfterSync {
|
for _, afterGame := range gamesAfterSync {
|
||||||
if beforeSoundtrack.SoundtrackName == afterSoundtrack.SoundtrackName {
|
if beforeGame.GameName == afterGame.GameName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
soundtracksRemovedTemp = append(soundtracksRemovedTemp, beforeSoundtrack.SoundtrackName)
|
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.GameName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, soundtrack := range soundtracksRemovedTemp {
|
for _, game := range gamesRemovedTemp {
|
||||||
var found bool = false
|
var found bool = false
|
||||||
for key := range soundtracksChangedTitle {
|
for key := range gamesChangedTitle {
|
||||||
if soundtrack == key {
|
if game == key {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
soundtracksRemoved = append(soundtracksRemoved, soundtrack)
|
gamesRemoved = append(gamesRemoved, game)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(soundtracksRemoved) > 0 {
|
if len(gamesRemoved) > 0 {
|
||||||
logging.GetLogger().Debug("Soundtracks removed", zap.Strings("soundtracks", soundtracksRemoved))
|
logging.GetLogger().Debug("Games removed", zap.Strings("games", gamesRemoved))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(catchedErrors) > 0 {
|
if len(catchedErrors) > 0 {
|
||||||
@@ -166,29 +159,29 @@ func SyncResult() SyncResponse {
|
|||||||
logging.GetLogger().Info("Sync completed", zap.String("total_time", out.Format("15:04:05.00000")))
|
logging.GetLogger().Info("Sync completed", zap.String("total_time", out.Format("15:04:05.00000")))
|
||||||
|
|
||||||
return SyncResponse{
|
return SyncResponse{
|
||||||
SoundtracksAdded: soundtracksAdded,
|
GamesAdded: gamesAdded,
|
||||||
SoundtracksReAdded: soundtracksReAdded,
|
GamesReAdded: gamesReAdded,
|
||||||
SoundtracksChangedTitle: soundtracksChangedTitle,
|
GamesChangedTitle: gamesChangedTitle,
|
||||||
SoundtracksChangedContent: soundtracksChangedContent,
|
GamesChangedContent: gamesChangedContent,
|
||||||
SoundtracksRemoved: soundtracksRemoved,
|
GamesRemoved: gamesRemoved,
|
||||||
CatchedErrors: catchedErrors,
|
CatchedErrors: catchedErrors,
|
||||||
TotalTime: out.Format("15:04:05"),
|
TotalTime: out.Format("15:04:05"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncSoundtracksFull() {
|
func SyncGamesNewFull() {
|
||||||
syncSoundtracks(true)
|
syncGamesNew(true)
|
||||||
Reset()
|
Reset()
|
||||||
ReloadSearchCache()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncSoundtracksOnlyChanges() {
|
func SyncGamesNewOnlyChanges() {
|
||||||
syncSoundtracks(false)
|
syncGamesNew(false)
|
||||||
Reset()
|
Reset()
|
||||||
ReloadSearchCache()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncSoundtracks(full bool) {
|
func syncGamesNew(full bool) {
|
||||||
|
Syncing = true
|
||||||
|
|
||||||
musicPath := os.Getenv("MUSIC_PATH")
|
musicPath := os.Getenv("MUSIC_PATH")
|
||||||
fmt.Printf("dir: %s\n", musicPath)
|
fmt.Printf("dir: %s\n", musicPath)
|
||||||
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
||||||
@@ -200,31 +193,32 @@ func syncSoundtracks(full bool) {
|
|||||||
|
|
||||||
initRepo()
|
initRepo()
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
foldersToSkip := []string{".sync", "characters", "dist", "old"}
|
foldersToSkip := []string{".sync", "dist", "old", "characters"}
|
||||||
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
soundtracksAdded = nil
|
gamesAdded = nil
|
||||||
soundtracksReAdded = nil
|
gamesReAdded = nil
|
||||||
soundtracksChangedTitle = nil
|
gamesChangedTitle = nil
|
||||||
soundtracksChangedContent = nil
|
gamesChangedContent = nil
|
||||||
soundtracksRemoved = nil
|
gamesRemoved = nil
|
||||||
catchedErrors = nil
|
catchedErrors = nil
|
||||||
brokenSongs = nil
|
brokenSongs = nil
|
||||||
|
|
||||||
soundtracksBeforeSync, err = repo.FindAllSoundtracks(BackendCtx())
|
gamesBeforeSync, err = repo.FindAllGames(BackendCtx())
|
||||||
handleError("FindAllSoundtracks Before", err, "")
|
handleError("FindAllGames Before", err, "")
|
||||||
logging.GetLogger().Info("Starting sync", zap.Int("soundtracks_before", len(soundtracksBeforeSync)))
|
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
|
||||||
|
|
||||||
allSoundtracks, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx())
|
allGames, err = repo.GetAllGamesIncludingDeleted(BackendCtx())
|
||||||
handleError("GetAllSoundtracksIncludingDeleted", err, "")
|
handleError("GetAllGamesIncludingDeleted", err, "")
|
||||||
err = repo.SetSoundtrackDeletionDate(BackendCtx())
|
err = repo.SetGameDeletionDate(BackendCtx())
|
||||||
handleError("SetSoundtrackDeletionDate", err, "")
|
handleError("SetGameDeletionDate", err, "")
|
||||||
|
|
||||||
directories, err := os.ReadDir(musicPath)
|
directories, err := os.ReadDir(musicPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
|
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||||
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||||
defer pool.Release()
|
defer pool.Release()
|
||||||
@@ -236,14 +230,14 @@ func syncSoundtracks(full bool) {
|
|||||||
for _, dir := range directories {
|
for _, dir := range directories {
|
||||||
pool.Submit(func() {
|
pool.Submit(func() {
|
||||||
defer syncWg.Done()
|
defer syncWg.Done()
|
||||||
syncSoundtrack(dir, foldersToSkip, musicPath, full)
|
syncGameNew(dir, foldersToSkip, musicPath, full)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
syncWg.Wait()
|
syncWg.Wait()
|
||||||
checkBrokenSongs()
|
checkBrokenSongsNew()
|
||||||
|
|
||||||
soundtracksAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
gamesAfterSync, err = repo.FindAllGames(BackendCtx())
|
||||||
handleError("FindAllSoundtracks After", err, "")
|
handleError("FindAllGames After", err, "")
|
||||||
|
|
||||||
finished := time.Now()
|
finished := time.Now()
|
||||||
totalTime = finished.Sub(start)
|
totalTime = finished.Sub(start)
|
||||||
@@ -253,7 +247,7 @@ func syncSoundtracks(full bool) {
|
|||||||
Syncing = false
|
Syncing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBrokenSongs() {
|
func checkBrokenSongsNew() {
|
||||||
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
||||||
handleError("FetchAllSongs", err, "")
|
handleError("FetchAllSongs", err, "")
|
||||||
var brokenWg sync.WaitGroup
|
var brokenWg sync.WaitGroup
|
||||||
@@ -264,22 +258,20 @@ func checkBrokenSongs() {
|
|||||||
for _, song := range allSongs {
|
for _, song := range allSongs {
|
||||||
poolBroken.Submit(func() {
|
poolBroken.Submit(func() {
|
||||||
defer brokenWg.Done()
|
defer brokenWg.Done()
|
||||||
checkBrokenSong(song)
|
checkBrokenSongNew(song)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
brokenWg.Wait()
|
brokenWg.Wait()
|
||||||
for _, bs := range brokenSongs {
|
err = repo.RemoveBrokenSongs(BackendCtx(), brokenSongs)
|
||||||
err = repo.RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: bs.SoundtrackID, Path: bs.Path})
|
handleError("RemoveBrokenSongs", err, "")
|
||||||
handleError("RemoveBrokenSong", err, "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBrokenSong(song repository.Song) {
|
func checkBrokenSongNew(song repository.Song) {
|
||||||
//Check if file exists and open
|
//Check if file exists and open
|
||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//File not found
|
//File not found
|
||||||
brokenSongs = append(brokenSongs, brokenSong{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
brokenSongs = append(brokenSongs, song.Path)
|
||||||
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
||||||
} else {
|
} else {
|
||||||
err = openFile.Close()
|
err = openFile.Close()
|
||||||
@@ -289,93 +281,118 @@ func checkBrokenSong(song repository.Song) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncSoundtrack(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
||||||
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
||||||
logging.GetLogger().Debug("Syncing soundtrack", zap.String("soundtrack", file.Name()))
|
logging.GetLogger().Debug("Syncing game", zap.String("game", file.Name()))
|
||||||
soundtrackDir := baseDir + file.Name() + "/"
|
gameDir := baseDir + file.Name() + "/"
|
||||||
dirHash := getHashForDir(soundtrackDir)
|
dirHash := getHashForDir(gameDir)
|
||||||
|
|
||||||
var status SoundtrackStatus = NewSoundtrack
|
var status GameStatus = NewGame
|
||||||
var oldSoundtrack repository.Soundtrack
|
var oldGame repository.Game
|
||||||
var id int32 = -1
|
var id int32 = -1
|
||||||
var soundtrackUuid uuid.UUID
|
|
||||||
|
|
||||||
//fmt.Printf("Soundtracks before: %d\n", len(soundtracksBeforeSync))
|
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
|
||||||
|
|
||||||
for _, currentSoundtrack := range allSoundtracks {
|
for _, currentGame := range allGames {
|
||||||
oldSoundtrack = currentSoundtrack
|
oldGame = currentGame
|
||||||
//fmt.Printf("%s | %s\n", oldSoundtrack.SoundtrackName, oldSoundtrack.Hash)
|
//fmt.Printf("%s | %s\n", oldGame.GameName, oldGame.Hash)
|
||||||
if oldSoundtrack.SoundtrackName == file.Name() && oldSoundtrack.Hash == dirHash {
|
if oldGame.GameName == file.Name() && oldGame.Hash == dirHash {
|
||||||
status = NotChanged
|
status = NotChanged
|
||||||
id = oldSoundtrack.ID
|
id = oldGame.ID
|
||||||
soundtrackUuid = oldSoundtrack.Uuid
|
//fmt.Printf("Game not changed\n")
|
||||||
//fmt.Printf("Soundtrack not changed\n")
|
|
||||||
break
|
break
|
||||||
} else if oldSoundtrack.SoundtrackName == file.Name() && oldSoundtrack.Hash != dirHash {
|
} else if oldGame.GameName == file.Name() && oldGame.Hash != dirHash {
|
||||||
status = SoundtrackChanged
|
status = GameChanged
|
||||||
id = oldSoundtrack.ID
|
id = oldGame.ID
|
||||||
soundtrackUuid = oldSoundtrack.Uuid
|
//fmt.Printf("Game changed\n")
|
||||||
//fmt.Printf("Soundtrack changed\n")
|
|
||||||
break
|
break
|
||||||
} else if oldSoundtrack.SoundtrackName != file.Name() && oldSoundtrack.Hash == dirHash {
|
} else if oldGame.GameName != file.Name() && oldGame.Hash == dirHash {
|
||||||
status = TitleChanged
|
status = TitleChanged
|
||||||
id = oldSoundtrack.ID
|
id = oldGame.ID
|
||||||
soundtrackUuid = oldSoundtrack.Uuid
|
//fmt.Printf("GameName changed\n")
|
||||||
//fmt.Printf("SoundtrackName changed\n")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if full && status != NewSoundtrack {
|
if full {
|
||||||
status = TitleChanged
|
status = TitleChanged
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(soundtrackDir)
|
entries, err := os.ReadDir(gameDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to read soundtrack directory", zap.String("path", soundtrackDir), zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to read game directory", zap.String("path", gameDir), zap.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case NewSoundtrack:
|
case NewGame:
|
||||||
id, soundtrackUuid = insertSoundtrack(file.Name(), soundtrackDir, dirHash)
|
if id != -1 {
|
||||||
logging.GetLogger().Debug("New soundtrack detected",
|
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.InsertGameWithExistingId(BackendCtx(), repository.InsertGameWithExistingIdParams{ID: id, GameName: file.Name(), Path: gameDir, Hash: dirHash})
|
||||||
|
handleError("InsertGameWithExistingId", err, "")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Debug("Game already exists, removing old ID file",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("uuid", soundtrackUuid.String()),
|
zap.String("game_dir", gameDir))
|
||||||
zap.String("soundtrack", file.Name()),
|
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("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
soundtracksAdded = append(soundtracksAdded, file.Name())
|
gamesAdded = append(gamesAdded, file.Name())
|
||||||
checkSongs(entries, soundtrackDir, id, soundtrackUuid)
|
newCheckSongs(entries, gameDir, id)
|
||||||
case SoundtrackChanged:
|
case GameChanged:
|
||||||
logging.GetLogger().Debug("Soundtrack changed",
|
logging.GetLogger().Debug("Game changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("uuid", soundtrackUuid.String()),
|
zap.String("game", file.Name()),
|
||||||
zap.String("soundtrack", file.Name()),
|
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
err = repo.UpdateGameHash(BackendCtx(), repository.UpdateGameHashParams{Hash: dirHash, ID: id})
|
||||||
handleError("UpdateSoundtrackHash", err, "")
|
handleError("UpdateGameHash", err, "")
|
||||||
soundtracksChangedContent = append(soundtracksChangedContent, file.Name())
|
gamesChangedContent = append(gamesChangedContent, file.Name())
|
||||||
checkSongs(entries, soundtrackDir, id, soundtrackUuid)
|
newCheckSongs(entries, gameDir, id)
|
||||||
case TitleChanged:
|
case TitleChanged:
|
||||||
logging.GetLogger().Debug("Soundtrack title changed",
|
logging.GetLogger().Debug("Game title changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("uuid", soundtrackUuid.String()),
|
zap.String("oldName", oldGame.GameName),
|
||||||
zap.String("oldName", oldSoundtrack.SoundtrackName),
|
|
||||||
zap.String("newName", file.Name()),
|
zap.String("newName", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: soundtrackDir, ID: id})
|
err = repo.UpdateGameName(BackendCtx(), repository.UpdateGameNameParams{Name: file.Name(), Path: gameDir, ID: id})
|
||||||
handleError("UpdateSoundtrackName", err, "")
|
handleError("UpdateGameName", err, "")
|
||||||
checkSongs(entries, soundtrackDir, id, soundtrackUuid)
|
newCheckSongs(entries, gameDir, id)
|
||||||
if soundtracksChangedTitle == nil {
|
if gamesChangedTitle == nil {
|
||||||
soundtracksChangedTitle = make(map[string]string)
|
gamesChangedTitle = make(map[string]string)
|
||||||
}
|
}
|
||||||
soundtracksChangedTitle[oldSoundtrack.SoundtrackName] = file.Name()
|
gamesChangedTitle[oldGame.GameName] = file.Name()
|
||||||
case NotChanged:
|
case NotChanged:
|
||||||
var found bool = false
|
var found bool = false
|
||||||
for _, beforeSoundtrack := range soundtracksBeforeSync {
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
if dirHash == beforeSoundtrack.Hash {
|
if dirHash == beforeGame.Hash {
|
||||||
found = true
|
found = true
|
||||||
logging.GetLogger().Debug("Soundtrack not changed",
|
logging.GetLogger().Debug("Game not changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("newName", file.Name()),
|
zap.String("newName", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
@@ -383,33 +400,23 @@ func syncSoundtrack(file os.DirEntry, foldersToSkip []string, baseDir string, fu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
checkSongs(entries, soundtrackDir, id, soundtrackUuid)
|
newCheckSongs(entries, gameDir, id)
|
||||||
soundtracksReAdded = append(soundtracksReAdded, file.Name())
|
gamesReAdded = append(gamesReAdded, file.Name())
|
||||||
logging.GetLogger().Debug("Soundtrack added again",
|
logging.GetLogger().Debug("Game added again",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("uuid", soundtrackUuid.String()),
|
|
||||||
zap.String("newName", file.Name()),
|
zap.String("newName", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logging.GetLogger().Debug("Soundtrack sync status",
|
logging.GetLogger().Debug("Game sync status",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("soundtrack", file.Name()),
|
zap.String("game", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.RemoveSoundtrackDeletionDate(BackendCtx(), id)
|
err = repo.RemoveDeletionDate(BackendCtx(), id)
|
||||||
handleError("RemoveSoundtrackDeletionDate", err, "")
|
handleError("RemoveDeletionDate", err, "")
|
||||||
|
|
||||||
// Remove .id files and update hash if any were removed
|
|
||||||
if removeIdFilesInSoundtrack(soundtrackDir) {
|
|
||||||
newHash := getHashForDir(soundtrackDir)
|
|
||||||
if newHash != dirHash {
|
|
||||||
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: newHash, ID: id})
|
|
||||||
handleError("UpdateSoundtrackHash for .id cleanup", err, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
foldersSynced++
|
foldersSynced++
|
||||||
logging.GetLogger().Debug("Sync progress",
|
logging.GetLogger().Debug("Sync progress",
|
||||||
@@ -418,31 +425,24 @@ func syncSoundtrack(file os.DirEntry, foldersToSkip []string, baseDir string, fu
|
|||||||
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertSoundtrack(name string, path string, hash string) (int32, uuid.UUID) {
|
func insertGameNew(name string, path string, hash string) int32 {
|
||||||
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
||||||
soundtrackUuid := uuid.New()
|
id, err := repo.InsertGame(BackendCtx(), repository.InsertGameParams{GameName: name, Path: path, Hash: hash})
|
||||||
result, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{
|
handleError("InsertGame", err, "")
|
||||||
SoundtrackName: name,
|
|
||||||
Path: path,
|
|
||||||
Hash: hash,
|
|
||||||
Uuid: soundtrackUuid,
|
|
||||||
})
|
|
||||||
handleError("InsertSoundtrack", err, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Warn("ID collision or UUID duplicate detected, resetting sequence")
|
logging.GetLogger().Warn("ID collision detected, resetting sequence")
|
||||||
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
||||||
logging.GetLogger().Debug("Resetting soundtrack ID sequence")
|
logging.GetLogger().Debug("Resetting game ID sequence")
|
||||||
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
_, err = repo.ResetGameIdSeq(BackendCtx())
|
||||||
handleError("ResetSoundtrackIdSeq", err, "")
|
handleError("ResetGameIdSeq", err, "")
|
||||||
id, uuid := insertSoundtrack(name, path, hash)
|
id = insertGameNew(name, path, hash)
|
||||||
return id, uuid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result.ID, result.Uuid
|
return id
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSongs(entries []os.DirEntry, soundtrackDir string, id int32, soundtrackUuid uuid.UUID) int32 {
|
func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
|
||||||
//hasher := md5.New()
|
//hasher := md5.New()
|
||||||
var numberOfSongs int32
|
var numberOfSongs int32
|
||||||
numberOfFiles := len(entries)
|
numberOfFiles := len(entries)
|
||||||
@@ -452,7 +452,7 @@ func checkSongs(entries []os.DirEntry, soundtrackDir string, id int32, soundtrac
|
|||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
poolSong.Submit(func() {
|
poolSong.Submit(func() {
|
||||||
defer songWg.Done()
|
defer songWg.Done()
|
||||||
if checkSong(entry, soundtrackDir, id, soundtrackUuid) {
|
if newCheckSong(entry, gameDir, id) {
|
||||||
numberOfSongs++
|
numberOfSongs++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -461,7 +461,7 @@ func checkSongs(entries []os.DirEntry, soundtrackDir string, id int32, soundtrac
|
|||||||
return numberOfSongs
|
return numberOfSongs
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSong(entry os.DirEntry, soundtrackDir string, id int32, soundtrackUuid uuid.UUID) bool {
|
func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||||
fileInfo, err := entry.Info()
|
fileInfo, err := entry.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
||||||
@@ -469,7 +469,7 @@ func checkSong(entry os.DirEntry, soundtrackDir string, id int32, soundtrackUuid
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isSong(fileInfo) {
|
if isSong(fileInfo) {
|
||||||
path := soundtrackDir + entry.Name()
|
path := gameDir + entry.Name()
|
||||||
|
|
||||||
songHash := getHashForFile(path)
|
songHash := getHashForFile(path)
|
||||||
//numberOfSongs++
|
//numberOfSongs++
|
||||||
@@ -478,53 +478,44 @@ func checkSong(entry os.DirEntry, soundtrackDir string, id int32, soundtrackUuid
|
|||||||
songName, _ := strings.CutSuffix(fileName, ".mp3")
|
songName, _ := strings.CutSuffix(fileName, ".mp3")
|
||||||
|
|
||||||
song, err := repo.GetSongWithHash(BackendCtx(), songHash)
|
song, err := repo.GetSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("GetSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if song.SongName == songName && song.Path == path {
|
if song.SongName == songName && song.Path == path {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logging.GetLogger().Debug("Song changed",
|
logging.GetLogger().Debug("Song changed",
|
||||||
zap.Int32("soundtrack_id", id),
|
zap.Int32("game_id", id),
|
||||||
zap.String("path", path),
|
zap.String("path", path),
|
||||||
zap.String("song_name", songName),
|
zap.String("song_name", songName),
|
||||||
zap.String("song_hash", songHash))
|
zap.String("song_hash", songHash))
|
||||||
|
|
||||||
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("CheckSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
handleError("CheckSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
count2, err := repo.CheckSong(BackendCtx(), path)
|
||||||
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
if count2 > 0 {
|
if count2 > 0 {
|
||||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
||||||
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("CheckSongWithHash 2", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("CheckSongWithHash 2", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//count, _ := repo.CheckSong(ctx, path)
|
//count, _ := repo.CheckSong(ctx, path)
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
||||||
handleError("UpdateSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("UpdateSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
} else {
|
} else {
|
||||||
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
count2, err := repo.CheckSong(BackendCtx(), path)
|
||||||
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
if count2 > 0 {
|
if count2 > 0 {
|
||||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
||||||
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
} else {
|
} else {
|
||||||
songUuid := uuid.New()
|
err = repo.AddSong(BackendCtx(), repository.AddSongParams{GameID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
|
||||||
err = repo.AddSong(BackendCtx(), repository.AddSongParams{
|
handleError("AddSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
SoundtrackID: id,
|
|
||||||
SongName: songName,
|
|
||||||
Path: path,
|
|
||||||
FileName: &fileName,
|
|
||||||
Hash: songHash,
|
|
||||||
Uuid: songUuid,
|
|
||||||
SoundtrackUuid: soundtrackUuid,
|
|
||||||
})
|
|
||||||
handleError("AddSong", err, fmt.Sprintf("SoundtrackID: %d | UUID: %s | Path: %s | SongName: %s | SongHash: %s", id, songUuid.String(), path, entry.Name(), songHash))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,8 +543,8 @@ func handleError(funcName string, err error, msg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHashForDir(soundtrackDir string) string {
|
func getHashForDir(gameDir string) string {
|
||||||
directory, _ := directory_checksum.ScanDirectory(soundtrackDir, afero.NewOsFs())
|
directory, _ := directory_checksum.ScanDirectory(gameDir, afero.NewOsFs())
|
||||||
hash, _ := directory.ComputeDirectoryChecksums()
|
hash, _ := directory.ComputeDirectoryChecksums()
|
||||||
|
|
||||||
return hash
|
return hash
|
||||||
@@ -574,7 +565,7 @@ func getHashForFile(path string) string {
|
|||||||
return hex.EncodeToString(hasher.Sum(nil))
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIdFromFile(file os.FileInfo) int32 {
|
func getIdFromFileNew(file os.FileInfo) int32 {
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
if !file.IsDir() && strings.HasSuffix(name, ".id") {
|
if !file.IsDir() && strings.HasSuffix(name, ".id") {
|
||||||
name = strings.Replace(name, ".id", "", 1)
|
name = strings.Replace(name, ".id", "", 1)
|
||||||
@@ -594,33 +585,6 @@ func isCoverImage(entry fs.FileInfo) bool {
|
|||||||
(strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".png"))
|
(strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".png"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeIdFilesInSoundtrack(soundtrackDir string) bool {
|
|
||||||
entries, err := os.ReadDir(soundtrackDir)
|
|
||||||
if err != nil {
|
|
||||||
logging.GetLogger().Error("Failed to read soundtrack directory for .id file cleanup",
|
|
||||||
zap.String("path", soundtrackDir),
|
|
||||||
zap.String("error", err.Error()))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
anyRemoved := false
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".id") {
|
|
||||||
filePath := soundtrackDir + entry.Name()
|
|
||||||
err := os.Remove(filePath)
|
|
||||||
if err != nil {
|
|
||||||
logging.GetLogger().Error("Failed to remove .id file",
|
|
||||||
zap.String("path", filePath),
|
|
||||||
zap.String("error", err.Error()))
|
|
||||||
} else {
|
|
||||||
logging.GetLogger().Info("Removed .id file", zap.String("path", filePath))
|
|
||||||
anyRemoved = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return anyRemoved
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(s []string, searchTerm string) bool {
|
func contains(s []string, searchTerm string) bool {
|
||||||
i := sort.SearchStrings(s, searchTerm)
|
i := sort.SearchStrings(s, searchTerm)
|
||||||
return i < len(s) && s[i] == searchTerm
|
return i < len(s) && s[i] == searchTerm
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ func TestGetIdFromFileNew(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := getIdFromFile(tt.fileInfo)
|
result := getIdFromFileNew(tt.fileInfo)
|
||||||
if result != tt.expected {
|
if result != tt.expected {
|
||||||
t.Errorf("getIdFromFile() = %v, want %v", result, tt.expected)
|
t.Errorf("getIdFromFileNew() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
type VersionData struct {
|
|
||||||
Version string `json:"version" example:"1.0.0"`
|
|
||||||
Changelog []string `json:"changelog" example:"[\"Initial release\",\"Bug fixes\"]"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = []VersionData{
|
|
||||||
{
|
|
||||||
Version: "5.0.0",
|
|
||||||
Changelog: []string{
|
|
||||||
"#9 - Major routes refactor and expansion",
|
|
||||||
"#15 - Added API for statistics",
|
|
||||||
"#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",
|
|
||||||
"Added search cache backend",
|
|
||||||
"Backend sync improvements",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"music-server/internal/logging"
|
"music-server/internal/logging"
|
||||||
|
|
||||||
@@ -60,26 +59,6 @@ func (db *Database) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health checks the health of the database connection by pinging the database.
|
|
||||||
// It returns a map with keys indicating various health statistics.
|
|
||||||
func (db *Database) Health() map[string]string {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
stats := make(map[string]string)
|
|
||||||
|
|
||||||
// Ping the database
|
|
||||||
err := db.Pool.Ping(ctx)
|
|
||||||
if err != nil {
|
|
||||||
stats["status"] = "down"
|
|
||||||
stats["error"] = err.Error()
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
stats["status"] = "up"
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunMigrations runs all pending database migrations to the latest version.
|
// RunMigrations runs all pending database migrations to the latest version.
|
||||||
// Uses the existing pool to extract connection details.
|
// Uses the existing pool to extract connection details.
|
||||||
func (db *Database) RunMigrations() error {
|
func (db *Database) RunMigrations() error {
|
||||||
|
|||||||
+19
-6
@@ -20,9 +20,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: DEPRECATED - Remove these global variables once all code is migrated to use Database struct
|
// TODO: Remove these global variables once test_helpers.go is fully migrated to use Database struct
|
||||||
// Use database.go's Database struct instead. These globals remain for backward compatibility
|
|
||||||
// with legacy code paths. New code should use the Database struct from database.go.
|
|
||||||
var Dbpool *pgxpool.Pool
|
var Dbpool *pgxpool.Pool
|
||||||
var Ctx = context.Background()
|
var Ctx = context.Background()
|
||||||
|
|
||||||
@@ -56,10 +54,25 @@ func CloseDb() {
|
|||||||
Dbpool.Close()
|
Dbpool.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetSoundtrackIdSeq() {
|
func Testf() {
|
||||||
_, err := Dbpool.Query(Ctx, "SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);")
|
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);")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to reset soundtrack ID sequence", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to reset game ID sequence", zap.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestMigrationsStepByStep tests applying migrations incrementally
|
|
||||||
// Then adding data manually, then completing migrations
|
|
||||||
func TestMigrationsStepByStep(t *testing.T) {
|
|
||||||
host := os.Getenv("DB_HOST")
|
|
||||||
port := os.Getenv("DB_PORT")
|
|
||||||
user := os.Getenv("DB_USERNAME")
|
|
||||||
password := os.Getenv("DB_PASSWORD")
|
|
||||||
// Use a unique database name for this test
|
|
||||||
dbname := "music_server_migration_test"
|
|
||||||
|
|
||||||
if host == "" || port == "" || user == "" || password == "" {
|
|
||||||
t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up: drop database if it exists
|
|
||||||
cleanupDB(t, host, port, user, password, dbname)
|
|
||||||
defer cleanupDB(t, host, port, user, password, dbname)
|
|
||||||
|
|
||||||
// Create the database
|
|
||||||
createTestDB(t, host, port, user, password, dbname)
|
|
||||||
|
|
||||||
// Step 1: Apply first 4 migrations (before soundtrack rename)
|
|
||||||
// This creates: game, song, vgmq, song_list tables
|
|
||||||
// And sessions table with indexes
|
|
||||||
t.Run("ApplyFirst4Migrations", func(t *testing.T) {
|
|
||||||
applyMigrations(t, host, port, user, password, dbname, 4)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 2: Add data manually to game and song tables
|
|
||||||
t.Run("AddManualData", func(t *testing.T) {
|
|
||||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
host, port, user, password, dbname)
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Insert 5 games manually
|
|
||||||
for i := 1; i <= 5; i++ {
|
|
||||||
gameName := fmt.Sprintf("Manual Game %d", i)
|
|
||||||
path := fmt.Sprintf("/manual/path/game%d", i)
|
|
||||||
hash := fmt.Sprintf("hash-%d", i)
|
|
||||||
|
|
||||||
_, err := db.Exec(`INSERT INTO game (game_name, path, hash, added)
|
|
||||||
VALUES ($1, $2, $3, NOW())`,
|
|
||||||
gameName, path, hash)
|
|
||||||
require.NoError(t, err, "Failed to insert game %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert songs for each game
|
|
||||||
songs := []struct {
|
|
||||||
gameID int
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
}{
|
|
||||||
{1, "Song A", "/path/a.mp3"},
|
|
||||||
{1, "Song B", "/path/b.mp3"},
|
|
||||||
{2, "Song C", "/path/c.mp3"},
|
|
||||||
{2, "Song D", "/path/d.mp3"},
|
|
||||||
{3, "Song E", "/path/e.mp3"},
|
|
||||||
{4, "Song F", "/path/f.mp3"},
|
|
||||||
{4, "Song G", "/path/g.mp3"},
|
|
||||||
{4, "Song H", "/path/h.mp3"},
|
|
||||||
{5, "Song I", "/path/i.mp3"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range songs {
|
|
||||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
|
|
||||||
VALUES ($1, $2, $3, $4)`,
|
|
||||||
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
|
|
||||||
require.NoError(t, err, "Failed to insert song %s", s.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify data was inserted
|
|
||||||
var gameCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM game").Scan(&gameCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 5, gameCount, "Expected 5 games")
|
|
||||||
|
|
||||||
var songCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 9, songCount, "Expected 9 songs")
|
|
||||||
|
|
||||||
t.Log("✓ Manually inserted 5 games with 9 songs")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 3: Apply migration 5 (rename game→soundtrack)
|
|
||||||
t.Run("ApplyMigration5", func(t *testing.T) {
|
|
||||||
// Apply the remaining migrations (just migration 5)
|
|
||||||
applyMigrations(t, host, port, user, password, dbname, 1)
|
|
||||||
|
|
||||||
// Verify tables were renamed
|
|
||||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
host, port, user, password, dbname)
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Check that soundtrack table exists
|
|
||||||
var soundtrackCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 5, soundtrackCount, "Expected 5 soundtracks after migration")
|
|
||||||
|
|
||||||
// Check that game table no longer exists
|
|
||||||
_, err = db.Exec("SELECT 1 FROM game LIMIT 1")
|
|
||||||
require.Error(t, err, "game table should not exist after migration")
|
|
||||||
|
|
||||||
// Check that song table has soundtrack_id column
|
|
||||||
var songCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 9, songCount, "Expected 9 songs after migration")
|
|
||||||
|
|
||||||
// Verify data integrity: soundtrack_name values
|
|
||||||
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
expectedNames := []string{"Manual Game 1", "Manual Game 2", "Manual Game 3", "Manual Game 4", "Manual Game 5"}
|
|
||||||
actualNames := make([]string, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var name string
|
|
||||||
err := rows.Scan(&name)
|
|
||||||
require.NoError(t, err)
|
|
||||||
actualNames = append(actualNames, name)
|
|
||||||
}
|
|
||||||
require.Equal(t, expectedNames, actualNames, "Soundtrack names should match original game names")
|
|
||||||
|
|
||||||
t.Log("✓ Migration 5 applied successfully, data preserved")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupDB drops the test database
|
|
||||||
func cleanupDB(t *testing.T, host, port, user, password, dbname string) {
|
|
||||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
|
||||||
host, port, user, password)
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Warning: could not connect to cleanup DB: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Check if database exists before dropping
|
|
||||||
var exists int
|
|
||||||
err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
t.Logf("Warning: could not check if DB exists: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exists == 1 {
|
|
||||||
_, err = db.Exec("DROP DATABASE " + dbname + " WITH (FORCE)")
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Warning: could not drop DB: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTestDB creates a fresh test database
|
|
||||||
func createTestDB(t *testing.T, host, port, user, password, dbname string) {
|
|
||||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
|
||||||
host, port, user, password)
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Drop if exists
|
|
||||||
cleanupDB(t, host, port, user, password, dbname)
|
|
||||||
|
|
||||||
// Create database
|
|
||||||
_, err = db.Exec("CREATE DATABASE " + dbname)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Enable UUID extension if needed
|
|
||||||
connStrDB := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
host, port, user, password, dbname)
|
|
||||||
db2, err := sql.Open("postgres", connStrDB)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer db2.Close()
|
|
||||||
|
|
||||||
_, err = db2.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Note: uuid-ossp extension may not be available: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyMigrations applies n migrations to the database using Go migrate library
|
|
||||||
func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) {
|
|
||||||
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
|
||||||
user, password, host, port, dbname)
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", migrationURL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
m, err := migrate.NewWithDatabaseInstance(
|
|
||||||
"file://migrations",
|
|
||||||
"postgres", driver)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Get current version
|
|
||||||
version, _, err := m.Version()
|
|
||||||
if err != nil && err != migrate.ErrNilVersion {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
if err == migrate.ErrNilVersion {
|
|
||||||
version = 0
|
|
||||||
}
|
|
||||||
t.Logf("Current migration version: %d", version)
|
|
||||||
|
|
||||||
// Apply exactly 'steps' migrations
|
|
||||||
if steps > 0 {
|
|
||||||
err = m.Steps(steps)
|
|
||||||
if err != nil && err != migrate.ErrNoChange {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
} else if steps < 0 {
|
|
||||||
err = m.Steps(steps)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get new version
|
|
||||||
newVersion, _, err := m.Version()
|
|
||||||
if err != nil && err != migrate.ErrNilVersion {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
if err == migrate.ErrNilVersion {
|
|
||||||
newVersion = 0
|
|
||||||
}
|
|
||||||
t.Logf("Migration version after applying %d steps: %d", steps, newVersion)
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
-- Rollback: Remove UUID columns and revert rename
|
|
||||||
|
|
||||||
-- Step 1: Drop UUID-related indexes
|
|
||||||
DROP INDEX IF EXISTS idx_song_uuid;
|
|
||||||
DROP INDEX IF EXISTS idx_song_soundtrack_uuid;
|
|
||||||
DROP INDEX IF EXISTS idx_soundtrack_uuid;
|
|
||||||
DROP INDEX IF EXISTS idx_song_soundtrack_id;
|
|
||||||
DROP INDEX IF EXISTS idx_song_path;
|
|
||||||
|
|
||||||
-- Step 1.5: Remove NOT NULL constraints before dropping columns (Issue #28 rollback)
|
|
||||||
ALTER TABLE song ALTER COLUMN soundtrack_uuid DROP NOT NULL;
|
|
||||||
ALTER TABLE song ALTER COLUMN uuid DROP NOT NULL;
|
|
||||||
ALTER TABLE soundtrack ALTER COLUMN uuid DROP NOT NULL;
|
|
||||||
|
|
||||||
-- Step 2: Drop UUID-related constraints
|
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_path_unique;
|
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_uuid_fkey;
|
|
||||||
ALTER TABLE soundtrack DROP CONSTRAINT IF EXISTS soundtrack_uuid_unique;
|
|
||||||
|
|
||||||
-- Step 3: Drop UUID columns from song table
|
|
||||||
ALTER TABLE song DROP COLUMN IF EXISTS soundtrack_uuid;
|
|
||||||
ALTER TABLE song DROP COLUMN IF EXISTS uuid;
|
|
||||||
|
|
||||||
-- Step 4: Drop UUID column from soundtrack table
|
|
||||||
ALTER TABLE soundtrack DROP COLUMN IF EXISTS uuid;
|
|
||||||
|
|
||||||
-- Step 5: Drop the uuid-based primary key
|
|
||||||
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
|
||||||
|
|
||||||
-- Step 6: Restore composite primary key on song
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (soundtrack_id, path);
|
|
||||||
|
|
||||||
-- Step 7: Drop foreign key constraint
|
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
|
||||||
|
|
||||||
-- Step 8: Revert rename operations
|
|
||||||
-- Revert song_list table references
|
|
||||||
ALTER TABLE song_list RENAME COLUMN soundtrack_name TO game_name;
|
|
||||||
|
|
||||||
-- Revert song table: rename soundtrack_id back to game_id
|
|
||||||
ALTER TABLE song RENAME COLUMN soundtrack_id TO game_id;
|
|
||||||
|
|
||||||
-- Revert columns in game table
|
|
||||||
ALTER TABLE game RENAME COLUMN soundtrack_name TO game_name;
|
|
||||||
|
|
||||||
-- Revert primary key sequence
|
|
||||||
ALTER SEQUENCE game_id_seq RENAME TO soundtrack_id_seq;
|
|
||||||
|
|
||||||
-- Revert: Rename soundtrack table back to game
|
|
||||||
ALTER TABLE soundtrack RENAME TO game;
|
|
||||||
|
|
||||||
-- Revert foreign key constraint
|
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_game_id_fkey;
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_game_id_fkey
|
|
||||||
FOREIGN KEY (game_id) REFERENCES game(id);
|
|
||||||
|
|
||||||
-- Revert indexes
|
|
||||||
ALTER INDEX IF EXISTS idx_soundtrack_deleted RENAME TO idx_game_deleted;
|
|
||||||
ALTER INDEX IF EXISTS idx_soundtrack_hash RENAME TO idx_game_hash;
|
|
||||||
ALTER INDEX IF EXISTS idx_soundtrack_path RENAME TO idx_game_path;
|
|
||||||
ALTER INDEX IF EXISTS idx_soundtrack_name RENAME TO idx_game_name;
|
|
||||||
ALTER INDEX IF EXISTS idx_song_soundtrack_id RENAME TO idx_song_game_id;
|
|
||||||
ALTER INDEX IF EXISTS idx_song_soundtrack_id_song_name RENAME TO idx_song_game_id_song_name;
|
|
||||||
ALTER INDEX IF EXISTS song_list_soundtrack_name_idx RENAME TO song_list_game_name_idx;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
-- Migration: Rename game to soundtrack AND add UUID columns
|
|
||||||
-- Combines original migrations 000005 (rename), 000006 (UUID columns), and Issue #28 (Phase 2: NOT NULL constraints)
|
|
||||||
-- Phase 1 & 2: Dual-write with required UUIDs (integer IDs still exist alongside UUIDs)
|
|
||||||
|
|
||||||
-- ============ PART 1: RENAME (from original migration 000005) ============
|
|
||||||
|
|
||||||
-- Rename game table to soundtrack
|
|
||||||
ALTER TABLE game RENAME TO soundtrack;
|
|
||||||
|
|
||||||
-- Rename primary key sequence
|
|
||||||
ALTER SEQUENCE game_id_seq RENAME TO soundtrack_id_seq;
|
|
||||||
|
|
||||||
-- Rename columns in soundtrack table
|
|
||||||
ALTER TABLE soundtrack RENAME COLUMN game_name TO soundtrack_name;
|
|
||||||
|
|
||||||
-- Update song table: rename game_id to soundtrack_id
|
|
||||||
ALTER TABLE song RENAME COLUMN game_id TO soundtrack_id;
|
|
||||||
|
|
||||||
-- Update song_list table references
|
|
||||||
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
|
||||||
|
|
||||||
-- Rename foreign key constraint
|
|
||||||
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_game_id_fkey;
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
|
||||||
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
|
||||||
|
|
||||||
-- Rename indexes
|
|
||||||
ALTER INDEX IF EXISTS idx_game_deleted RENAME TO idx_soundtrack_deleted;
|
|
||||||
ALTER INDEX IF EXISTS idx_game_hash RENAME TO idx_soundtrack_hash;
|
|
||||||
ALTER INDEX IF EXISTS idx_game_path RENAME TO idx_soundtrack_path;
|
|
||||||
ALTER INDEX IF EXISTS idx_game_name RENAME TO idx_soundtrack_name;
|
|
||||||
ALTER INDEX IF EXISTS idx_song_game_id RENAME TO idx_song_soundtrack_id;
|
|
||||||
ALTER INDEX IF EXISTS idx_song_game_id_song_name RENAME TO idx_song_soundtrack_id_song_name;
|
|
||||||
ALTER INDEX IF EXISTS song_list_game_name_idx RENAME TO song_list_soundtrack_name_idx;
|
|
||||||
|
|
||||||
-- ============ PART 2: UUID COLUMNS (from original migration 000006) ============
|
|
||||||
|
|
||||||
-- Step 0: Enable uuid-ossp extension for UUID generation
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
||||||
|
|
||||||
-- Add UUID column to soundtrack table
|
|
||||||
ALTER TABLE soundtrack ADD COLUMN uuid UUID DEFAULT gen_random_uuid();
|
|
||||||
|
|
||||||
-- Add unique constraint on soundtrack.uuid
|
|
||||||
ALTER TABLE soundtrack ADD CONSTRAINT soundtrack_uuid_unique UNIQUE (uuid);
|
|
||||||
|
|
||||||
-- Backfill existing soundtracks with UUIDs (should already have values from DEFAULT)
|
|
||||||
UPDATE soundtrack SET uuid = gen_random_uuid() WHERE uuid IS NULL;
|
|
||||||
|
|
||||||
-- Add index on soundtrack.uuid for query performance
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_soundtrack_uuid ON soundtrack(uuid);
|
|
||||||
|
|
||||||
-- Add UUID columns to song table
|
|
||||||
ALTER TABLE song ADD COLUMN uuid UUID DEFAULT gen_random_uuid();
|
|
||||||
ALTER TABLE song ADD COLUMN soundtrack_uuid UUID;
|
|
||||||
|
|
||||||
-- Backfill existing songs with UUIDs
|
|
||||||
UPDATE song SET uuid = gen_random_uuid() WHERE uuid IS NULL;
|
|
||||||
|
|
||||||
-- Backfill song.soundtrack_uuid from soundtrack.uuid via JOIN
|
|
||||||
UPDATE song s
|
|
||||||
SET soundtrack_uuid = st.uuid
|
|
||||||
FROM soundtrack st
|
|
||||||
WHERE s.soundtrack_id = st.id AND s.soundtrack_uuid IS NULL;
|
|
||||||
|
|
||||||
-- Add foreign key constraint from song.soundtrack_uuid to soundtrack.uuid
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_soundtrack_uuid_fkey
|
|
||||||
FOREIGN KEY (soundtrack_uuid) REFERENCES soundtrack(uuid);
|
|
||||||
|
|
||||||
-- Add indexes on song UUID columns
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_song_uuid ON song(uuid);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_uuid ON song(soundtrack_uuid);
|
|
||||||
|
|
||||||
-- Drop the composite primary key (soundtrack_id, path)
|
|
||||||
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
|
||||||
|
|
||||||
-- Add new primary key on uuid column
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (uuid);
|
|
||||||
|
|
||||||
-- Add unique constraint to maintain same semantics as old PK
|
|
||||||
-- This ensures (soundtrack, path) combinations remain unique
|
|
||||||
ALTER TABLE song ADD CONSTRAINT song_soundtrack_path_unique UNIQUE (soundtrack_uuid, path);
|
|
||||||
|
|
||||||
-- Create index on soundtrack_id for query performance
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_id ON song(soundtrack_id);
|
|
||||||
|
|
||||||
-- Create index on path for lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
|
||||||
|
|
||||||
-- ============ PART 3: MAKE UUID COLUMNS NOT NULL (Issue #28 - Phase 2) ============
|
|
||||||
|
|
||||||
-- Add NOT NULL constraint to soundtrack.uuid
|
|
||||||
ALTER TABLE soundtrack ALTER COLUMN uuid SET NOT NULL;
|
|
||||||
|
|
||||||
-- Add NOT NULL constraint to song.uuid
|
|
||||||
ALTER TABLE song ALTER COLUMN uuid SET NOT NULL;
|
|
||||||
|
|
||||||
-- Add NOT NULL constraint to song.soundtrack_uuid
|
|
||||||
ALTER TABLE song ALTER COLUMN soundtrack_uuid SET NOT NULL;
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- name: ResetGameIdSeq :one
|
||||||
|
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);
|
||||||
|
|
||||||
|
-- name: GetGameNameById :one
|
||||||
|
SELECT game_name FROM game WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetGameById :one
|
||||||
|
SELECT *
|
||||||
|
FROM game
|
||||||
|
WHERE id = $1
|
||||||
|
AND deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: SetGameDeletionDate :exec
|
||||||
|
UPDATE game SET deleted=now() WHERE deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: ClearGames :exec
|
||||||
|
DELETE FROM game;
|
||||||
|
|
||||||
|
-- name: UpdateGameName :exec
|
||||||
|
UPDATE game SET game_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: UpdateGameHash :exec
|
||||||
|
UPDATE game SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: RemoveDeletionDate :exec
|
||||||
|
UPDATE game SET deleted=NULL WHERE id=$1;
|
||||||
|
|
||||||
|
-- name: GetIdByGameName :one
|
||||||
|
SELECT id FROM game WHERE game_name = $1;
|
||||||
|
|
||||||
|
-- name: InsertGame :one
|
||||||
|
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
|
||||||
|
|
||||||
|
-- name: InsertGameWithExistingId :exec
|
||||||
|
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
||||||
|
|
||||||
|
-- name: FindAllGames :many
|
||||||
|
SELECT *
|
||||||
|
FROM game
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
ORDER BY game_name;
|
||||||
|
|
||||||
|
-- name: GetAllGamesIncludingDeleted :many
|
||||||
|
SELECT *
|
||||||
|
FROM game
|
||||||
|
ORDER BY game_name;
|
||||||
|
|
||||||
|
-- name: AddGamePlayed :exec
|
||||||
|
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
-- name: ClearSongs :exec
|
-- name: ClearSongs :exec
|
||||||
DELETE FROM song;
|
DELETE FROM song;
|
||||||
|
|
||||||
-- name: ClearSongsBySoundtrackId :exec
|
-- name: ClearSongsByGameId :exec
|
||||||
DELETE FROM song WHERE soundtrack_id = $1;
|
DELETE FROM song WHERE game_id = $1;
|
||||||
|
|
||||||
-- name: AddSong :exec
|
-- name: AddSong :exec
|
||||||
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash, uuid, soundtrack_uuid) VALUES ($1, $2, $3, $4, $5, $6, $7);
|
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: CheckSong :one
|
-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2;
|
SELECT COUNT(*) FROM song WHERE path = $1;
|
||||||
|
|
||||||
-- name: CheckSongWithHash :one
|
-- name: CheckSongWithHash :one
|
||||||
SELECT COUNT(*) FROM song WHERE hash = $1;
|
SELECT COUNT(*) FROM song WHERE hash = $1;
|
||||||
@@ -20,33 +20,22 @@ SELECT * FROM song WHERE hash = $1;
|
|||||||
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
||||||
|
|
||||||
-- name: AddHashToSong :exec
|
-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3;
|
UPDATE song SET hash=$1 where path=$2;
|
||||||
|
|
||||||
-- name: FindSongsFromSoundtrack :many
|
-- name: FindSongsFromGame :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM song
|
FROM song
|
||||||
WHERE soundtrack_id = $1;
|
WHERE game_id = $1;
|
||||||
|
|
||||||
-- name: AddSongPlayed :exec
|
-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE soundtrack_id = $1 AND song_name = $2;
|
WHERE game_id = $1 AND song_name = $2;
|
||||||
|
|
||||||
-- name: FetchAllSongs :many
|
-- name: FetchAllSongs :many
|
||||||
SELECT * FROM song;
|
SELECT * FROM song;
|
||||||
|
|
||||||
-- name: GetSongByUUID :one
|
|
||||||
SELECT * FROM song WHERE uuid = $1;
|
|
||||||
|
|
||||||
-- name: RemoveBrokenSong :exec
|
-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2;
|
DELETE FROM song WHERE path = $1;
|
||||||
|
|
||||||
-- name: RemoveBrokenSongs :exec
|
-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song WHERE uuid = ANY($1);
|
DELETE FROM song where path = any (sqlc.slice('paths'));
|
||||||
|
|
||||||
-- name: GetSongsBySoundtrackUUID :many
|
|
||||||
SELECT *
|
|
||||||
FROM song
|
|
||||||
WHERE soundtrack_uuid = $1;
|
|
||||||
|
|
||||||
-- name: GetSongUUIDsBySoundtrackId :many
|
|
||||||
SELECT uuid, soundtrack_uuid FROM song WHERE soundtrack_id = $1;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
-- name: InsertSongInList :exec
|
-- name: InsertSongInList :exec
|
||||||
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name)
|
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
|
||||||
VALUES ($1, $2, $3, $4, $5);
|
VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: GetSongList :many
|
-- name: GetSongList :many
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
-- name: ResetSoundtrackIdSeq :one
|
|
||||||
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);
|
|
||||||
|
|
||||||
-- name: GetSoundtrackNameById :one
|
|
||||||
SELECT soundtrack_name FROM soundtrack WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetSoundtrackById :one
|
|
||||||
SELECT *
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: SetSoundtrackDeletionDate :exec
|
|
||||||
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: ClearSoundtracks :exec
|
|
||||||
DELETE FROM soundtrack;
|
|
||||||
|
|
||||||
-- name: UpdateSoundtrackName :exec
|
|
||||||
UPDATE soundtrack SET soundtrack_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: UpdateSoundtrackHash :exec
|
|
||||||
UPDATE soundtrack SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: RemoveSoundtrackDeletionDate :exec
|
|
||||||
UPDATE soundtrack SET deleted=NULL WHERE id=$1;
|
|
||||||
|
|
||||||
-- name: GetIdBySoundtrackName :one
|
|
||||||
SELECT id FROM soundtrack WHERE soundtrack_name = $1;
|
|
||||||
|
|
||||||
-- name: InsertSoundtrack :one
|
|
||||||
INSERT INTO soundtrack (soundtrack_name, path, hash, added, uuid) VALUES ($1, $2, $3, now(), $4) returning id, uuid;
|
|
||||||
|
|
||||||
-- name: InsertSoundtrackWithExistingId :exec
|
|
||||||
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
|
||||||
|
|
||||||
-- name: FindAllSoundtracks :many
|
|
||||||
SELECT *
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY soundtrack_name;
|
|
||||||
|
|
||||||
-- name: GetAllSoundtracksIncludingDeleted :many
|
|
||||||
SELECT *
|
|
||||||
FROM soundtrack
|
|
||||||
ORDER BY soundtrack_name;
|
|
||||||
|
|
||||||
-- name: AddSoundtrackPlayed :exec
|
|
||||||
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetSoundtrackByUUID :one
|
|
||||||
SELECT *
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE uuid = $1
|
|
||||||
AND deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: GetSoundtrackUUIDById :one
|
|
||||||
SELECT uuid FROM soundtrack WHERE id = $1;
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
-- Most played soundtracks with their songs
|
-- Most played games with their songs
|
||||||
-- name: GetMostPlayedSoundtracksWithSongs :many
|
-- name: GetMostPlayedGamesWithSongs :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -13,20 +13,20 @@ SELECT
|
|||||||
'file_name', s.file_name
|
'file_name', s.file_name
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.times_played DESC, g.soundtrack_name
|
ORDER BY g.times_played DESC, g.game_name
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Least played soundtracks with their songs
|
-- Least played games with their songs
|
||||||
-- name: GetLeastPlayedSoundtracksWithSongs :many
|
-- name: GetLeastPlayedGamesWithSongs :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -35,49 +35,49 @@ SELECT
|
|||||||
'file_name', s.file_name
|
'file_name', s.file_name
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.times_played ASC, g.soundtrack_name
|
ORDER BY g.times_played ASC, g.game_name
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Most played songs with their soundtrack info
|
-- Most played songs with their game info
|
||||||
-- name: GetMostPlayedSongsWithSoundtrack :many
|
-- name: GetMostPlayedSongsWithGame :many
|
||||||
SELECT
|
SELECT
|
||||||
s.soundtrack_id as soundtrack_id,
|
s.game_id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
s.song_name,
|
s.song_name,
|
||||||
s.path,
|
s.path,
|
||||||
s.times_played,
|
s.times_played,
|
||||||
s.file_name
|
s.file_name
|
||||||
FROM song s
|
FROM song s
|
||||||
JOIN soundtrack g ON s.soundtrack_id = g.id
|
JOIN game g ON s.game_id = g.id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
ORDER BY s.times_played DESC, s.song_name
|
ORDER BY s.times_played DESC, s.song_name
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Least played songs with their soundtrack info
|
-- Least played songs with their game info
|
||||||
-- name: GetLeastPlayedSongsWithSoundtrack :many
|
-- name: GetLeastPlayedSongsWithGame :many
|
||||||
SELECT
|
SELECT
|
||||||
s.soundtrack_id as soundtrack_id,
|
s.game_id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
s.song_name,
|
s.song_name,
|
||||||
s.path,
|
s.path,
|
||||||
s.times_played,
|
s.times_played,
|
||||||
s.file_name
|
s.file_name
|
||||||
FROM song s
|
FROM song s
|
||||||
JOIN soundtrack g ON s.soundtrack_id = g.id
|
JOIN game g ON s.game_id = g.id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
ORDER BY s.times_played ASC, s.song_name
|
ORDER BY s.times_played ASC, s.song_name
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Soundtracks that have never been played (times_played = 0)
|
-- Games that have never been played (times_played = 0)
|
||||||
-- name: GetNeverPlayedSoundtracks :many
|
-- name: GetNeverPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.added,
|
g.added,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
@@ -86,19 +86,19 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.times_played = 0
|
WHERE g.deleted IS NULL AND g.times_played = 0
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
GROUP BY g.id, g.game_name, g.times_played, g.added
|
||||||
ORDER BY g.soundtrack_name;
|
ORDER BY g.game_name;
|
||||||
|
|
||||||
-- Last played soundtracks (most recently played)
|
-- Last played games (most recently played)
|
||||||
-- name: GetLastPlayedSoundtracks :many
|
-- name: GetLastPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -106,20 +106,20 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.last_played DESC
|
ORDER BY g.last_played DESC
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Oldest played soundtracks (least recently played, but has been played at least once)
|
-- Oldest played games (least recently played, but has been played at least once)
|
||||||
-- name: GetOldestPlayedSoundtracks :many
|
-- name: GetOldestPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -127,22 +127,22 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.last_played ASC
|
ORDER BY g.last_played ASC
|
||||||
LIMIT $1;
|
LIMIT $1;
|
||||||
|
|
||||||
-- Get statistics summary
|
-- Get statistics summary
|
||||||
-- name: GetStatisticsSummary :one
|
-- name: GetStatisticsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_soundtracks,
|
COUNT(*) as total_games,
|
||||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_games,
|
||||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_games,
|
||||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
COALESCE(SUM(times_played), 0)::bigint as total_game_plays,
|
||||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
COALESCE(AVG(times_played), 0)::float as avg_game_plays,
|
||||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
COALESCE(MAX(times_played), 0)::bigint as max_game_plays,
|
||||||
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
COALESCE(MIN(times_played), 0)::bigint as min_game_plays
|
||||||
FROM soundtrack
|
FROM game
|
||||||
WHERE deleted IS NULL;
|
WHERE deleted IS NULL;
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: game.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const addGamePlayed = `-- name: AddGamePlayed :exec
|
||||||
|
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) AddGamePlayed(ctx context.Context, id int32) error {
|
||||||
|
_, err := q.db.Exec(ctx, addGamePlayed, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearGames = `-- name: ClearGames :exec
|
||||||
|
DELETE FROM game
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ClearGames(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, clearGames)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAllGames = `-- name: FindAllGames :many
|
||||||
|
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM game
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
ORDER BY game_name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) FindAllGames(ctx context.Context) ([]Game, error) {
|
||||||
|
rows, err := q.db.Query(ctx, findAllGames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Game
|
||||||
|
for rows.Next() {
|
||||||
|
var i Game
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.GameName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllGamesIncludingDeleted = `-- name: GetAllGamesIncludingDeleted :many
|
||||||
|
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM game
|
||||||
|
ORDER BY game_name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllGamesIncludingDeleted(ctx context.Context) ([]Game, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getAllGamesIncludingDeleted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Game
|
||||||
|
for rows.Next() {
|
||||||
|
var i Game
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.GameName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGameById = `-- name: GetGameById :one
|
||||||
|
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM game
|
||||||
|
WHERE id = $1
|
||||||
|
AND deleted IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetGameById(ctx context.Context, id int32) (Game, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getGameById, id)
|
||||||
|
var i Game
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.GameName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGameNameById = `-- name: GetGameNameById :one
|
||||||
|
SELECT game_name FROM game WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetGameNameById(ctx context.Context, id int32) (string, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getGameNameById, id)
|
||||||
|
var game_name string
|
||||||
|
err := row.Scan(&game_name)
|
||||||
|
return game_name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIdByGameName = `-- name: GetIdByGameName :one
|
||||||
|
SELECT id FROM game WHERE game_name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetIdByGameName(ctx context.Context, gameName string) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getIdByGameName, gameName)
|
||||||
|
var id int32
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertGame = `-- name: InsertGame :one
|
||||||
|
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertGameParams struct {
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertGame(ctx context.Context, arg InsertGameParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, insertGame, arg.GameName, arg.Path, arg.Hash)
|
||||||
|
var id int32
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertGameWithExistingId = `-- name: InsertGameWithExistingId :exec
|
||||||
|
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertGameWithExistingIdParams struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertGameWithExistingId(ctx context.Context, arg InsertGameWithExistingIdParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, insertGameWithExistingId,
|
||||||
|
arg.ID,
|
||||||
|
arg.GameName,
|
||||||
|
arg.Path,
|
||||||
|
arg.Hash,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDeletionDate = `-- name: RemoveDeletionDate :exec
|
||||||
|
UPDATE game SET deleted=NULL WHERE id=$1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) RemoveDeletionDate(ctx context.Context, id int32) error {
|
||||||
|
_, err := q.db.Exec(ctx, removeDeletionDate, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetGameIdSeq = `-- name: ResetGameIdSeq :one
|
||||||
|
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1)
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ResetGameIdSeq(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, resetGameIdSeq)
|
||||||
|
var setval int64
|
||||||
|
err := row.Scan(&setval)
|
||||||
|
return setval, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setGameDeletionDate = `-- name: SetGameDeletionDate :exec
|
||||||
|
UPDATE game SET deleted=now() WHERE deleted IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SetGameDeletionDate(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, setGameDeletionDate)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGameHash = `-- name: UpdateGameHash :exec
|
||||||
|
UPDATE game SET hash=$1, last_changed=now() WHERE id=$2
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateGameHashParams struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateGameHash(ctx context.Context, arg UpdateGameHashParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateGameHash, arg.Hash, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGameName = `-- name: UpdateGameName :exec
|
||||||
|
UPDATE game SET game_name=$1, path=$2, last_changed=now() WHERE id=$3
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateGameNameParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateGameName(ctx context.Context, arg UpdateGameNameParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateGameName, arg.Name, arg.Path, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -7,10 +7,22 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
Added time.Time `json:"added"`
|
||||||
|
Deleted *time.Time `json:"deleted"`
|
||||||
|
LastChanged *time.Time `json:"last_changed"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
LastPlayed *time.Time `json:"last_played"`
|
||||||
|
NumberOfSongs int32 `json:"number_of_songs"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
IpAddress string `json:"ip_address"`
|
IpAddress string `json:"ip_address"`
|
||||||
@@ -21,38 +33,22 @@ type Session struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Song struct {
|
type Song struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SongList struct {
|
type SongList struct {
|
||||||
MatchDate time.Time `json:"match_date"`
|
MatchDate time.Time `json:"match_date"`
|
||||||
MatchID int32 `json:"match_id"`
|
MatchID int32 `json:"match_id"`
|
||||||
SongNo int32 `json:"song_no"`
|
SongNo int32 `json:"song_no"`
|
||||||
SoundtrackName *string `json:"soundtrack_name"`
|
GameName *string `json:"game_name"`
|
||||||
SongName *string `json:"song_name"`
|
SongName *string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Soundtrack struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
|
||||||
Added time.Time `json:"added"`
|
|
||||||
Deleted *time.Time `json:"deleted"`
|
|
||||||
LastChanged *time.Time `json:"last_changed"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
TimesPlayed int32 `json:"times_played"`
|
|
||||||
LastPlayed *time.Time `json:"last_played"`
|
|
||||||
NumberOfSongs int32 `json:"number_of_songs"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Vgmq struct {
|
type Vgmq struct {
|
||||||
SongNo int32 `json:"song_no"`
|
SongNo int32 `json:"song_no"`
|
||||||
Path *string `json:"path"`
|
Path *string `json:"path"`
|
||||||
|
|||||||
@@ -7,78 +7,66 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const addHashToSong = `-- name: AddHashToSong :exec
|
const addHashToSong = `-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3
|
UPDATE song SET hash=$1 where path=$2
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddHashToSongParams struct {
|
type AddHashToSongParams struct {
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.SoundtrackID, arg.Path)
|
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSong = `-- name: AddSong :exec
|
const addSong = `-- name: AddSong :exec
|
||||||
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash, uuid, soundtrack_uuid) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongParams struct {
|
type AddSongParams struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSong,
|
_, err := q.db.Exec(ctx, addSong,
|
||||||
arg.SoundtrackID,
|
arg.GameID,
|
||||||
arg.SongName,
|
arg.SongName,
|
||||||
arg.Path,
|
arg.Path,
|
||||||
arg.FileName,
|
arg.FileName,
|
||||||
arg.Hash,
|
arg.Hash,
|
||||||
arg.Uuid,
|
|
||||||
arg.SoundtrackUuid,
|
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSongPlayed = `-- name: AddSongPlayed :exec
|
const addSongPlayed = `-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE soundtrack_id = $1 AND song_name = $2
|
WHERE game_id = $1 AND song_name = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongPlayedParams struct {
|
type AddSongPlayedParams struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSongPlayed, arg.SoundtrackID, arg.SongName)
|
_, err := q.db.Exec(ctx, addSongPlayed, arg.GameID, arg.SongName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSong = `-- name: CheckSong :one
|
const checkSong = `-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2
|
SELECT COUNT(*) FROM song WHERE path = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type CheckSongParams struct {
|
func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
row := q.db.QueryRow(ctx, checkSong, path)
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CheckSong(ctx context.Context, arg CheckSongParams) (int64, error) {
|
|
||||||
row := q.db.QueryRow(ctx, checkSong, arg.SoundtrackID, arg.Path)
|
|
||||||
var count int64
|
var count int64
|
||||||
err := row.Scan(&count)
|
err := row.Scan(&count)
|
||||||
return count, err
|
return count, err
|
||||||
@@ -104,17 +92,17 @@ func (q *Queries) ClearSongs(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSongsBySoundtrackId = `-- name: ClearSongsBySoundtrackId :exec
|
const clearSongsByGameId = `-- name: ClearSongsByGameId :exec
|
||||||
DELETE FROM song WHERE soundtrack_id = $1
|
DELETE FROM song WHERE game_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int32) error {
|
func (q *Queries) ClearSongsByGameId(ctx context.Context, gameID int32) error {
|
||||||
_, err := q.db.Exec(ctx, clearSongsBySoundtrackId, soundtrackID)
|
_, err := q.db.Exec(ctx, clearSongsByGameId, gameID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, uuid, soundtrack_uuid FROM song
|
SELECT game_id, song_name, path, times_played, hash, file_name FROM song
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||||
@@ -127,14 +115,12 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.Uuid,
|
|
||||||
&i.SoundtrackUuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -146,14 +132,14 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
const findSongsFromGame = `-- name: FindSongsFromGame :many
|
||||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, uuid, soundtrack_uuid
|
SELECT game_id, song_name, path, times_played, hash, file_name
|
||||||
FROM song
|
FROM song
|
||||||
WHERE soundtrack_id = $1
|
WHERE game_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int32) ([]Song, error) {
|
func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song, error) {
|
||||||
rows, err := q.db.Query(ctx, findSongsFromSoundtrack, soundtrackID)
|
rows, err := q.db.Query(ctx, findSongsFromGame, gameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -162,14 +148,12 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.Uuid,
|
|
||||||
&i.SoundtrackUuid,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -181,130 +165,39 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSongByUUID = `-- name: GetSongByUUID :one
|
|
||||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, uuid, soundtrack_uuid FROM song WHERE uuid = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSongByUUID(ctx context.Context, argUuid uuid.UUID) (Song, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getSongByUUID, argUuid)
|
|
||||||
var i Song
|
|
||||||
err := row.Scan(
|
|
||||||
&i.SoundtrackID,
|
|
||||||
&i.SongName,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.Hash,
|
|
||||||
&i.FileName,
|
|
||||||
&i.Uuid,
|
|
||||||
&i.SoundtrackUuid,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSongUUIDsBySoundtrackId = `-- name: GetSongUUIDsBySoundtrackId :many
|
|
||||||
SELECT uuid, soundtrack_uuid FROM song WHERE soundtrack_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetSongUUIDsBySoundtrackIdRow struct {
|
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetSongUUIDsBySoundtrackId(ctx context.Context, soundtrackID int32) ([]GetSongUUIDsBySoundtrackIdRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, getSongUUIDsBySoundtrackId, soundtrackID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetSongUUIDsBySoundtrackIdRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetSongUUIDsBySoundtrackIdRow
|
|
||||||
if err := rows.Scan(&i.Uuid, &i.SoundtrackUuid); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, uuid, soundtrack_uuid FROM song WHERE hash = $1
|
SELECT game_id, song_name, path, times_played, hash, file_name FROM song WHERE hash = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
||||||
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
||||||
var i Song
|
var i Song
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
&i.Uuid,
|
|
||||||
&i.SoundtrackUuid,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSongsBySoundtrackUUID = `-- name: GetSongsBySoundtrackUUID :many
|
|
||||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, uuid, soundtrack_uuid
|
|
||||||
FROM song
|
|
||||||
WHERE soundtrack_uuid = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSongsBySoundtrackUUID(ctx context.Context, soundtrackUuid uuid.UUID) ([]Song, error) {
|
|
||||||
rows, err := q.db.Query(ctx, getSongsBySoundtrackUUID, soundtrackUuid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Song
|
|
||||||
for rows.Next() {
|
|
||||||
var i Song
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.SoundtrackID,
|
|
||||||
&i.SongName,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.Hash,
|
|
||||||
&i.FileName,
|
|
||||||
&i.Uuid,
|
|
||||||
&i.SoundtrackUuid,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2
|
DELETE FROM song WHERE path = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type RemoveBrokenSongParams struct {
|
func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
_, err := q.db.Exec(ctx, removeBrokenSong, path)
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSong(ctx context.Context, arg RemoveBrokenSongParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSong, arg.SoundtrackID, arg.Path)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song WHERE uuid = ANY($1)
|
DELETE FROM song where path = any ($1)
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSongs(ctx context.Context, argUuid uuid.UUID) error {
|
func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSongs, argUuid)
|
_, err := q.db.Exec(ctx, removeBrokenSongs, paths)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getSongList = `-- name: GetSongList :many
|
const getSongList = `-- name: GetSongList :many
|
||||||
SELECT match_date, match_id, song_no, soundtrack_name, song_name
|
SELECT match_date, match_id, song_no, game_name, song_name
|
||||||
FROM song_list
|
FROM song_list
|
||||||
WHERE match_date = $1
|
WHERE match_date = $1
|
||||||
ORDER BY song_no DESC
|
ORDER BY song_no DESC
|
||||||
@@ -30,7 +30,7 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
|
|||||||
&i.MatchDate,
|
&i.MatchDate,
|
||||||
&i.MatchID,
|
&i.MatchID,
|
||||||
&i.SongNo,
|
&i.SongNo,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -44,7 +44,7 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertSongInList = `-- name: InsertSongInList :exec
|
const insertSongInList = `-- name: InsertSongInList :exec
|
||||||
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name)
|
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ type InsertSongInListParams struct {
|
|||||||
MatchDate time.Time `json:"match_date"`
|
MatchDate time.Time `json:"match_date"`
|
||||||
MatchID int32 `json:"match_id"`
|
MatchID int32 `json:"match_id"`
|
||||||
SongNo int32 `json:"song_no"`
|
SongNo int32 `json:"song_no"`
|
||||||
SoundtrackName *string `json:"soundtrack_name"`
|
GameName *string `json:"game_name"`
|
||||||
SongName *string `json:"song_name"`
|
SongName *string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListPara
|
|||||||
arg.MatchDate,
|
arg.MatchDate,
|
||||||
arg.MatchID,
|
arg.MatchID,
|
||||||
arg.SongNo,
|
arg.SongNo,
|
||||||
arg.SoundtrackName,
|
arg.GameName,
|
||||||
arg.SongName,
|
arg.SongName,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: soundtrack.sql
|
|
||||||
|
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const addSoundtrackPlayed = `-- name: AddSoundtrackPlayed :exec
|
|
||||||
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) AddSoundtrackPlayed(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, addSoundtrackPlayed, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSoundtracks = `-- name: ClearSoundtracks :exec
|
|
||||||
DELETE FROM soundtrack
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ClearSoundtracks(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, clearSoundtracks)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
|
|
||||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY soundtrack_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error) {
|
|
||||||
rows, err := q.db.Query(ctx, findAllSoundtracks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Soundtrack
|
|
||||||
for rows.Next() {
|
|
||||||
var i Soundtrack
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SoundtrackName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
|
|
||||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
|
||||||
FROM soundtrack
|
|
||||||
ORDER BY soundtrack_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soundtrack, error) {
|
|
||||||
rows, err := q.db.Query(ctx, getAllSoundtracksIncludingDeleted)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Soundtrack
|
|
||||||
for rows.Next() {
|
|
||||||
var i Soundtrack
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SoundtrackName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
&i.Uuid,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIdBySoundtrackName = `-- name: GetIdBySoundtrackName :one
|
|
||||||
SELECT id FROM soundtrack WHERE soundtrack_name = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName string) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getIdBySoundtrackName, soundtrackName)
|
|
||||||
var id int32
|
|
||||||
err := row.Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSoundtrackById = `-- name: GetSoundtrackById :one
|
|
||||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getSoundtrackById, id)
|
|
||||||
var i Soundtrack
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SoundtrackName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
&i.Uuid,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSoundtrackByUUID = `-- name: GetSoundtrackByUUID :one
|
|
||||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
|
||||||
FROM soundtrack
|
|
||||||
WHERE uuid = $1
|
|
||||||
AND deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSoundtrackByUUID(ctx context.Context, argUuid uuid.UUID) (Soundtrack, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getSoundtrackByUUID, argUuid)
|
|
||||||
var i Soundtrack
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SoundtrackName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
&i.Uuid,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSoundtrackNameById = `-- name: GetSoundtrackNameById :one
|
|
||||||
SELECT soundtrack_name FROM soundtrack WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSoundtrackNameById(ctx context.Context, id int32) (string, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getSoundtrackNameById, id)
|
|
||||||
var soundtrack_name string
|
|
||||||
err := row.Scan(&soundtrack_name)
|
|
||||||
return soundtrack_name, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSoundtrackUUIDById = `-- name: GetSoundtrackUUIDById :one
|
|
||||||
SELECT uuid FROM soundtrack WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSoundtrackUUIDById(ctx context.Context, id int32) (uuid.UUID, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getSoundtrackUUIDById, id)
|
|
||||||
var uuid uuid.UUID
|
|
||||||
err := row.Scan(&uuid)
|
|
||||||
return uuid, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertSoundtrack = `-- name: InsertSoundtrack :one
|
|
||||||
INSERT INTO soundtrack (soundtrack_name, path, hash, added, uuid) VALUES ($1, $2, $3, now(), $4) returning id, uuid
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertSoundtrackParams struct {
|
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InsertSoundtrackRow struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
Uuid uuid.UUID `json:"uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertSoundtrack(ctx context.Context, arg InsertSoundtrackParams) (InsertSoundtrackRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, insertSoundtrack,
|
|
||||||
arg.SoundtrackName,
|
|
||||||
arg.Path,
|
|
||||||
arg.Hash,
|
|
||||||
arg.Uuid,
|
|
||||||
)
|
|
||||||
var i InsertSoundtrackRow
|
|
||||||
err := row.Scan(&i.ID, &i.Uuid)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertSoundtrackWithExistingId = `-- name: InsertSoundtrackWithExistingId :exec
|
|
||||||
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertSoundtrackWithExistingIdParams struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertSoundtrackWithExistingId(ctx context.Context, arg InsertSoundtrackWithExistingIdParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, insertSoundtrackWithExistingId,
|
|
||||||
arg.ID,
|
|
||||||
arg.SoundtrackName,
|
|
||||||
arg.Path,
|
|
||||||
arg.Hash,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSoundtrackDeletionDate = `-- name: RemoveSoundtrackDeletionDate :exec
|
|
||||||
UPDATE soundtrack SET deleted=NULL WHERE id=$1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) RemoveSoundtrackDeletionDate(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, removeSoundtrackDeletionDate, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetSoundtrackIdSeq = `-- name: ResetSoundtrackIdSeq :one
|
|
||||||
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1)
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ResetSoundtrackIdSeq(ctx context.Context) (int64, error) {
|
|
||||||
row := q.db.QueryRow(ctx, resetSoundtrackIdSeq)
|
|
||||||
var setval int64
|
|
||||||
err := row.Scan(&setval)
|
|
||||||
return setval, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSoundtrackDeletionDate = `-- name: SetSoundtrackDeletionDate :exec
|
|
||||||
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) SetSoundtrackDeletionDate(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, setSoundtrackDeletionDate)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSoundtrackHash = `-- name: UpdateSoundtrackHash :exec
|
|
||||||
UPDATE soundtrack SET hash=$1, last_changed=now() WHERE id=$2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateSoundtrackHashParams struct {
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSoundtrackHash(ctx context.Context, arg UpdateSoundtrackHashParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateSoundtrackHash, arg.Hash, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSoundtrackName = `-- name: UpdateSoundtrackName :exec
|
|
||||||
UPDATE soundtrack SET soundtrack_name=$1, path=$2, last_changed=now() WHERE id=$3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateSoundtrackNameParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSoundtrackName(ctx context.Context, arg UpdateSoundtrackNameParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateSoundtrackName, arg.Name, arg.Path, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const getLastPlayedSoundtracks = `-- name: GetLastPlayedSoundtracks :many
|
const getLastPlayedGames = `-- name: GetLastPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -23,37 +23,37 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.last_played DESC
|
ORDER BY g.last_played DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetLastPlayedSoundtracksRow struct {
|
type GetLastPlayedGamesRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
GamePlayed int32 `json:"game_played"`
|
||||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||||
Songs []byte `json:"songs"`
|
Songs []byte `json:"songs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last played soundtracks (most recently played)
|
// Last played games (most recently played)
|
||||||
func (q *Queries) GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]GetLastPlayedSoundtracksRow, error) {
|
func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getLastPlayedSoundtracks, limit)
|
rows, err := q.db.Query(ctx, getLastPlayedGames, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetLastPlayedSoundtracksRow
|
var items []GetLastPlayedGamesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetLastPlayedSoundtracksRow
|
var i GetLastPlayedGamesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SoundtrackPlayed,
|
&i.GamePlayed,
|
||||||
&i.SoundtrackLastPlayed,
|
&i.GameLastPlayed,
|
||||||
&i.Songs,
|
&i.Songs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,43 +66,100 @@ func (q *Queries) GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLeastPlayedSongsWithSoundtrack = `-- name: GetLeastPlayedSongsWithSoundtrack :many
|
const getLeastPlayedGamesWithSongs = `-- name: GetLeastPlayedGamesWithSongs :many
|
||||||
SELECT
|
SELECT
|
||||||
s.soundtrack_id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
|
g.times_played as game_played,
|
||||||
|
g.last_played as game_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM game g
|
||||||
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played ASC, g.game_name
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetLeastPlayedGamesWithSongsRow struct {
|
||||||
|
GameID int32 `json:"game_id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
GamePlayed int32 `json:"game_played"`
|
||||||
|
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Least played games with their songs
|
||||||
|
func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetLeastPlayedGamesWithSongsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetLeastPlayedGamesWithSongsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.GameID,
|
||||||
|
&i.GameName,
|
||||||
|
&i.GamePlayed,
|
||||||
|
&i.GameLastPlayed,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLeastPlayedSongsWithGame = `-- name: GetLeastPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.game_id as game_id,
|
||||||
|
g.game_name,
|
||||||
s.song_name,
|
s.song_name,
|
||||||
s.path,
|
s.path,
|
||||||
s.times_played,
|
s.times_played,
|
||||||
s.file_name
|
s.file_name
|
||||||
FROM song s
|
FROM song s
|
||||||
JOIN soundtrack g ON s.soundtrack_id = g.id
|
JOIN game g ON s.game_id = g.id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
ORDER BY s.times_played ASC, s.song_name
|
ORDER BY s.times_played ASC, s.song_name
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetLeastPlayedSongsWithSoundtrackRow struct {
|
type GetLeastPlayedSongsWithGameRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Least played songs with their soundtrack info
|
// Least played songs with their game info
|
||||||
func (q *Queries) GetLeastPlayedSongsWithSoundtrack(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithSoundtrackRow, error) {
|
func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithSoundtrack, limit)
|
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetLeastPlayedSongsWithSoundtrackRow
|
var items []GetLeastPlayedSongsWithGameRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetLeastPlayedSongsWithSoundtrackRow
|
var i GetLeastPlayedSongsWithGameRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
@@ -118,12 +175,12 @@ func (q *Queries) GetLeastPlayedSongsWithSoundtrack(ctx context.Context, limit i
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLeastPlayedSoundtracksWithSongs = `-- name: GetLeastPlayedSoundtracksWithSongs :many
|
const getMostPlayedGamesWithSongs = `-- name: GetMostPlayedGamesWithSongs :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -132,37 +189,37 @@ SELECT
|
|||||||
'file_name', s.file_name
|
'file_name', s.file_name
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.times_played ASC, g.soundtrack_name
|
ORDER BY g.times_played DESC, g.game_name
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetLeastPlayedSoundtracksWithSongsRow struct {
|
type GetMostPlayedGamesWithSongsRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
GamePlayed int32 `json:"game_played"`
|
||||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||||
Songs []byte `json:"songs"`
|
Songs []byte `json:"songs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Least played soundtracks with their songs
|
// Most played games with their songs
|
||||||
func (q *Queries) GetLeastPlayedSoundtracksWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedSoundtracksWithSongsRow, error) {
|
func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getLeastPlayedSoundtracksWithSongs, limit)
|
rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetLeastPlayedSoundtracksWithSongsRow
|
var items []GetMostPlayedGamesWithSongsRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetLeastPlayedSoundtracksWithSongsRow
|
var i GetMostPlayedGamesWithSongsRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SoundtrackPlayed,
|
&i.GamePlayed,
|
||||||
&i.SoundtrackLastPlayed,
|
&i.GameLastPlayed,
|
||||||
&i.Songs,
|
&i.Songs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -175,43 +232,43 @@ func (q *Queries) GetLeastPlayedSoundtracksWithSongs(ctx context.Context, limit
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMostPlayedSongsWithSoundtrack = `-- name: GetMostPlayedSongsWithSoundtrack :many
|
const getMostPlayedSongsWithGame = `-- name: GetMostPlayedSongsWithGame :many
|
||||||
SELECT
|
SELECT
|
||||||
s.soundtrack_id as soundtrack_id,
|
s.game_id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
s.song_name,
|
s.song_name,
|
||||||
s.path,
|
s.path,
|
||||||
s.times_played,
|
s.times_played,
|
||||||
s.file_name
|
s.file_name
|
||||||
FROM song s
|
FROM song s
|
||||||
JOIN soundtrack g ON s.soundtrack_id = g.id
|
JOIN game g ON s.game_id = g.id
|
||||||
WHERE g.deleted IS NULL
|
WHERE g.deleted IS NULL
|
||||||
ORDER BY s.times_played DESC, s.song_name
|
ORDER BY s.times_played DESC, s.song_name
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetMostPlayedSongsWithSoundtrackRow struct {
|
type GetMostPlayedSongsWithGameRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most played songs with their soundtrack info
|
// Most played songs with their game info
|
||||||
func (q *Queries) GetMostPlayedSongsWithSoundtrack(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithSoundtrackRow, error) {
|
func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getMostPlayedSongsWithSoundtrack, limit)
|
rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetMostPlayedSongsWithSoundtrackRow
|
var items []GetMostPlayedSongsWithGameRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetMostPlayedSongsWithSoundtrackRow
|
var i GetMostPlayedSongsWithGameRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
@@ -227,68 +284,11 @@ func (q *Queries) GetMostPlayedSongsWithSoundtrack(ctx context.Context, limit in
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMostPlayedSoundtracksWithSongs = `-- name: GetMostPlayedSoundtracksWithSongs :many
|
const getNeverPlayedGames = `-- name: GetNeverPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'song_name', s.song_name,
|
|
||||||
'path', s.path,
|
|
||||||
'times_played', s.times_played,
|
|
||||||
'file_name', s.file_name
|
|
||||||
)
|
|
||||||
) as songs
|
|
||||||
FROM soundtrack g
|
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
|
||||||
WHERE g.deleted IS NULL
|
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
|
||||||
ORDER BY g.times_played DESC, g.soundtrack_name
|
|
||||||
LIMIT $1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetMostPlayedSoundtracksWithSongsRow struct {
|
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
|
||||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
|
||||||
Songs []byte `json:"songs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Most played soundtracks with their songs
|
|
||||||
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 []GetMostPlayedSoundtracksWithSongsRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetMostPlayedSoundtracksWithSongsRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.SoundtrackID,
|
|
||||||
&i.SoundtrackName,
|
|
||||||
&i.SoundtrackPlayed,
|
|
||||||
&i.SoundtrackLastPlayed,
|
|
||||||
&i.Songs,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNeverPlayedSoundtracks = `-- name: GetNeverPlayedSoundtracks :many
|
|
||||||
SELECT
|
|
||||||
g.id as soundtrack_id,
|
|
||||||
g.soundtrack_name,
|
|
||||||
g.times_played as soundtrack_played,
|
|
||||||
g.added,
|
g.added,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
@@ -297,35 +297,35 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.times_played = 0
|
WHERE g.deleted IS NULL AND g.times_played = 0
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
GROUP BY g.id, g.game_name, g.times_played, g.added
|
||||||
ORDER BY g.soundtrack_name
|
ORDER BY g.game_name
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetNeverPlayedSoundtracksRow struct {
|
type GetNeverPlayedGamesRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
GamePlayed int32 `json:"game_played"`
|
||||||
Added time.Time `json:"added"`
|
Added time.Time `json:"added"`
|
||||||
Songs []byte `json:"songs"`
|
Songs []byte `json:"songs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soundtracks that have never been played (times_played = 0)
|
// Games that have never been played (times_played = 0)
|
||||||
func (q *Queries) GetNeverPlayedSoundtracks(ctx context.Context) ([]GetNeverPlayedSoundtracksRow, error) {
|
func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGamesRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getNeverPlayedSoundtracks)
|
rows, err := q.db.Query(ctx, getNeverPlayedGames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetNeverPlayedSoundtracksRow
|
var items []GetNeverPlayedGamesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetNeverPlayedSoundtracksRow
|
var i GetNeverPlayedGamesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SoundtrackPlayed,
|
&i.GamePlayed,
|
||||||
&i.Added,
|
&i.Added,
|
||||||
&i.Songs,
|
&i.Songs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -339,12 +339,12 @@ func (q *Queries) GetNeverPlayedSoundtracks(ctx context.Context) ([]GetNeverPlay
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOldestPlayedSoundtracks = `-- name: GetOldestPlayedSoundtracks :many
|
const getOldestPlayedGames = `-- name: GetOldestPlayedGames :many
|
||||||
SELECT
|
SELECT
|
||||||
g.id as soundtrack_id,
|
g.id as game_id,
|
||||||
g.soundtrack_name,
|
g.game_name,
|
||||||
g.times_played as soundtrack_played,
|
g.times_played as game_played,
|
||||||
g.last_played as soundtrack_last_played,
|
g.last_played as game_last_played,
|
||||||
json_agg(
|
json_agg(
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'song_name', s.song_name,
|
'song_name', s.song_name,
|
||||||
@@ -352,37 +352,37 @@ SELECT
|
|||||||
'times_played', s.times_played
|
'times_played', s.times_played
|
||||||
)
|
)
|
||||||
) as songs
|
) as songs
|
||||||
FROM soundtrack g
|
FROM game g
|
||||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
LEFT JOIN song s ON g.id = s.game_id
|
||||||
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
GROUP BY g.id, g.game_name, g.times_played, g.last_played
|
||||||
ORDER BY g.last_played ASC
|
ORDER BY g.last_played ASC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetOldestPlayedSoundtracksRow struct {
|
type GetOldestPlayedGamesRow struct {
|
||||||
SoundtrackID int32 `json:"soundtrack_id"`
|
GameID int32 `json:"game_id"`
|
||||||
SoundtrackName string `json:"soundtrack_name"`
|
GameName string `json:"game_name"`
|
||||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
GamePlayed int32 `json:"game_played"`
|
||||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||||
Songs []byte `json:"songs"`
|
Songs []byte `json:"songs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Oldest played soundtracks (least recently played, but has been played at least once)
|
// Oldest played games (least recently played, but has been played at least once)
|
||||||
func (q *Queries) GetOldestPlayedSoundtracks(ctx context.Context, limit int32) ([]GetOldestPlayedSoundtracksRow, error) {
|
func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getOldestPlayedSoundtracks, limit)
|
rows, err := q.db.Query(ctx, getOldestPlayedGames, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetOldestPlayedSoundtracksRow
|
var items []GetOldestPlayedGamesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetOldestPlayedSoundtracksRow
|
var i GetOldestPlayedGamesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.SoundtrackID,
|
&i.GameID,
|
||||||
&i.SoundtrackName,
|
&i.GameName,
|
||||||
&i.SoundtrackPlayed,
|
&i.GamePlayed,
|
||||||
&i.SoundtrackLastPlayed,
|
&i.GameLastPlayed,
|
||||||
&i.Songs,
|
&i.Songs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -397,25 +397,25 @@ func (q *Queries) GetOldestPlayedSoundtracks(ctx context.Context, limit int32) (
|
|||||||
|
|
||||||
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_soundtracks,
|
COUNT(*) as total_games,
|
||||||
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_games,
|
||||||
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_games,
|
||||||
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
COALESCE(SUM(times_played), 0)::bigint as total_game_plays,
|
||||||
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
COALESCE(AVG(times_played), 0)::float as avg_game_plays,
|
||||||
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
COALESCE(MAX(times_played), 0)::bigint as max_game_plays,
|
||||||
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
COALESCE(MIN(times_played), 0)::bigint as min_game_plays
|
||||||
FROM soundtrack
|
FROM game
|
||||||
WHERE deleted IS NULL
|
WHERE deleted IS NULL
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetStatisticsSummaryRow struct {
|
type GetStatisticsSummaryRow struct {
|
||||||
TotalSoundtracks int64 `json:"total_soundtracks"`
|
TotalGames int64 `json:"total_games"`
|
||||||
PlayedSoundtracks int64 `json:"played_soundtracks"`
|
PlayedGames int64 `json:"played_games"`
|
||||||
NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"`
|
NeverPlayedGames int64 `json:"never_played_games"`
|
||||||
TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"`
|
TotalGamePlays int64 `json:"total_game_plays"`
|
||||||
AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"`
|
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||||
MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"`
|
MaxGamePlays int64 `json:"max_game_plays"`
|
||||||
MinSoundtrackPlays int64 `json:"min_soundtrack_plays"`
|
MinGamePlays int64 `json:"min_game_plays"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get statistics summary
|
// Get statistics summary
|
||||||
@@ -423,13 +423,13 @@ func (q *Queries) GetStatisticsSummary(ctx context.Context) (GetStatisticsSummar
|
|||||||
row := q.db.QueryRow(ctx, getStatisticsSummary)
|
row := q.db.QueryRow(ctx, getStatisticsSummary)
|
||||||
var i GetStatisticsSummaryRow
|
var i GetStatisticsSummaryRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.TotalSoundtracks,
|
&i.TotalGames,
|
||||||
&i.PlayedSoundtracks,
|
&i.PlayedGames,
|
||||||
&i.NeverPlayedSoundtracks,
|
&i.NeverPlayedGames,
|
||||||
&i.TotalSoundtrackPlays,
|
&i.TotalGamePlays,
|
||||||
&i.AvgSoundtrackPlays,
|
&i.AvgGamePlays,
|
||||||
&i.MaxSoundtrackPlays,
|
&i.MaxGamePlays,
|
||||||
&i.MinSoundtrackPlays,
|
&i.MinGamePlays,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -54,19 +54,8 @@ func TestSetupDB(t *testing.T) {
|
|||||||
t.Fatalf("Failed to initialize test database: %v", err)
|
t.Fatalf("Failed to initialize test database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any existing schema to ensure clean state
|
|
||||||
ctx := context.Background()
|
|
||||||
_, err = TestDatabase.Pool.Exec(ctx, "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;")
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Warning: Could not clean schema: %v", err)
|
|
||||||
// Continue anyway, migrations might still work
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations
|
||||||
if err := TestDatabase.RunMigrations(); err != nil {
|
if err := TestDatabase.RunMigrations(); err != nil {
|
||||||
// Clean up on failure to prevent nil pointer issues in other tests
|
|
||||||
TestDatabase.Close()
|
|
||||||
TestDatabase = nil
|
|
||||||
t.Fatalf("Failed to run migrations: %v", err)
|
t.Fatalf("Failed to run migrations: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -108,11 +97,10 @@ func createTestDatabase(host, port, dbname, user, password string) {
|
|||||||
// "closed pool" errors when tests run sequentially
|
// "closed pool" errors when tests run sequentially
|
||||||
func TestTearDownDB(t *testing.T) {
|
func TestTearDownDB(t *testing.T) {
|
||||||
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
||||||
// Note: We also don't nil TestDatabase to allow reuse across tests
|
if TestDatabase != nil {
|
||||||
// if TestDatabase != nil {
|
TestDatabase.Close()
|
||||||
// TestDatabase.Close()
|
TestDatabase = nil
|
||||||
// TestDatabase = nil
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestClearDatabase clears all data from the test database
|
// TestClearDatabase clears all data from the test database
|
||||||
@@ -124,13 +112,10 @@ func TestClearDatabase(t *testing.T) {
|
|||||||
|
|
||||||
// Clear all tables in reverse order to respect foreign keys
|
// Clear all tables in reverse order to respect foreign keys
|
||||||
// Note: This assumes the tables exist and have the expected structure
|
// Note: This assumes the tables exist and have the expected structure
|
||||||
// After migration 000005, game table was renamed to soundtrack
|
|
||||||
tables := []string{
|
tables := []string{
|
||||||
"song_list",
|
"song_list",
|
||||||
"song",
|
"song",
|
||||||
"soundtrack",
|
"game",
|
||||||
"vgmq",
|
|
||||||
"sessions",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -141,10 +126,9 @@ func TestClearDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
// Reset sequences
|
||||||
var seqErr error
|
_, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
|
||||||
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
if err != nil {
|
||||||
if seqErr != nil {
|
t.Logf("Failed to reset game_id_seq: %v", err)
|
||||||
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
|
||||||
"music-server/internal/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CharacterHandler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCharacterHandler() *CharacterHandler {
|
|
||||||
return &CharacterHandler{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCharacterList godoc
|
|
||||||
// @Summary Get list of characters
|
|
||||||
// @Description Returns a list of all available characters
|
|
||||||
// @Tags characters
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {array} string
|
|
||||||
// @Router /characters [get]
|
|
||||||
func (c *CharacterHandler) GetCharacterList(ctx *echo.Context) error {
|
|
||||||
characters := backend.GetCharacterList()
|
|
||||||
return ctx.JSON(http.StatusOK, characters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCharacter godoc
|
|
||||||
// @Summary Get character image
|
|
||||||
// @Description Returns the image for a specific character
|
|
||||||
// @Tags characters
|
|
||||||
// @Accept json
|
|
||||||
// @Produce image/png
|
|
||||||
// @Param name query string true "Character name"
|
|
||||||
// @Success 200 {file} file
|
|
||||||
// @Router /character [get]
|
|
||||||
func (c *CharacterHandler) GetCharacter(ctx *echo.Context) error {
|
|
||||||
character := ctx.QueryParam("name")
|
|
||||||
characterPath := backend.GetCharacter(character)
|
|
||||||
file, err := os.Open(characterPath)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, err.Error())
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
return ctx.Stream(http.StatusOK, "image/png", file)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
|
||||||
"music-server/internal/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthHandler struct {
|
|
||||||
db *db.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHealthHandler(database *db.Database) *HealthHandler {
|
|
||||||
return &HealthHandler{db: database}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheck godoc
|
|
||||||
//
|
|
||||||
// @Summary Check server health
|
|
||||||
// @Description Returns the health status of the server
|
|
||||||
// @Tags health
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {string} string "OK"
|
|
||||||
// @Router /health [get]
|
|
||||||
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
|
||||||
return ctx.JSON(http.StatusOK, h.db.Health())
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHealthCheck verifies the health endpoint returns database status
|
|
||||||
func TestHealthCheck(t *testing.T) {
|
|
||||||
e := StartTestServer(t)
|
|
||||||
// No explicit teardown - handled by StartTestServer's sync.Once
|
|
||||||
|
|
||||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
|
||||||
|
|
||||||
var healthData map[string]string
|
|
||||||
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, healthData)
|
|
||||||
assert.Equal(t, "up", healthData["status"])
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/db"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexHandler() *IndexHandler {
|
||||||
|
return &IndexHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion godoc
|
||||||
|
//
|
||||||
|
// @Summary Getting the version of the backend
|
||||||
|
// @Description get string by ID
|
||||||
|
// @Tags accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} backend.VersionData
|
||||||
|
// @Failure 404 {object} string
|
||||||
|
// @Router /version [get]
|
||||||
|
func (i *IndexHandler) GetVersion(ctx *echo.Context) error {
|
||||||
|
versionHistory := backend.GetVersionHistory()
|
||||||
|
if versionHistory.Version == "" {
|
||||||
|
return ctx.JSON(http.StatusNotFound, "version not found")
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, versionHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDBTest godoc
|
||||||
|
// @Summary Test database connection
|
||||||
|
// @Description Tests the database connection
|
||||||
|
// @Tags database
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {string} string "TestedDB"
|
||||||
|
// @Router /dbtest [get]
|
||||||
|
func (i *IndexHandler) GetDBTest(ctx *echo.Context) error {
|
||||||
|
backend.TestDB()
|
||||||
|
return ctx.JSON(http.StatusOK, "TestedDB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck godoc
|
||||||
|
// @Summary Check server health
|
||||||
|
// @Description Returns the health status of the server
|
||||||
|
// @Tags health
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {string} string "OK"
|
||||||
|
// @Router /health [get]
|
||||||
|
func (i *IndexHandler) HealthCheck(ctx *echo.Context) error {
|
||||||
|
return ctx.JSON(http.StatusOK, db.Health())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCharacterList godoc
|
||||||
|
// @Summary Get list of characters
|
||||||
|
// @Description Returns a list of all available characters
|
||||||
|
// @Tags characters
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} string
|
||||||
|
// @Router /characters [get]
|
||||||
|
func (i *IndexHandler) GetCharacterList(ctx *echo.Context) error {
|
||||||
|
characters := backend.GetCharacterList()
|
||||||
|
return ctx.JSON(http.StatusOK, characters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCharacter godoc
|
||||||
|
// @Summary Get character image
|
||||||
|
// @Description Returns the image for a specific character
|
||||||
|
// @Tags characters
|
||||||
|
// @Accept json
|
||||||
|
// @Produce image/png
|
||||||
|
// @Param name query string true "Character name"
|
||||||
|
// @Success 200 {file} file
|
||||||
|
// @Router /character [get]
|
||||||
|
func (i *IndexHandler) GetCharacter(ctx *echo.Context) error {
|
||||||
|
character := ctx.QueryParam("name")
|
||||||
|
return ctx.File(backend.GetCharacter(character))
|
||||||
|
}
|
||||||
@@ -5,9 +5,45 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/db"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestHealthCheck verifies the health endpoint returns database status
|
||||||
|
func TestHealthCheck(t *testing.T) {
|
||||||
|
// Setup database
|
||||||
|
db.TestSetupDB(t)
|
||||||
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
|
e := StartTestServer(t)
|
||||||
|
|
||||||
|
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
|
var healthData map[string]string
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &healthData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, healthData)
|
||||||
|
assert.Equal(t, "up", healthData["status"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetVersion verifies the version endpoint returns version history
|
||||||
|
func TestGetVersion(t *testing.T) {
|
||||||
|
e := StartTestServer(t)
|
||||||
|
|
||||||
|
resp := MakeTestRequest(t, e, "GET", "/version")
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
|
var versionData backend.VersionData
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, versionData.Version)
|
||||||
|
assert.NotEmpty(t, versionData.Changelog)
|
||||||
|
assert.NotEmpty(t, versionData.History)
|
||||||
|
}
|
||||||
|
|
||||||
// TestGetCharacterList verifies the characters endpoint returns list of characters
|
// TestGetCharacterList verifies the characters endpoint returns list of characters
|
||||||
func TestGetCharacterList(t *testing.T) {
|
func TestGetCharacterList(t *testing.T) {
|
||||||
e := StartTestServer(t)
|
e := StartTestServer(t)
|
||||||
@@ -45,3 +81,16 @@ func TestGetCharacterNotFound(t *testing.T) {
|
|||||||
// Should return 404 or similar error
|
// Should return 404 or similar error
|
||||||
assert.NotEqual(t, http.StatusOK, resp.Code)
|
assert.NotEqual(t, http.StatusOK, resp.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDBTest verifies the database test endpoint
|
||||||
|
func TestDBTest(t *testing.T) {
|
||||||
|
// Setup database
|
||||||
|
db.TestSetupDB(t)
|
||||||
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
|
e := StartTestServer(t)
|
||||||
|
|
||||||
|
resp := MakeTestRequest(t, e, "GET", "/dbtest")
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
assert.Contains(t, resp.Body.String(), "TestedDB")
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"music-server/internal/logging"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RateLimiter implements a simple IP-based rate limiter
|
|
||||||
type RateLimiter struct {
|
|
||||||
visits map[string]*Visit
|
|
||||||
mu sync.Mutex
|
|
||||||
limit int // requests per window
|
|
||||||
window time.Duration // time window
|
|
||||||
}
|
|
||||||
|
|
||||||
type Visit struct {
|
|
||||||
count int
|
|
||||||
firstSeen time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRateLimiter creates a new rate limiter
|
|
||||||
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
|
||||||
rl := &RateLimiter{
|
|
||||||
visits: make(map[string]*Visit),
|
|
||||||
limit: limit,
|
|
||||||
window: window,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start cleanup goroutine
|
|
||||||
go rl.cleanup()
|
|
||||||
return rl
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup periodically removes old entries
|
|
||||||
func (rl *RateLimiter) cleanup() {
|
|
||||||
for {
|
|
||||||
time.Sleep(rl.window)
|
|
||||||
rl.mu.Lock()
|
|
||||||
now := time.Now()
|
|
||||||
for ip, visit := range rl.visits {
|
|
||||||
if now.Sub(visit.firstSeen) > rl.window {
|
|
||||||
delete(rl.visits, ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rl.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RateLimitMiddleware returns an Echo middleware for rate limiting
|
|
||||||
func RateLimitMiddleware(rl *RateLimiter) echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c *echo.Context) error {
|
|
||||||
ip := c.RealIP()
|
|
||||||
|
|
||||||
rl.mu.Lock()
|
|
||||||
defer rl.mu.Unlock()
|
|
||||||
|
|
||||||
visit, exists := rl.visits[ip]
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
// First visit from this IP
|
|
||||||
rl.visits[ip] = &Visit{
|
|
||||||
count: 1,
|
|
||||||
firstSeen: now,
|
|
||||||
}
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if window has expired
|
|
||||||
if now.Sub(visit.firstSeen) > rl.window {
|
|
||||||
// Reset for new window
|
|
||||||
visit.count = 1
|
|
||||||
visit.firstSeen = now
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if limit exceeded
|
|
||||||
if visit.count >= rl.limit {
|
|
||||||
logging.GetLogger().Warn("Rate limit exceeded",
|
|
||||||
zap.String("ip", ip),
|
|
||||||
zap.String("path", c.Request().URL.Path))
|
|
||||||
return c.JSON(http.StatusTooManyRequests, map[string]string{
|
|
||||||
"error": "Rate limit exceeded. Please try again later.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment count
|
|
||||||
visit.count++
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,15 +17,6 @@ import (
|
|||||||
func TokenAuthMiddleware(pool *pgxpool.Pool) echo.MiddlewareFunc {
|
func TokenAuthMiddleware(pool *pgxpool.Pool) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c *echo.Context) error {
|
return func(c *echo.Context) error {
|
||||||
// Session auth disable bypass - allow all requests through without token validation
|
|
||||||
disableSessionAuth := os.Getenv("DISABLE_SESSION_AUTH") == "true"
|
|
||||||
if disableSessionAuth {
|
|
||||||
logging.GetLogger().Debug("Session auth disabled - bypassing token authentication",
|
|
||||||
zap.String("path", c.Request().URL.Path),
|
|
||||||
zap.String("ip", c.RealIP()))
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract token from Authorization header
|
// Extract token from Authorization header
|
||||||
authHeader := c.Request().Header.Get("Authorization")
|
authHeader := c.Request().Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ func NewMusicHandler() *MusicHandler {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music [get]
|
// @Router /music [get]
|
||||||
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
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")
|
song := ctx.QueryParam("song")
|
||||||
if song == "" {
|
if song == "" {
|
||||||
return ctx.String(http.StatusBadRequest, "song can't be empty")
|
return ctx.String(http.StatusBadRequest, "song can't be empty")
|
||||||
@@ -54,6 +58,10 @@ func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/soundTest [get]
|
// @Router /music/soundTest [get]
|
||||||
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetSoundCheckSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,6 +80,10 @@ func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/reset [get]
|
// @Router /music/reset [get]
|
||||||
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
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()
|
backend.Reset()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -86,6 +98,10 @@ func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand [get]
|
// @Router /music/rand [get]
|
||||||
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetRandomSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,6 +121,10 @@ func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand/low [get]
|
// @Router /music/rand/low [get]
|
||||||
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetRandomSongLowChance()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,6 +144,10 @@ func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand/classic [get]
|
// @Router /music/rand/classic [get]
|
||||||
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetRandomSongClassic()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,6 +193,10 @@ func (m *MusicHandler) GetPlayedSongs(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/next [get]
|
// @Router /music/next [get]
|
||||||
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetNextSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,6 +216,10 @@ func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/previous [get]
|
// @Router /music/previous [get]
|
||||||
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
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()
|
songPath := backend.GetPreviousSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -197,32 +229,40 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
|||||||
return ctx.Stream(http.StatusOK, "audio/mpeg", file)
|
return ctx.Stream(http.StatusOK, "audio/mpeg", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllSoundtracks godoc
|
// GetAllGames godoc
|
||||||
// @Summary Get all soundtracks
|
// @Summary Get all games
|
||||||
// @Description Returns a list of all soundtracks in order
|
// @Description Returns a list of all games in order
|
||||||
// @Tags music
|
// @Tags music
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} map[string]interface{}
|
// @Success 200 {array} map[string]interface{}
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/order [get]
|
// @Router /music/all/order [get]
|
||||||
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllGames(ctx *echo.Context) error {
|
||||||
soundtrackList := backend.GetAllSoundtracks()
|
if backend.Syncing {
|
||||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
logging.GetLogger().Info("Syncing is in progress")
|
||||||
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
|
}
|
||||||
|
gameList := backend.GetAllGames()
|
||||||
|
return ctx.JSON(http.StatusOK, gameList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllSoundtracksRandom godoc
|
// GetAllGamesRandom godoc
|
||||||
// @Summary Get all soundtracks random
|
// @Summary Get all games random
|
||||||
// @Description Returns a list of all soundtracks in random order
|
// @Description Returns a list of all games in random order
|
||||||
// @Tags music
|
// @Tags music
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} map[string]interface{}
|
// @Success 200 {array} map[string]interface{}
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/random [get]
|
// @Router /music/all/random [get]
|
||||||
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllGamesRandom(ctx *echo.Context) error {
|
||||||
soundtrackList := backend.GetAllSoundtracksRandom()
|
if backend.Syncing {
|
||||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
logging.GetLogger().Info("Syncing is in progress")
|
||||||
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
|
}
|
||||||
|
gameList := backend.GetAllGamesRandom()
|
||||||
|
return ctx.JSON(http.StatusOK, gameList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutPlayed godoc
|
// PutPlayed godoc
|
||||||
@@ -237,6 +277,10 @@ func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/played [put]
|
// @Router /music/played [put]
|
||||||
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
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"))
|
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.JSON(http.StatusBadRequest, err.Error())
|
return ctx.JSON(http.StatusBadRequest, err.Error())
|
||||||
@@ -255,6 +299,10 @@ func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/addQue [get]
|
// @Router /music/addQue [get]
|
||||||
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
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()
|
backend.AddLatestToQue()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -268,6 +316,10 @@ func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/addPlayed [get]
|
// @Router /music/addPlayed [get]
|
||||||
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
|
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()
|
backend.AddLatestPlayed()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
+55
-169
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
@@ -51,8 +50,8 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
fileServer := http.FileServer(http.FS(web.Assets))
|
fileServer := http.FileServer(http.FS(web.Assets))
|
||||||
e.GET("/assets/*", echo.WrapHandler(fileServer))
|
e.GET("/assets/*", echo.WrapHandler(fileServer))
|
||||||
|
|
||||||
e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm())))
|
e.GET("/search", echo.WrapHandler(templ.Handler(web.HelloForm())))
|
||||||
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackWebHandler)))
|
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindGameWebHandler)))
|
||||||
|
|
||||||
e.Static("/", "/frontend")
|
e.Static("/", "/frontend")
|
||||||
|
|
||||||
@@ -64,16 +63,12 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// ============================================
|
// ============================================
|
||||||
deprecatedMiddleware := middleware.DeprecationMiddleware
|
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||||
|
|
||||||
health := NewHealthHandler(s.db)
|
index := NewIndexHandler()
|
||||||
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
e.GET("/version", deprecatedMiddleware(index.GetVersion))
|
||||||
|
e.GET("/dbtest", deprecatedMiddleware(index.GetDBTest))
|
||||||
version := NewVersionHandler()
|
e.GET("/health", deprecatedMiddleware(index.HealthCheck))
|
||||||
e.GET("/version", deprecatedMiddleware(version.GetLatestVersion))
|
e.GET("/character", deprecatedMiddleware(index.GetCharacter))
|
||||||
e.GET("/version/history", deprecatedMiddleware(version.GetVersionHistory))
|
e.GET("/characters", deprecatedMiddleware(index.GetCharacterList))
|
||||||
|
|
||||||
character := NewCharacterHandler()
|
|
||||||
e.GET("/character", deprecatedMiddleware(character.GetCharacter))
|
|
||||||
e.GET("/characters", deprecatedMiddleware(character.GetCharacterList))
|
|
||||||
|
|
||||||
download := NewDownloadHandler()
|
download := NewDownloadHandler()
|
||||||
e.GET("/download", deprecatedMiddleware(download.checkLatest))
|
e.GET("/download", deprecatedMiddleware(download.checkLatest))
|
||||||
@@ -83,32 +78,32 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
|
|
||||||
sync := NewSyncHandler()
|
sync := NewSyncHandler()
|
||||||
syncGroup := e.Group("/sync")
|
syncGroup := e.Group("/sync")
|
||||||
syncGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
syncGroup.GET("", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
|
||||||
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
||||||
syncGroup.GET("/new", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
syncGroup.GET("/new", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
|
||||||
syncGroup.GET("/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
syncGroup.GET("/full", deprecatedMiddleware(sync.SyncGamesNewFull))
|
||||||
syncGroup.GET("/new/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncGamesNewFull))
|
||||||
syncGroup.GET("/quick", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncGamesNewOnlyChanges))
|
||||||
syncGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.ResetDB)))
|
syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetGames))
|
||||||
|
|
||||||
music := NewMusicHandler()
|
music := NewMusicHandler()
|
||||||
musicGroup := e.Group("/music")
|
musicGroup := e.Group("/music")
|
||||||
musicGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSong)))
|
musicGroup.GET("", deprecatedMiddleware(music.GetSong))
|
||||||
musicGroup.GET("/soundTest", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSoundCheckSong)))
|
musicGroup.GET("/soundTest", deprecatedMiddleware(music.GetSoundCheckSong))
|
||||||
musicGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.ResetMusic)))
|
musicGroup.GET("/reset", deprecatedMiddleware(music.ResetMusic))
|
||||||
musicGroup.GET("/rand", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSong)))
|
musicGroup.GET("/rand", deprecatedMiddleware(music.GetRandomSong))
|
||||||
musicGroup.GET("/rand/low", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongLowChance)))
|
musicGroup.GET("/rand/low", deprecatedMiddleware(music.GetRandomSongLowChance))
|
||||||
musicGroup.GET("/rand/classic", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongClassic)))
|
musicGroup.GET("/rand/classic", deprecatedMiddleware(music.GetRandomSongClassic))
|
||||||
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
||||||
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
||||||
musicGroup.GET("/next", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetNextSong)))
|
musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong))
|
||||||
musicGroup.GET("/previous", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetPreviousSong)))
|
musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong))
|
||||||
musicGroup.GET("/all", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
musicGroup.GET("/all", deprecatedMiddleware(music.GetAllGamesRandom))
|
||||||
musicGroup.GET("/all/order", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracks)))
|
musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllGames))
|
||||||
musicGroup.GET("/all/random", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllGamesRandom))
|
||||||
musicGroup.PUT("/played", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.PutPlayed)))
|
musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed))
|
||||||
musicGroup.GET("/addQue", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestToQue)))
|
musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue))
|
||||||
musicGroup.GET("/addPlayed", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestPlayed)))
|
musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// API v1 Routes with Token Authentication
|
// API v1 Routes with Token Authentication
|
||||||
@@ -117,162 +112,53 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// Create /api/v1 group
|
// Create /api/v1 group
|
||||||
apiV1 := e.Group("/api/v1")
|
apiV1 := e.Group("/api/v1")
|
||||||
|
|
||||||
// Create rate limiter for token creation: 10 requests per minute per IP
|
// Public endpoints - no token required
|
||||||
tokenRateLimiter := middleware.NewRateLimiter(10, time.Minute)
|
apiV1.POST("/token", func(c *echo.Context) error {
|
||||||
tokenRateLimitMiddleware := middleware.RateLimitMiddleware(tokenRateLimiter)
|
|
||||||
|
|
||||||
// Token endpoints (public - no token required)
|
|
||||||
tokenGroup := apiV1.Group("/token")
|
|
||||||
tokenGroup.POST("", func(c *echo.Context) error {
|
|
||||||
return s.tokenHandler.CreateTokenHandler(c)
|
return s.tokenHandler.CreateTokenHandler(c)
|
||||||
}, tokenRateLimitMiddleware)
|
})
|
||||||
tokenGroup.DELETE("", func(c *echo.Context) error {
|
apiV1.DELETE("/token", func(c *echo.Context) error {
|
||||||
return s.tokenHandler.DeleteTokenHandler(c)
|
return s.tokenHandler.DeleteTokenHandler(c)
|
||||||
})
|
})
|
||||||
tokenGroup.POST("/cleanup", func(c *echo.Context) error {
|
apiV1.POST("/token/cleanup", func(c *echo.Context) error {
|
||||||
return s.tokenHandler.CleanupExpiredSessionsHandler(c)
|
return s.tokenHandler.CleanupExpiredSessionsHandler(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Protected endpoints - require valid token
|
// Protected endpoints - require valid token
|
||||||
// Create token auth middleware with pool access
|
// Create token auth middleware with pool access
|
||||||
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
||||||
|
|
||||||
|
// Protected group with token authentication
|
||||||
protectedV1 := apiV1.Group("", tokenAuthMiddleware)
|
protectedV1 := apiV1.Group("", tokenAuthMiddleware)
|
||||||
|
|
||||||
// Statistics endpoints (protected)
|
// Statistics API endpoints (protected by token auth)
|
||||||
statisticsHandler := s.statisticsHandler
|
statistics := s.statisticsHandler
|
||||||
statsGroup := protectedV1.Group("/statistics")
|
protectedV1.GET("/statistics/games/most-played", func(c *echo.Context) error {
|
||||||
statsGroup.GET("/soundtracks/most-played", func(c *echo.Context) error {
|
return statistics.GetMostPlayedGames(c)
|
||||||
return statisticsHandler.GetMostPlayedSoundtracks(c)
|
|
||||||
})
|
})
|
||||||
statsGroup.GET("/soundtracks/least-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/games/least-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetLeastPlayedSoundtracks(c)
|
return statistics.GetLeastPlayedGames(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/soundtracks/never-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/games/never-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetNeverPlayedSoundtracks(c)
|
return statistics.GetNeverPlayedGames(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/soundtracks/last-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/games/last-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetLastPlayedSoundtracks(c)
|
return statistics.GetLastPlayedGames(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/soundtracks/oldest-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/games/oldest-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetOldestPlayedSoundtracks(c)
|
return statistics.GetOldestPlayedGames(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/songs/most-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/songs/most-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetMostPlayedSongs(c)
|
return statistics.GetMostPlayedSongs(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/songs/least-played", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/songs/least-played", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetLeastPlayedSongs(c)
|
return statistics.GetLeastPlayedSongs(c)
|
||||||
})
|
})
|
||||||
statsGroup.GET("/summary", func(c *echo.Context) error {
|
protectedV1.GET("/statistics/summary", func(c *echo.Context) error {
|
||||||
return statisticsHandler.GetStatisticsSummary(c)
|
return statistics.GetStatisticsSummary(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Version endpoints (protected)
|
// Future: VGMQ endpoints will be added to protectedV1 group
|
||||||
versionGroup := protectedV1.Group("/version")
|
_ = protectedV1 // Use the variable to avoid unused variable error
|
||||||
versionGroup.GET("", func(c *echo.Context) error {
|
|
||||||
return version.GetLatestVersion(c)
|
|
||||||
})
|
|
||||||
versionGroup.GET("/history", func(c *echo.Context) error {
|
|
||||||
return version.GetVersionHistory(c)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Character endpoints (protected)
|
|
||||||
characterGroup := protectedV1.Group("/character")
|
|
||||||
characterGroup.GET("", func(c *echo.Context) error {
|
|
||||||
return character.GetCharacter(c)
|
|
||||||
})
|
|
||||||
characterGroup.GET("/list", func(c *echo.Context) error {
|
|
||||||
return character.GetCharacterList(c)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Download endpoints (protected)
|
|
||||||
downloadGroup := protectedV1.Group("/download")
|
|
||||||
downloadGroup.GET("", func(c *echo.Context) error {
|
|
||||||
return download.checkLatest(c)
|
|
||||||
})
|
|
||||||
downloadGroup.GET("/list", func(c *echo.Context) error {
|
|
||||||
return download.listAssetsOfLatest(c)
|
|
||||||
})
|
|
||||||
downloadGroup.GET("/windows", func(c *echo.Context) error {
|
|
||||||
return download.downloadLatestWindows(c)
|
|
||||||
})
|
|
||||||
downloadGroup.GET("/linux", func(c *echo.Context) error {
|
|
||||||
return download.downloadLatestLinux(c)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sync endpoints (protected)
|
|
||||||
protectedSyncGroup := protectedV1.Group("/sync")
|
|
||||||
protectedSyncGroup.GET("", func(c *echo.Context) error {
|
|
||||||
return sync.SyncSoundtracksNewOnlyChanges(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/progress", func(c *echo.Context) error {
|
|
||||||
return sync.SyncProgress(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/new", func(c *echo.Context) error {
|
|
||||||
return sync.SyncSoundtracksNewOnlyChanges(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/full", func(c *echo.Context) error {
|
|
||||||
return sync.SyncSoundtracksNewFull(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/new/full", func(c *echo.Context) error {
|
|
||||||
return sync.SyncSoundtracksNewFull(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/quick", func(c *echo.Context) error {
|
|
||||||
return sync.SyncSoundtracksNewOnlyChanges(c)
|
|
||||||
})
|
|
||||||
protectedSyncGroup.GET("/reset", func(c *echo.Context) error {
|
|
||||||
return sync.ResetDB(c)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Music endpoints (protected)
|
|
||||||
protectedMusicGroup := protectedV1.Group("/music")
|
|
||||||
protectedMusicGroup.GET("", func(c *echo.Context) error {
|
|
||||||
return music.GetSong(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/soundTest", func(c *echo.Context) error {
|
|
||||||
return music.GetSoundCheckSong(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/reset", func(c *echo.Context) error {
|
|
||||||
return music.ResetMusic(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/rand", func(c *echo.Context) error {
|
|
||||||
return music.GetRandomSong(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/rand/low", func(c *echo.Context) error {
|
|
||||||
return music.GetRandomSongLowChance(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/rand/classic", func(c *echo.Context) error {
|
|
||||||
return music.GetRandomSongClassic(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/info", func(c *echo.Context) error {
|
|
||||||
return music.GetSongInfo(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/list", func(c *echo.Context) error {
|
|
||||||
return music.GetPlayedSongs(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/next", func(c *echo.Context) error {
|
|
||||||
return music.GetNextSong(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/previous", func(c *echo.Context) error {
|
|
||||||
return music.GetPreviousSong(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/all", func(c *echo.Context) error {
|
|
||||||
return music.GetAllSoundtracksRandom(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/all/order", func(c *echo.Context) error {
|
|
||||||
return music.GetAllSoundtracks(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/all/random", func(c *echo.Context) error {
|
|
||||||
return music.GetAllSoundtracksRandom(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.PUT("/played", func(c *echo.Context) error {
|
|
||||||
return music.PutPlayed(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/addQue", func(c *echo.Context) error {
|
|
||||||
return music.AddLatestToQue(c)
|
|
||||||
})
|
|
||||||
protectedMusicGroup.GET("/addPlayed", func(c *echo.Context) error {
|
|
||||||
return music.AddLatestPlayed(c)
|
|
||||||
})
|
|
||||||
|
|
||||||
routes := e.Router().Routes()
|
routes := e.Router().Routes()
|
||||||
sort.Slice(routes, func(i, j int) bool {
|
sort.Slice(routes, func(i, j int) bool {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
host = os.Getenv("DB_HOST")
|
||||||
|
dbPort = os.Getenv("DB_PORT")
|
||||||
|
dbName = os.Getenv("DB_NAME")
|
||||||
|
username = os.Getenv("DB_USERNAME")
|
||||||
|
password = os.Getenv("DB_PASSWORD")
|
||||||
musicPath = os.Getenv("MUSIC_PATH")
|
musicPath = os.Getenv("MUSIC_PATH")
|
||||||
charactersPath = os.Getenv("CHARACTERS_PATH")
|
charactersPath = os.Getenv("CHARACTERS_PATH")
|
||||||
logLevel = os.Getenv("LOG_LEVEL")
|
logLevel = os.Getenv("LOG_LEVEL")
|
||||||
@@ -32,20 +37,11 @@ var (
|
|||||||
// NewServerInstance creates a new Server instance with all dependencies initialized.
|
// NewServerInstance creates a new Server instance with all dependencies initialized.
|
||||||
// Use this for dependency injection and proper lifecycle management.
|
// Use this for dependency injection and proper lifecycle management.
|
||||||
func NewServerInstance() *Server {
|
func NewServerInstance() *Server {
|
||||||
// Re-read environment variables at server creation time (not at package import time)
|
|
||||||
// This allows tests to override environment variables
|
|
||||||
host := os.Getenv("DB_HOST")
|
|
||||||
dbPort := os.Getenv("DB_PORT")
|
|
||||||
dbName := os.Getenv("DB_NAME")
|
|
||||||
username := os.Getenv("DB_USERNAME")
|
|
||||||
password := os.Getenv("DB_PASSWORD")
|
|
||||||
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
currentLogLevel := logLevel
|
if logLevel == "" {
|
||||||
if currentLogLevel == "" {
|
logLevel = "info"
|
||||||
currentLogLevel = "info"
|
|
||||||
}
|
}
|
||||||
logging.Init(currentLogLevel, logJSON)
|
logging.Init(logLevel, logJSON)
|
||||||
|
|
||||||
logger := logging.GetLogger()
|
logger := logging.GetLogger()
|
||||||
|
|
||||||
|
|||||||
@@ -1,411 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/testcontainers/testcontainers-go"
|
|
||||||
"github.com/testcontainers/testcontainers-go/wait"
|
|
||||||
)
|
|
||||||
|
|
||||||
// saveEnvVars saves the current state of relevant environment variables
|
|
||||||
func saveEnvVars() map[string]string {
|
|
||||||
vars := []string{
|
|
||||||
"DB_HOST", "DB_PORT", "DB_USERNAME", "DB_PASSWORD", "DB_NAME",
|
|
||||||
"MUSIC_PATH", "CHARACTERS_PATH", "PORT", "DISABLE_SESSION_AUTH", "LOG_LEVEL",
|
|
||||||
}
|
|
||||||
saved := make(map[string]string)
|
|
||||||
for _, v := range vars {
|
|
||||||
saved[v] = os.Getenv(v)
|
|
||||||
}
|
|
||||||
return saved
|
|
||||||
}
|
|
||||||
|
|
||||||
// restoreEnvVars restores environment variables from a saved state
|
|
||||||
func restoreEnvVars(saved map[string]string) {
|
|
||||||
for k, v := range saved {
|
|
||||||
if v == "" {
|
|
||||||
os.Unsetenv(k)
|
|
||||||
} else {
|
|
||||||
os.Setenv(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkTableExists returns true if the specified table exists in the database
|
|
||||||
func checkTableExists(ctx context.Context, pool *pgxpool.Pool, tableName string) (bool, error) {
|
|
||||||
var exists bool
|
|
||||||
err := pool.QueryRow(ctx, `
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_name = $1
|
|
||||||
)
|
|
||||||
`, tableName).Scan(&exists)
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadSQLDumpIntoDatabase loads a SQL dump file into a PostgreSQL database
|
|
||||||
func loadSQLDumpIntoDatabase(t *testing.T, ctx context.Context, container testcontainers.Container, dbname, dumpPath string) error {
|
|
||||||
// Copy the SQL dump file to the container
|
|
||||||
basename := filepath.Base(dumpPath)
|
|
||||||
targetPath := "/tmp/" + basename
|
|
||||||
if err := container.CopyFileToContainer(ctx, dumpPath, targetPath, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy dump file to container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute psql to load the dump as the postgres superuser
|
|
||||||
// We execute psql inside the container, so we use localhost and port 5432 (the container's internal port)
|
|
||||||
// The postgres user is the superuser created by the Docker image
|
|
||||||
// Use sh instead of bash as postgres:15-alpine uses Alpine Linux
|
|
||||||
cmd := []string{"sh", "-c", "PGPASSWORD=postgres psql -h localhost -U postgres -d " + dbname + " -f " + targetPath + " -v ON_ERROR_STOP=1"}
|
|
||||||
|
|
||||||
exitCode, reader, err := container.Exec(ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to execute psql: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the output
|
|
||||||
output, err := io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read psql output: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if exitCode != 0 {
|
|
||||||
return fmt.Errorf("psql exited with code %d, output: %s", exitCode, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Successfully loaded SQL dump from %s into database %s", dumpPath, dbname)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createRoleInContainer creates a role in the PostgreSQL container
|
|
||||||
func createRoleInContainer(t *testing.T, ctx context.Context, container testcontainers.Container, roleName, superuserPassword string) error {
|
|
||||||
// Get the mapped port for the container
|
|
||||||
mappedPort, err := container.MappedPort(ctx, "5432")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get mapped port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to the postgres database to create the role
|
|
||||||
connStr := fmt.Sprintf("host=localhost port=%s user=postgres password=postgres dbname=postgres sslmode=disable",
|
|
||||||
mappedPort.Port())
|
|
||||||
|
|
||||||
pool, err := pgxpool.New(ctx, connStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to postgres database: %w", err)
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
// Check if role already exists
|
|
||||||
var exists bool
|
|
||||||
err = pool.QueryRow(ctx, "SELECT 1 FROM pg_roles WHERE rolname = $1", roleName).Scan(&exists)
|
|
||||||
if err == nil && exists {
|
|
||||||
return nil // Role already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear any error from the role check (role might not exist, which is expected)
|
|
||||||
// Create the role
|
|
||||||
_, err = pool.Exec(ctx, fmt.Sprintf("CREATE ROLE %s WITH LOGIN PASSWORD 'testpass'", roleName))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create role %s: %w", roleName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createDatabaseInContainer creates a new database in the PostgreSQL container
|
|
||||||
func createDatabaseInContainer(t *testing.T, ctx context.Context, container testcontainers.Container, dbname string) error {
|
|
||||||
// Get the mapped port for the container
|
|
||||||
mappedPort, err := container.MappedPort(ctx, "5432")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get mapped port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a small delay to ensure PostgreSQL is fully ready
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// Connect to the default 'postgres' database to create the new database
|
|
||||||
// Use localhost and the mapped port
|
|
||||||
connStr := fmt.Sprintf("host=localhost port=%s user=postgres password=postgres dbname=postgres sslmode=disable",
|
|
||||||
mappedPort.Port())
|
|
||||||
|
|
||||||
pool, err := pgxpool.New(ctx, connStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to postgres database: %w", err)
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
// Check if database already exists and drop it if so
|
|
||||||
var exists int
|
|
||||||
err = pool.QueryRow(ctx, "SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists)
|
|
||||||
if err == nil && exists == 1 {
|
|
||||||
_, err = pool.Exec(ctx, "DROP DATABASE "+dbname+" WITH (FORCE)")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to drop existing database: %w", err)
|
|
||||||
}
|
|
||||||
t.Logf("Dropped existing database %s", dbname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new database
|
|
||||||
_, err = pool.Exec(ctx, "CREATE DATABASE "+dbname)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Created database %s", dbname)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startPostgreSQLContainer starts a PostgreSQL container and returns it
|
|
||||||
func startPostgreSQLContainer(t *testing.T, ctx context.Context) (testcontainers.Container, error) {
|
|
||||||
req := testcontainers.ContainerRequest{
|
|
||||||
Image: "postgres:15-alpine",
|
|
||||||
ExposedPorts: []string{"5432/tcp"},
|
|
||||||
Env: map[string]string{
|
|
||||||
"POSTGRES_PASSWORD": "postgres",
|
|
||||||
"POSTGRES_DB": "postgres",
|
|
||||||
},
|
|
||||||
WaitingFor: wait.ForLog("database system is ready to accept connections"),
|
|
||||||
}
|
|
||||||
|
|
||||||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
||||||
ContainerRequest: req,
|
|
||||||
Started: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return container, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMusicRefDump tests that the application can start with music_ref_dump.sql and migrations work
|
|
||||||
func TestMusicRefDump(t *testing.T) {
|
|
||||||
testSQLDump(t, "music_ref_dump.sql", "music_server_ref_test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMusicTestDump tests that the application can start with music_test_dump.sql and migrations work
|
|
||||||
func TestMusicTestDump(t *testing.T) {
|
|
||||||
testSQLDump(t, "music_test_dump.sql", "music_server_test_dump_test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMusicProdDump tests that the application can start with music_prod_dump.sql and migrations work
|
|
||||||
func TestMusicProdDump(t *testing.T) {
|
|
||||||
testSQLDump(t, "music_prod_dump.sql", "music_server_prod_test")
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSQLDump is the common test function for all SQL dump tests
|
|
||||||
func testSQLDump(t *testing.T, dumpFile, dbname string) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Save original environment variables
|
|
||||||
originalEnv := saveEnvVars()
|
|
||||||
defer restoreEnvVars(originalEnv)
|
|
||||||
|
|
||||||
// Start PostgreSQL container
|
|
||||||
t.Log("Starting PostgreSQL container...")
|
|
||||||
container, err := startPostgreSQLContainer(t, ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
t.Log("Stopping container...")
|
|
||||||
container.Terminate(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Get container connection details
|
|
||||||
endpoint, err := container.Endpoint(ctx, "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Container running at: %s", endpoint)
|
|
||||||
|
|
||||||
// Create database for this test
|
|
||||||
t.Logf("Creating database %s...", dbname)
|
|
||||||
err = createDatabaseInContainer(t, ctx, container, dbname)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create common roles that might be referenced in dump files
|
|
||||||
err = createRoleInContainer(t, ctx, container, "sansan", "postgres")
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Note: Could not create role 'sansan': %v (may not be needed)", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the SQL dump
|
|
||||||
// The dump files are in the project root
|
|
||||||
// Get the absolute path to the dump file
|
|
||||||
absDumpPath, err := filepath.Abs(filepath.Join("..", "..", dumpFile))
|
|
||||||
require.NoError(t, err)
|
|
||||||
if _, err := os.Stat(absDumpPath); os.IsNotExist(err) {
|
|
||||||
// Try relative to current working directory
|
|
||||||
absDumpPath = filepath.Join(".", dumpFile)
|
|
||||||
if _, err := os.Stat(absDumpPath); os.IsNotExist(err) {
|
|
||||||
absDumpPath = dumpFile // Try as-is
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Loading SQL dump from: %s", absDumpPath)
|
|
||||||
err = loadSQLDumpIntoDatabase(t, ctx, container, dbname, absDumpPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PRE-MIGRATION CHECK
|
|
||||||
// ============================================
|
|
||||||
t.Log("Running pre-migration checks...")
|
|
||||||
|
|
||||||
// For connecting to the database from the test, we need to use the container's endpoint
|
|
||||||
// But the container's endpoint might be localhost:random_port
|
|
||||||
// We need to parse it to get host and port
|
|
||||||
// endpoint format: "host:port" e.g., "localhost:12345" or "127.0.0.1:23456"
|
|
||||||
// Actually, for testcontainers with Docker, the endpoint should be localhost:mapped_port
|
|
||||||
// Let's extract the mapped port
|
|
||||||
|
|
||||||
// Get the mapped port from the container
|
|
||||||
mappedPort, err := container.MappedPort(ctx, "5432")
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Container mapped port: %s", mappedPort)
|
|
||||||
|
|
||||||
// Use localhost as the host since we're on the same machine
|
|
||||||
dbHost := "localhost"
|
|
||||||
dbPort := mappedPort.Port()
|
|
||||||
|
|
||||||
// Create a connection to the database to check pre-migration state
|
|
||||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
dbHost, dbPort, "postgres", "postgres", dbname)
|
|
||||||
|
|
||||||
pool, err := pgxpool.New(ctx, connStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
// Check that game table exists (pre-migration state)
|
|
||||||
gameExists, err := checkTableExists(ctx, pool, "game")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, gameExists, "Before migration: game table should exist")
|
|
||||||
|
|
||||||
// Check that soundtrack table does NOT exist
|
|
||||||
soundtrackExists, err := checkTableExists(ctx, pool, "soundtrack")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, soundtrackExists, "Before migration: soundtrack table should NOT exist")
|
|
||||||
|
|
||||||
t.Log("✓ Pre-migration check passed: game table exists, soundtrack table does not")
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// START APPLICATION (triggers migrations)
|
|
||||||
// ============================================
|
|
||||||
t.Log("Starting application...")
|
|
||||||
|
|
||||||
// Set environment variables for the server
|
|
||||||
// The application needs to connect to the container's mapped port
|
|
||||||
os.Setenv("DB_HOST", dbHost)
|
|
||||||
os.Setenv("DB_PORT", dbPort)
|
|
||||||
os.Setenv("DB_USERNAME", "postgres")
|
|
||||||
os.Setenv("DB_PASSWORD", "postgres")
|
|
||||||
os.Setenv("DB_NAME", dbname)
|
|
||||||
os.Setenv("MUSIC_PATH", "./testMusic")
|
|
||||||
os.Setenv("CHARACTERS_PATH", "./testCharacters")
|
|
||||||
|
|
||||||
// Use different ports for each test to allow for sequential execution
|
|
||||||
// and to make port detection reliable
|
|
||||||
portMap := map[string]string{
|
|
||||||
"music_server_ref_test": "8082",
|
|
||||||
"music_server_test_dump_test": "8083",
|
|
||||||
"music_server_prod_test": "8084",
|
|
||||||
}
|
|
||||||
appPort := portMap[dbname]
|
|
||||||
if appPort == "" {
|
|
||||||
appPort = "8085" // fallback
|
|
||||||
}
|
|
||||||
os.Setenv("PORT", appPort)
|
|
||||||
|
|
||||||
os.Setenv("DISABLE_SESSION_AUTH", "true")
|
|
||||||
os.Setenv("LOG_LEVEL", "error")
|
|
||||||
|
|
||||||
// Create server instance (this runs migrations automatically)
|
|
||||||
serverInstance := NewServerInstance()
|
|
||||||
httpServer := serverInstance.HTTPServer()
|
|
||||||
|
|
||||||
// Start HTTP server in background
|
|
||||||
go func() {
|
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
t.Logf("Server error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
t.Log("Stopping HTTP server...")
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
httpServer.Shutdown(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for server to start by polling /health endpoint
|
|
||||||
// Use the fixed port we configured for the application
|
|
||||||
healthURL := fmt.Sprintf("http://localhost:%s/health", appPort)
|
|
||||||
|
|
||||||
maxAttempts := 30
|
|
||||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
||||||
resp, err := http.Get(healthURL)
|
|
||||||
if err == nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
var healthData map[string]string
|
|
||||||
json.NewDecoder(resp.Body).Decode(&healthData)
|
|
||||||
if healthData["status"] == "up" {
|
|
||||||
t.Logf("Server is healthy on %s after %d attempts", healthURL, attempt)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if attempt == maxAttempts {
|
|
||||||
require.Fail(t, "Server did not become healthy within timeout")
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// POST-MIGRATION CHECK
|
|
||||||
// ============================================
|
|
||||||
t.Log("Running post-migration checks...")
|
|
||||||
|
|
||||||
// Check that game table no longer exists
|
|
||||||
gameExistsAfter, err := checkTableExists(ctx, pool, "game")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, gameExistsAfter, "After migration: game table should NOT exist")
|
|
||||||
|
|
||||||
// Check that soundtrack table now exists
|
|
||||||
soundtrackExistsAfter, err := checkTableExists(ctx, pool, "soundtrack")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, soundtrackExistsAfter, "After migration: soundtrack table should exist")
|
|
||||||
|
|
||||||
// Verify data was preserved
|
|
||||||
var soundtrackCount int
|
|
||||||
err = pool.QueryRow(ctx, "SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Greater(t, soundtrackCount, 0, "Migration should preserve data: soundtrack count should be > 0")
|
|
||||||
t.Logf("✓ Post-migration check passed: game table gone, soundtrack table exists with %d records", soundtrackCount)
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// HEALTH CHECK
|
|
||||||
// ============================================
|
|
||||||
t.Log("Running health check...")
|
|
||||||
|
|
||||||
resp, err := http.Get(healthURL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
||||||
|
|
||||||
var healthData map[string]string
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&healthData)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "up", healthData["status"])
|
|
||||||
|
|
||||||
t.Log("✓ Health check passed")
|
|
||||||
t.Log("✓ Test completed successfully!")
|
|
||||||
}
|
|
||||||
@@ -23,20 +23,20 @@ func NewStatisticsHandler() *StatisticsHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostPlayedSoundtracks returns top N most played soundtracks with songs
|
// GetMostPlayedGames returns top N most played games with songs
|
||||||
// GET /api/v1/statistics/soundtracks/most-played
|
// GET /api/v1/statistics/games/most-played
|
||||||
//
|
//
|
||||||
// @Summary Get most played soundtracks
|
// @Summary Get most played games
|
||||||
// @Description Returns the top N most played soundtracks with their songs
|
// @Description Returns the top N most played games with their songs
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query int false "Number of results (default: 10)"
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
// @Failure 400 {object} map[string]string
|
// @Failure 400 {object} map[string]string
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/statistics/soundtracks/most-played [get]
|
// @Router /api/v1/statistics/games/most-played [get]
|
||||||
func (h *StatisticsHandler) GetMostPlayedSoundtracks(ctx *echo.Context) error {
|
func (h *StatisticsHandler) GetMostPlayedGames(ctx *echo.Context) error {
|
||||||
limit := 10 // default
|
limit := 10 // default
|
||||||
limitStr := ctx.QueryParam("limit")
|
limitStr := ctx.QueryParam("limit")
|
||||||
if limitStr != "" {
|
if limitStr != "" {
|
||||||
@@ -51,28 +51,28 @@ func (h *StatisticsHandler) GetMostPlayedSoundtracks(ctx *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
soundtracks, err := h.statsBackend.GetMostPlayedSoundtracksWithSongs(int32(limit))
|
games, err := h.statsBackend.GetMostPlayedGamesWithSongs(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get most played soundtracks", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get most played games", zap.String("error", err.Error()))
|
||||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
}
|
}
|
||||||
return ctx.JSON(http.StatusOK, soundtracks)
|
return ctx.JSON(http.StatusOK, games)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLeastPlayedSoundtracks returns top N least played soundtracks with songs
|
// GetLeastPlayedGames returns top N least played games with songs
|
||||||
// GET /api/v1/statistics/soundtracks/least-played
|
// GET /api/v1/statistics/games/least-played
|
||||||
//
|
//
|
||||||
// @Summary Get least played soundtracks
|
// @Summary Get least played games
|
||||||
// @Description Returns the top N least played soundtracks with their songs
|
// @Description Returns the top N least played games with their songs
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query int false "Number of results (default: 10)"
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
// @Failure 400 {object} map[string]string
|
// @Failure 400 {object} map[string]string
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/statistics/soundtracks/least-played [get]
|
// @Router /api/v1/statistics/games/least-played [get]
|
||||||
func (h *StatisticsHandler) GetLeastPlayedSoundtracks(ctx *echo.Context) error {
|
func (h *StatisticsHandler) GetLeastPlayedGames(ctx *echo.Context) error {
|
||||||
limit := 10
|
limit := 10
|
||||||
limitStr := ctx.QueryParam("limit")
|
limitStr := ctx.QueryParam("limit")
|
||||||
if limitStr != "" {
|
if limitStr != "" {
|
||||||
@@ -86,19 +86,19 @@ func (h *StatisticsHandler) GetLeastPlayedSoundtracks(ctx *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
soundtracks, err := h.statsBackend.GetLeastPlayedSoundtracksWithSongs(int32(limit))
|
games, err := h.statsBackend.GetLeastPlayedGamesWithSongs(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get least played soundtracks", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get least played games", zap.String("error", err.Error()))
|
||||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
}
|
}
|
||||||
return ctx.JSON(http.StatusOK, soundtracks)
|
return ctx.JSON(http.StatusOK, games)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostPlayedSongs returns top N most played songs with soundtrack info
|
// GetMostPlayedSongs returns top N most played songs with game info
|
||||||
// GET /api/v1/statistics/songs/most-played
|
// GET /api/v1/statistics/songs/most-played
|
||||||
//
|
//
|
||||||
// @Summary Get most played songs
|
// @Summary Get most played songs
|
||||||
// @Description Returns the top N most played songs with their soundtrack info
|
// @Description Returns the top N most played songs with their game info
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -121,7 +121,7 @@ func (h *StatisticsHandler) GetMostPlayedSongs(ctx *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
songs, err := h.statsBackend.GetMostPlayedSongsWithSoundtrack(int32(limit))
|
songs, err := h.statsBackend.GetMostPlayedSongsWithGame(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get most played songs", zap.String("error", err.Error()))
|
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"})
|
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)
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLeastPlayedSongs returns top N least played songs with soundtrack info
|
// GetLeastPlayedSongs returns top N least played songs with game info
|
||||||
// GET /api/v1/statistics/songs/least-played
|
// GET /api/v1/statistics/songs/least-played
|
||||||
//
|
//
|
||||||
// @Summary Get least played songs
|
// @Summary Get least played songs
|
||||||
// @Description Returns the top N least played songs with their soundtrack info
|
// @Description Returns the top N least played songs with their game info
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -156,7 +156,7 @@ func (h *StatisticsHandler) GetLeastPlayedSongs(ctx *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
songs, err := h.statsBackend.GetLeastPlayedSongsWithSoundtrack(int32(limit))
|
songs, err := h.statsBackend.GetLeastPlayedSongsWithGame(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get least played songs", zap.String("error", err.Error()))
|
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"})
|
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)
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNeverPlayedSoundtracks returns soundtracks that have never been played
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
// GET /api/v1/statistics/soundtracks/never-played
|
// GET /api/v1/statistics/games/never-played
|
||||||
//
|
//
|
||||||
// @Summary Get never played soundtracks
|
// @Summary Get never played games
|
||||||
// @Description Returns all soundtracks that have never been played (times_played = 0)
|
// @Description Returns all games that have never been played (times_played = 0)
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/statistics/soundtracks/never-played [get]
|
// @Router /api/v1/statistics/games/never-played [get]
|
||||||
func (h *StatisticsHandler) GetNeverPlayedSoundtracks(ctx *echo.Context) error {
|
func (h *StatisticsHandler) GetNeverPlayedGames(ctx *echo.Context) error {
|
||||||
soundtracks, err := h.statsBackend.GetNeverPlayedSoundtracks()
|
games, err := h.statsBackend.GetNeverPlayedGames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get never played soundtracks", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get never played games", zap.String("error", err.Error()))
|
||||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
}
|
}
|
||||||
return ctx.JSON(http.StatusOK, soundtracks)
|
return ctx.JSON(http.StatusOK, games)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLastPlayedSoundtracks returns most recently played soundtracks
|
// GetLastPlayedGames returns most recently played games
|
||||||
// GET /api/v1/statistics/soundtracks/last-played
|
// GET /api/v1/statistics/games/last-played
|
||||||
//
|
//
|
||||||
// @Summary Get last played soundtracks
|
// @Summary Get last played games
|
||||||
// @Description Returns the most recently played soundtracks
|
// @Description Returns the most recently played games
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query int false "Number of results (default: 10)"
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
// @Failure 400 {object} map[string]string
|
// @Failure 400 {object} map[string]string
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/statistics/soundtracks/last-played [get]
|
// @Router /api/v1/statistics/games/last-played [get]
|
||||||
func (h *StatisticsHandler) GetLastPlayedSoundtracks(ctx *echo.Context) error {
|
func (h *StatisticsHandler) GetLastPlayedGames(ctx *echo.Context) error {
|
||||||
limit := 10
|
limit := 10
|
||||||
limitStr := ctx.QueryParam("limit")
|
limitStr := ctx.QueryParam("limit")
|
||||||
if limitStr != "" {
|
if limitStr != "" {
|
||||||
@@ -211,28 +211,28 @@ func (h *StatisticsHandler) GetLastPlayedSoundtracks(ctx *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
soundtracks, err := h.statsBackend.GetLastPlayedSoundtracks(int32(limit))
|
games, err := h.statsBackend.GetLastPlayedGames(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get last played soundtracks", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get last played games", zap.String("error", err.Error()))
|
||||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
}
|
}
|
||||||
return ctx.JSON(http.StatusOK, soundtracks)
|
return ctx.JSON(http.StatusOK, games)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOldestPlayedSoundtracks returns least recently played soundtracks
|
// GetOldestPlayedGames returns least recently played games
|
||||||
// GET /api/v1/statistics/soundtracks/oldest-played
|
// GET /api/v1/statistics/games/oldest-played
|
||||||
//
|
//
|
||||||
// @Summary Get oldest played soundtracks
|
// @Summary Get oldest played games
|
||||||
// @Description Returns the least recently played soundtracks (that have been played at least once)
|
// @Description Returns the least recently played games (that have been played at least once)
|
||||||
// @Tags statistics
|
// @Tags statistics
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query int false "Number of results (default: 10)"
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
// @Success 200 {array} backend.SoundtrackWithSongs
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
// @Failure 400 {object} map[string]string
|
// @Failure 400 {object} map[string]string
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/statistics/soundtracks/oldest-played [get]
|
// @Router /api/v1/statistics/games/oldest-played [get]
|
||||||
func (h *StatisticsHandler) GetOldestPlayedSoundtracks(ctx *echo.Context) error {
|
func (h *StatisticsHandler) GetOldestPlayedGames(ctx *echo.Context) error {
|
||||||
limit := 10
|
limit := 10
|
||||||
limitStr := ctx.QueryParam("limit")
|
limitStr := ctx.QueryParam("limit")
|
||||||
if limitStr != "" {
|
if limitStr != "" {
|
||||||
@@ -246,12 +246,12 @@ func (h *StatisticsHandler) GetOldestPlayedSoundtracks(ctx *echo.Context) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
soundtracks, err := h.statsBackend.GetOldestPlayedSoundtracks(int32(limit))
|
games, err := h.statsBackend.GetOldestPlayedGames(int32(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get oldest played soundtracks", zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get oldest played games", zap.String("error", err.Error()))
|
||||||
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
}
|
}
|
||||||
return ctx.JSON(http.StatusOK, soundtracks)
|
return ctx.JSON(http.StatusOK, games)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatisticsSummary returns overall statistics
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"music-server/internal/backend"
|
|
||||||
"music-server/internal/db"
|
|
||||||
"music-server/internal/db/repository"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/labstack/echo/v5"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestStatisticsEndpoints tests the statistics API endpoints
|
|
||||||
func TestStatisticsEndpoints(t *testing.T) {
|
|
||||||
// Skip if test database not configured
|
|
||||||
e := StartTestServer(t)
|
|
||||||
if e == nil {
|
|
||||||
t.Skip("Test database not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get token first
|
|
||||||
token := getTestToken(t, e)
|
|
||||||
if token == "" {
|
|
||||||
t.Skip("Could not get test token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test /api/v1/statistics/summary
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var summary backend.StatisticsSummary
|
|
||||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPartialMigrationThenSyncThenComplete tests migration workflow
|
|
||||||
// Note: This test requires the database to be in a specific state
|
|
||||||
// It tests: partial migration → data insert → sync → complete migration
|
|
||||||
func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
|
||||||
// This test is complex and requires careful setup
|
|
||||||
// For now, we test the final state: all migrations + sync
|
|
||||||
|
|
||||||
e := StartTestServer(t)
|
|
||||||
if e == nil {
|
|
||||||
t.Skip("Test database not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get token
|
|
||||||
token := getTestToken(t, e)
|
|
||||||
if token == "" {
|
|
||||||
t.Skip("Could not get test token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert test data manually (5 soundtracks with songs)
|
|
||||||
insertTestData(t)
|
|
||||||
|
|
||||||
// Run sync to ensure data is properly loaded
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/sync/new", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
// Wait for sync to complete
|
|
||||||
if !waitForSyncCompletion(t, e, 60) {
|
|
||||||
t.Error("Sync did not complete within timeout")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify data via statistics endpoint
|
|
||||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
rec = httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var summary backend.StatisticsSummary
|
|
||||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// After sync with /sync/new, only soundtracks matching filesystem remain
|
|
||||||
// testMusic has 3 soundtracks
|
|
||||||
require.Equal(t, int64(3), summary.TotalSoundtracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertTestData inserts 5 test soundtracks with songs into the database
|
|
||||||
func insertTestData(t *testing.T) {
|
|
||||||
if db.TestDatabase == nil || db.TestDatabase.Pool == nil {
|
|
||||||
t.Skip("Test database not initialized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
queries := repository.New(db.TestDatabase.Pool)
|
|
||||||
|
|
||||||
// Insert 5 soundtracks
|
|
||||||
soundtracks := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
}{
|
|
||||||
{"Test Soundtrack 1", "/path/to/soundtrack1"},
|
|
||||||
{"Test Soundtrack 2", "/path/to/soundtrack2"},
|
|
||||||
{"Test Soundtrack 3", "/path/to/soundtrack3"},
|
|
||||||
{"Test Soundtrack 4", "/path/to/soundtrack4"},
|
|
||||||
{"Test Soundtrack 5", "/path/to/soundtrack5"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, st := range soundtracks {
|
|
||||||
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
|
||||||
SoundtrackName: st.name,
|
|
||||||
Path: st.path,
|
|
||||||
Hash: "test-hash-" + st.name,
|
|
||||||
Uuid: uuid.New(),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Failed to insert soundtrack: %s", st.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get soundtrack IDs and UUIDs
|
|
||||||
soundtrackIDs, err := queries.FindAllSoundtracks(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.GreaterOrEqual(t, len(soundtrackIDs), 5)
|
|
||||||
|
|
||||||
// Insert songs for each soundtrack
|
|
||||||
songData := []struct {
|
|
||||||
soundtrackID int32
|
|
||||||
soundtrackUuid uuid.UUID
|
|
||||||
songs []string
|
|
||||||
}{
|
|
||||||
{soundtrackIDs[0].ID, soundtrackIDs[0].Uuid, []string{"Song A", "Song B"}},
|
|
||||||
{soundtrackIDs[1].ID, soundtrackIDs[1].Uuid, []string{"Song C", "Song D"}},
|
|
||||||
{soundtrackIDs[2].ID, soundtrackIDs[2].Uuid, []string{"Song E"}},
|
|
||||||
{soundtrackIDs[3].ID, soundtrackIDs[3].Uuid, []string{"Song F", "Song G", "Song H"}},
|
|
||||||
{soundtrackIDs[4].ID, soundtrackIDs[4].Uuid, []string{"Song I"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sd := range songData {
|
|
||||||
for _, songName := range sd.songs {
|
|
||||||
err := queries.AddSong(ctx, repository.AddSongParams{
|
|
||||||
SoundtrackID: sd.soundtrackID,
|
|
||||||
SongName: songName,
|
|
||||||
Path: "/path/to/" + songName + ".mp3",
|
|
||||||
Uuid: uuid.New(),
|
|
||||||
SoundtrackUuid: sd.soundtrackUuid,
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Failed to insert song: %s", songName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Inserted %d soundtracks with %d total songs", len(soundtracks), 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTestToken gets a valid token for testing
|
|
||||||
func getTestToken(t *testing.T, e *echo.Echo) string {
|
|
||||||
reqBody := `{"client_type": "test"}`
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/token", strings.NewReader(reqBody))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Logf("Failed to get token: %s", rec.Body.String())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return resp.Token
|
|
||||||
}
|
|
||||||
@@ -34,49 +34,59 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
|
|||||||
return ctx.JSON(http.StatusOK, response)
|
return ctx.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncSoundtracksNewOnlyChanges godoc
|
// SyncGamesNewOnlyChanges godoc
|
||||||
// @Summary Sync soundtracks with only changes
|
// @Summary Sync games with only changes
|
||||||
// @Description Starts syncing soundtracks with only new changes
|
// @Description Starts syncing games with only new changes
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Start syncing soundtracks"
|
// @Success 200 {string} string "Start syncing games"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync [get]
|
// @Router /sync [get]
|
||||||
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncGamesNewOnlyChanges(ctx *echo.Context) error {
|
||||||
|
if backend.Syncing {
|
||||||
|
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")
|
logging.GetLogger().Info("Starting sync with only changes")
|
||||||
backend.Syncing = true
|
go backend.SyncGamesNewOnlyChanges()
|
||||||
go backend.SyncSoundtracksOnlyChanges()
|
return ctx.JSON(http.StatusOK, "Start syncing games")
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncSoundtracksNewFull godoc
|
// SyncGamesNewFull godoc
|
||||||
// @Summary Sync all soundtracks fully
|
// @Summary Sync all games fully
|
||||||
// @Description Starts a full sync of all soundtracks
|
// @Description Starts a full sync of all games
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Start syncing soundtracks full"
|
// @Success 200 {string} string "Start syncing games full"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/full [get]
|
// @Router /sync/full [get]
|
||||||
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncGamesNewFull(ctx *echo.Context) error {
|
||||||
|
if backend.Syncing {
|
||||||
|
logging.GetLogger().Warn("Syncing is already in progress")
|
||||||
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
|
}
|
||||||
logging.GetLogger().Info("Starting full sync")
|
logging.GetLogger().Info("Starting full sync")
|
||||||
backend.Syncing = true
|
go backend.SyncGamesNewFull()
|
||||||
go backend.SyncSoundtracksFull()
|
return ctx.JSON(http.StatusOK, "Start syncing games full")
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetDB godoc
|
// ResetGames godoc
|
||||||
// @Summary Reset soundtracks database
|
// @Summary Reset games database
|
||||||
// @Description Resets the soundtracks database by deleting all soundtracks and songs
|
// @Description Resets the games database by deleting all games and songs
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Soundtracks and songs are deleted from the database"
|
// @Success 200 {string} string "Games and songs are deleted from the database"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/reset [get]
|
// @Router /sync/reset [get]
|
||||||
func (s *SyncHandler) ResetDB(ctx *echo.Context) error {
|
func (s *SyncHandler) ResetGames(ctx *echo.Context) error {
|
||||||
logging.GetLogger().Info("Resetting soundtracks database")
|
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 games database")
|
||||||
backend.ResetDB()
|
backend.ResetDB()
|
||||||
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database")
|
return ctx.JSON(http.StatusOK, "Games and songs are deleted from the database")
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func waitForSyncCompletion(t *testing.T, e *echo.Echo, maxAttempts int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSyncPopulatesDatabase verifies that sync populates the database with soundtracks
|
// TestSyncPopulatesDatabase verifies that sync populates the database with games
|
||||||
func TestSyncPopulatesDatabase(t *testing.T) {
|
func TestSyncPopulatesDatabase(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
@@ -74,12 +74,12 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
|||||||
// Clear any existing data first
|
// Clear any existing data first
|
||||||
db.TestClearDatabase(t)
|
db.TestClearDatabase(t)
|
||||||
|
|
||||||
// Before sync - should have no soundtracks
|
// Before sync - should have no games
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracksBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
beforeCount := len(soundtracksBefore)
|
beforeCount := len(gamesBefore)
|
||||||
t.Logf("Soundtracks before sync: %d", beforeCount)
|
t.Logf("Games before sync: %d", beforeCount)
|
||||||
assert.Equal(t, 0, beforeCount, "Database should be empty after clear")
|
assert.Equal(t, 0, beforeCount, "Database should be empty after clear")
|
||||||
|
|
||||||
// Run sync
|
// Run sync
|
||||||
@@ -91,14 +91,14 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
|||||||
t.Error("Sync did not complete within timeout")
|
t.Error("Sync did not complete within timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
// After sync - should have soundtracks
|
// After sync - should have games
|
||||||
soundtracksAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
afterCount := len(soundtracksAfter)
|
afterCount := len(gamesAfter)
|
||||||
t.Logf("Soundtracks after sync: %d", afterCount)
|
t.Logf("Games after sync: %d", afterCount)
|
||||||
|
|
||||||
// Should have more soundtracks than before (unless database was already populated)
|
// Should have more games than before (unless database was already populated)
|
||||||
assert.True(t, afterCount > 0, "Database should have soundtracks after sync")
|
assert.True(t, afterCount > 0, "Database should have games after sync")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSyncMakesDifference verifies that sync actually changes the database state
|
// TestSyncMakesDifference verifies that sync actually changes the database state
|
||||||
@@ -111,11 +111,11 @@ func TestSyncMakesDifference(t *testing.T) {
|
|||||||
// Clear any existing data first
|
// Clear any existing data first
|
||||||
db.TestClearDatabase(t)
|
db.TestClearDatabase(t)
|
||||||
|
|
||||||
// Before sync - should have no soundtracks
|
// Before sync - should have no games
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracksBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, len(soundtracksBefore), "Should have no soundtracks before sync")
|
assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync")
|
||||||
|
|
||||||
// Run sync
|
// Run sync
|
||||||
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
||||||
@@ -126,10 +126,10 @@ func TestSyncMakesDifference(t *testing.T) {
|
|||||||
t.Error("Sync did not complete within timeout")
|
t.Error("Sync did not complete within timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
// After sync - should have soundtracks
|
// After sync - should have games
|
||||||
soundtracksAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, len(soundtracksAfter) > 0, "Should have soundtracks after sync")
|
assert.True(t, len(gamesAfter) > 0, "Should have games after sync")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSyncProgress verifies the sync progress endpoint
|
// TestSyncProgress verifies the sync progress endpoint
|
||||||
@@ -183,8 +183,8 @@ func TestSyncProgress(t *testing.T) {
|
|||||||
assert.True(t, foundComplete, "Should have seen completion")
|
assert.True(t, foundComplete, "Should have seen completion")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSyncSoundtracksNewOnlyChanges verifies the incremental sync endpoint
|
// TestSyncGamesNewOnlyChanges verifies the incremental sync endpoint
|
||||||
func TestSyncSoundtracksNewOnlyChanges(t *testing.T) {
|
func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
@@ -200,8 +200,8 @@ func TestSyncSoundtracksNewOnlyChanges(t *testing.T) {
|
|||||||
|
|
||||||
// Get initial count
|
// Get initial count
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracksBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
|
||||||
beforeCount := len(soundtracksBefore)
|
beforeCount := len(gamesBefore)
|
||||||
|
|
||||||
// Run incremental sync (should not change count if nothing changed)
|
// Run incremental sync (should not change count if nothing changed)
|
||||||
resp := MakeTestRequest(t, e, "GET", "/sync/new")
|
resp := MakeTestRequest(t, e, "GET", "/sync/new")
|
||||||
@@ -211,16 +211,16 @@ func TestSyncSoundtracksNewOnlyChanges(t *testing.T) {
|
|||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Count should be the same
|
// Count should be the same
|
||||||
soundtracksAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
|
||||||
afterCount := len(soundtracksAfter)
|
afterCount := len(gamesAfter)
|
||||||
|
|
||||||
// Note: This might not be exactly equal due to timing, but should be close
|
// Note: This might not be exactly equal due to timing, but should be close
|
||||||
t.Logf("Soundtracks before incremental sync: %d, after: %d", beforeCount, afterCount)
|
t.Logf("Games before incremental sync: %d, after: %d", beforeCount, afterCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestResetSoundtracks verifies the reset endpoint clears the database
|
// TestResetGames verifies the reset endpoint clears the database
|
||||||
// RUN THIS LAST
|
// RUN THIS LAST
|
||||||
func TestResetSoundtracks(t *testing.T) {
|
func TestResetGames(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
@@ -228,8 +228,8 @@ func TestResetSoundtracks(t *testing.T) {
|
|||||||
|
|
||||||
// First ensure we have data
|
// First ensure we have data
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracksBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
|
||||||
beforeCount := len(soundtracksBefore)
|
beforeCount := len(gamesBefore)
|
||||||
|
|
||||||
if beforeCount == 0 {
|
if beforeCount == 0 {
|
||||||
// Run sync to populate
|
// Run sync to populate
|
||||||
@@ -238,12 +238,12 @@ func TestResetSoundtracks(t *testing.T) {
|
|||||||
t.Error("Sync did not complete within timeout")
|
t.Error("Sync did not complete within timeout")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
soundtracksBefore, _ = repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesBefore, _ = repo.FindAllGames(backend.BackendCtx())
|
||||||
beforeCount = len(soundtracksBefore)
|
beforeCount = len(gamesBefore)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Soundtracks before reset: %d", beforeCount)
|
t.Logf("Games before reset: %d", beforeCount)
|
||||||
assert.True(t, beforeCount > 0, "Should have soundtracks to reset")
|
assert.True(t, beforeCount > 0, "Should have games to reset")
|
||||||
|
|
||||||
// Call reset
|
// Call reset
|
||||||
resp := MakeTestRequest(t, e, "GET", "/sync/reset")
|
resp := MakeTestRequest(t, e, "GET", "/sync/reset")
|
||||||
@@ -253,16 +253,16 @@ func TestResetSoundtracks(t *testing.T) {
|
|||||||
// Note: reset might take a moment to propagate
|
// Note: reset might take a moment to propagate
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
soundtracksAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
|
||||||
afterCount := len(soundtracksAfter)
|
afterCount := len(gamesAfter)
|
||||||
|
|
||||||
t.Logf("Soundtracks after reset: %d", afterCount)
|
t.Logf("Games after reset: %d", afterCount)
|
||||||
assert.Equal(t, 0, afterCount, "Database should be empty after reset")
|
assert.Equal(t, 0, afterCount, "Database should be empty after reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSyncSoundtracksNewFull verifies the full sync endpoint
|
// TestSyncGamesNewFull verifies the full sync endpoint
|
||||||
// RUN THIS LAST (before TestResetSoundtracks)
|
// RUN THIS LAST (before TestResetGames)
|
||||||
func TestSyncSoundtracksNewFull(t *testing.T) {
|
func TestSyncGamesNewFull(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
@@ -282,8 +282,8 @@ func TestSyncSoundtracksNewFull(t *testing.T) {
|
|||||||
|
|
||||||
// Verify database is populated
|
// Verify database is populated
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracks, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
games, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, len(soundtracks) > 0, "Database should be populated after full sync")
|
assert.True(t, len(games) > 0, "Database should be populated after full sync")
|
||||||
t.Logf("Full sync populated %d soundtracks", len(soundtracks))
|
t.Logf("Full sync populated %d games", len(games))
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,6 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
|||||||
s := &Server{
|
s := &Server{
|
||||||
db: db.TestDatabase,
|
db: db.TestDatabase,
|
||||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||||
statisticsHandler: NewStatisticsHandler(),
|
|
||||||
}
|
}
|
||||||
handler := s.RegisterRoutes()
|
handler := s.RegisterRoutes()
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,20 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"music-server/internal/db/repository"
|
"music-server/internal/db/repository"
|
||||||
"music-server/internal/logging"
|
"music-server/internal/logging"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenRequest represents a request to generate a new token
|
// TokenRequest represents a request to generate a new token
|
||||||
type TokenRequest struct {
|
type TokenRequest struct {
|
||||||
Password string `json:"password"` // Required: API password
|
|
||||||
ClientType string `json:"client_type"` // Optional: "web", "mobile", "api"
|
ClientType string `json:"client_type"` // Optional: "web", "mobile", "api"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,14 +53,13 @@ func (h *TokenHandler) generateToken() (string, error) {
|
|||||||
// POST /api/v1/token
|
// POST /api/v1/token
|
||||||
//
|
//
|
||||||
// @Summary Create session token
|
// @Summary Create session token
|
||||||
// @Description Returns a new session token for API access. Requires valid API password.
|
// @Description Returns a new session token for API access
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param request body TokenRequest true "Password and client type"
|
// @Param request body TokenRequest true "Client type"
|
||||||
// @Success 200 {object} TokenResponse
|
// @Success 200 {object} TokenResponse
|
||||||
// @Failure 400 {object} map[string]string
|
// @Failure 400 {object} map[string]string
|
||||||
// @Failure 401 {object} map[string]string
|
|
||||||
// @Failure 500 {object} map[string]string
|
// @Failure 500 {object} map[string]string
|
||||||
// @Router /api/v1/token [post]
|
// @Router /api/v1/token [post]
|
||||||
func (h *TokenHandler) CreateTokenHandler(c *echo.Context) error {
|
func (h *TokenHandler) CreateTokenHandler(c *echo.Context) error {
|
||||||
@@ -71,27 +68,6 @@ func (h *TokenHandler) CreateTokenHandler(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if session auth is disabled
|
|
||||||
disableSessionAuth := os.Getenv("DISABLE_SESSION_AUTH") == "true"
|
|
||||||
apiPassword := os.Getenv("API_PASSWORD")
|
|
||||||
|
|
||||||
// Password validation logic:
|
|
||||||
// - If session auth is disabled, skip validation
|
|
||||||
// - If API_PASSWORD is empty, allow any password (open access mode)
|
|
||||||
// - If API_PASSWORD is set, require exact password match
|
|
||||||
if !disableSessionAuth {
|
|
||||||
if apiPassword != "" {
|
|
||||||
// API password is set - require exact match
|
|
||||||
if req.Password != apiPassword {
|
|
||||||
logging.GetLogger().Warn("Invalid password attempt for token creation",
|
|
||||||
zap.String("ip", c.RealIP()),
|
|
||||||
zap.String("user_agent", c.Request().UserAgent()))
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid password"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If API_PASSWORD is empty, allow any password through (open access mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.ClientType == "" {
|
if req.ClientType == "" {
|
||||||
req.ClientType = "web"
|
req.ClientType = "web"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
|
||||||
"music-server/internal/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VersionHandler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVersionHandler() *VersionHandler {
|
|
||||||
return &VersionHandler{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVersionHistory godoc
|
|
||||||
//
|
|
||||||
// @Summary Getting the version history of the backend
|
|
||||||
// @Description get version history
|
|
||||||
// @Tags version
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {array} backend.VersionData
|
|
||||||
// @Failure 404 {object} string
|
|
||||||
// @Router /version/history [get]
|
|
||||||
func (v *VersionHandler) GetVersionHistory(ctx *echo.Context) error {
|
|
||||||
versionHistory := backend.GetVersionHistory()
|
|
||||||
if len(versionHistory) == 0 {
|
|
||||||
return ctx.JSON(http.StatusNotFound, "version not found")
|
|
||||||
}
|
|
||||||
return ctx.JSON(http.StatusOK, versionHistory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLatestVersion godoc
|
|
||||||
//
|
|
||||||
// @Summary Getting the latest version of the backend
|
|
||||||
// @Description get latest version info
|
|
||||||
// @Tags version
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} backend.VersionData
|
|
||||||
// @Failure 404 {object} string
|
|
||||||
// @Router /version [get]
|
|
||||||
func (v *VersionHandler) GetLatestVersion(ctx *echo.Context) error {
|
|
||||||
latestVersion := backend.GetLatestVersion()
|
|
||||||
if latestVersion.Version == "" {
|
|
||||||
return ctx.JSON(http.StatusNotFound, "version not found")
|
|
||||||
}
|
|
||||||
return ctx.JSON(http.StatusOK, latestVersion)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"music-server/internal/backend"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetLatestVersion verifies the version endpoint returns latest version
|
|
||||||
func TestGetLatestVersion(t *testing.T) {
|
|
||||||
e := StartTestServer(t)
|
|
||||||
|
|
||||||
resp := MakeTestRequest(t, e, "GET", "/version")
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
|
||||||
|
|
||||||
var versionData backend.VersionData
|
|
||||||
err := json.Unmarshal(resp.Body.Bytes(), &versionData)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, versionData.Version)
|
|
||||||
assert.NotEmpty(t, versionData.Changelog)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetVersionHistory verifies the version history endpoint returns version history
|
|
||||||
func TestGetVersionHistory(t *testing.T) {
|
|
||||||
e := StartTestServer(t)
|
|
||||||
|
|
||||||
resp := MakeTestRequest(t, e, "GET", "/version/history")
|
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
|
||||||
|
|
||||||
var versionHistory []backend.VersionData
|
|
||||||
err := json.Unmarshal(resp.Body.Bytes(), &versionHistory)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, versionHistory)
|
|
||||||
assert.NotEmpty(t, versionHistory[0].Version)
|
|
||||||
assert.NotEmpty(t, versionHistory[0].Changelog)
|
|
||||||
}
|
|
||||||
@@ -16,12 +16,12 @@ import (
|
|||||||
// ensureSyncRan ensures that sync has been run before testing music endpoints
|
// ensureSyncRan ensures that sync has been run before testing music endpoints
|
||||||
func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
soundtracks, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
games, err := repo.FindAllGames(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
if len(soundtracks) == 0 {
|
if len(games) == 0 {
|
||||||
// Run sync
|
// Run sync
|
||||||
t.Log("No soundtracks found, running sync first...")
|
t.Log("No games found, running sync first...")
|
||||||
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
resp := MakeTestRequest(t, e, "GET", "/sync/full")
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetAllSoundtracks verifies the /music/all/order endpoint
|
// TestGetAllGames verifies the /music/all/order endpoint
|
||||||
func TestZGetAllSoundtracks(t *testing.T) {
|
func TestZGetAllGames(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
@@ -45,15 +45,15 @@ func TestZGetAllSoundtracks(t *testing.T) {
|
|||||||
resp := MakeTestRequest(t, e, "GET", "/music/all/order")
|
resp := MakeTestRequest(t, e, "GET", "/music/all/order")
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
var soundtracks []string
|
var games []string
|
||||||
err := json.Unmarshal(resp.Body.Bytes(), &soundtracks)
|
err := json.Unmarshal(resp.Body.Bytes(), &games)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, soundtracks, "Should have soundtracks after sync")
|
assert.NotEmpty(t, games, "Should have games after sync")
|
||||||
t.Logf("Found %d soundtracks", len(soundtracks))
|
t.Logf("Found %d games", len(games))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetAllSoundtracksRandom verifies the /music/all/random endpoint
|
// TestGetAllGamesRandom verifies the /music/all/random endpoint
|
||||||
func TestZGetAllSoundtracksRandom(t *testing.T) {
|
func TestZGetAllGamesRandom(t *testing.T) {
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
defer db.TestTearDownDB(t)
|
defer db.TestTearDownDB(t)
|
||||||
|
|
||||||
@@ -65,17 +65,17 @@ func TestZGetAllSoundtracksRandom(t *testing.T) {
|
|||||||
resp := MakeTestRequest(t, e, "GET", "/music/all/random")
|
resp := MakeTestRequest(t, e, "GET", "/music/all/random")
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
var soundtracks []string
|
var games []string
|
||||||
err := json.Unmarshal(resp.Body.Bytes(), &soundtracks)
|
err := json.Unmarshal(resp.Body.Bytes(), &games)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, soundtracks, "Should have soundtracks after sync")
|
assert.NotEmpty(t, games, "Should have games after sync")
|
||||||
|
|
||||||
// Verify it's shuffled (not in original order)
|
// Verify it's shuffled (not in original order)
|
||||||
// We can't easily verify randomness, but we can check it's the same length
|
// We can't easily verify randomness, but we can check it's the same length
|
||||||
resp2 := MakeTestRequest(t, e, "GET", "/music/all/order")
|
resp2 := MakeTestRequest(t, e, "GET", "/music/all/order")
|
||||||
var soundtracksOrdered []string
|
var gamesOrdered []string
|
||||||
json.Unmarshal(resp2.Body.Bytes(), &soundtracksOrdered)
|
json.Unmarshal(resp2.Body.Bytes(), &gamesOrdered)
|
||||||
assert.Equal(t, len(soundtracks), len(soundtracksOrdered), "Random and ordered should have same count")
|
assert.Equal(t, len(games), len(gamesOrdered), "Random and ordered should have same count")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetRandomSong verifies the /music/rand endpoint
|
// TestGetRandomSong verifies the /music/rand endpoint
|
||||||
@@ -153,7 +153,7 @@ func TestZGetSongInfo(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// Note: CurrentlyPlaying might be false if no song is currently set
|
// Note: CurrentlyPlaying might be false if no song is currently set
|
||||||
// Just verify we got a valid response
|
// Just verify we got a valid response
|
||||||
t.Logf("Song info: Soundtrack=%s, Song=%s", info.Soundtrack, info.Song)
|
t.Logf("Song info: Game=%s, Song=%s", info.Game, info.Song)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetPlayedSongs verifies the /music/list endpoint
|
// TestGetPlayedSongs verifies the /music/list endpoint
|
||||||
|
|||||||
@@ -80,92 +80,61 @@ run:
|
|||||||
@templ generate
|
@templ generate
|
||||||
@go run cmd/main.go
|
@go run cmd/main.go
|
||||||
|
|
||||||
build-run: build
|
|
||||||
@go run cmd/main.go
|
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
@echo "Starting test database container..."
|
@echo "Testing..."
|
||||||
@docker compose -f compose.test.yaml up -d
|
@go test ./... -v
|
||||||
@sleep 10
|
|
||||||
@echo "Running integration tests..."
|
|
||||||
@just test-integration
|
|
||||||
@echo "Stopping test database container..."
|
|
||||||
@just test-integration-down
|
|
||||||
|
|
||||||
# Clean the binary
|
# Clean the binary
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning..."
|
@echo "Cleaning..."
|
||||||
@rm -f main
|
@rm -f main
|
||||||
|
|
||||||
docker-build:
|
podman-build:
|
||||||
@echo "Building Docker image with docker..."
|
@echo "Building Docker image with podman..."
|
||||||
@docker build -t music-server .
|
@podman build -t music-server .
|
||||||
|
|
||||||
docker-run:
|
podman-run:
|
||||||
@docker compose up --build
|
@podman-compose up --build
|
||||||
|
|
||||||
docker-down:
|
podman-down:
|
||||||
@docker compose down
|
@podman-compose down
|
||||||
|
|
||||||
# Run integration tests with docker
|
# Run integration tests with podman
|
||||||
# Starts a test PostgreSQL container, runs tests, then cleans up
|
# Starts a test PostgreSQL container, runs tests, then cleans up
|
||||||
test-integration:
|
test-integration:
|
||||||
@echo "Cleaning old test database..."
|
@echo "Starting test database container..."
|
||||||
@docker compose -f compose.test.yaml down -v
|
@podman-compose -f compose.test.yaml up -d
|
||||||
@echo "Starting fresh test database container..."
|
|
||||||
@docker compose -f compose.test.yaml up -d
|
|
||||||
@sleep 10
|
@sleep 10
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net DB_HOST=localhost DB_PORT=5433 DB_USERNAME=testuser DB_PASSWORD=testpass DB_NAME=music_server_test MUSIC_PATH=/Users/sebastian/projects/MusicServer/testMusic CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters PORT=8081 LOG_LEVEL=debug LOG_JSON=false go test -v -timeout 30m -p 1 -parallel 1 ./internal/...
|
@DB_HOST=localhost DB_PORT=5433 DB_USERNAME=testuser DB_PASSWORD=testpass DB_NAME=music_server_test MUSIC_PATH=/Users/sebastian/projects/MusicServer/testMusic CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters PORT=8081 LOG_LEVEL=debug LOG_JSON=false go test -v -timeout 30m -p 1 -parallel 1 ./internal/...
|
||||||
|
|
||||||
# Alternative: Run integration tests using testcontainers with docker provider
|
# Alternative: Run integration tests using testcontainers with podman provider
|
||||||
test-integration-tc:
|
test-integration-tc:
|
||||||
@echo "Creating test network (if it doesn't exist)..."
|
@echo "Running integration tests with testcontainers (podman provider)..."
|
||||||
@docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
|
@TESTCONTAINERS_PROVIDER=podman go test -v -timeout 30m .
|
||||||
@echo "Running integration tests with testcontainers (docker provider)..."
|
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 30m .
|
|
||||||
|
|
||||||
# Run SQL dump migration tests
|
|
||||||
# Tests that the application can start with each SQL dump and migrations work correctly
|
|
||||||
test-sql-dump:
|
|
||||||
@echo "Creating test network (if it doesn't exist)..."
|
|
||||||
@docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
|
|
||||||
@echo "Running SQL dump migration tests..."
|
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 15m -run "TestMusic.*Dump" ./internal/server/
|
|
||||||
|
|
||||||
# Run individual SQL dump tests
|
|
||||||
test-sql-dump-ref:
|
|
||||||
@echo "Creating test network (if it doesn't exist)..."
|
|
||||||
@docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
|
|
||||||
@echo "Running SQL dump migration test for ref dump..."
|
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicRefDump" ./internal/server/
|
|
||||||
|
|
||||||
test-sql-dump-test:
|
|
||||||
@echo "Creating test network (if it doesn't exist)..."
|
|
||||||
@docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
|
|
||||||
@echo "Running SQL dump migration test for test dump..."
|
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicTestDump" ./internal/server/
|
|
||||||
|
|
||||||
test-sql-dump-prod:
|
|
||||||
@echo "Creating test network (if it doesn't exist)..."
|
|
||||||
@docker network inspect testcontainers-test-net > /dev/null 2>&1 || docker network create testcontainers-test-net
|
|
||||||
@echo "Running SQL dump migration test for prod dump..."
|
|
||||||
@DOCKER_HOST=unix:///Users/sebastian/.docker/run/docker.sock TESTCONTAINERS_PROVIDER=docker TESTCONTAINERS_RYUK_CONTAINER_NETWORK=testcontainers-test-net go test -v -timeout 5m -run "TestMusicProdDump" ./internal/server/
|
|
||||||
|
|
||||||
# Stop and remove test database container
|
# Stop and remove test database container
|
||||||
test-integration-down:
|
test-integration-down:
|
||||||
@echo "Stopping test database container..."
|
@echo "Stopping test database container..."
|
||||||
@docker compose -f compose.test.yaml down -v
|
@podman-compose -f compose.test.yaml down -v
|
||||||
|
|
||||||
# Build and run with Docker Compose
|
# Create DB container
|
||||||
build-run-docker:
|
docker-run:
|
||||||
@echo "Building and running with Docker Compose..."
|
@if docker compose up --build 2>/dev/null; then \
|
||||||
@docker compose up --build
|
: ; \
|
||||||
|
else \
|
||||||
|
echo "Falling back to Docker Compose V1"; \
|
||||||
|
docker-compose up --build; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Shutdown containers
|
# Shutdown DB container
|
||||||
stop-docker:
|
docker-down:
|
||||||
@echo "Stopping containers..."
|
@if docker compose down 2>/dev/null; then \
|
||||||
@docker compose down
|
: ; \
|
||||||
|
else \
|
||||||
|
echo "Falling back to Docker Compose V1"; \
|
||||||
|
docker-compose down; \
|
||||||
|
fi
|
||||||
|
|
||||||
watch-templ:
|
watch-templ:
|
||||||
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
|
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
|
||||||
|
|||||||
@@ -39,12 +39,3 @@ sql:
|
|||||||
go_type:
|
go_type:
|
||||||
import: "time"
|
import: "time"
|
||||||
type: "Time"
|
type: "Time"
|
||||||
- db_type: "uuid"
|
|
||||||
go_type:
|
|
||||||
import: "github.com/google/uuid"
|
|
||||||
type: "UUID"
|
|
||||||
- db_type: "uuid"
|
|
||||||
nullable: true
|
|
||||||
go_type:
|
|
||||||
import: "github.com/google/uuid"
|
|
||||||
type: "UUID"
|
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Reference in new issue
Block a user