27 Commits

Author SHA1 Message Date
Sansan f0653489d6 Added some files 2026-05-21 22:25:31 +02:00
Sansan d0fbba86f1 Remove frontend-build from build command 2026-05-21 09:56:23 +02:00
Sansan bd0e7f4a8d Add frontend-build command to justfile 2026-05-21 09:54:12 +02:00
Sansan b5926e3b31 Fix frontend build by updating dependencies and ESLint configuration 2026-05-21 09:51:14 +02:00
Sansan 37909139de Add Zap logging framework with structured logging for Echo and Grafana 2026-05-20 22:29:45 +02:00
Sansan 82252ce1ff Use latest for templ CLI in justfile 2026-05-20 22:08:37 +02:00
Sansan 1dab9d6e7c Update all dependencies including templ CLI to latest versions 2026-05-20 22:05:39 +02:00
Sansan b80ad90eab Add echo-swagger/v2 for Echo v5 compatibility 2026-05-20 22:00:52 +02:00
Sansan 2cff8d16d7 Upgrade Echo framework from v4 to v5 2026-05-20 21:56:06 +02:00
Sansan 12f18ba12c Replace Tailwind CSS with pure CSS for frontend 2026-05-20 21:30:20 +02:00
Sansan 6e2c381d90 Update generate_godot_openapi.py to pass base_url as parameter to _init 2026-05-20 21:23:09 +02:00
Sansan efca22834b Update generate_godot_openapi.py to take file input and generate one file per API tag 2026-05-20 21:19:20 +02:00
Sansan e57609725e Add Swag annotations to all handler endpoints for OpenAPI documentation 2026-05-18 21:50:53 +02:00
Sansan fabd6a6931 Fix OpenAPI endpoint to serve swagger.json directly 2026-05-18 21:46:51 +02:00
Sansan f03e001bdd Add swag-generate to justfile and include in build 2026-05-18 21:44:58 +02:00
Sansan 1d77ae491c Add OpenAPI endpoint at /openapi with Swagger documentation 2026-05-18 21:43:06 +02:00
Sansan c0d1aaa4d1 Update Echo framework to v5.1.1 2026-05-18 21:36:50 +02:00
Sansan 76aaa884fa Change domain from sanplex.tech to sanplex.xyz 2026-05-18 21:27:34 +02:00
Sansan 290d79ef5e Changed how time are sent to frontend during sync
Build / build (push) Successful in 40s
2025-11-15 14:55:03 +01:00
Sansan aa0b8275e7 Fix so that ending slash doesn't matter for characters path
Build / build (push) Successful in 40s
2025-11-08 12:04:44 +01:00
Sansan c369b13fae Added characters to Dockerfile
Build / build (push) Successful in 39s
Publish / publish (push) Successful in 47s
2025-11-08 11:54:51 +01:00
Sansan bef915ac6d Fixed gitea script
Build / build (push) Successful in 39s
2025-11-07 21:10:40 +01:00
Sansan cff777f278 Small fixes to getting character images
Build / build (push) Successful in 44s
Publish / publish (push) Successful in 51s
2025-11-07 20:24:46 +01:00
Sansan 61cab73ffc Small fix to update song played
Build / build (push) Successful in 50s
2025-10-26 20:40:48 +01:00
Sansan a6294e46f2 Small changes to sync progress
Build / build (push) Successful in 52s
2025-09-19 22:10:27 +02:00
Sansan 5f91643b4d Added time to sync and progress respond
Build / build (push) Successful in 1m58s
2025-08-30 13:36:45 +02:00
Sansan 806e88adeb #1 - Created request to check newest version of the app
Build / build (push) Successful in 2m35s
#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
2025-08-23 11:36:03 +02:00
41 changed files with 14667 additions and 28770 deletions
+15 -15
View File
@@ -1,6 +1,6 @@
name: Build name: Build
run-name: ${{ gitea.actor }} is runs ci pipeline
#on: #on:
# release: # release:
# types: [published] # types: [published]
@@ -22,19 +22,19 @@ jobs:
uses: https://github.com/docker/setup-buildx-action@v3 uses: https://github.com/docker/setup-buildx-action@v3
with: with:
config-inline: | config-inline: |
[registry."gitea.sanplex.tech/sansan"] [registry."gitea.sanplex.xyz/sansan"]
http = true http = true
insecure = true insecure = true
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: gitea.sanplex.tech registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }} password: ${{ secrets.TOKEN }}
- name: Build - name: Build
uses: https://github.com/docker/build-push-action@v5 uses: https://github.com/docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: false push: false
#tags: "gitea.sanplex.tech/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.tech/sansan/musicserver:latest" #tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
+15 -15
View File
@@ -1,6 +1,6 @@
name: Publish name: Publish
run-name: ${{ gitea.actor }} is runs ci pipeline
#on: #on:
# release: # release:
# types: [published] # types: [published]
@@ -23,19 +23,19 @@ jobs:
uses: https://github.com/docker/setup-buildx-action@v3 uses: https://github.com/docker/setup-buildx-action@v3
with: with:
config-inline: | config-inline: |
[registry."gitea.sanplex.tech/sansan"] [registry."gitea.sanplex.xyz/sansan"]
http = true http = true
insecure = true insecure = true
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: gitea.sanplex.tech registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }} password: ${{ secrets.TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5 uses: https://github.com/docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: "gitea.sanplex.tech/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.tech/sansan/musicserver:latest" tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
+7
View File
@@ -25,5 +25,12 @@
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://ssh.sanplex.xyz:9432/music_prod</jdbc-url> <jdbc-url>jdbc:postgresql://ssh.sanplex.xyz:9432/music_prod</jdbc-url>
</data-source> </data-source>
<data-source source="LOCAL" name="music_test2@localhost" uuid="a423ab0a-55b0-42e1-8070-25d8ef34bfac">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/music_test2</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component> </component>
</project> </project>
+2
View File
@@ -21,6 +21,7 @@ FROM golang:1.23-alpine
EXPOSE 8080 EXPOSE 8080
VOLUME /sorted VOLUME /sorted
VOLUME /frontend VOLUME /frontend
VOLUME /characters
ENV PORT 8080 ENV PORT 8080
ENV DB_HOST "" ENV DB_HOST ""
@@ -29,6 +30,7 @@ ENV DB_USERNAME ""
ENV DB_PASSWORD "" ENV DB_PASSWORD ""
ENV DB_NAME "" ENV DB_NAME ""
ENV MUSIC_PATH "" ENV MUSIC_PATH ""
ENV CHARACTERS_PATH ""
COPY --from=build_go /app/main . COPY --from=build_go /app/main .
COPY ./songs/ ./songs/ COPY ./songs/ ./songs/
+885 -25
View File
@@ -1,44 +1,904 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT // Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import (
"bytes"
"encoding/json"
"strings"
"text/template"
const docTemplate = `{ "github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }}, "schemes": {{ marshal .Schemes }},
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"description": "{{escape .Description}}", "description": "{{escape .Description}}",
"title": "{{.Title}}", "title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/", "contact": {},
"contact": {
"name": "Sebastian Olsson",
"email": "zarnor91@gmail.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}" "version": "{{.Version}}"
}, },
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": {} "paths": {
"/character": {
"get": {
"description": "Returns the image for a specific character",
"consumes": [
"application/json"
],
"produces": [
"image/png"
],
"tags": [
"characters"
],
"summary": "Get character image",
"parameters": [
{
"type": "string",
"description": "Character name",
"name": "name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/characters": {
"get": {
"description": "Returns a list of all available characters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"characters"
],
"summary": "Get list of characters",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/dbtest": {
"get": {
"description": "Tests the database connection",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"database"
],
"summary": "Test database connection",
"responses": {
"200": {
"description": "TestedDB",
"schema": {
"type": "string"
}
}
}
}
},
"/download": {
"get": {
"description": "Checks for the latest version of the application",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"download"
],
"summary": "Check for latest version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/download/linux": {
"get": {
"description": "Redirects to download the latest Linux version",
"produces": [
"application/octet-stream"
],
"tags": [
"download"
],
"summary": "Download latest Linux version",
"responses": {
"302": {
"description": "Found",
"schema": {
"type": "string"
}
}
}
}
},
"/download/list": {
"get": {
"description": "Lists all assets available for the latest version",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"download"
],
"summary": "List assets of latest version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/download/windows": {
"get": {
"description": "Redirects to download the latest Windows version",
"produces": [
"application/octet-stream"
],
"tags": [
"download"
],
"summary": "Download latest Windows version",
"responses": {
"302": {
"description": "Found",
"schema": {
"type": "string"
}
}
}
}
},
"/health": {
"get": {
"description": "Returns the health status of the server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Check server health",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/music": {
"get": {
"description": "Returns a specific song by name",
"consumes": [
"application/json"
],
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get a specific song",
"parameters": [
{
"type": "string",
"description": "Song name",
"name": "song",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "song can't be empty",
"schema": {
"type": "string"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/addPlayed": {
"get": {
"description": "Adds the latest song to the played list",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Add latest to played",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/addQue": {
"get": {
"description": "Adds the latest song to the queue",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Add latest to queue",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/all/order": {
"get": {
"description": "Returns a list of all games in order",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get all games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/all/random": {
"get": {
"description": "Returns a list of all games in random order",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get all games random",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/info": {
"get": {
"description": "Returns information about the current song",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get current song info",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/music/list": {
"get": {
"description": "Returns a list of played songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get played songs list",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"/music/next": {
"get": {
"description": "Returns the next song in the queue",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get next song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/played": {
"put": {
"description": "Marks a song as played by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Mark song as played",
"parameters": [
{
"type": "integer",
"description": "Song ID",
"name": "song",
"in": "query",
"required": true
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/previous": {
"get": {
"description": "Returns the previous song in the queue",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get previous song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand": {
"get": {
"description": "Returns a random song",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand/classic": {
"get": {
"description": "Returns a random song from the classic selection",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random classic song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand/low": {
"get": {
"description": "Returns a random song with low chance selection",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random song with low chance",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/reset": {
"get": {
"description": "Resets the music state",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Reset music state",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/soundTest": {
"get": {
"description": "Returns the sound check song",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get sound check song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync": {
"get": {
"description": "Starts syncing games with only new changes",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Sync games with only changes",
"responses": {
"200": {
"description": "Start syncing games",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync/full": {
"get": {
"description": "Starts a full sync of all games",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Sync all games fully",
"responses": {
"200": {
"description": "Start syncing games full",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync/progress": {
"get": {
"description": "Returns the current sync progress or result",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Get sync progress",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/sync/reset": {
"get": {
"description": "Resets the games database by deleting all games and songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Reset games database",
"responses": {
"200": {
"description": "Games and songs are deleted from the database",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/version": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Getting the version of the backend",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/backend.VersionData"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {
"backend.VersionData": {
"type": "object",
"properties": {
"changelog": {
"type": "string",
"example": "account name"
},
"history": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.VersionData"
}
},
"version": {
"type": "string",
"example": "1.0.0"
}
}
}
}
}` }`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = swaggerInfo{
Version: "0.5", Version: "",
Host: "localhost:8080", Host: "",
BasePath: "", BasePath: "",
Schemes: []string{}, Schemes: []string{},
Title: "Swagger Example API", Title: "",
Description: "This is a sample server Petstore server.", Description: "",
InfoInstanceName: "swagger", }
SwaggerTemplate: docTemplate,
LeftDelim: "{{", type s struct{}
RightDelim: "}}",
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
"escape": func(v interface{}) string {
// escape tabs
str := strings.Replace(v.(string), "\t", "\\t", -1)
// replace " with \", and if that results in \\", replace that with \\\"
str = strings.Replace(str, "\"", "\\\"", -1)
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
} }
func init() { func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) swag.Register("swagger", &s{})
} }
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
import argparse
import json
import re
from pathlib import Path
from collections import defaultdict
def load_openapi_spec(path: str) -> dict:
"""Load OpenAPI spec from YAML or JSON file."""
try:
import yaml
with open(path, "r") as f:
return yaml.safe_load(f)
except ImportError:
# Fallback to JSON if PyYAML is not installed
with open(path, "r") as f:
return json.load(f)
def map_type(openapi_type: str) -> str:
"""Map OpenAPI types to GDScript types."""
type_mapping = {
"integer": "int",
"number": "float",
"boolean": "bool",
"array": "Array",
"object": "Dictionary",
}
return type_mapping.get(openapi_type, "String")
def default_value(openapi_type: str):
"""Return default values for GDScript types."""
default_mapping = {
"integer": "0",
"number": "0.0",
"boolean": "false",
"array": "[]",
"object": "{}",
}
return default_mapping.get(openapi_type, '""')
def sanitize_class_name(name: str) -> str:
"""Convert a name to a valid GDScript class name."""
# Replace invalid characters with underscores
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
# Capitalize first letter
return name[0].upper() + name[1:] if name else "Model"
def generate_model_class(class_name: str, schema: dict) -> str:
"""Generate a GDScript class for a model."""
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
]
# Add properties
properties = schema.get("properties", {})
for prop_name, prop_schema in properties.items():
prop_type = map_type(prop_schema.get("type", "string"))
lines.append(f"var {prop_name}: {prop_type}")
# Add _init method
lines.extend([
"",
"func _init(data: Dictionary):",
])
for prop_name in properties:
prop_type = map_type(properties[prop_name].get("type", "string"))
default = default_value(properties[prop_name].get("type", "string"))
lines.append(f' {prop_name} = data.get("{prop_name}", {default})')
return "\n".join(lines)
def generate_api_client(path: str, method: str, endpoint: dict) -> str:
"""Generate a GDScript API client for an endpoint."""
# Sanitize path for class name
class_name = sanitize_class_name(path.replace("/", "_").replace("{", "").replace("}", "")) + method.capitalize()
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
full_url = f'"{BASE_URL}{url}"'
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"",
"func _init(node: Node):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
"",
f"func call(params: Dictionary, callback: Callable):",
f" var url := {full_url}",
' var headers = ["User-Agent: MyGodotApp"]',
" var error := http_request.request(url, headers)",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
]
return "\n".join(lines)
def generate_tag_client(tag: str, endpoints: list) -> str:
"""Generate a GDScript API client for all endpoints with a given tag."""
class_name = sanitize_class_name(tag) + "API"
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"var base_url: String",
"",
"func _init(node: Node, base_url_param: String):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
" base_url = base_url_param",
"",
]
# Generate a method for each endpoint
for path, method, endpoint in endpoints:
# Sanitize method name for GDScript
method_name = method.lower()
# Create a valid function name from the path
func_name = "call_" + path.replace("/", "_").replace("{", "").replace("}", "").replace("-", "_")
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
lines.extend([
f"func {func_name}(params: Dictionary = {{}}, callback: Callable):",
f' var url := base_url + "{url}"',
' var headers = ["User-Agent: MyGodotApp"]',
f" var error := http_request.request(url, headers, false, HTTPClient.METHOD_{method.upper()})",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
])
# Add the completion handler
lines.extend([
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
])
return "\n".join(lines)
def generate_code(spec: dict, output_dir: str):
"""Generate all GDScript files from the OpenAPI spec."""
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Generate models
schemas = spec.get("definitions", {}) # Swagger 2.0 uses "definitions"
if not schemas:
schemas = spec.get("components", {}).get("schemas", {})
for schema_name, schema in schemas.items():
class_name = sanitize_class_name(schema_name)
code = generate_model_class(class_name, schema)
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated model: {output_path}")
# Group endpoints by tag
paths = spec.get("paths", {})
tag_endpoints = defaultdict(list)
for path, methods in paths.items():
for method, endpoint in methods.items():
tags = endpoint.get("tags", ["default"])
for tag in tags:
tag_endpoints[tag].append((path, method, endpoint))
# Generate one file per tag
for tag, endpoints in tag_endpoints.items():
code = generate_tag_client(tag, endpoints)
class_name = sanitize_class_name(tag) + "API"
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated API client for tag '{tag}': {output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate Godot API clients from OpenAPI spec")
parser.add_argument("input", help="Path to the OpenAPI JSON/YAML file")
parser.add_argument("-o", "--output", default="godot_generated", help="Output directory for GDScript files")
args = parser.parse_args()
spec = load_openapi_spec(args.input)
generate_code(spec, args.output)
print("Done!")
print("Note: When initializing the API classes, pass the base URL as a parameter:")
print(" var music_api = MusicAPI.new()")
print(" music_api._init(get_node('/root'), 'http://localhost:8080')")
if __name__ == "__main__":
main()
+826 -14
View File
@@ -1,19 +1,831 @@
{ {
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"description": "This is a sample server Petstore server.", "contact": {}
"title": "Swagger Example API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "Sebastian Olsson",
"email": "zarnor91@gmail.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "0.5"
}, },
"host": "localhost:8080", "paths": {
"paths": {} "/character": {
"get": {
"description": "Returns the image for a specific character",
"consumes": [
"application/json"
],
"produces": [
"image/png"
],
"tags": [
"characters"
],
"summary": "Get character image",
"parameters": [
{
"type": "string",
"description": "Character name",
"name": "name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
}
}
},
"/characters": {
"get": {
"description": "Returns a list of all available characters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"characters"
],
"summary": "Get list of characters",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/dbtest": {
"get": {
"description": "Tests the database connection",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"database"
],
"summary": "Test database connection",
"responses": {
"200": {
"description": "TestedDB",
"schema": {
"type": "string"
}
}
}
}
},
"/download": {
"get": {
"description": "Checks for the latest version of the application",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"download"
],
"summary": "Check for latest version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/download/linux": {
"get": {
"description": "Redirects to download the latest Linux version",
"produces": [
"application/octet-stream"
],
"tags": [
"download"
],
"summary": "Download latest Linux version",
"responses": {
"302": {
"description": "Found",
"schema": {
"type": "string"
}
}
}
}
},
"/download/list": {
"get": {
"description": "Lists all assets available for the latest version",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"download"
],
"summary": "List assets of latest version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/download/windows": {
"get": {
"description": "Redirects to download the latest Windows version",
"produces": [
"application/octet-stream"
],
"tags": [
"download"
],
"summary": "Download latest Windows version",
"responses": {
"302": {
"description": "Found",
"schema": {
"type": "string"
}
}
}
}
},
"/health": {
"get": {
"description": "Returns the health status of the server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Check server health",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/music": {
"get": {
"description": "Returns a specific song by name",
"consumes": [
"application/json"
],
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get a specific song",
"parameters": [
{
"type": "string",
"description": "Song name",
"name": "song",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "song can't be empty",
"schema": {
"type": "string"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/addPlayed": {
"get": {
"description": "Adds the latest song to the played list",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Add latest to played",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/addQue": {
"get": {
"description": "Adds the latest song to the queue",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Add latest to queue",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/all/order": {
"get": {
"description": "Returns a list of all games in order",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get all games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/all/random": {
"get": {
"description": "Returns a list of all games in random order",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get all games random",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/info": {
"get": {
"description": "Returns information about the current song",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get current song info",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/music/list": {
"get": {
"description": "Returns a list of played songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Get played songs list",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"/music/next": {
"get": {
"description": "Returns the next song in the queue",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get next song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/played": {
"put": {
"description": "Marks a song as played by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"music"
],
"summary": "Mark song as played",
"parameters": [
{
"type": "integer",
"description": "Song ID",
"name": "song",
"in": "query",
"required": true
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/previous": {
"get": {
"description": "Returns the previous song in the queue",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get previous song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand": {
"get": {
"description": "Returns a random song",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand/classic": {
"get": {
"description": "Returns a random song from the classic selection",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random classic song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/rand/low": {
"get": {
"description": "Returns a random song with low chance selection",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get random song with low chance",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/reset": {
"get": {
"description": "Resets the music state",
"consumes": [
"application/json"
],
"tags": [
"music"
],
"summary": "Reset music state",
"responses": {
"204": {
"description": ""
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/music/soundTest": {
"get": {
"description": "Returns the sound check song",
"produces": [
"audio/mpeg"
],
"tags": [
"music"
],
"summary": "Get sound check song",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync": {
"get": {
"description": "Starts syncing games with only new changes",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Sync games with only changes",
"responses": {
"200": {
"description": "Start syncing games",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync/full": {
"get": {
"description": "Starts a full sync of all games",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Sync all games fully",
"responses": {
"200": {
"description": "Start syncing games full",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/sync/progress": {
"get": {
"description": "Returns the current sync progress or result",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Get sync progress",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/sync/reset": {
"get": {
"description": "Resets the games database by deleting all games and songs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sync"
],
"summary": "Reset games database",
"responses": {
"200": {
"description": "Games and songs are deleted from the database",
"schema": {
"type": "string"
}
},
"423": {
"description": "Syncing is in progress",
"schema": {
"type": "string"
}
}
}
}
},
"/version": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Getting the version of the backend",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/backend.VersionData"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {
"backend.VersionData": {
"type": "object",
"properties": {
"changelog": {
"type": "string",
"example": "account name"
},
"history": {
"type": "array",
"items": {
"$ref": "#/definitions/backend.VersionData"
}
},
"version": {
"type": "string",
"example": "1.0.0"
}
}
}
}
} }
+543 -12
View File
@@ -1,14 +1,545 @@
host: localhost:8080 definitions:
backend.VersionData:
properties:
changelog:
example: account name
type: string
history:
items:
$ref: '#/definitions/backend.VersionData'
type: array
version:
example: 1.0.0
type: string
type: object
info: info:
contact: contact: {}
email: zarnor91@gmail.com paths:
name: Sebastian Olsson /character:
description: This is a sample server Petstore server. get:
license: consumes:
name: Apache 2.0 - application/json
url: http://www.apache.org/licenses/LICENSE-2.0.html description: Returns the image for a specific character
termsOfService: http://swagger.io/terms/ parameters:
title: Swagger Example API - description: Character name
version: "0.5" in: query
paths: {} name: name
required: true
type: string
produces:
- image/png
responses:
"200":
description: OK
schema:
type: file
summary: Get character image
tags:
- characters
/characters:
get:
consumes:
- application/json
description: Returns a list of all available characters
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
summary: Get list of characters
tags:
- characters
/dbtest:
get:
consumes:
- application/json
description: Tests the database connection
produces:
- application/json
responses:
"200":
description: TestedDB
schema:
type: string
summary: Test database connection
tags:
- database
/download:
get:
consumes:
- application/json
description: Checks for the latest version of the application
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Check for latest version
tags:
- download
/download/linux:
get:
description: Redirects to download the latest Linux version
produces:
- application/octet-stream
responses:
"302":
description: Found
schema:
type: string
summary: Download latest Linux version
tags:
- download
/download/list:
get:
consumes:
- application/json
description: Lists all assets available for the latest version
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
summary: List assets of latest version
tags:
- download
/download/windows:
get:
description: Redirects to download the latest Windows version
produces:
- application/octet-stream
responses:
"302":
description: Found
schema:
type: string
summary: Download latest Windows version
tags:
- download
/health:
get:
consumes:
- application/json
description: Returns the health status of the server
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Check server health
tags:
- health
/music:
get:
consumes:
- application/json
description: Returns a specific song by name
parameters:
- description: Song name
in: query
name: song
required: true
type: string
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"400":
description: song can't be empty
schema:
type: string
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get a specific song
tags:
- music
/music/addPlayed:
get:
consumes:
- application/json
description: Adds the latest song to the played list
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Add latest to played
tags:
- music
/music/addQue:
get:
consumes:
- application/json
description: Adds the latest song to the queue
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Add latest to queue
tags:
- music
/music/all/order:
get:
consumes:
- application/json
description: Returns a list of all games in order
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"423":
description: Syncing is in progress
schema:
type: string
summary: Get all games
tags:
- music
/music/all/random:
get:
consumes:
- application/json
description: Returns a list of all games in random order
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"423":
description: Syncing is in progress
schema:
type: string
summary: Get all games random
tags:
- music
/music/info:
get:
consumes:
- application/json
description: Returns information about the current song
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: Get current song info
tags:
- music
/music/list:
get:
consumes:
- application/json
description: Returns a list of played songs
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
summary: Get played songs list
tags:
- music
/music/next:
get:
description: Returns the next song in the queue
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get next song
tags:
- music
/music/played:
put:
consumes:
- application/json
description: Marks a song as played by its ID
parameters:
- description: Song ID
in: query
name: song
required: true
type: integer
produces:
- application/json
responses:
"204":
description: ""
"400":
description: Bad Request
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Mark song as played
tags:
- music
/music/previous:
get:
description: Returns the previous song in the queue
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get previous song
tags:
- music
/music/rand:
get:
description: Returns a random song
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random song
tags:
- music
/music/rand/classic:
get:
description: Returns a random song from the classic selection
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random classic song
tags:
- music
/music/rand/low:
get:
description: Returns a random song with low chance selection
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get random song with low chance
tags:
- music
/music/reset:
get:
consumes:
- application/json
description: Resets the music state
responses:
"204":
description: ""
"423":
description: Syncing is in progress
schema:
type: string
summary: Reset music state
tags:
- music
/music/soundTest:
get:
description: Returns the sound check song
produces:
- audio/mpeg
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Get sound check song
tags:
- music
/sync:
get:
consumes:
- application/json
description: Starts syncing games with only new changes
produces:
- application/json
responses:
"200":
description: Start syncing games
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Sync games with only changes
tags:
- sync
/sync/full:
get:
consumes:
- application/json
description: Starts a full sync of all games
produces:
- application/json
responses:
"200":
description: Start syncing games full
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Sync all games fully
tags:
- sync
/sync/progress:
get:
consumes:
- application/json
description: Returns the current sync progress or result
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: Get sync progress
tags:
- sync
/sync/reset:
get:
consumes:
- application/json
description: Resets the games database by deleting all games and songs
produces:
- application/json
responses:
"200":
description: Games and songs are deleted from the database
schema:
type: string
"423":
description: Syncing is in progress
schema:
type: string
summary: Reset games database
tags:
- sync
/version:
get:
consumes:
- application/json
description: get string by ID
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/backend.VersionData'
"404":
description: Not Found
schema:
type: string
summary: Getting the version of the backend
tags:
- accounts
swagger: "2.0" swagger: "2.0"
+10916 -27824
View File
File diff suppressed because it is too large Load Diff
+45 -36
View File
@@ -7,42 +7,51 @@
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^1.7.2",
"config.js": "^0.1.0", "core-js": "^3.37.1",
"core-js": "^3.8.2", "cors": "^2.8.5",
"cors": "^2.8.5", "express": "^4.19.2",
"express": "^4.17.1", "vue": "^3.4.31",
"nodemon": "^2.0.7", "vue-axios": "^3.5.2",
"vue": "^3.0.5", "vuex": "^4.1.0"
"vue-axios": "^3.2.2", },
"vuex": "^4.0.0-rc.2" "devDependencies": {
}, "@vue/cli-plugin-babel": "^5.0.8",
"devDependencies": { "@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-babel": "^4.5.10", "@vue/cli-service": "^5.0.8",
"@vue/cli-plugin-eslint": "^4.5.10", "@vue/compiler-sfc": "^3.4.31",
"@vue/cli-service": "^4.5.10", "@babel/eslint-parser": "^7.25.1",
"@vue/compiler-sfc": "^3.0.5", "eslint": "^8.57.0",
"babel-eslint": "^10.1.0", "eslint-plugin-vue": "^9.27.0"
"eslint": "^6.7.2", },
"eslint-plugin-vue": "^7.4.1" "eslintConfig": {
}, "root": true,
"eslintConfig": { "env": {
"root": true, "node": true
"env": { },
"node": true "extends": [
}, "plugin:vue/vue3-essential",
"extends": [ "eslint:recommended"
"plugin:vue/vue3-essential", ],
"eslint:recommended" "parserOptions": {
], "parser": "@babel/eslint-parser",
"parserOptions": { "requireConfigFile": false,
"parser": "babel-eslint" "ecmaVersion": 2020,
}, "sourceType": "module",
"rules": { "ecmaFeatures": {
"no-debugger": 1 "globalReturn": false,
} "impliedStrict": true,
}, "jsx": true
},
"babelOptions": {
"presets": ["@babel/preset-env"]
}
},
"rules": {
"no-debugger": 1
}
},
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
+5 -7
View File
@@ -7,14 +7,13 @@ import (
"music-server/internal/db" "music-server/internal/db"
"music-server/internal/server" "music-server/internal/server"
"net/http" "net/http"
"os"
"os/signal" "os/signal"
"runtime/pprof"
"syscall" "syscall"
"time" "time"
) )
// @title Swagger Example API //
// @Title Swagger Example API
// @version 0.5 // @version 0.5
// @description This is a sample server Petstore server. // @description This is a sample server Petstore server.
// @termsOfService http://swagger.io/terms/ // @termsOfService http://swagger.io/terms/
@@ -25,15 +24,14 @@ import (
// @license.name Apache 2.0 // @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080 // @host localhost:8080
func main() { func main() {
f, perr := os.Create("cpu.pprof") /*f, perr := os.Create("cpu.pprof")
if perr != nil { if perr != nil {
log.Fatal(perr) log.Fatal(perr)
} }
pprof.StartCPUProfile(f) pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()*/
server := server.NewServer() server := server.NewServer()
-18
View File
@@ -1,18 +0,0 @@
@import "tailwindcss";
#search-container {
text-align: center;
}
#search_term {
width: 60vw;
font-size: 2vh;
}
#clear {
font-size: 2vh;
}
#games-container{
font-size: 2vh;
}
+94
View File
@@ -0,0 +1,94 @@
/* Pure CSS styles for Music Search */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.5;
background-color: #f3f4f6;
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
#search-container {
text-align: center;
margin-bottom: 2rem;
}
#search_term {
width: 60vw;
max-width: 600px;
font-size: 1.5rem;
padding: 0.5rem;
border: 1px solid #9ca3af;
border-radius: 0.5rem;
background-color: #e5e7eb;
color: #000;
}
#search_term:focus {
outline: none;
border-color: #6b7280;
}
#clear {
font-size: 1.5rem;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.5rem;
background-color: #f97316;
color: #fff;
cursor: pointer;
margin-left: 1rem;
}
#clear:hover {
background-color: #ea580c;
}
#games-container {
font-size: 1.5rem;
}
/* Game result cards */
.bg-green-100 {
background-color: #dcfce7;
}
.p-4 {
padding: 1rem;
}
.shadow-md {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.rounded-lg {
border-radius: 0.5rem;
}
.mt-6 {
margin-top: 1.5rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
#search_term {
width: 80vw;
font-size: 1.2rem;
}
#clear {
font-size: 1.2rem;
padding: 0.4rem 0.8rem;
}
}
+4 -4
View File
@@ -2,15 +2,15 @@ package web
templ Base() { templ Base() {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="h-screen"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>Music Search</title> <title>Music Search</title>
<link href="assets/css/output.css" rel="stylesheet"/> <link href="assets/css/styles.css" rel="stylesheet"/>
<script src="assets/js/htmx.min.js"></script> <script src="assets/js/htmx.min.js"></script>
</head> </head>
<body class="bg-gray-100"> <body>
<main class="mx-auto p-4"> <main>
{ children... } { children... }
</main> </main>
</body> </body>
+3 -3
View File
@@ -3,12 +3,12 @@ package web
templ HelloForm() { templ HelloForm() {
@Base() { @Base() {
<div id="search-container"> <div id="search-container">
<input class="bg-gray-200 text-black p-2 border border-gray-400 rounded-lg" id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/> <input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
<button type="button" class="bg-orange-500 hover:bg-orange-700 text-white py-2 px-4 rounded" id="clear" name="clear">Clear</button> <button type="button" id="clear" name="clear">Clear</button>
</div> </div>
<div id="games-container"></div> <div id="games-container"></div>
<script> <script>
document.addEventListener('readystatechange', () => { document.addEventListener('readystatechange', () => {
if (document.readyState == 'complete') { if (document.readyState == 'complete') {
htmx.ajax('POST', '/find', '#games-container'); htmx.ajax('POST', '/find', '#games-container');
document.getElementById("search_term").focus(); document.getElementById("search_term").focus();
+40 -43
View File
@@ -1,54 +1,51 @@
module music-server module music-server
go 1.23.0 go 1.25.0
toolchain go1.24.2
require ( require (
github.com/MShekow/directory-checksum v1.4.6 github.com/MShekow/directory-checksum v1.4.18
github.com/a-h/templ v0.3.865 github.com/a-h/templ v0.3.1020
github.com/golang-migrate/migrate/v4 v4.18.1 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/jackc/pgx/v5 v5.5.5 github.com/jackc/pgx/v5 v5.9.2
github.com/labstack/echo/v4 v4.13.3 github.com/labstack/echo/v5 v5.1.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.12.3
github.com/spf13/afero v1.11.0 github.com/panjf2000/ants/v2 v2.12.0
github.com/swaggo/echo-swagger v1.4.1 github.com/spf13/afero v1.15.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/echo-swagger/v2 v2.0.1
github.com/swaggo/swag v1.16.6
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect github.com/go-errors/errors v1.5.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading 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/yamlutils v0.26.0 // indirect
github.com/google/go-cmp v0.7.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-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/sv-tools/openapi v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/zap v1.28.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect golang.org/x/mod v0.36.0 // indirect
go.uber.org/atomic v1.7.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/tools v0.45.0 // indirect
golang.org/x/sys v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/text v0.24.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.32.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+111 -124
View File
@@ -2,173 +2,160 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/MShekow/directory-checksum v1.4.6 h1:2fhlCYbpjEN1iH9S0tdmEM0p1wvNT9x5x0rIchGI7nE= github.com/MShekow/directory-checksum v1.4.18 h1:1nPPVl7uREa6WMTAPKoWW/GylhnASs0C9C+GPiwLwXA=
github.com/MShekow/directory-checksum v1.4.6/go.mod h1:bMfFBkaIlNk7O9VgEi8D2X7Q2Jfk3c7d67z3t6cpIi4= github.com/MShekow/directory-checksum v1.4.18/go.mod h1:iUupsPb0X0BumQQymLrpD5Pkqe/CbV13OSgosw1oFc4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4=
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/echo-swagger/v2 v2.0.1 h1:jKR3QiK+ciGjxE0+7qZ/azjtlx/pTVls7pJFJqdJoJI=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/echo-swagger/v2 v2.0.1/go.mod h1:BbgiO9XKX6yYU5Rq4ejqVlQI0mVRv6ziFKd0XgdztnQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+14 -7
View File
@@ -1,14 +1,18 @@
package backend package backend
import ( import (
"fmt"
"log" "log"
"os" "os"
"strings" "strings"
) )
func GetCharacters() []string { func GetCharacterList() []string {
musicPath := os.Getenv("MUSIC_PATH") charactersPath := os.Getenv("CHARACTERS_PATH")
charactersPath := musicPath + "characters/" fmt.Printf("dir: %s\n", charactersPath)
if !strings.HasSuffix(charactersPath, "/") {
charactersPath += "/"
}
files, err := os.ReadDir(charactersPath) files, err := os.ReadDir(charactersPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -24,12 +28,15 @@ func GetCharacters() []string {
} }
func GetCharacter(character string) string { func GetCharacter(character string) string {
musicPath := os.Getenv("MUSIC_PATH") charactersPath := os.Getenv("CHARACTERS_PATH")
charactersPath := musicPath + "characters/" fmt.Printf("dir: %s\n", charactersPath)
if !strings.HasSuffix(charactersPath, "/") {
charactersPath += "/"
}
return charactersPath + character return charactersPath + character
} }
func isImage(entry os.DirEntry) bool { func isImage(entry os.DirEntry) bool {
return !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".png")) return !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".jpeg") ||
strings.HasSuffix(entry.Name(), ".png"))
} }
+106
View File
@@ -0,0 +1,106 @@
package backend
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
type giteaResponse struct {
Id int `json:"id"`
Name string `json:"name"`
Assets []assetResponse `json:"assets"`
}
type assetResponse struct {
Id int `json:"id"`
Name string `json:"name"`
DownloadUrl string `json:"browser_download_url"`
}
func CheckLatest() string {
resp, err := http.Get("https://gitea.sanplex.xyz/api/v1/repos/sansan/MusicPlayer/releases/latest")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var cResp giteaResponse
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&cResp); err != nil {
fmt.Println(err)
log.Fatal("ooopsss! an error occurred, please try again")
}
log.Printf("Id: %v, Name: %v", cResp.Id, cResp.Name)
return cResp.Name
}
func ListAssetsOfLatest() []string {
resp, err := http.Get("https://gitea.sanplex.xyz/api/v1/repos/sansan/MusicPlayer/releases/latest")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var cResp giteaResponse
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&cResp); err != nil {
fmt.Println(err)
log.Fatal("ooopsss! an error occurred, please try again")
}
log.Printf("Id: %v, Name: %v", cResp.Id, cResp.Name)
var assets []string
for _, asset := range cResp.Assets {
log.Printf("Id: %v, Name: %v, Asset: %v", cResp.Id, cResp.Name, asset.Name)
assets = append(assets, asset.Name)
}
return assets
}
func DownloadLatestWindows() string {
resp, err := http.Get("https://gitea.sanplex.xyz/api/v1/repos/sansan/MusicPlayer/releases/latest")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var cResp giteaResponse
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&cResp); err != nil {
fmt.Println(err)
log.Fatal("ooopsss! an error occurred, please try again")
}
log.Printf("Id: %v, Name: %v", cResp.Id, cResp.Name)
for _, asset := range cResp.Assets {
log.Printf("Id: %v, Name: %v, Asset: %v", cResp.Id, cResp.Name, asset.Name)
if strings.HasSuffix(asset.Name, ".exe") {
return asset.DownloadUrl
}
}
return ""
}
func DownloadLatestLinux() string {
resp, err := http.Get("https://gitea.sanplex.xyz/api/v1/repos/sansan/MusicPlayer/releases/latest")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var cResp giteaResponse
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&cResp); err != nil {
fmt.Println(err)
log.Fatal("ooopsss! an error occurred, please try again")
}
log.Printf("Id: %v, Name: %v", cResp.Id, cResp.Name)
for _, asset := range cResp.Assets {
log.Printf("Id: %v, Name: %v, Asset: %v", cResp.Id, cResp.Name, asset.Name)
if strings.HasSuffix(asset.Name, ".x86_64") {
return asset.DownloadUrl
}
}
return ""
}
+19 -2
View File
@@ -15,9 +15,26 @@ type VersionData struct {
} }
func GetVersionHistory() VersionData { func GetVersionHistory() VersionData {
data := VersionData{Version: "3.2", data := VersionData{Version: "4.5.0",
Changelog: "Upgraded Go version and the version of all dependencies. Fixed som more bugs.", 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{ 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", 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.", 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.",
+2 -41
View File
@@ -21,13 +21,10 @@ type SongInfo struct {
var currentSong = -1 var currentSong = -1
// var games []models.GameData
var gamesNew []repository.Game var gamesNew []repository.Game
// var songQue []models.SongData
var songQueNew []repository.Song var songQueNew []repository.Song
// var lastFetched models.SongData
var lastFetchedNew repository.Song var lastFetchedNew repository.Song
var repo *repository.Queries var repo *repository.Queries
@@ -60,7 +57,6 @@ func Reset() {
currentSong = -1 currentSong = -1
initRepo() initRepo()
gamesNew, _ = repo.FindAllGames(db.Ctx) gamesNew, _ = repo.FindAllGames(db.Ctx)
//games = db.FindAllGames()
} }
func AddLatestToQue() { func AddLatestToQue() {
@@ -80,8 +76,6 @@ func AddLatestPlayed() {
initRepo() initRepo()
repo.AddGamePlayed(db.Ctx, currentSongData.GameID) repo.AddGamePlayed(db.Ctx, currentSongData.GameID)
repo.AddSongPlayed(db.Ctx, repository.AddSongPlayedParams{GameID: currentSongData.GameID, SongName: currentSongData.SongName}) repo.AddSongPlayed(db.Ctx, repository.AddSongPlayedParams{GameID: currentSongData.GameID, SongName: currentSongData.SongName})
//db.AddGamePlayed(currentSongData.GameId)
//db.AddSongPlayed(currentSongData.GameId, currentSongData.SongName)
} }
func SetPlayed(songNumber int) { func SetPlayed(songNumber int) {
@@ -92,14 +86,9 @@ func SetPlayed(songNumber int) {
initRepo() initRepo()
repo.AddGamePlayed(db.Ctx, songData.GameID) repo.AddGamePlayed(db.Ctx, songData.GameID)
repo.AddSongPlayed(db.Ctx, repository.AddSongPlayedParams{GameID: songData.GameID, SongName: songData.SongName}) repo.AddSongPlayed(db.Ctx, repository.AddSongPlayedParams{GameID: songData.GameID, SongName: songData.SongName})
//db.AddGamePlayed(songData.GameId)
//db.AddSongPlayed(songData.GameId, songData.SongName)
} }
func GetRandomSong() string { func GetRandomSong() string {
/*if len(games) == 0 {
games = db.FindAllGames()
}*/
getAllGames() getAllGames()
if len(gamesNew) == 0 { if len(gamesNew) == 0 {
return "" return ""
@@ -111,12 +100,8 @@ func GetRandomSong() string {
} }
func GetRandomSongLowChance() string { func GetRandomSongLowChance() string {
/*if len(games) == 0 {
games = db.FindAllGames()
}*/
getAllGames() getAllGames()
//var listOfGames []models.GameData
var listOfGames []repository.Game var listOfGames []repository.Game
var averagePlayed = getAveragePlayed() var averagePlayed = getAveragePlayed()
@@ -139,15 +124,10 @@ func GetRandomSongLowChance() string {
} }
func GetRandomSongClassic() string { func GetRandomSongClassic() string {
/*if games == nil || len(games) == 0 {
games = db.FindAllGames()
}*/
getAllGames() getAllGames()
var listOfAllSongs []repository.Song var listOfAllSongs []repository.Song
for _, game := range gamesNew { for _, game := range gamesNew {
//listOfAllSongs = append(listOfAllSongs, db.FindSongsFromGame(game.Id)...)
songList, _ := repo.FindSongsFromGame(db.Ctx, game.ID) songList, _ := repo.FindSongsFromGame(db.Ctx, game.ID)
listOfAllSongs = append(listOfAllSongs, songList...) listOfAllSongs = append(listOfAllSongs, songList...)
} }
@@ -156,14 +136,11 @@ func GetRandomSongClassic() string {
var song repository.Song var song repository.Song
for !songFound { for !songFound {
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))] song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
//gameData, err := db.GetGameById(song.GameId)
gameData, err := repo.GetGameById(db.Ctx, song.GameID) gameData, err := repo.GetGameById(db.Ctx, song.GameID)
if err != nil { if err != nil {
//db.RemoveBrokenSong(song)
repo.RemoveBrokenSong(db.Ctx, song.Path) repo.RemoveBrokenSong(db.Ctx, song.Path)
//log.Println("Song not found, song '" + song.SongName + "' deleted from game '" + gameData.GameName + "' FileName: " + song.FileName) log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %s\n", song.SongName, gameData.GameName, *song.FileName)
log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %v\n", song.SongName, gameData.GameName, song.FileName)
continue continue
} }
@@ -171,10 +148,8 @@ func GetRandomSongClassic() string {
openFile, err := os.Open(song.Path) openFile, err := os.Open(song.Path)
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) { if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
//File not found //File not found
//db.RemoveBrokenSong(song)
repo.RemoveBrokenSong(db.Ctx, song.Path) repo.RemoveBrokenSong(db.Ctx, song.Path)
//log.Println("Song not found, song '" + song.SongName + "' deleted from game '" + gameData.GameName + "' FileName: " + song.FileName) log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %s\n", song.SongName, gameData.GameName, *song.FileName)
log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %v\n", song.SongName, gameData.GameName, song.FileName)
} else { } else {
songFound = true songFound = true
} }
@@ -234,9 +209,6 @@ func GetSong(song string) string {
} }
func GetAllGames() []string { func GetAllGames() []string {
/*if games == nil || len(games) == 0 {
games = db.FindAllGames()
}*/
getAllGames() getAllGames()
var jsonArray []string var jsonArray []string
@@ -247,9 +219,6 @@ func GetAllGames() []string {
} }
func GetAllGamesRandom() []string { func GetAllGamesRandom() []string {
/*if games == nil || len(games) == 0 {
games = db.FindAllGames()
}*/
getAllGames() getAllGames()
var jsonArray []string var jsonArray []string
@@ -293,10 +262,7 @@ func getSongFromList(games []repository.Game) repository.Song {
var song repository.Song var song repository.Song
for !songFound { for !songFound {
game := getRandomGame(games) game := getRandomGame(games)
//log.Println("game = ", game)
//songs := db.FindSongsFromGame(game.Id)
songs, _ := repo.FindSongsFromGame(db.Ctx, game.ID) songs, _ := repo.FindSongsFromGame(db.Ctx, game.ID)
//log.Println("songs = ", songs)
if len(songs) == 0 { if len(songs) == 0 {
continue continue
} }
@@ -305,14 +271,9 @@ func getSongFromList(games []repository.Game) 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)
//log.Println("game.Path+song.FileName: ", game.Path+song.FileName)
//log.Println("song.Path: ", song.Path)
//log.Println("game.Path+song.FileName != song.Path: ", game.Path+song.FileName != song.Path)
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) { if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
//File not found //File not found
//db.RemoveBrokenSong(song)
repo.RemoveBrokenSong(db.Ctx, song.Path) repo.RemoveBrokenSong(db.Ctx, song.Path)
//log.Println("Song not found, song '" + song.SongName + "' deleted from game '" + game.GameName + "' FileName: " + song.FileName)
log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %v\n", song.SongName, game.GameName, song.FileName) log.Printf("Song not found, song '%s' deleted from game '%s' FileName: %v\n", song.SongName, game.GameName, song.FileName)
} else { } else {
songFound = true songFound = true
+130 -67
View File
@@ -17,10 +17,19 @@ import (
"sync" "sync"
"time" "time"
"github.com/panjf2000/ants/v2"
"github.com/MShekow/directory-checksum/directory_checksum" "github.com/MShekow/directory-checksum/directory_checksum"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
var Syncing = false
var foldersSynced float32
var numberOfFoldersToSync float32
var start time.Time
var totalTime time.Duration
var timeSpent time.Duration
var allGames []repository.Game var allGames []repository.Game
var gamesBeforeSync []repository.Game var gamesBeforeSync []repository.Game
var gamesAfterSync []repository.Game var gamesAfterSync []repository.Game
@@ -32,13 +41,19 @@ var gamesRemoved []string
var catchedErrors []string var catchedErrors []string
var brokenSongs []string var brokenSongs []string
type Response struct { type SyncResponse struct {
GamesAdded []string `json:"games_added"` GamesAdded []string `json:"games_added"`
GamesReAdded []string `json:"games_re_added"` GamesReAdded []string `json:"games_re_added"`
GamesChangedTitle map[string]string `json:"games_changed_title"` GamesChangedTitle map[string]string `json:"games_changed_title"`
GamesChangedContent []string `json:"games_changed_content"` GamesChangedContent []string `json:"games_changed_content"`
GamesRemoved []string `json:"games_removed"` GamesRemoved []string `json:"games_removed"`
CatchedErrors []string `json:"catched_errors"` CatchedErrors []string `json:"catched_errors"`
TotalTime string `json:"total_time"`
}
type ProgressResponse struct {
Progress string `json:"progress"`
TimeSpent string `json:"time_spent"`
} }
type GameStatus int type GameStatus int
@@ -61,67 +76,26 @@ func (gs GameStatus) String() string {
return statusName[gs] return statusName[gs]
} }
var syncWg sync.WaitGroup
func ResetDB() { func ResetDB() {
//db.ClearSongs(-1)
repo.ClearSongs(db.Ctx) repo.ClearSongs(db.Ctx)
//db.ClearGames()
repo.ClearGames(db.Ctx) repo.ClearGames(db.Ctx)
} }
func SyncGamesNewFull() Response { func SyncProgress() ProgressResponse {
return syncGamesNew(true) progress := int((foldersSynced / numberOfFoldersToSync) * 100)
currentTime := time.Now()
timeSpent = currentTime.Sub(start)
out := time.Time{}.Add(timeSpent)
fmt.Printf("\nTime spent: %v\n", timeSpent)
fmt.Printf("Time spent: %v\n", out.Format("15:04:05.00000"))
log.Printf("Progress: %v/%v %v%%\n", int(foldersSynced), int(numberOfFoldersToSync), progress)
return ProgressResponse{
Progress: fmt.Sprintf("%v", progress),
TimeSpent: out.Format("15:04:05"),
}
} }
func SyncGamesNewOnlyChanges() Response { func SyncResult() SyncResponse {
return syncGamesNew(false)
}
func syncGamesNew(full bool) Response {
musicPath := os.Getenv("MUSIC_PATH")
fmt.Printf("dir: %s\n", musicPath)
initRepo()
start := time.Now()
foldersToSkip := []string{".sync", "dist", "old", "characters"}
fmt.Println(foldersToSkip)
var err error
gamesAdded = nil
gamesReAdded = nil
gamesChangedTitle = nil
gamesChangedContent = nil
gamesRemoved = nil
catchedErrors = nil
brokenSongs = nil
gamesBeforeSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames Before", err, "")
fmt.Printf("Games Before: %d\n", len(gamesBeforeSync))
allGames, err = repo.GetAllGamesIncludingDeleted(db.Ctx)
handleError("GetAllGamesIncludingDeleted", err, "")
err = repo.SetGameDeletionDate(db.Ctx)
handleError("SetGameDeletionDate", err, "")
directories, err := os.ReadDir(musicPath)
if err != nil {
log.Fatal(err)
}
syncWg.Add(len(directories))
for _, dir := range directories {
go func() {
defer syncWg.Done()
syncGameNew(dir, foldersToSkip, musicPath, full)
}()
}
syncWg.Wait()
checkBrokenSongsNew()
gamesAfterSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames After", err, "")
fmt.Printf("\nGames Before: %d\n", len(gamesBeforeSync)) fmt.Printf("\nGames Before: %d\n", len(gamesBeforeSync))
fmt.Printf("Games After: %d\n", len(gamesAfterSync)) fmt.Printf("Games After: %d\n", len(gamesAfterSync))
@@ -148,7 +122,7 @@ func syncGamesNew(full bool) Response {
fmt.Printf("\n\n") fmt.Printf("\n\n")
var gamesRemovedTemp []string var gamesRemovedTemp []string
for _, beforeGame := range gamesBeforeSync { for _, beforeGame := range gamesBeforeSync {
var found bool = false var found = false
for _, afterGame := range gamesAfterSync { for _, afterGame := range gamesAfterSync {
if beforeGame.GameName == afterGame.GameName { if beforeGame.GameName == afterGame.GameName {
found = true found = true
@@ -186,32 +160,110 @@ func syncGamesNew(full bool) Response {
fmt.Printf("%s\n", error) fmt.Printf("%s\n", error)
} }
finished := time.Now()
totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime) out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime) fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000")) fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
return Response{ return SyncResponse{
GamesAdded: gamesAdded, GamesAdded: gamesAdded,
GamesReAdded: gamesReAdded, GamesReAdded: gamesReAdded,
GamesChangedTitle: gamesChangedTitle, GamesChangedTitle: gamesChangedTitle,
GamesChangedContent: gamesChangedContent, GamesChangedContent: gamesChangedContent,
GamesRemoved: gamesRemoved, GamesRemoved: gamesRemoved,
CatchedErrors: catchedErrors, CatchedErrors: catchedErrors,
TotalTime: out.Format("15:04:05"),
} }
} }
func SyncGamesNewFull() {
syncGamesNew(true)
Reset()
}
func SyncGamesNewOnlyChanges() {
syncGamesNew(false)
Reset()
}
func syncGamesNew(full bool) {
Syncing = true
musicPath := os.Getenv("MUSIC_PATH")
fmt.Printf("dir: %s\n", musicPath)
if !strings.HasSuffix(musicPath, "/") {
musicPath += "/"
}
var syncWg sync.WaitGroup
initRepo()
start = time.Now()
foldersToSkip := []string{".sync", "dist", "old", "characters"}
fmt.Println(foldersToSkip)
var err error
gamesAdded = nil
gamesReAdded = nil
gamesChangedTitle = nil
gamesChangedContent = nil
gamesRemoved = nil
catchedErrors = nil
brokenSongs = nil
gamesBeforeSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames Before", err, "")
fmt.Printf("Games Before: %d\n", len(gamesBeforeSync))
allGames, err = repo.GetAllGamesIncludingDeleted(db.Ctx)
handleError("GetAllGamesIncludingDeleted", err, "")
err = repo.SetGameDeletionDate(db.Ctx)
handleError("SetGameDeletionDate", err, "")
directories, err := os.ReadDir(musicPath)
if err != nil {
log.Fatal(err)
}
pool, _ := ants.NewPool(50, ants.WithPreAlloc(true))
defer pool.Release()
foldersSynced = 0
numberOfFoldersToSync = float32(len(directories))
syncWg.Add(int(numberOfFoldersToSync))
for _, dir := range directories {
pool.Submit(func() {
defer syncWg.Done()
syncGameNew(dir, foldersToSkip, musicPath, full)
})
}
syncWg.Wait()
checkBrokenSongsNew()
gamesAfterSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames After", err, "")
finished := time.Now()
totalTime = finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
Syncing = false
}
func checkBrokenSongsNew() { func checkBrokenSongsNew() {
allSongs, err := repo.FetchAllSongs(db.Ctx) allSongs, err := repo.FetchAllSongs(db.Ctx)
handleError("FetchAllSongs", err, "") handleError("FetchAllSongs", err, "")
var brokenWg sync.WaitGroup var brokenWg sync.WaitGroup
poolBroken, _ := ants.NewPool(50, ants.WithPreAlloc(true))
defer poolBroken.Release()
brokenWg.Add(len(allSongs)) brokenWg.Add(len(allSongs))
for _, song := range allSongs { for _, song := range allSongs {
go func() { poolBroken.Submit(func() {
defer brokenWg.Done() defer brokenWg.Done()
checkBrokenSongNew(song) checkBrokenSongNew(song)
}() })
} }
brokenWg.Wait() brokenWg.Wait()
err = repo.RemoveBrokenSongs(db.Ctx, brokenSongs) err = repo.RemoveBrokenSongs(db.Ctx, brokenSongs)
@@ -343,6 +395,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
err = repo.RemoveDeletionDate(db.Ctx, id) err = repo.RemoveDeletionDate(db.Ctx, id)
handleError("RemoveDeletionDate", err, "") handleError("RemoveDeletionDate", err, "")
} }
foldersSynced++
log.Printf("Progress: %v/%v %v%%\n", int(foldersSynced), int(numberOfFoldersToSync), int((foldersSynced/numberOfFoldersToSync)*100))
} }
func insertGameNew(name string, path string, hash string) int32 { func insertGameNew(name string, path string, hash string) int32 {
@@ -365,19 +419,26 @@ func insertGameNew(name string, path string, hash string) int32 {
func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) 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)
var songWg sync.WaitGroup var songWg sync.WaitGroup
songWg.Add(len(entries)) poolSong, _ := ants.NewPool(numberOfFiles, ants.WithPreAlloc(true))
defer poolSong.Release()
songWg.Add(numberOfFiles)
for _, entry := range entries { for _, entry := range entries {
go func() { poolSong.Submit(func() {
defer songWg.Done() defer songWg.Done()
newCheckSong(entry, gameDir, id) if newCheckSong(entry, gameDir, id) {
}() numberOfSongs++
}
})
} }
songWg.Wait() songWg.Wait()
return numberOfSongs return numberOfSongs
} }
func newCheckSong(entry os.DirEntry, gameDir string, id int32) { func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
fileInfo, err := entry.Info() fileInfo, err := entry.Info()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -396,7 +457,7 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) {
handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash)) handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", 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 return false
} }
} }
fmt.Printf("Song Changed\n") fmt.Printf("Song Changed\n")
@@ -432,9 +493,11 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) {
} }
} }
return true
} else if isCoverImage(fileInfo) { } else if isCoverImage(fileInfo) {
//TODO: Later add cover art image here in db //TODO: Later add cover art image here in db
} }
return false
} }
func handleError(funcName string, err error, msg string) { func handleError(funcName string, err error, msg string) {
-103
View File
@@ -1,103 +0,0 @@
package database
import (
"fmt"
"music-server/internal/db"
"os"
"time"
)
type gameData struct {
Id int
GameName string
Added time.Time
Deleted time.Time
LastChanged time.Time
Path string
TimesPlayed int
LastPlayed time.Time
NumberOfSongs int32
}
func GetGameName(gameId int) string {
var gameName = ""
err := db.Dbpool.QueryRow(db.Ctx,
"SELECT game_name FROM game WHERE id = $1", gameId).Scan(&gameName)
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return ""
}
return gameName
}
func SetGameDeletionDate() {
_, err := db.Dbpool.Exec(db.Ctx,
"UPDATE game SET deleted=$1 WHERE deleted IS NULL", time.Now())
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func UpdateGameName(id int, name string, path string) {
_, err := db.Dbpool.Exec(db.Ctx,
"UPDATE game SET game_name=$1, path=$2, last_changed=$3 WHERE id=$4",
name, path, time.Now(), id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func RemoveDeletionDate(id int) {
_, err := db.Dbpool.Exec(db.Ctx,
"UPDATE game SET deleted=null WHERE id=$1", id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func GetIdByGameName(name string) int {
var gameId = -1
err := db.Dbpool.QueryRow(db.Ctx,
"SELECT id FROM game WHERE game_name = $1", name).Scan(&gameId)
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return -1
}
return gameId
}
func InsertGame(name string, path string) int {
gameId := -1
err := db.Dbpool.QueryRow(db.Ctx,
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId)
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
db.ResetGameIdSeq()
err2 := db.Dbpool.QueryRow(db.Ctx,
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId)
if err2 != nil {
if compareError.Error() != err2.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return -1
}
}
return gameId
}
func InsertGameWithExistingId(id int, name string, path string) {
_, err := db.Dbpool.Exec(db.Ctx,
"INSERT INTO game(id, game_name, path, added) VALUES ($1, $2, $3, $4)",
id, name, path, time.Now())
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
-206
View File
@@ -1,206 +0,0 @@
package database
import (
"fmt"
"io/fs"
"log"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var wg sync.WaitGroup
func SyncGames() {
start := time.Now()
dir := os.Getenv("MUSIC_PATH")
fmt.Printf("dir: %s\n", dir)
foldersToSkip := []string{".sync", "dist", "old", "characters"}
fmt.Println(foldersToSkip)
SetGameDeletionDate()
checkBrokenSongs()
files, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
syncGame(file, foldersToSkip, dir)
}
finished := time.Now()
totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
}
func SyncGamesQuick() {
start := time.Now()
dir := os.Getenv("MUSIC_PATH")
fmt.Printf("dir: %s\n", dir)
foldersToSkip := []string{".sync", "dist", "old", "characters"}
fmt.Println(foldersToSkip)
SetGameDeletionDate()
checkBrokenSongs()
files, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
wg.Add(1)
go func() {
defer wg.Done()
syncGame(file, foldersToSkip, dir)
}()
}
wg.Wait()
finished := time.Now()
totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
}
func syncGame(file os.DirEntry, foldersToSkip []string, dir string) {
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
fmt.Println(file.Name())
path := dir + file.Name() + "/"
fmt.Println(path)
entries, err := os.ReadDir(path)
if err != nil {
log.Println(err)
}
id := -1
for _, entry := range entries {
fileInfo, err := entry.Info()
if err != nil {
log.Println(err)
}
id = getIdFromFile(fileInfo)
if id != -1 {
break
}
}
if id == -1 {
addNewGame(file.Name(), path)
} else {
checkIfChanged(id, file.Name(), path)
checkSongs(path, id)
}
}
}
func getIdFromFile(file os.FileInfo) int {
name := file.Name()
if !file.IsDir() && strings.HasSuffix(name, ".id") {
name = strings.Replace(name, ".id", "", 1)
name = strings.Replace(name, ".", "", 1)
i, _ := strconv.Atoi(name)
return i
}
return -1
}
func checkIfChanged(id int, name string, path string) {
fmt.Printf("Id from file: %v\n", id)
nameFromDb := GetGameName(id)
fmt.Printf("Name from file: %v\n", name)
fmt.Printf("Name from DB: %v\n", nameFromDb)
if nameFromDb == "" {
fmt.Println("Not in db")
InsertGameWithExistingId(id, name, path)
fmt.Println("Added to db")
} else if name != nameFromDb {
fmt.Println("Diff name")
UpdateGameName(id, name, path)
checkBrokenSongs()
}
RemoveDeletionDate(id)
}
func addNewGame(name string, path string) {
newId := GetIdByGameName(name)
if newId != -1 {
checkBrokenSongs()
RemoveDeletionDate(newId)
} else {
newId = InsertGame(name, path)
}
fmt.Printf("newId = %v", newId)
fileName := path + "/." + strconv.Itoa(newId) + ".id"
fmt.Printf("fileName = %v", fileName)
err := os.WriteFile(fileName, nil, 0644)
if err != nil {
panic(err)
}
checkSongs(path, newId)
}
func checkSongs(gameDir string, gameId int) {
files, err := os.ReadDir(gameDir)
if err != nil {
log.Println(err)
}
for _, file := range files {
entry, err := file.Info()
if err != nil {
log.Println(err)
}
if isSong(entry) {
path := gameDir + entry.Name()
fileName := entry.Name()
songName, _ := strings.CutSuffix(fileName, ".mp3")
if CheckSong(path) {
UpdateSong(songName, fileName, path)
} else {
AddSong(SongData{GameId: gameId, SongName: songName, Path: path, FileName: fileName})
}
} else if isCoverImage(entry) {
//TODO: Later add cover art image here in db
}
}
//TODO: Add number of songs here
}
func checkBrokenSongs() {
allSongs := FetchAllSongs()
var brokenSongs []SongData
for _, song := range allSongs {
//Check if file exists and open
openFile, err := os.Open(song.Path)
if err != nil {
//File not found
brokenSongs = append(brokenSongs, song)
fmt.Printf("song broken: %v", song.Path)
} else {
err = openFile.Close()
if err != nil {
log.Println(err)
}
}
}
RemoveBrokenSongs(brokenSongs)
}
func isSong(entry fs.FileInfo) bool {
return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".mp3")
}
func isCoverImage(entry fs.FileInfo) bool {
return !entry.IsDir() && strings.Contains(entry.Name(), "cover") &&
(strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".png"))
}
func contains(s []string, searchTerm string) bool {
i := sort.SearchStrings(s, searchTerm)
return i < len(s) && s[i] == searchTerm
}
-92
View File
@@ -1,92 +0,0 @@
package database
import (
"errors"
"fmt"
"music-server/internal/db"
"os"
"strings"
)
type SongData struct {
GameId int
SongName string
Path string
TimesPlayed int
FileName string
}
var compareError = errors.New("no rows in result set")
func AddSong(song SongData) {
_, err := db.Dbpool.Exec(db.Ctx,
"INSERT INTO song(game_id, song_name, path, file_name) VALUES ($1, $2, $3, $4)",
song.GameId, song.SongName, song.Path, song.FileName)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func CheckSong(songPath string) bool {
var path string
err := db.Dbpool.QueryRow(db.Ctx,
"SELECT path FROM song WHERE path = $1", songPath).Scan(&path)
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
}
return path != ""
}
func UpdateSong(songName string, fileName string, path string) {
_, err := db.Dbpool.Exec(db.Ctx,
"UPDATE song SET song_name=$1, file_name=$2 WHERE path = $3",
songName, fileName, path)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func FetchAllSongs() []SongData {
rows, err := db.Dbpool.Query(db.Ctx,
"SELECT song_name, path FROM song")
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return nil
}
var songDataList []SongData
for rows.Next() {
var songName string
var path string
err := rows.Scan(&songName, &path)
if err != nil {
if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
}
songDataList = append(songDataList, SongData{
SongName: songName,
Path: path,
})
}
return songDataList
}
func RemoveBrokenSongs(songs []SongData) {
joined := ""
for _, song := range songs {
joined += "'" + song.Path + "',"
}
joined = strings.TrimSuffix(joined, ",")
_, err := db.Dbpool.Exec(db.Ctx, "DELETE FROM song where path in ($1)", joined)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
-1
View File
@@ -46,7 +46,6 @@ func InitDB(host string, port string, user string, password string, dbname strin
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
os.Exit(1) os.Exit(1)
} }
Testf()
fmt.Println(success) fmt.Println(success)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.27.0 // sqlc v1.31.1
package repository package repository
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.27.0 // sqlc v1.31.1
// source: game.sql // source: game.sql
package repository package repository
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.27.0 // sqlc v1.31.1
package repository package repository
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.27.0 // sqlc v1.31.1
// source: song.sql // source: song.sql
package repository package repository
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.27.0 // sqlc v1.31.1
// source: song_list.sql // source: song_list.sql
package repository package repository
+44
View File
@@ -0,0 +1,44 @@
package logging
import (
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"go.uber.org/zap"
)
// RequestLogger is an Echo middleware that logs HTTP requests using Zap
func RequestLogger() echo.MiddlewareFunc {
return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogStatus: true,
LogURI: true,
LogMethod: true,
HandleError: true,
LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
logger := GetLogger()
fields := []zap.Field{
zap.String("method", v.Method),
zap.String("uri", v.URI),
zap.Int("status", v.Status),
}
if v.Error != nil {
fields = append(fields, zap.String("error", v.Error.Error()))
logger.Error("Request error", fields...)
} else {
logger.Info("Request completed", fields...)
}
return nil
},
})
}
// ErrorHandler is a custom error handler that logs errors
func ErrorHandler(err error, c *echo.Context) {
logger := GetLogger()
logger.Error("Error occurred",
zap.String("method", c.Request().Method),
zap.String("path", c.Request().URL.Path),
zap.String("error", err.Error()),
)
}
+104
View File
@@ -0,0 +1,104 @@
package logging
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
// Logger is the global logger instance
Logger *zap.Logger
// SugaredLogger is the global sugared logger instance
SugaredLogger *zap.SugaredLogger
)
// Init initializes the logger with the specified level and config
func Init(level string, jsonOutput bool) {
var config zap.Config
// Set the log level
logLevel := zap.NewAtomicLevel()
err := logLevel.UnmarshalText([]byte(level))
if err != nil {
logLevel.SetLevel(zap.InfoLevel)
}
if jsonOutput {
// JSON output for Grafana Loki
config = zap.Config{
Level: logLevel,
Development: false,
Sampling: nil,
Encoding: "json",
EncoderConfig: zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "time",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "stacktrace",
SkipLineEnding: false,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
},
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
InitialFields: map[string]interface{}{"service": "music-server"},
}
} else {
// Human-readable output for development
config = zap.Config{
Level: logLevel,
Development: true,
Sampling: nil,
Encoding: "console",
EncoderConfig: zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "time",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "stacktrace",
SkipLineEnding: false,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
},
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
InitialFields: map[string]interface{}{"service": "music-server"},
}
}
logger, err := config.Build()
if err != nil {
panic(err)
}
Logger = logger
SugaredLogger = logger.Sugar()
}
// GetLogger returns the global logger
func GetLogger() *zap.Logger {
if Logger == nil {
Init("info", false)
}
return Logger
}
// GetSugaredLogger returns the global sugared logger
func GetSugaredLogger() *zap.SugaredLogger {
if SugaredLogger == nil {
Init("info", false)
}
return SugaredLogger
}
+71
View File
@@ -0,0 +1,71 @@
package server
import (
"github.com/labstack/echo/v5"
"music-server/internal/backend"
"music-server/internal/logging"
"net/http"
)
type DownloadHandler struct {
}
func NewDownloadHandler() *DownloadHandler {
return &DownloadHandler{}
}
// CheckLatest godoc
// @Summary Check for latest version
// @Description Checks for the latest version of the application
// @Tags download
// @Accept json
// @Produce json
// @Success 200 {string} string
// @Router /download [get]
func (d *DownloadHandler) checkLatest(ctx *echo.Context) error {
logging.GetLogger().Info("Checking latest version")
latest := backend.CheckLatest()
return ctx.JSON(http.StatusOK, latest)
}
// ListAssetsOfLatest godoc
// @Summary List assets of latest version
// @Description Lists all assets available for the latest version
// @Tags download
// @Accept json
// @Produce json
// @Success 200 {array} string
// @Router /download/list [get]
func (d *DownloadHandler) listAssetsOfLatest(ctx *echo.Context) error {
logging.GetLogger().Info("Listing assets")
assets := backend.ListAssetsOfLatest()
return ctx.JSON(http.StatusOK, assets)
}
// DownloadLatestWindows godoc
// @Summary Download latest Windows version
// @Description Redirects to download the latest Windows version
// @Tags download
// @Produce octet-stream
// @Success 302 {string} string
// @Router /download/windows [get]
func (d *DownloadHandler) downloadLatestWindows(ctx *echo.Context) error {
logging.GetLogger().Info("Downloading latest windows")
asset := backend.DownloadLatestWindows()
ctx.Response().Header().Set("Content-Type", "application/octet-stream")
return ctx.Redirect(http.StatusFound, asset)
}
// DownloadLatestLinux godoc
// @Summary Download latest Linux version
// @Description Redirects to download the latest Linux version
// @Tags download
// @Produce octet-stream
// @Success 302 {string} string
// @Router /download/linux [get]
func (d *DownloadHandler) downloadLatestLinux(ctx *echo.Context) error {
logging.GetLogger().Info("Downloading latest linux")
asset := backend.DownloadLatestLinux()
ctx.Response().Header().Set("Content-Type", "application/octet-stream")
return ctx.Redirect(http.StatusFound, asset)
}
+41 -8
View File
@@ -5,7 +5,7 @@ import (
"music-server/internal/db" "music-server/internal/db"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
type IndexHandler struct { type IndexHandler struct {
@@ -25,7 +25,7 @@ func NewIndexHandler() *IndexHandler {
// @Success 200 {object} backend.VersionData // @Success 200 {object} backend.VersionData
// @Failure 404 {object} string // @Failure 404 {object} string
// @Router /version [get] // @Router /version [get]
func (i *IndexHandler) GetVersion(ctx echo.Context) error { func (i *IndexHandler) GetVersion(ctx *echo.Context) error {
versionHistory := backend.GetVersionHistory() versionHistory := backend.GetVersionHistory()
if versionHistory.Version == "" { if versionHistory.Version == "" {
return ctx.JSON(http.StatusNotFound, "version not found") return ctx.JSON(http.StatusNotFound, "version not found")
@@ -33,21 +33,54 @@ func (i *IndexHandler) GetVersion(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, versionHistory) return ctx.JSON(http.StatusOK, versionHistory)
} }
func (i *IndexHandler) GetDBTest(ctx echo.Context) error { // 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() backend.TestDB()
return ctx.JSON(http.StatusOK, "TestedDB") return ctx.JSON(http.StatusOK, "TestedDB")
} }
func (i *IndexHandler) HealthCheck(ctx echo.Context) error { // 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()) return ctx.JSON(http.StatusOK, db.Health())
} }
func (i *IndexHandler) GetCharacters(ctx echo.Context) error { // GetCharacterList godoc
characters := backend.GetCharacters() // @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) return ctx.JSON(http.StatusOK, characters)
} }
func (i *IndexHandler) GetCharacter(ctx echo.Context) error { // GetCharacter godoc
character := ctx.QueryParam("character") // @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)) return ctx.File(backend.GetCharacter(character))
} }
+218 -32
View File
@@ -2,10 +2,13 @@ package server
import ( import (
"music-server/internal/backend" "music-server/internal/backend"
"music-server/internal/logging"
"net/http" "net/http"
"os" "os"
"strconv"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"go.uber.org/zap"
) )
type MusicHandler struct { type MusicHandler struct {
@@ -15,7 +18,23 @@ func NewMusicHandler() *MusicHandler {
return &MusicHandler{} return &MusicHandler{}
} }
func (m *MusicHandler) GetSong(ctx echo.Context) error { // GetSong godoc
// @Summary Get a specific song
// @Description Returns a specific song by name
// @Tags music
// @Accept json
// @Produce audio/mpeg
// @Param song query string true "Song name"
// @Success 200 {file} file
// @Failure 400 {string} string "song can't be empty"
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music [get]
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
song := ctx.QueryParam("song") 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")
@@ -23,117 +42,284 @@ func (m *MusicHandler) GetSong(ctx echo.Context) error {
songPath := backend.GetSong(song) songPath := backend.GetSong(song)
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetSoundCheckSong(ctx echo.Context) error { // GetSoundCheckSong godoc
// @Summary Get sound check song
// @Description Returns the sound check song
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/soundTest [get]
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetSoundCheckSong() songPath := backend.GetSoundCheckSong()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) ResetMusic(ctx echo.Context) error { // ResetMusic godoc
// @Summary Reset music state
// @Description Resets the music state
// @Tags music
// @Accept json
// @Success 204
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/reset [get]
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
backend.Reset() backend.Reset()
return ctx.NoContent(http.StatusOK) return ctx.NoContent(http.StatusOK)
} }
func (m *MusicHandler) GetRandomSong(ctx echo.Context) error { // GetRandomSong godoc
// @Summary Get random song
// @Description Returns a random song
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/rand [get]
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetRandomSong() songPath := backend.GetRandomSong()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetRandomSongLowChance(ctx echo.Context) error { // GetRandomSongLowChance godoc
// @Summary Get random song with low chance
// @Description Returns a random song with low chance selection
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/rand/low [get]
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetRandomSongLowChance() songPath := backend.GetRandomSongLowChance()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetRandomSongClassic(ctx echo.Context) error { // GetRandomSongClassic godoc
// @Summary Get random classic song
// @Description Returns a random song from the classic selection
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/rand/classic [get]
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetRandomSongClassic() songPath := backend.GetRandomSongClassic()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetSongInfo(ctx echo.Context) error { // GetSongInfo godoc
// @Summary Get current song info
// @Description Returns information about the current song
// @Tags music
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /music/info [get]
func (m *MusicHandler) GetSongInfo(ctx *echo.Context) error {
song := backend.GetSongInfo() song := backend.GetSongInfo()
return ctx.JSON(http.StatusOK, song) return ctx.JSON(http.StatusOK, song)
} }
func (m *MusicHandler) GetPlayedSongs(ctx echo.Context) error { // GetPlayedSongs godoc
// @Summary Get played songs list
// @Description Returns a list of played songs
// @Tags music
// @Accept json
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Router /music/list [get]
func (m *MusicHandler) GetPlayedSongs(ctx *echo.Context) error {
songList := backend.GetPlayedSongs() songList := backend.GetPlayedSongs()
return ctx.JSON(http.StatusOK, songList) return ctx.JSON(http.StatusOK, songList)
} }
func (m *MusicHandler) GetNextSong(ctx echo.Context) error { // GetNextSong godoc
// @Summary Get next song
// @Description Returns the next song in the queue
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/next [get]
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetNextSong() songPath := backend.GetNextSong()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetPreviousSong(ctx echo.Context) error { // GetPreviousSong godoc
// @Summary Get previous song
// @Description Returns the previous song in the queue
// @Tags music
// @Produce audio/mpeg
// @Success 200 {file} file
// @Failure 404 {string} string "Not Found"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/previous [get]
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
songPath := backend.GetPreviousSong() songPath := backend.GetPreviousSong()
file, err := os.Open(songPath) file, err := os.Open(songPath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err) return echo.NewHTTPError(http.StatusNotFound, err.Error())
} }
defer file.Close() defer file.Close()
return ctx.Stream(http.StatusOK, "audio/mpeg", file) return ctx.Stream(http.StatusOK, "audio/mpeg", file)
} }
func (m *MusicHandler) GetAllGames(ctx echo.Context) error { // GetAllGames godoc
// @Summary Get all games
// @Description Returns a list of all games in order
// @Tags music
// @Accept json
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/all/order [get]
func (m *MusicHandler) GetAllGames(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
gameList := backend.GetAllGames() gameList := backend.GetAllGames()
return ctx.JSON(http.StatusOK, gameList) return ctx.JSON(http.StatusOK, gameList)
} }
func (m *MusicHandler) GetAllGamesRandom(ctx echo.Context) error { // GetAllGamesRandom godoc
// @Summary Get all games random
// @Description Returns a list of all games in random order
// @Tags music
// @Accept json
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/all/random [get]
func (m *MusicHandler) GetAllGamesRandom(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
gameList := backend.GetAllGamesRandom() gameList := backend.GetAllGamesRandom()
return ctx.JSON(http.StatusOK, gameList) return ctx.JSON(http.StatusOK, gameList)
} }
type played struct { // PutPlayed godoc
Song int // @Summary Mark song as played
} // @Description Marks a song as played by its ID
// @Tags music
func (m *MusicHandler) PutPlayed(ctx echo.Context) error { // @Accept json
var played played // @Produce json
err := ctx.Bind(&played) // @Param song query int true "Song ID"
if err != nil { // @Success 204
return ctx.JSON(http.StatusBadRequest, err) // @Failure 400 {string} string "Bad Request"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/played [put]
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
} }
backend.SetPlayed(played.Song) song, err := strconv.Atoi(ctx.QueryParam("song"))
if err != nil {
return ctx.JSON(http.StatusBadRequest, err.Error())
}
logging.GetLogger().Info("Marking song as played", zap.Int("song_id", song))
backend.SetPlayed(song)
return ctx.NoContent(http.StatusOK) return ctx.NoContent(http.StatusOK)
} }
func (m *MusicHandler) AddLatestToQue(ctx echo.Context) error { // AddLatestToQue godoc
// @Summary Add latest to queue
// @Description Adds the latest song to the queue
// @Tags music
// @Accept json
// @Success 204
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/addQue [get]
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
backend.AddLatestToQue() backend.AddLatestToQue()
return ctx.NoContent(http.StatusOK) return ctx.NoContent(http.StatusOK)
} }
func (m *MusicHandler) AddLatestPlayed(ctx echo.Context) error { // AddLatestPlayed godoc
// @Summary Add latest to played
// @Description Adds the latest song to the played list
// @Tags music
// @Accept json
// @Success 204
// @Failure 423 {string} string "Syncing is in progress"
// @Router /music/addPlayed [get]
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Info("Syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
backend.AddLatestPlayed() backend.AddLatestPlayed()
return ctx.NoContent(http.StatusOK) return ctx.NoContent(http.StatusOK)
} }
+39 -21
View File
@@ -7,18 +7,35 @@ import (
"sort" "sort"
"strings" "strings"
_ "music-server/cmd/docs"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v5/middleware"
echoSwagger "github.com/swaggo/echo-swagger" // echo-swagger middleware echoSwagger "github.com/swaggo/echo-swagger/v2"
//_ "github.com/swaggo/echo-swagger/example/docs" // docs is generated by Swag CLI, you have to import it. "music-server/internal/logging"
) )
// @Title MusicServer API
// @version 1.0
// @description API for the MusicServer application
// @termsOfService http://sanplex.xyz/terms/
// @contact.name Sebastian Olsson
// @contact.email zarnor91@gmail.com
// @license.name MIT
// @license.url http://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /
func (s *Server) RegisterRoutes() http.Handler { func (s *Server) RegisterRoutes() http.Handler {
e := echo.New() e := echo.New()
e.Use(middleware.Logger())
// Serve OpenAPI spec at /openapi
e.GET("/openapi", echo.WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
http.ServeFile(w, r, "cmd/docs/swagger.json")
})))
e.Use(logging.RequestLogger())
e.Use(middleware.Recover()) e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
@@ -37,29 +54,30 @@ func (s *Server) RegisterRoutes() http.Handler {
e.Static("/", "/frontend") e.Static("/", "/frontend")
swagger := http.FileServer(http.FS(web.Swagger)) // Swagger UI
e.GET("/swagger/*", echo.WrapHandler(swagger)) e.GET("/swagger/*", echoSwagger.WrapHandler)
swaggerRedirect := func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/doc/index.html")
}
e.GET("/doc", swaggerRedirect)
e.GET("/doc/", swaggerRedirect)
e.GET("/doc/*", echoSwagger.WrapHandler)
index := NewIndexHandler() index := NewIndexHandler()
e.GET("/version", index.GetVersion) e.GET("/version", index.GetVersion)
e.GET("/dbtest", index.GetDBTest) e.GET("/dbtest", index.GetDBTest)
e.GET("/health", index.HealthCheck) e.GET("/health", index.HealthCheck)
e.GET("/character", index.GetCharacter) e.GET("/character", index.GetCharacter)
e.GET("/characters", index.GetCharacters) e.GET("/characters", index.GetCharacterList)
download := NewDownloadHandler()
e.GET("/download", download.checkLatest)
e.GET("/download/list", download.listAssetsOfLatest)
e.GET("/download/windows", download.downloadLatestWindows)
e.GET("/download/linux", download.downloadLatestLinux)
sync := NewSyncHandler() sync := NewSyncHandler()
syncGroup := e.Group("/sync") syncGroup := e.Group("/sync")
syncGroup.GET("", sync.SyncGames) syncGroup.GET("", sync.SyncGamesNewOnlyChanges)
syncGroup.GET("/progress", sync.SyncProgress)
syncGroup.GET("/new", sync.SyncGamesNewOnlyChanges) syncGroup.GET("/new", sync.SyncGamesNewOnlyChanges)
syncGroup.GET("/full", sync.SyncGamesNewFull)
syncGroup.GET("/new/full", sync.SyncGamesNewFull) syncGroup.GET("/new/full", sync.SyncGamesNewFull)
syncGroup.GET("/quick", sync.SyncGamesQuick) syncGroup.GET("/quick", sync.SyncGamesNewOnlyChanges)
syncGroup.GET("/reset", sync.ResetGames) syncGroup.GET("/reset", sync.ResetGames)
music := NewMusicHandler() music := NewMusicHandler()
@@ -81,13 +99,13 @@ func (s *Server) RegisterRoutes() http.Handler {
musicGroup.GET("/addQue", music.AddLatestToQue) musicGroup.GET("/addQue", music.AddLatestToQue)
musicGroup.GET("/addPlayed", music.AddLatestPlayed) musicGroup.GET("/addPlayed", music.AddLatestPlayed)
routes := e.Routes() routes := e.Router().Routes()
sort.Slice(routes, func(i, j int) bool { sort.Slice(routes, func(i, j int) bool {
return routes[i].Path < routes[j].Path return routes[i].Path < routes[j].Path
}) })
for _, r := range routes { for _, r := range routes {
if (r.Method == "GET" || r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE") && !strings.Contains(r.Name, "github") { if (r.Method == "GET" || r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE") && !strings.Contains(r.Name, "github") {
fmt.Printf(" %s %s\n", r.Method, r.Path) fmt.Printf(" %s\t\t%s\n", r.Method, r.Path)
} }
} }
return e return e
+36 -16
View File
@@ -2,12 +2,15 @@ package server
import ( import (
"fmt" "fmt"
"log"
"music-server/internal/db"
"net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
"music-server/internal/db"
"music-server/internal/logging"
"net/http"
"go.uber.org/zap"
) )
type Server struct { type Server struct {
@@ -15,31 +18,48 @@ type Server struct {
} }
var ( var (
host = os.Getenv("DB_HOST") host = os.Getenv("DB_HOST")
dbPort = os.Getenv("DB_PORT") dbPort = os.Getenv("DB_PORT")
dbName = os.Getenv("DB_NAME") dbName = os.Getenv("DB_NAME")
username = os.Getenv("DB_USERNAME") username = os.Getenv("DB_USERNAME")
password = os.Getenv("DB_PASSWORD") password = os.Getenv("DB_PASSWORD")
musicPath = os.Getenv("MUSIC_PATH") musicPath = os.Getenv("MUSIC_PATH")
charactersPath = os.Getenv("CHARACTERS_PATH")
logLevel = os.Getenv("LOG_LEVEL")
logJSON = os.Getenv("LOG_JSON") == "true"
) )
func NewServer() *http.Server { func NewServer() *http.Server {
// Initialize logger
if logLevel == "" {
logLevel = "info"
}
logging.Init(logLevel, logJSON)
logger := logging.GetLogger()
port, _ := strconv.Atoi(os.Getenv("PORT")) port, _ := strconv.Atoi(os.Getenv("PORT"))
NewServer := &Server{ NewServer := &Server{
port: port, port: port,
} }
logger.Info("Starting server",
zap.String("host", host),
zap.String("dbPort", dbPort),
zap.String("username", username),
zap.String("dbName", dbName),
)
logger.Info("Paths",
zap.String("musicPath", musicPath),
zap.String("charactersPath", charactersPath),
)
//conf.SetupDb() //conf.SetupDb()
if host == "" || dbPort == "" || username == "" || password == "" || dbName == "" || musicPath == "" { if host == "" || dbPort == "" || username == "" || password == "" || dbName == "" || musicPath == "" || charactersPath == "" {
log.Fatal("Invalid settings") logging.GetLogger().Fatal("Invalid settings - missing required environment variables")
} }
fmt.Printf("host: %s, dbPort: %v, username: %s, password: %s, dbName: %s\n",
host, dbPort, username, password, dbName)
log.Printf("Path: %s\n", musicPath)
db.Migrate_db(host, dbPort, username, password, dbName) db.Migrate_db(host, dbPort, username, password, dbName)
db.InitDB(host, dbPort, username, password, dbName) db.InitDB(host, dbPort, username, password, dbName)
+69 -25
View File
@@ -1,12 +1,11 @@
package server package server
import ( import (
"log"
"music-server/internal/backend" "music-server/internal/backend"
"music-server/internal/database" "music-server/internal/logging"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
type SyncHandler struct { type SyncHandler struct {
@@ -16,33 +15,78 @@ func NewSyncHandler() *SyncHandler {
return &SyncHandler{} return &SyncHandler{}
} }
func (s *SyncHandler) SyncGames(ctx echo.Context) error { // SyncProgress godoc
database.SyncGames() // @Summary Get sync progress
backend.Reset() // @Description Returns the current sync progress or result
return ctx.JSON(http.StatusOK, "Games are synced") // @Tags sync
} // @Accept json
// @Produce json
func (s *SyncHandler) SyncGamesQuick(ctx echo.Context) error { // @Success 200 {object} map[string]interface{}
database.SyncGamesQuick() // @Router /sync/progress [get]
backend.Reset() func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
return ctx.JSON(http.StatusOK, "Games are synced") if backend.Syncing {
} logging.GetLogger().Info("Getting sync progress")
response := backend.SyncProgress()
func (s *SyncHandler) SyncGamesNewOnlyChanges(ctx echo.Context) error { return ctx.JSON(http.StatusOK, response)
log.Println("Syncing games new") }
response := backend.SyncGamesNewOnlyChanges() logging.GetLogger().Info("Getting sync result")
backend.Reset() response := backend.SyncResult()
return ctx.JSON(http.StatusOK, response) return ctx.JSON(http.StatusOK, response)
} }
func (s *SyncHandler) SyncGamesNewFull(ctx echo.Context) error { // SyncGamesNewOnlyChanges godoc
log.Println("Syncing games new full") // @Summary Sync games with only changes
response := backend.SyncGamesNewFull() // @Description Starts syncing games with only new changes
backend.Reset() // @Tags sync
return ctx.JSON(http.StatusOK, response) // @Accept json
// @Produce json
// @Success 200 {string} string "Start syncing games"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /sync [get]
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")
go backend.SyncGamesNewOnlyChanges()
return ctx.JSON(http.StatusOK, "Start syncing games")
} }
func (s *SyncHandler) ResetGames(ctx echo.Context) error { // SyncGamesNewFull godoc
// @Summary Sync all games fully
// @Description Starts a full sync of all games
// @Tags sync
// @Accept json
// @Produce json
// @Success 200 {string} string "Start syncing games full"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /sync/full [get]
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")
go backend.SyncGamesNewFull()
return ctx.JSON(http.StatusOK, "Start syncing games full")
}
// ResetGames godoc
// @Summary Reset games database
// @Description Resets the games database by deleting all games and songs
// @Tags sync
// @Accept json
// @Produce json
// @Success 200 {string} string "Games and songs are deleted from the database"
// @Failure 423 {string} string "Syncing is in progress"
// @Router /sync/reset [get]
func (s *SyncHandler) ResetGames(ctx *echo.Context) error {
if backend.Syncing {
logging.GetLogger().Warn("Cannot reset - syncing is in progress")
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
}
logging.GetLogger().Info("Resetting games database")
backend.ResetDB() backend.ResetDB()
return ctx.JSON(http.StatusOK, "Games and songs are deleted from the database") return ctx.JSON(http.StatusOK, "Games and songs are deleted from the database")
} }
+34 -4
View File
@@ -41,10 +41,40 @@ sqlc-generate:
migrate-create name: migrate-create name:
@migrate create -ext sql -dir internal/db/migrations -seq {{name}} @migrate create -ext sql -dir internal/db/migrations -seq {{name}}
build: sqlc-generate templ-build tailwind-build swag-install:
@echo "Building..." @if ! command -v swag > /dev/null; then \
@swag init -d ./cmd/,./internal/backend/ -o ./cmd/docs read -p "Swag is not installed on your machine. Do you want to install it? [Y/n] " choice; \
@go build -o main cmd/main.go if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
go install github.com/swaggo/swag/cmd/swag@latest; \
if [ ! -x "$$(command -v swag)" ]; then \
echo "swag installation failed. Exiting..."; \
exit 1; \
fi; \
else \
echo "You chose not to install swag. Exiting..."; \
exit 1; \
fi; \
fi
swag-generate: swag-install
@echo "Generating OpenAPI docs..."
@swag init -g internal/server/routes.go -o cmd/docs
frontend-install:
@if ! command -v npm > /dev/null; then \
echo "npm is not installed on your machine. Please install Node.js first."; \
exit 1; \
fi
@cd cmd/frontend && npm install
frontend-build: frontend-install
@echo "Building frontend..."
@cd cmd/frontend && npm run build
[no-cd]
build: sqlc-generate templ-build swag-generate
@echo "Building..."
@go build -o main cmd/main.go
run: run:
@templ generate @templ generate
BIN
View File
Binary file not shown.