Compare commits
86 Commits
660d27aa2a
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| dbef39b828 | |||
| 4e5bdc4ee2 | |||
| 0894d65ec5 | |||
| 4033899a68 | |||
| c6a07e69e7 | |||
| 6d4a034753 | |||
| b0418b4f38 | |||
| 176848bb6d | |||
| fb387901cf | |||
| 0f29c33b1a | |||
| cec408187d | |||
| c60f40d7e3 | |||
| 2f407f6eef | |||
| 4c2db11cc5 | |||
| 06cbad708d | |||
| 89e884fae9 | |||
| 24a9111333 | |||
| 6cc014ffa3 | |||
| 8f8b555ea5 | |||
| a446dad7b6 | |||
| d152ec1f11 | |||
| 7a3934babf | |||
| 08f539abd9 | |||
| 87a1a2d89a | |||
| 1ada52f5f8 | |||
| 92b82da3af | |||
| b71072f6c8 | |||
| d481be04a7 | |||
| 870f1787cb | |||
| 89c31c2856 | |||
| f0653489d6 | |||
| d0fbba86f1 | |||
| bd0e7f4a8d | |||
| b5926e3b31 | |||
| 37909139de | |||
| 82252ce1ff | |||
| 1dab9d6e7c | |||
| b80ad90eab | |||
| 2cff8d16d7 | |||
| 12f18ba12c | |||
| 6e2c381d90 | |||
| efca22834b | |||
| e57609725e | |||
| fabd6a6931 | |||
| f03e001bdd | |||
| 1d77ae491c | |||
| c0d1aaa4d1 | |||
| 76aaa884fa | |||
| 290d79ef5e | |||
| aa0b8275e7 | |||
| c369b13fae | |||
| bef915ac6d | |||
| cff777f278 | |||
| 61cab73ffc | |||
| a6294e46f2 | |||
| 5f91643b4d | |||
| 806e88adeb | |||
| 0d1c69d95e | |||
| b024c0b747 | |||
| 75ee924783 | |||
| f86c33d5e6 | |||
| ef41d0fa11 | |||
| fd666dd3fa | |||
| 231867de40 | |||
| 9a9d318771 | |||
| f06a7fe927 | |||
| 0f017407ff | |||
| 29ba39f5fe | |||
| 8d01fe100a | |||
| 2821774215 | |||
| 00f0981ce4 | |||
| 53a9031cb0 | |||
| 85204026bb | |||
| 1ffddd1154 | |||
| a4ef66a3f8 | |||
| 478de6e3d4 | |||
| 052b699025 | |||
| 999668fc9c | |||
| 11e6233753 | |||
| 3f73ea1f5e | |||
| d15d1422da | |||
| 73d85adc42 | |||
| d653463f58 | |||
| db8214cb02 | |||
| 5b640375c3 | |||
| 034ba35fbb |
@@ -0,0 +1,15 @@
|
|||||||
|
# Test Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5433
|
||||||
|
DB_USERNAME=testuser
|
||||||
|
DB_PASSWORD=testpass
|
||||||
|
DB_NAME=music_server_test
|
||||||
|
|
||||||
|
# Test Paths
|
||||||
|
MUSIC_PATH=/Users/sebastian/projects/MusicServer/testMusic
|
||||||
|
CHARACTERS_PATH=/Users/sebastian/projects/MusicServer/testCharacters
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=8081
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
LOG_JSON=false
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
#on:
|
||||||
|
# release:
|
||||||
|
# types: [published]
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# test:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - run: echo "The release ${{ gitea.ref }} ${{ gitea.ref_name }} was published"
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."gitea.sanplex.xyz/sansan"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
- name: Login to Gitea
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.sanplex.xyz
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
- name: Build
|
||||||
|
uses: https://github.com/docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: false
|
||||||
|
#tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
name: Publish
|
||||||
|
|
||||||
|
#on:
|
||||||
|
# release:
|
||||||
|
# types: [published]
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# test:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - run: echo "The release ${{ gitea.ref }} ${{ gitea.ref_name }} was published"
|
||||||
|
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."gitea.sanplex.xyz/sansan"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
- name: Login to Gitea
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.sanplex.xyz
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: https://github.com/docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: "gitea.sanplex.xyz/sansan/musicserver:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicserver:latest"
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Integration Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # Manual trigger only
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: golang:1.25
|
||||||
|
options: --privileged
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: testuser
|
||||||
|
POSTGRES_PASSWORD: testpass
|
||||||
|
POSTGRES_DB: music_server_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Install testcontainers
|
||||||
|
run: go install github.com/testcontainers/testcontainers-go@latest
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
env:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USERNAME: testuser
|
||||||
|
DB_PASSWORD: testpass
|
||||||
|
DB_NAME: music_server_test
|
||||||
|
MUSIC_PATH: ./testMusic
|
||||||
|
CHARACTERS_PATH: ./testCharacters
|
||||||
|
run: go test -v -timeout 30m ./...
|
||||||
@@ -6,3 +6,9 @@ conf.yaml
|
|||||||
output.css
|
output.css
|
||||||
compose.yaml
|
compose.yaml
|
||||||
tailwindcss
|
tailwindcss
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
cpu.pprof
|
||||||
|
main
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,33 +1,42 @@
|
|||||||
FROM golang:1.23-alpine as build_go
|
# Stage 1: Build frontend
|
||||||
RUN apk add --no-cache curl
|
FROM node:18-alpine AS frontend-builder
|
||||||
|
RUN apk add --no-cache git
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN git clone https://gitea.sanplex.xyz/Sansan/MusicFrontend.git
|
||||||
|
WORKDIR /app/MusicFrontend
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
# Generate config.js with empty API_HOSTNAME (relative paths)
|
||||||
|
RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > dist/config.js
|
||||||
|
|
||||||
|
# Stage 2: Build backend
|
||||||
|
FROM golang:1.25-alpine as build_go
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go install github.com/a-h/templ/cmd/templ@latest && \
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
templ generate && \
|
RUN templ generate
|
||||||
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && \
|
|
||||||
chmod +x tailwindcss && \
|
|
||||||
./tailwindcss -i cmd/web/assets/css/input.css -o cmd/web/assets/css/output.css
|
|
||||||
|
|
||||||
RUN go build -o main cmd/main.go
|
RUN go build -o main cmd/main.go
|
||||||
|
|
||||||
# Stage 2, distribution container
|
# Stage 3: Final image
|
||||||
FROM golang:1.23-alpine
|
FROM golang:1.25-alpine
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME /sorted
|
VOLUME /sorted
|
||||||
VOLUME /frontend
|
VOLUME /characters
|
||||||
|
|
||||||
|
COPY --from=build_go /app/main .
|
||||||
|
COPY --from=frontend-builder /app/MusicFrontend/dist /frontend
|
||||||
|
COPY ./songs/ ./songs/
|
||||||
|
|
||||||
|
ENV PORT 8080
|
||||||
ENV DB_HOST ""
|
ENV DB_HOST ""
|
||||||
ENV DB_PORT ""
|
ENV DB_PORT ""
|
||||||
ENV DB_USERNAME ""
|
ENV DB_USERNAME ""
|
||||||
ENV DB_PASSWORD ""
|
ENV DB_PASSWORD ""
|
||||||
ENV DB_NAME ""
|
ENV DB_NAME ""
|
||||||
|
ENV MUSIC_PATH ""
|
||||||
COPY --from=build_go /app/main .
|
ENV CHARACTERS_PATH ""
|
||||||
COPY ./songs/ ./songs/
|
|
||||||
|
|
||||||
CMD ./main
|
CMD ./main
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,964 @@
|
|||||||
|
definitions:
|
||||||
|
backend.GameWithSongs:
|
||||||
|
properties:
|
||||||
|
game_id:
|
||||||
|
type: integer
|
||||||
|
game_last_played:
|
||||||
|
type: string
|
||||||
|
game_name:
|
||||||
|
type: string
|
||||||
|
game_played:
|
||||||
|
type: integer
|
||||||
|
songs:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
backend.SongInfoForStats:
|
||||||
|
properties:
|
||||||
|
file_name:
|
||||||
|
type: string
|
||||||
|
game_id:
|
||||||
|
type: integer
|
||||||
|
game_name:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
song_name:
|
||||||
|
type: string
|
||||||
|
times_played:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
backend.StatisticsSummary:
|
||||||
|
properties:
|
||||||
|
avg_game_plays:
|
||||||
|
type: number
|
||||||
|
max_game_plays:
|
||||||
|
type: integer
|
||||||
|
min_game_plays:
|
||||||
|
type: integer
|
||||||
|
never_played_games:
|
||||||
|
type: integer
|
||||||
|
played_games:
|
||||||
|
type: integer
|
||||||
|
total_game_plays:
|
||||||
|
type: integer
|
||||||
|
total_games:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
backend.VersionData:
|
||||||
|
properties:
|
||||||
|
changelog:
|
||||||
|
example:
|
||||||
|
- '["Initial release"'
|
||||||
|
- '"Bug fixes"]'
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
version:
|
||||||
|
example: 1.0.0
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
server.TokenRequest:
|
||||||
|
properties:
|
||||||
|
client_type:
|
||||||
|
description: 'Optional: "web", "mobile", "api"'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
server.TokenResponse:
|
||||||
|
properties:
|
||||||
|
client_type:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
paths:
|
||||||
|
/api/v1/statistics/games/last-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the most recently played games
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get last played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/least-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N least played games with their songs
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get least played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/most-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N most played games with their songs
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get most played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/never-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all games that have never been played (times_played = 0)
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get never played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/oldest-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the least recently played games (that have been played
|
||||||
|
at least once)
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get oldest played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/songs/least-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N least played songs with their game info
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get least played songs
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/songs/most-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N most played songs with their game info
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get most played songs
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/summary:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns overall statistics about the music library
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/backend.StatisticsSummary'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get statistics summary
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/token:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Deletes the current session token
|
||||||
|
parameters:
|
||||||
|
- description: Bearer token
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Invalidate session token
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns a new session token for API access
|
||||||
|
parameters:
|
||||||
|
- description: Client type
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/server.TokenRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/server.TokenResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Create session token
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/api/v1/token/cleanup:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Removes all expired session tokens from the database
|
||||||
|
parameters:
|
||||||
|
- description: Bearer token
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Cleanup expired sessions
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/character:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the image for a specific character
|
||||||
|
parameters:
|
||||||
|
- description: Character name
|
||||||
|
in: query
|
||||||
|
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
|
||||||
|
/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 soundtracks
|
||||||
|
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 soundtracks 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 soundtracks
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"423":
|
||||||
|
description: Syncing is in progress
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Sync soundtracks 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 soundtracks 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: Soundtracks and songs are deleted from the database
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"423":
|
||||||
|
description: Syncing is in progress
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Reset soundtracks database
|
||||||
|
tags:
|
||||||
|
- sync
|
||||||
|
/version:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: get latest version info
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/backend.VersionData'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Getting the latest version of the backend
|
||||||
|
tags:
|
||||||
|
- version
|
||||||
|
/version/history:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: get version history
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.VersionData'
|
||||||
|
type: array
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Getting the version history of the backend
|
||||||
|
tags:
|
||||||
|
- version
|
||||||
|
swagger: "2.0"
|
||||||
@@ -8,24 +8,22 @@
|
|||||||
"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.17.1",
|
"express": "^4.19.2",
|
||||||
"nodemon": "^2.0.7",
|
"vue": "^3.4.31",
|
||||||
"vue": "^3.0.5",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-axios": "^3.2.2",
|
"vuex": "^4.1.0"
|
||||||
"vuex": "^4.0.0-rc.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.5.10",
|
"@vue/cli-plugin-babel": "^5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "^4.5.10",
|
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||||
"@vue/cli-service": "^4.5.10",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
"@vue/compiler-sfc": "^3.4.31",
|
||||||
"babel-eslint": "^10.1.0",
|
"@babel/eslint-parser": "^7.25.1",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^7.4.1"
|
"eslint-plugin-vue": "^9.27.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
@@ -37,7 +35,18 @@
|
|||||||
"eslint:recommended"
|
"eslint:recommended"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"parser": "babel-eslint"
|
"parser": "@babel/eslint-parser",
|
||||||
|
"requireConfigFile": false,
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"globalReturn": false,
|
||||||
|
"impliedStrict": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"babelOptions": {
|
||||||
|
"presets": ["@babel/preset-env"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-debugger": 1
|
"no-debugger": 1
|
||||||
|
|||||||
@@ -1,17 +1,85 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"context"
|
||||||
"music-server/pkg/conf"
|
"music-server/internal/logging"
|
||||||
|
"music-server/internal/server"
|
||||||
|
"net/http"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed swagger
|
//
|
||||||
var swagger embed.FS
|
// @Title Swagger Example API
|
||||||
|
// @version 0.5
|
||||||
|
// @description This is a sample server Petstore server.
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
|
//
|
||||||
|
// @contact.name Sebastian Olsson
|
||||||
|
// @contact.email zarnor91@gmail.com
|
||||||
|
|
||||||
|
//
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
// @host localhost:8080
|
||||||
func main() {
|
func main() {
|
||||||
conf.SetupDb()
|
/*f, perr := os.Create("cpu.pprof")
|
||||||
|
if perr != nil {
|
||||||
|
log.Fatal(perr)
|
||||||
|
}
|
||||||
|
pprof.StartCPUProfile(f)
|
||||||
|
defer pprof.StopCPUProfile()*/
|
||||||
|
|
||||||
conf.SetupRestServer(swagger)
|
appServer := server.NewServerInstance()
|
||||||
|
httpServer := appServer.HTTPServer()
|
||||||
|
|
||||||
conf.CloseDb()
|
// Create a done channel to signal when the shutdown is complete
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
|
||||||
|
// Run graceful shutdown in a separate goroutine
|
||||||
|
go gracefulShutdown(appServer, httpServer, done)
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Server starting", zap.String("address", httpServer.Addr))
|
||||||
|
err := httpServer.ListenAndServe()
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
logging.GetLogger().Fatal("HTTP server error", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the graceful shutdown to complete
|
||||||
|
<-done
|
||||||
|
logging.GetLogger().Info("Graceful shutdown complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func gracefulShutdown(appServer *server.Server, httpServer *http.Server, done chan bool) {
|
||||||
|
// Create context that listens for the interrupt signal from the OS.
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
// Listen for the interrupt signal.
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Shutting down gracefully, press Ctrl+C again to force")
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
if appServer != nil && appServer.DB() != nil {
|
||||||
|
appServer.DB().Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The context is used to inform the server it has 5 seconds to finish
|
||||||
|
// the request it is currently handling
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := httpServer.Shutdown(ctx); err != nil {
|
||||||
|
logging.GetLogger().Error("Server forced to shutdown with error", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Server exiting")
|
||||||
|
|
||||||
|
// Notify the main goroutine that the shutdown is complete
|
||||||
|
done <- true
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.6 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://b6bierb3v4yoq"
|
|
||||||
path="res://.godot/imported/index.144x144.png-03ea9bc9a40782e35fa8c68f683d2623.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.144x144.png"
|
|
||||||
dest_files=["res://.godot/imported/index.144x144.png-03ea9bc9a40782e35fa8c68f683d2623.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://b0euyv6xwufjw"
|
|
||||||
path="res://.godot/imported/index.180x180.png-9c97e3aaba17027b76e6e765bcdc0add.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.180x180.png"
|
|
||||||
dest_files=["res://.godot/imported/index.180x180.png-9c97e3aaba17027b76e6e765bcdc0add.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
|
Before Width: | Height: | Size: 49 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://c30mhkc1yernc"
|
|
||||||
path="res://.godot/imported/index.512x512.png-4cda31773312dccaaf53a4ff122aad13.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.512x512.png"
|
|
||||||
dest_files=["res://.godot/imported/index.512x512.png-4cda31773312dccaaf53a4ff122aad13.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://d0tny2k1tdlnq"
|
|
||||||
path="res://.godot/imported/index.apple-touch-icon.png-939ec69c79bbf504b92ad8ceed469d51.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.apple-touch-icon.png"
|
|
||||||
dest_files=["res://.godot/imported/index.apple-touch-icon.png-939ec69c79bbf504b92ad8ceed469d51.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
/**************************************************************************/
|
|
||||||
/* audio.worklet.js */
|
|
||||||
/**************************************************************************/
|
|
||||||
/* This file is part of: */
|
|
||||||
/* GODOT ENGINE */
|
|
||||||
/* https://godotengine.org */
|
|
||||||
/**************************************************************************/
|
|
||||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
|
||||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
|
||||||
/* */
|
|
||||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
||||||
/* a copy of this software and associated documentation files (the */
|
|
||||||
/* "Software"), to deal in the Software without restriction, including */
|
|
||||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
||||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
||||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
||||||
/* the following conditions: */
|
|
||||||
/* */
|
|
||||||
/* The above copyright notice and this permission notice shall be */
|
|
||||||
/* included in all copies or substantial portions of the Software. */
|
|
||||||
/* */
|
|
||||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
||||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
||||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
|
||||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
||||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
||||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
||||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
||||||
/**************************************************************************/
|
|
||||||
|
|
||||||
class RingBuffer {
|
|
||||||
constructor(p_buffer, p_state, p_threads) {
|
|
||||||
this.buffer = p_buffer;
|
|
||||||
this.avail = p_state;
|
|
||||||
this.threads = p_threads;
|
|
||||||
this.rpos = 0;
|
|
||||||
this.wpos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
data_left() {
|
|
||||||
return this.threads ? Atomics.load(this.avail, 0) : this.avail;
|
|
||||||
}
|
|
||||||
|
|
||||||
space_left() {
|
|
||||||
return this.buffer.length - this.data_left();
|
|
||||||
}
|
|
||||||
|
|
||||||
read(output) {
|
|
||||||
const size = this.buffer.length;
|
|
||||||
let from = 0;
|
|
||||||
let to_write = output.length;
|
|
||||||
if (this.rpos + to_write > size) {
|
|
||||||
const high = size - this.rpos;
|
|
||||||
output.set(this.buffer.subarray(this.rpos, size));
|
|
||||||
from = high;
|
|
||||||
to_write -= high;
|
|
||||||
this.rpos = 0;
|
|
||||||
}
|
|
||||||
if (to_write) {
|
|
||||||
output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
|
|
||||||
}
|
|
||||||
this.rpos += to_write;
|
|
||||||
if (this.threads) {
|
|
||||||
Atomics.add(this.avail, 0, -output.length);
|
|
||||||
Atomics.notify(this.avail, 0);
|
|
||||||
} else {
|
|
||||||
this.avail -= output.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write(p_buffer) {
|
|
||||||
const to_write = p_buffer.length;
|
|
||||||
const mw = this.buffer.length - this.wpos;
|
|
||||||
if (mw >= to_write) {
|
|
||||||
this.buffer.set(p_buffer, this.wpos);
|
|
||||||
this.wpos += to_write;
|
|
||||||
if (mw === to_write) {
|
|
||||||
this.wpos = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const high = p_buffer.subarray(0, mw);
|
|
||||||
const low = p_buffer.subarray(mw);
|
|
||||||
this.buffer.set(high, this.wpos);
|
|
||||||
this.buffer.set(low);
|
|
||||||
this.wpos = low.length;
|
|
||||||
}
|
|
||||||
if (this.threads) {
|
|
||||||
Atomics.add(this.avail, 0, to_write);
|
|
||||||
Atomics.notify(this.avail, 0);
|
|
||||||
} else {
|
|
||||||
this.avail += to_write;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GodotProcessor extends AudioWorkletProcessor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.threads = false;
|
|
||||||
this.running = true;
|
|
||||||
this.lock = null;
|
|
||||||
this.notifier = null;
|
|
||||||
this.output = null;
|
|
||||||
this.output_buffer = new Float32Array();
|
|
||||||
this.input = null;
|
|
||||||
this.input_buffer = new Float32Array();
|
|
||||||
this.port.onmessage = (event) => {
|
|
||||||
const cmd = event.data['cmd'];
|
|
||||||
const data = event.data['data'];
|
|
||||||
this.parse_message(cmd, data);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
process_notify() {
|
|
||||||
if (this.notifier) {
|
|
||||||
Atomics.add(this.notifier, 0, 1);
|
|
||||||
Atomics.notify(this.notifier, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_message(p_cmd, p_data) {
|
|
||||||
if (p_cmd === 'start' && p_data) {
|
|
||||||
const state = p_data[0];
|
|
||||||
let idx = 0;
|
|
||||||
this.threads = true;
|
|
||||||
this.lock = state.subarray(idx, ++idx);
|
|
||||||
this.notifier = state.subarray(idx, ++idx);
|
|
||||||
const avail_in = state.subarray(idx, ++idx);
|
|
||||||
const avail_out = state.subarray(idx, ++idx);
|
|
||||||
this.input = new RingBuffer(p_data[1], avail_in, true);
|
|
||||||
this.output = new RingBuffer(p_data[2], avail_out, true);
|
|
||||||
} else if (p_cmd === 'stop') {
|
|
||||||
this.running = false;
|
|
||||||
this.output = null;
|
|
||||||
this.input = null;
|
|
||||||
this.lock = null;
|
|
||||||
this.notifier = null;
|
|
||||||
} else if (p_cmd === 'start_nothreads') {
|
|
||||||
this.output = new RingBuffer(p_data[0], p_data[0].length, false);
|
|
||||||
} else if (p_cmd === 'chunk') {
|
|
||||||
this.output.write(p_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static array_has_data(arr) {
|
|
||||||
return arr.length && arr[0].length && arr[0][0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
|
||||||
if (!this.running) {
|
|
||||||
return false; // Stop processing.
|
|
||||||
}
|
|
||||||
if (this.output === null) {
|
|
||||||
return true; // Not ready yet, keep processing.
|
|
||||||
}
|
|
||||||
const process_input = GodotProcessor.array_has_data(inputs);
|
|
||||||
if (process_input) {
|
|
||||||
const input = inputs[0];
|
|
||||||
const chunk = input[0].length * input.length;
|
|
||||||
if (this.input_buffer.length !== chunk) {
|
|
||||||
this.input_buffer = new Float32Array(chunk);
|
|
||||||
}
|
|
||||||
if (!this.threads) {
|
|
||||||
GodotProcessor.write_input(this.input_buffer, input);
|
|
||||||
this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
|
|
||||||
} else if (this.input.space_left() >= chunk) {
|
|
||||||
GodotProcessor.write_input(this.input_buffer, input);
|
|
||||||
this.input.write(this.input_buffer);
|
|
||||||
} else {
|
|
||||||
// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const process_output = GodotProcessor.array_has_data(outputs);
|
|
||||||
if (process_output) {
|
|
||||||
const output = outputs[0];
|
|
||||||
const chunk = output[0].length * output.length;
|
|
||||||
if (this.output_buffer.length !== chunk) {
|
|
||||||
this.output_buffer = new Float32Array(chunk);
|
|
||||||
}
|
|
||||||
if (this.output.data_left() >= chunk) {
|
|
||||||
this.output.read(this.output_buffer);
|
|
||||||
GodotProcessor.write_output(output, this.output_buffer);
|
|
||||||
if (!this.threads) {
|
|
||||||
this.port.postMessage({ 'cmd': 'read', 'data': chunk });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.process_notify();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static write_output(dest, source) {
|
|
||||||
const channels = dest.length;
|
|
||||||
for (let ch = 0; ch < channels; ch++) {
|
|
||||||
for (let sample = 0; sample < dest[ch].length; sample++) {
|
|
||||||
dest[ch][sample] = source[sample * channels + ch];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static write_input(dest, source) {
|
|
||||||
const channels = source.length;
|
|
||||||
for (let ch = 0; ch < channels; ch++) {
|
|
||||||
for (let sample = 0; sample < source[ch].length; sample++) {
|
|
||||||
dest[sample * channels + ch] = source[ch][sample];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerProcessor('godot-processor', GodotProcessor);
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
|
|
||||||
<title>MusicSearch</title>
|
|
||||||
<style>
|
|
||||||
html, body, #canvas {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: white;
|
|
||||||
background-color: black;
|
|
||||||
overflow: hidden;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status, #status-splash, #status-progress {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status, #status-splash {
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status {
|
|
||||||
background-color: #242424;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-splash {
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-progress, #status-notice {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-progress {
|
|
||||||
bottom: 10%;
|
|
||||||
width: 50%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-notice {
|
|
||||||
background-color: #5b3943;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid #9b3943;
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
|
|
||||||
line-height: 1.3;
|
|
||||||
margin: 0 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
|
|
||||||
<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
|
|
||||||
<link rel="manifest" href="index.manifest.json">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<canvas id="canvas">
|
|
||||||
Your browser does not support the canvas tag.
|
|
||||||
</canvas>
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
Your browser does not support JavaScript.
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="status">
|
|
||||||
<img id="status-splash" src="index.png" alt="">
|
|
||||||
<progress id="status-progress"></progress>
|
|
||||||
<div id="status-notice"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="index.js"></script>
|
|
||||||
<script>
|
|
||||||
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":false,"executable":"index","experimentalVK":false,"fileSizes":{"index.pck":86816,"index.wasm":35376909},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
|
|
||||||
const GODOT_THREADS_ENABLED = false;
|
|
||||||
const engine = new Engine(GODOT_CONFIG);
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
const statusOverlay = document.getElementById('status');
|
|
||||||
const statusProgress = document.getElementById('status-progress');
|
|
||||||
const statusNotice = document.getElementById('status-notice');
|
|
||||||
|
|
||||||
let initializing = true;
|
|
||||||
let statusMode = '';
|
|
||||||
|
|
||||||
function setStatusMode(mode) {
|
|
||||||
if (statusMode === mode || !initializing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === 'hidden') {
|
|
||||||
statusOverlay.remove();
|
|
||||||
initializing = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
statusOverlay.style.visibility = 'visible';
|
|
||||||
statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
|
|
||||||
statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
|
|
||||||
statusMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatusNotice(text) {
|
|
||||||
while (statusNotice.lastChild) {
|
|
||||||
statusNotice.removeChild(statusNotice.lastChild);
|
|
||||||
}
|
|
||||||
const lines = text.split('\n');
|
|
||||||
lines.forEach((line) => {
|
|
||||||
statusNotice.appendChild(document.createTextNode(line));
|
|
||||||
statusNotice.appendChild(document.createElement('br'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayFailureNotice(err) {
|
|
||||||
console.error(err);
|
|
||||||
if (err instanceof Error) {
|
|
||||||
setStatusNotice(err.message);
|
|
||||||
} else if (typeof err === 'string') {
|
|
||||||
setStatusNotice(err);
|
|
||||||
} else {
|
|
||||||
setStatusNotice('An unknown error occured');
|
|
||||||
}
|
|
||||||
setStatusMode('notice');
|
|
||||||
initializing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const missing = Engine.getMissingFeatures({
|
|
||||||
threads: GODOT_THREADS_ENABLED,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (missing.length !== 0) {
|
|
||||||
if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
|
|
||||||
// There's a chance that installing the service worker would fix the issue
|
|
||||||
Promise.race([
|
|
||||||
navigator.serviceWorker.getRegistration().then((registration) => {
|
|
||||||
if (registration != null) {
|
|
||||||
return Promise.reject(new Error('Service worker already exists.'));
|
|
||||||
}
|
|
||||||
return registration;
|
|
||||||
}).then(() => engine.installServiceWorker()),
|
|
||||||
// For some reason, `getRegistration()` can stall
|
|
||||||
new Promise((resolve) => {
|
|
||||||
setTimeout(() => resolve(), 2000);
|
|
||||||
}),
|
|
||||||
]).catch((err) => {
|
|
||||||
console.error('Error while registering service worker:', err);
|
|
||||||
}).then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Display the message as usual
|
|
||||||
const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
|
|
||||||
displayFailureNotice(missingMsg + missing.join('\n'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setStatusMode('progress');
|
|
||||||
engine.startGame({
|
|
||||||
'onProgress': function (current, total) {
|
|
||||||
if (current > 0 && total > 0) {
|
|
||||||
statusProgress.value = current;
|
|
||||||
statusProgress.max = total;
|
|
||||||
} else {
|
|
||||||
statusProgress.removeAttribute('value');
|
|
||||||
statusProgress.removeAttribute('max');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
setStatusMode('hidden');
|
|
||||||
}, displayFailureNotice);
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://c8gblkbcsrrwo"
|
|
||||||
path="res://.godot/imported/index.icon.png-c615af856eabc03ca93d51f6a8f07b60.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.icon.png"
|
|
||||||
dest_files=["res://.godot/imported/index.icon.png-c615af856eabc03ca93d51f6a8f07b60.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"background_color":"#000000","display":"standalone","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"MusicSearch","orientation":"any","start_url":"./index.html"}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>You are offline</title>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
background-color: #000000;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
margin: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-block: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
margin: 3rem auto 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>You are offline</h1>
|
|
||||||
<p>This application requires an Internet connection to run for the first time.</p>
|
|
||||||
<p>Press the button below to try reloading:</p>
|
|
||||||
<button type="button">Reload</button>
|
|
||||||
<script>
|
|
||||||
document.querySelector('button').addEventListener('click', () => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,34 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://bxh15ilo8hoq"
|
|
||||||
path="res://.godot/imported/index.png-80964ad67552d78b1e33f58ad16188ff.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://web/index.png"
|
|
||||||
dest_files=["res://.godot/imported/index.png-80964ad67552d78b1e33f58ad16188ff.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
// This service worker is required to expose an exported Godot project as a
|
|
||||||
// Progressive Web App. It provides an offline fallback page telling the user
|
|
||||||
// that they need an Internet connection to run the project if desired.
|
|
||||||
// Incrementing CACHE_VERSION will kick off the install event and force
|
|
||||||
// previously cached resources to be updated from the network.
|
|
||||||
/** @type {string} */
|
|
||||||
const CACHE_VERSION = '1726998131|1051448710';
|
|
||||||
/** @type {string} */
|
|
||||||
const CACHE_PREFIX = 'MusicSearch-sw-cache-';
|
|
||||||
const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
|
|
||||||
/** @type {string} */
|
|
||||||
const OFFLINE_URL = 'index.offline.html';
|
|
||||||
/** @type {boolean} */
|
|
||||||
const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = false;
|
|
||||||
// Files that will be cached on load.
|
|
||||||
/** @type {string[]} */
|
|
||||||
const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
|
|
||||||
// Files that we might not want the user to preload, and will only be cached on first load.
|
|
||||||
/** @type {string[]} */
|
|
||||||
const CACHABLE_FILES = ["index.wasm","index.pck"];
|
|
||||||
const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
|
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
event.waitUntil(caches.keys().then(
|
|
||||||
function (keys) {
|
|
||||||
// Remove old caches.
|
|
||||||
return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
|
|
||||||
}
|
|
||||||
).then(function () {
|
|
||||||
// Enable navigation preload if available.
|
|
||||||
return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that the response has the correct COEP/COOP headers
|
|
||||||
* @param {Response} response
|
|
||||||
* @returns {Response}
|
|
||||||
*/
|
|
||||||
function ensureCrossOriginIsolationHeaders(response) {
|
|
||||||
if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
|
|
||||||
&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const crossOriginIsolatedHeaders = new Headers(response.headers);
|
|
||||||
crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
||||||
crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
||||||
const newResponse = new Response(response.body, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: crossOriginIsolatedHeaders,
|
|
||||||
});
|
|
||||||
|
|
||||||
return newResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls fetch and cache the result if it is cacheable
|
|
||||||
* @param {FetchEvent} event
|
|
||||||
* @param {Cache} cache
|
|
||||||
* @param {boolean} isCacheable
|
|
||||||
* @returns {Response}
|
|
||||||
*/
|
|
||||||
async function fetchAndCache(event, cache, isCacheable) {
|
|
||||||
// Use the preloaded response, if it's there
|
|
||||||
/** @type { Response } */
|
|
||||||
let response = await event.preloadResponse;
|
|
||||||
if (response == null) {
|
|
||||||
// Or, go over network.
|
|
||||||
response = await self.fetch(event.request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
|
|
||||||
response = ensureCrossOriginIsolationHeaders(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCacheable) {
|
|
||||||
// And update the cache
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener(
|
|
||||||
'fetch',
|
|
||||||
/**
|
|
||||||
* Triggered on fetch
|
|
||||||
* @param {FetchEvent} event
|
|
||||||
*/
|
|
||||||
(event) => {
|
|
||||||
const isNavigate = event.request.mode === 'navigate';
|
|
||||||
const url = event.request.url || '';
|
|
||||||
const referrer = event.request.referrer || '';
|
|
||||||
const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
|
|
||||||
const local = url.startsWith(base) ? url.replace(base, '') : '';
|
|
||||||
const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
|
|
||||||
if (isNavigate || isCachable) {
|
|
||||||
event.respondWith((async () => {
|
|
||||||
// Try to use cache first
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
if (isNavigate) {
|
|
||||||
// Check if we have full cache during HTML page request.
|
|
||||||
/** @type {Response[]} */
|
|
||||||
const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
|
|
||||||
const missing = fullCache.some((v) => v === undefined);
|
|
||||||
if (missing) {
|
|
||||||
try {
|
|
||||||
// Try network if some cached file is missing (so we can display offline page in case).
|
|
||||||
const response = await fetchAndCache(event, cache, isCachable);
|
|
||||||
return response;
|
|
||||||
} catch (e) {
|
|
||||||
// And return the hopefully always cached offline page in case of network failure.
|
|
||||||
console.error('Network error: ', e); // eslint-disable-line no-console
|
|
||||||
return caches.match(OFFLINE_URL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cached = await cache.match(event.request);
|
|
||||||
if (cached != null) {
|
|
||||||
if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
|
|
||||||
cached = ensureCrossOriginIsolationHeaders(cached);
|
|
||||||
}
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
// Try network if don't have it in cache.
|
|
||||||
const response = await fetchAndCache(event, cache, isCachable);
|
|
||||||
return response;
|
|
||||||
})());
|
|
||||||
} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
|
|
||||||
event.respondWith((async () => {
|
|
||||||
let response = await fetch(event.request);
|
|
||||||
response = ensureCrossOriginIsolationHeaders(response);
|
|
||||||
return response;
|
|
||||||
})());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
// No cross origin
|
|
||||||
if (event.origin !== self.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = event.source.id || '';
|
|
||||||
const msg = event.data || '';
|
|
||||||
// Ensure it's one of our clients.
|
|
||||||
self.clients.get(id).then(function (client) {
|
|
||||||
if (!client) {
|
|
||||||
return; // Not a valid client.
|
|
||||||
}
|
|
||||||
if (msg === 'claim') {
|
|
||||||
self.skipWaiting().then(() => self.clients.claim());
|
|
||||||
} else if (msg === 'clear') {
|
|
||||||
caches.delete(CACHE_NAME);
|
|
||||||
} else if (msg === 'update') {
|
|
||||||
self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
#search-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search_term {
|
|
||||||
width: 60vw;
|
|
||||||
font-size: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#clear {
|
|
||||||
font-size: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#games-container{
|
|
||||||
font-size: 2vh;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/* Pure CSS styles for Music Search */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Light mode colors (default) */
|
||||||
|
--bg-primary: #f3f4f6;
|
||||||
|
--bg-secondary: #e5e7eb;
|
||||||
|
--bg-tertiary: #dcfce7;
|
||||||
|
--text-primary: #000;
|
||||||
|
--text-secondary: #374151;
|
||||||
|
--border-primary: #9ca3af;
|
||||||
|
--border-focus: #6b7280;
|
||||||
|
--accent-primary: #f97316;
|
||||||
|
--accent-hover: #ea580c;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* Dark mode colors matching frontend */
|
||||||
|
--bg-primary: #555;
|
||||||
|
--bg-secondary: #333;
|
||||||
|
--bg-tertiary: #2a2a2a;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #ff9c00;
|
||||||
|
--border-primary: #666;
|
||||||
|
--border-focus: #ff9c00;
|
||||||
|
--accent-primary: #ff9c00;
|
||||||
|
--accent-hover: #e68a00;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
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: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 var(--border-primary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_term:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
#clear {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#clear:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
#games-container {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode toggle */
|
||||||
|
#dark-mode-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dark-mode-toggle:hover {
|
||||||
|
background-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game result cards */
|
||||||
|
.bg-green-100 {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,4 +3,7 @@ package web
|
|||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:embed "assets"
|
//go:embed "assets"
|
||||||
var Files embed.FS
|
var Assets embed.FS
|
||||||
|
|
||||||
|
//go:embed "swagger"
|
||||||
|
var Swagger embed.FS
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"music-server/pkg/server"
|
"music-server/internal/backend"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -30,7 +30,7 @@ func FindGameWebHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func search(searchText string) {
|
func search(searchText string) {
|
||||||
games_added = nil
|
games_added = nil
|
||||||
games := server.GetAllGames()
|
games := backend.GetAllSoundtracks()
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
if is_match_exact(searchText, game) {
|
if is_match_exact(searchText, game) {
|
||||||
add_game(game)
|
add_game(game)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package web
|
|||||||
|
|
||||||
templ HelloForm() {
|
templ HelloForm() {
|
||||||
@Base() {
|
@Base() {
|
||||||
|
<button id="dark-mode-toggle">🌙</button>
|
||||||
<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>
|
||||||
@@ -12,8 +13,29 @@ templ HelloForm() {
|
|||||||
if (document.readyState == 'complete') {
|
if (document.readyState == 'complete') {
|
||||||
htmx.ajax('POST', '/find', '#games-container');
|
htmx.ajax('POST', '/find', '#games-container');
|
||||||
document.getElementById("search_term").focus();
|
document.getElementById("search_term").focus();
|
||||||
|
|
||||||
|
// Initialize dark mode from localStorage (default to dark)
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
document.getElementById('dark-mode-toggle').textContent = '☀️';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dark mode toggle functionality
|
||||||
|
document.getElementById("dark-mode-toggle").addEventListener("click", function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
|
// Update toggle button text
|
||||||
|
this.textContent = newTheme === 'dark' ? '☀️' : '🌙';
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("clear").addEventListener("click", function (event) {
|
document.getElementById("clear").addEventListener("click", function (event) {
|
||||||
document.getElementById("search_term").value = "";
|
document.getElementById("search_term").value = "";
|
||||||
htmx.ajax('POST', '/find', '#games-container');
|
htmx.ajax('POST', '/find', '#games-container');
|
||||||
@@ -26,7 +48,7 @@ templ HelloForm() {
|
|||||||
templ FoundGames(games []string) {
|
templ FoundGames(games []string) {
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||||
<p>{ game }</p>
|
<p class="game-text">{ game }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 665 B After Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
test-db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: testuser
|
||||||
|
POSTGRES_PASSWORD: testpass
|
||||||
|
POSTGRES_DB: music_server_test
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- test-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
test-db-data:
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package newDb
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
//go:embed "migrations/*.sql"
|
|
||||||
var MigrationsFs embed.FS
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
-- name: ResetGameIdSeq :one
|
|
||||||
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);
|
|
||||||
|
|
||||||
-- name: GetGameNameById :one
|
|
||||||
SELECT game_name FROM game WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetGameById :one
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: SetGameDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=now() WHERE deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: ClearGames :exec
|
|
||||||
DELETE FROM game;
|
|
||||||
|
|
||||||
-- name: UpdateGameName :exec
|
|
||||||
UPDATE game SET game_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: UpdateGameHash :exec
|
|
||||||
UPDATE game SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: RemoveDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=NULL WHERE id=$1;
|
|
||||||
|
|
||||||
-- name: GetIdByGameName :one
|
|
||||||
SELECT id FROM game WHERE game_name = $1;
|
|
||||||
|
|
||||||
-- name: InsertGame :one
|
|
||||||
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
|
|
||||||
|
|
||||||
-- name: InsertGameWithExistingId :exec
|
|
||||||
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
|
||||||
|
|
||||||
-- name: FindAllGames :many
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY game_name;
|
|
||||||
|
|
||||||
-- name: GetAllGamesIncludingDeleted :many
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
ORDER BY game_name;
|
|
||||||
|
|
||||||
-- name: AddGamePlayed :exec
|
|
||||||
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: game.sql
|
|
||||||
|
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const addGamePlayed = `-- name: AddGamePlayed :exec
|
|
||||||
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) AddGamePlayed(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, addGamePlayed, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearGames = `-- name: ClearGames :exec
|
|
||||||
DELETE FROM game
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ClearGames(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, clearGames)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const findAllGames = `-- name: FindAllGames :many
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY game_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) FindAllGames(ctx context.Context) ([]Game, error) {
|
|
||||||
rows, err := q.db.Query(ctx, findAllGames)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Game
|
|
||||||
for rows.Next() {
|
|
||||||
var i Game
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllGamesIncludingDeleted = `-- name: GetAllGamesIncludingDeleted :many
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
ORDER BY game_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetAllGamesIncludingDeleted(ctx context.Context) ([]Game, error) {
|
|
||||||
rows, err := q.db.Query(ctx, getAllGamesIncludingDeleted)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Game
|
|
||||||
for rows.Next() {
|
|
||||||
var i Game
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGameById = `-- name: GetGameById :one
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetGameById(ctx context.Context, id int32) (Game, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getGameById, id)
|
|
||||||
var i Game
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGameNameById = `-- name: GetGameNameById :one
|
|
||||||
SELECT game_name FROM game WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetGameNameById(ctx context.Context, id int32) (string, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getGameNameById, id)
|
|
||||||
var game_name string
|
|
||||||
err := row.Scan(&game_name)
|
|
||||||
return game_name, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIdByGameName = `-- name: GetIdByGameName :one
|
|
||||||
SELECT id FROM game WHERE game_name = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetIdByGameName(ctx context.Context, gameName string) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getIdByGameName, gameName)
|
|
||||||
var id int32
|
|
||||||
err := row.Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertGame = `-- name: InsertGame :one
|
|
||||||
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertGameParams struct {
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertGame(ctx context.Context, arg InsertGameParams) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, insertGame, arg.GameName, arg.Path, arg.Hash)
|
|
||||||
var id int32
|
|
||||||
err := row.Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertGameWithExistingId = `-- name: InsertGameWithExistingId :exec
|
|
||||||
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertGameWithExistingIdParams struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertGameWithExistingId(ctx context.Context, arg InsertGameWithExistingIdParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, insertGameWithExistingId,
|
|
||||||
arg.ID,
|
|
||||||
arg.GameName,
|
|
||||||
arg.Path,
|
|
||||||
arg.Hash,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeDeletionDate = `-- name: RemoveDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=NULL WHERE id=$1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) RemoveDeletionDate(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, removeDeletionDate, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetGameIdSeq = `-- name: ResetGameIdSeq :one
|
|
||||||
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1)
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ResetGameIdSeq(ctx context.Context) (int64, error) {
|
|
||||||
row := q.db.QueryRow(ctx, resetGameIdSeq)
|
|
||||||
var setval int64
|
|
||||||
err := row.Scan(&setval)
|
|
||||||
return setval, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const setGameDeletionDate = `-- name: SetGameDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=now() WHERE deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) SetGameDeletionDate(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, setGameDeletionDate)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGameHash = `-- name: UpdateGameHash :exec
|
|
||||||
UPDATE game SET hash=$1, last_changed=now() WHERE id=$2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateGameHashParams struct {
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateGameHash(ctx context.Context, arg UpdateGameHashParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateGameHash, arg.Hash, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGameName = `-- name: UpdateGameName :exec
|
|
||||||
UPDATE game SET game_name=$1, path=$2, last_changed=now() WHERE id=$3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateGameNameParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateGameName(ctx context.Context, arg UpdateGameNameParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateGameName, arg.Name, arg.Path, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Game struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Added time.Time `json:"added"`
|
|
||||||
Deleted *time.Time `json:"deleted"`
|
|
||||||
LastChanged *time.Time `json:"last_changed"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
TimesPlayed *int32 `json:"times_played"`
|
|
||||||
LastPlayed *time.Time `json:"last_played"`
|
|
||||||
NumberOfSongs int32 `json:"number_of_songs"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Song struct {
|
|
||||||
GameID int32 `json:"game_id"`
|
|
||||||
SongName string `json:"song_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
TimesPlayed *int32 `json:"times_played"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
FileName *string `json:"file_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SongList struct {
|
|
||||||
MatchDate time.Time `json:"match_date"`
|
|
||||||
MatchID int32 `json:"match_id"`
|
|
||||||
SongNo int32 `json:"song_no"`
|
|
||||||
GameName *string `json:"game_name"`
|
|
||||||
SongName *string `json:"song_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Vgmq struct {
|
|
||||||
SongNo int32 `json:"song_no"`
|
|
||||||
Path *string `json:"path"`
|
|
||||||
Clue *string `json:"clue"`
|
|
||||||
Answered bool `json:"answered"`
|
|
||||||
Answer *string `json:"answer"`
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,98 @@
|
|||||||
module music-server
|
module music-server
|
||||||
|
|
||||||
go 1.22.2
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MShekow/directory-checksum v1.4.6
|
github.com/MShekow/directory-checksum v1.4.18
|
||||||
github.com/a-h/templ v0.2.793
|
github.com/a-h/templ v0.3.1020
|
||||||
github.com/gin-contrib/static v1.1.2
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1
|
github.com/labstack/echo/v5 v5.1.1
|
||||||
github.com/jackc/pgtype v1.14.3
|
github.com/lib/pq v1.12.3
|
||||||
github.com/jackc/pgx/v5 v5.5.5
|
github.com/panjf2000/ants/v2 v2.12.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/stretchr/testify v1.11.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
github.com/swaggo/echo-swagger/v2 v2.0.1
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
github.com/testcontainers/testcontainers-go v0.42.0
|
||||||
|
go.uber.org/zap v1.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-errors/errors v1.5.1 // indirect
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/go-openapi/spec v0.22.4 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/go-openapi/swag/conv v0.26.0 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.26.0 // 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/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-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/json-iterator/go v1.1.12 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/moby/go-archive v0.2.0 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/moby/moby/api v1.54.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/moby/moby/client v0.4.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
golang.org/x/crypto v0.27.0 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
github.com/sv-tools/openapi v0.4.0 // indirect
|
||||||
|
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||||
|
github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,350 +1,238 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/MShekow/directory-checksum v1.4.6 h1:2fhlCYbpjEN1iH9S0tdmEM0p1wvNT9x5x0rIchGI7nE=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/MShekow/directory-checksum v1.4.6/go.mod h1:bMfFBkaIlNk7O9VgEi8D2X7Q2Jfk3c7d67z3t6cpIi4=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/MShekow/directory-checksum v1.4.18 h1:1nPPVl7uREa6WMTAPKoWW/GylhnASs0C9C+GPiwLwXA=
|
||||||
|
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/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
|
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||||
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
|
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
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.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
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/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
|
|
||||||
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/logr v1.4.2/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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
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/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
|
||||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
|
||||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
|
||||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
|
||||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
|
||||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
|
||||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
|
||||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
|
||||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
|
||||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
|
||||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
|
||||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
|
||||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
|
||||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
|
||||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
|
||||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
|
||||||
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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
|
||||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
|
||||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
|
||||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
|
||||||
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
|
||||||
github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus=
|
|
||||||
github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
|
|
||||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
|
||||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
|
||||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
|
||||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
|
||||||
github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
|
|
||||||
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
|
||||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
|
||||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
|
||||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
|
||||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
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/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
|
||||||
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
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/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
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/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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
|
||||||
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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/swaggo/echo-swagger/v2 v2.0.1 h1:jKR3QiK+ciGjxE0+7qZ/azjtlx/pTVls7pJFJqdJoJI=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/swaggo/echo-swagger/v2 v2.0.1/go.mod h1:BbgiO9XKX6yYU5Rq4ejqVlQI0mVRv6ziFKd0XgdztnQ=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
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=
|
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
|
||||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
|
||||||
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-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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
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.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=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
"github.com/testcontainers/testcontainers-go/wait"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testContainer testcontainers.Container
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Start PostgreSQL container
|
||||||
|
log.Println("Starting PostgreSQL test container...")
|
||||||
|
req := testcontainers.ContainerRequest{
|
||||||
|
Image: "postgres:15-alpine",
|
||||||
|
ExposedPorts: []string{"5432/tcp"},
|
||||||
|
Env: map[string]string{
|
||||||
|
"POSTGRES_USER": "testuser",
|
||||||
|
"POSTGRES_PASSWORD": "testpass",
|
||||||
|
"POSTGRES_DB": "music_server_test",
|
||||||
|
},
|
||||||
|
WaitingFor: wait.ForLog("database system is ready to accept connections"),
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||||
|
ContainerRequest: req,
|
||||||
|
Started: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start container: %v", err)
|
||||||
|
}
|
||||||
|
testContainer = container
|
||||||
|
|
||||||
|
// Get container's host and port
|
||||||
|
host, err := container.Endpoint(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get container endpoint: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("PostgreSQL container running at: %s", host)
|
||||||
|
|
||||||
|
// Set environment variables for all tests
|
||||||
|
os.Setenv("DB_HOST", host)
|
||||||
|
os.Setenv("DB_PORT", "5432")
|
||||||
|
os.Setenv("DB_USERNAME", "testuser")
|
||||||
|
os.Setenv("DB_PASSWORD", "testpass")
|
||||||
|
os.Setenv("DB_NAME", "music_server_test")
|
||||||
|
os.Setenv("MUSIC_PATH", "./testMusic")
|
||||||
|
os.Setenv("CHARACTERS_PATH", "./testCharacters")
|
||||||
|
os.Setenv("PORT", "8081")
|
||||||
|
os.Setenv("LOG_LEVEL", "debug")
|
||||||
|
os.Setenv("LOG_JSON", "false")
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
log.Println("Running integration tests...")
|
||||||
|
exitCode := m.Run()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
log.Println("Stopping test container...")
|
||||||
|
if err := container.Terminate(ctx); err != nil {
|
||||||
|
log.Printf("Failed to terminate container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDatabaseConnection verifies we can connect to the test database
|
||||||
|
func TestDatabaseConnection(t *testing.T) {
|
||||||
|
// This will be tested by the individual handler tests
|
||||||
|
// Just verify env vars are set
|
||||||
|
host := os.Getenv("DB_HOST")
|
||||||
|
port := os.Getenv("DB_PORT")
|
||||||
|
|
||||||
|
if host == "" || port == "" {
|
||||||
|
t.Error("Database environment variables not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Database configuration: host=%s, port=%s", host, port)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"music-server/internal/db/repository"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global variables - these are initialized by InitBackend
|
||||||
|
var (
|
||||||
|
backendPool *pgxpool.Pool
|
||||||
|
repo *repository.Queries
|
||||||
|
backendCtx context.Context = context.Background()
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitBackend initializes the backend package with the database pool.
|
||||||
|
// This should be called once at application startup.
|
||||||
|
func InitBackend(pool *pgxpool.Pool) {
|
||||||
|
backendPool = pool
|
||||||
|
repo = repository.New(pool)
|
||||||
|
backendCtx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendCtx returns the context used by backend operations.
|
||||||
|
// This is exposed for use by the backend functions.
|
||||||
|
func BackendCtx() context.Context {
|
||||||
|
return backendCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendRepo returns the repository queries instance.
|
||||||
|
// This is exposed for use by the backend functions.
|
||||||
|
func BackendRepo() *repository.Queries {
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendPool returns the underlying database pool.
|
||||||
|
// This is exposed for test utilities that need direct pool access.
|
||||||
|
func BackendPool() *pgxpool.Pool {
|
||||||
|
return backendPool
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCharacterList() []string {
|
||||||
|
charactersPath := os.Getenv("CHARACTERS_PATH")
|
||||||
|
logging.GetLogger().Debug("Getting character list", zap.String("path", charactersPath))
|
||||||
|
// Clean the path - remove trailing slashes and then add one for consistency
|
||||||
|
charactersPath = strings.TrimSuffix(charactersPath, "/")
|
||||||
|
charactersPath += "/"
|
||||||
|
files, err := os.ReadDir(charactersPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to read characters directory", zap.String("path", charactersPath), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var characters []string
|
||||||
|
for _, file := range files {
|
||||||
|
if isImage(file) {
|
||||||
|
characters = append(characters, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return characters
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCharacter(character string) string {
|
||||||
|
charactersPath := os.Getenv("CHARACTERS_PATH")
|
||||||
|
// Clean the path - remove trailing slashes and then add one for consistency
|
||||||
|
charactersPath = strings.TrimSuffix(charactersPath, "/")
|
||||||
|
charactersPath += "/"
|
||||||
|
logging.GetLogger().Debug("Getting character", zap.String("character", character), zap.String("path", charactersPath+character))
|
||||||
|
return charactersPath + character
|
||||||
|
}
|
||||||
|
|
||||||
|
func isImage(entry os.DirEntry) bool {
|
||||||
|
return !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".jpg") || strings.HasSuffix(entry.Name(), ".jpeg") ||
|
||||||
|
strings.HasSuffix(entry.Name(), ".png"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsImage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry fs.DirEntry
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "jpg file",
|
||||||
|
entry: &mockDirEntry{name: "test.jpg", isDir: false},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jpeg file",
|
||||||
|
entry: &mockDirEntry{name: "test.jpeg", isDir: false},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "png file",
|
||||||
|
entry: &mockDirEntry{name: "test.png", isDir: false},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "directory",
|
||||||
|
entry: &mockDirEntry{name: "test", isDir: true},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "txt file",
|
||||||
|
entry: &mockDirEntry{name: "test.txt", isDir: false},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mp3 file",
|
||||||
|
entry: &mockDirEntry{name: "test.mp3", isDir: false},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := isImage(tt.entry)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("isImage() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockDirEntry struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDirEntry) Name() string { return m.name }
|
||||||
|
func (m *mockDirEntry) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockDirEntry) Type() fs.FileMode { return 0 }
|
||||||
|
func (m *mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil }
|
||||||
|
func (m *mockDirEntry) Sys() interface{} { return nil }
|
||||||
|
|
||||||
|
func TestGetCharacter(t *testing.T) {
|
||||||
|
os.Setenv("CHARACTERS_PATH", "/test/path")
|
||||||
|
defer os.Unsetenv("CHARACTERS_PATH")
|
||||||
|
|
||||||
|
result := GetCharacter("test.jpg")
|
||||||
|
expected := "/test/path/test.jpg"
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("GetCharacter() = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCharacterWithTrailingSlash(t *testing.T) {
|
||||||
|
os.Setenv("CHARACTERS_PATH", "/test/path/")
|
||||||
|
defer os.Unsetenv("CHARACTERS_PATH")
|
||||||
|
|
||||||
|
result := GetCharacter("test.jpg")
|
||||||
|
expected := "/test/path/test.jpg"
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("GetCharacter() = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to check latest version", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to decode response", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Checked latest version", zap.Int("id", cResp.Id), zap.String("name", 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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to list assets", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to decode response", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Listing assets", zap.Int("id", cResp.Id), zap.String("name", cResp.Name))
|
||||||
|
var assets []string
|
||||||
|
for _, asset := range cResp.Assets {
|
||||||
|
logging.GetLogger().Debug("Found asset", zap.Int("id", cResp.Id), zap.String("name", cResp.Name), zap.String("asset", 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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to download latest Windows version", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to decode response", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Downloading Windows version", zap.Int("id", cResp.Id), zap.String("name", cResp.Name))
|
||||||
|
for _, asset := range cResp.Assets {
|
||||||
|
logging.GetLogger().Debug("Checking asset", zap.Int("id", cResp.Id), zap.String("name", cResp.Name), zap.String("asset", 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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to download latest Linux version", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
logging.GetLogger().Fatal("Failed to decode response", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Downloading Linux version", zap.Int("id", cResp.Id), zap.String("name", cResp.Name))
|
||||||
|
for _, asset := range cResp.Assets {
|
||||||
|
logging.GetLogger().Debug("Checking asset", zap.Int("id", cResp.Id), zap.String("name", cResp.Name), zap.String("asset", asset.Name))
|
||||||
|
if strings.HasSuffix(asset.Name, ".x86_64") {
|
||||||
|
return asset.DownloadUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckLatest(t *testing.T) {
|
||||||
|
mockResponse := giteaResponse{
|
||||||
|
Id: 1,
|
||||||
|
Name: "v1.0.0",
|
||||||
|
Assets: []assetResponse{
|
||||||
|
{Id: 1, Name: "app.exe", DownloadUrl: "http://example.com/app.exe"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(mockResponse)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
originalURL := "https://gitea.sanplex.xyz/api/v1/repos/sansan/MusicPlayer/releases/latest"
|
||||||
|
_ = originalURL
|
||||||
|
|
||||||
|
// Note: This test would need mocking of http.Get to fully work
|
||||||
|
// For now, we'll just test the parsing logic
|
||||||
|
// In a real scenario, you'd use httpmock or similar
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAssetsOfLatest(t *testing.T) {
|
||||||
|
mockResponse := giteaResponse{
|
||||||
|
Id: 1,
|
||||||
|
Name: "v1.0.0",
|
||||||
|
Assets: []assetResponse{
|
||||||
|
{Id: 1, Name: "app.exe", DownloadUrl: "http://example.com/app.exe"},
|
||||||
|
{Id: 2, Name: "app.x86_64", DownloadUrl: "http://example.com/app.x86_64"},
|
||||||
|
{Id: 3, Name: "app.dmg", DownloadUrl: "http://example.com/app.dmg"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the parsing of the response
|
||||||
|
var cResp giteaResponse
|
||||||
|
data, _ := json.Marshal(mockResponse)
|
||||||
|
json.Unmarshal(data, &cResp)
|
||||||
|
|
||||||
|
var assets []string
|
||||||
|
for _, asset := range cResp.Assets {
|
||||||
|
assets = append(assets, asset.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assets) != 3 {
|
||||||
|
t.Errorf("Expected 3 assets, got %d", len(assets))
|
||||||
|
}
|
||||||
|
|
||||||
|
if assets[0] != "app.exe" {
|
||||||
|
t.Errorf("Expected first asset to be app.exe, got %s", assets[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"music-server/internal/db/repository"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SongInfo struct {
|
||||||
|
Game string `json:"Game"`
|
||||||
|
GamePlayed int32 `json:"GamePlayed"`
|
||||||
|
Song string `json:"Song"`
|
||||||
|
SongPlayed int32 `json:"SongPlayed"`
|
||||||
|
CurrentlyPlaying bool `json:"CurrentlyPlaying"`
|
||||||
|
SongNo int `json:"SongNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSong = -1
|
||||||
|
|
||||||
|
var gamesNew []repository.Soundtrack
|
||||||
|
|
||||||
|
var songQueNew []repository.Song
|
||||||
|
|
||||||
|
var lastFetchedNew repository.Song
|
||||||
|
|
||||||
|
func initRepo() {
|
||||||
|
// This function is kept for backward compatibility
|
||||||
|
// but now uses the backend package's initialized repo
|
||||||
|
// If not initialized, this will panic intentionally
|
||||||
|
if BackendRepo() == nil {
|
||||||
|
panic("backend not initialized - call backend.InitBackend() first")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllGames() []repository.Soundtrack {
|
||||||
|
if len(gamesNew) == 0 {
|
||||||
|
initRepo()
|
||||||
|
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||||
|
}
|
||||||
|
return gamesNew
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSoundCheckSong() string {
|
||||||
|
files, err := os.ReadDir("songs")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to read songs directory", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
fileInfo := files[rand.Intn(len(files))]
|
||||||
|
return "songs/" + fileInfo.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reset() {
|
||||||
|
songQueNew = nil
|
||||||
|
currentSong = -1
|
||||||
|
initRepo()
|
||||||
|
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLatestToQue() {
|
||||||
|
if lastFetchedNew.Path != "" {
|
||||||
|
currentSong = len(songQueNew)
|
||||||
|
songQueNew = append(songQueNew, lastFetchedNew)
|
||||||
|
lastFetchedNew = repository.Song{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLatestPlayed() {
|
||||||
|
if len(songQueNew) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentSongData := songQueNew[currentSong]
|
||||||
|
|
||||||
|
initRepo()
|
||||||
|
BackendRepo().AddSoundtrackPlayed(BackendCtx(), currentSongData.SoundtrackID)
|
||||||
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: currentSongData.SoundtrackID, SongName: currentSongData.SongName})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetPlayed(songNumber int) {
|
||||||
|
if len(songQueNew) == 0 || songNumber >= len(songQueNew) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
songData := songQueNew[songNumber]
|
||||||
|
initRepo()
|
||||||
|
BackendRepo().AddSoundtrackPlayed(BackendCtx(), songData.SoundtrackID)
|
||||||
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: songData.SoundtrackID, SongName: songData.SongName})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRandomSong() string {
|
||||||
|
getAllGames()
|
||||||
|
if len(gamesNew) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
song := getSongFromList(gamesNew)
|
||||||
|
lastFetchedNew = song
|
||||||
|
return song.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRandomSongLowChance() string {
|
||||||
|
getAllGames()
|
||||||
|
|
||||||
|
var listOfGames []repository.Soundtrack
|
||||||
|
|
||||||
|
var averagePlayed = getAveragePlayed()
|
||||||
|
|
||||||
|
for _, data := range gamesNew {
|
||||||
|
timesToAdd := averagePlayed - data.TimesPlayed
|
||||||
|
if timesToAdd <= 0 {
|
||||||
|
listOfGames = append(listOfGames, data)
|
||||||
|
} else {
|
||||||
|
for i := int32(0); i < timesToAdd; i++ {
|
||||||
|
listOfGames = append(listOfGames, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
song := getSongFromList(listOfGames)
|
||||||
|
|
||||||
|
lastFetchedNew = song
|
||||||
|
return song.Path
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRandomSongClassic() string {
|
||||||
|
getAllGames()
|
||||||
|
|
||||||
|
var listOfAllSongs []repository.Song
|
||||||
|
for _, game := range gamesNew {
|
||||||
|
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||||
|
listOfAllSongs = append(listOfAllSongs, songList...)
|
||||||
|
}
|
||||||
|
|
||||||
|
songFound := false
|
||||||
|
var song repository.Song
|
||||||
|
for !songFound {
|
||||||
|
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
||||||
|
gameData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
|
zap.String("song", song.SongName),
|
||||||
|
zap.String("game", gameData.SoundtrackName),
|
||||||
|
zap.String("filename", *song.FileName))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if file exists and open
|
||||||
|
openFile, err := os.Open(song.Path)
|
||||||
|
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
|
||||||
|
//File not found
|
||||||
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
|
zap.String("song", song.SongName),
|
||||||
|
zap.String("game", gameData.SoundtrackName),
|
||||||
|
zap.String("filename", *song.FileName))
|
||||||
|
} else {
|
||||||
|
songFound = true
|
||||||
|
}
|
||||||
|
err = openFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to close file", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastFetchedNew = song
|
||||||
|
return song.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSongInfo() SongInfo {
|
||||||
|
if songQueNew == nil {
|
||||||
|
return SongInfo{}
|
||||||
|
}
|
||||||
|
var currentSongData = songQueNew[currentSong]
|
||||||
|
|
||||||
|
currentGameData := getCurrentGame(currentSongData)
|
||||||
|
|
||||||
|
return SongInfo{
|
||||||
|
Game: currentGameData.SoundtrackName,
|
||||||
|
GamePlayed: currentGameData.TimesPlayed,
|
||||||
|
Song: currentSongData.SongName,
|
||||||
|
SongPlayed: currentSongData.TimesPlayed,
|
||||||
|
CurrentlyPlaying: true,
|
||||||
|
SongNo: currentSong,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlayedSongs() []SongInfo {
|
||||||
|
var songList []SongInfo
|
||||||
|
|
||||||
|
for i, song := range songQueNew {
|
||||||
|
gameData := getCurrentGame(song)
|
||||||
|
songList = append(songList, SongInfo{
|
||||||
|
Game: gameData.SoundtrackName,
|
||||||
|
GamePlayed: gameData.TimesPlayed,
|
||||||
|
Song: song.SongName,
|
||||||
|
SongPlayed: song.TimesPlayed,
|
||||||
|
CurrentlyPlaying: i == currentSong,
|
||||||
|
SongNo: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return songList
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSong(song string) string {
|
||||||
|
currentSong, _ = strconv.Atoi(song)
|
||||||
|
if currentSong >= len(songQueNew) {
|
||||||
|
currentSong = len(songQueNew) - 1
|
||||||
|
} else if currentSong < 0 {
|
||||||
|
currentSong = 0
|
||||||
|
}
|
||||||
|
songData := songQueNew[currentSong]
|
||||||
|
return songData.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllSoundtracks() []string {
|
||||||
|
getAllGames()
|
||||||
|
|
||||||
|
var jsonArray []string
|
||||||
|
for _, game := range gamesNew {
|
||||||
|
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||||
|
}
|
||||||
|
return jsonArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllSoundtracksRandom() []string {
|
||||||
|
getAllGames()
|
||||||
|
|
||||||
|
var jsonArray []string
|
||||||
|
for _, game := range gamesNew {
|
||||||
|
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||||
|
}
|
||||||
|
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
||||||
|
return jsonArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNextSong() string {
|
||||||
|
if songQueNew == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if currentSong == len(songQueNew)-1 || currentSong == -1 {
|
||||||
|
songData := songQueNew[currentSong]
|
||||||
|
return songData.Path
|
||||||
|
} else {
|
||||||
|
currentSong = currentSong + 1
|
||||||
|
songData := songQueNew[currentSong]
|
||||||
|
return songData.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPreviousSong() string {
|
||||||
|
if songQueNew == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if currentSong == -1 || currentSong == 0 {
|
||||||
|
songData := songQueNew[0]
|
||||||
|
return songData.Path
|
||||||
|
} else {
|
||||||
|
currentSong = currentSong - 1
|
||||||
|
songData := songQueNew[currentSong]
|
||||||
|
return songData.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSongFromList(games []repository.Soundtrack) repository.Song {
|
||||||
|
songFound := false
|
||||||
|
var song repository.Song
|
||||||
|
for !songFound {
|
||||||
|
game := getRandomGame(games)
|
||||||
|
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||||
|
if len(songs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
song = songs[rand.Intn(len(songs))]
|
||||||
|
logging.GetLogger().Debug("Selected song", zap.String("song", song.SongName), zap.String("path", song.Path))
|
||||||
|
|
||||||
|
//Check if file exists and open
|
||||||
|
openFile, err := os.Open(song.Path)
|
||||||
|
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
||||||
|
//File not found
|
||||||
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
|
zap.String("song", song.SongName),
|
||||||
|
zap.String("game", game.SoundtrackName),
|
||||||
|
zap.Any("filename", song.FileName))
|
||||||
|
} else {
|
||||||
|
songFound = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to close file", zap.String("path", song.Path), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return song
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentGame(currentSongData repository.Song) repository.Soundtrack {
|
||||||
|
for _, game := range gamesNew {
|
||||||
|
if game.ID == currentSongData.SoundtrackID {
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repository.Soundtrack{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAveragePlayed() int32 {
|
||||||
|
getAllGames()
|
||||||
|
var sum int32
|
||||||
|
for _, data := range gamesNew {
|
||||||
|
sum += data.TimesPlayed
|
||||||
|
}
|
||||||
|
return sum / int32(len(gamesNew))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomGame(listOfGames []repository.Soundtrack) repository.Soundtrack {
|
||||||
|
return listOfGames[rand.Intn(len(listOfGames))]
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"music-server/internal/db/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test the average calculation logic directly without database access
|
||||||
|
func TestCalculateAverage(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{
|
||||||
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum int32
|
||||||
|
for _, data := range games {
|
||||||
|
sum += data.TimesPlayed
|
||||||
|
}
|
||||||
|
result := sum / int32(len(games))
|
||||||
|
expected := int32(20)
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Average calculation = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateAverageEmpty(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{}
|
||||||
|
|
||||||
|
if len(games) == 0 {
|
||||||
|
result := int32(0)
|
||||||
|
expected := int32(0)
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Average calculation with empty list = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum int32
|
||||||
|
for _, data := range games {
|
||||||
|
sum += data.TimesPlayed
|
||||||
|
}
|
||||||
|
result := sum / int32(len(games))
|
||||||
|
expected := int32(0)
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Average calculation with empty list = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateAverageSingle(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{
|
||||||
|
{SoundtrackName: "Game1", TimesPlayed: 42},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum int32
|
||||||
|
for _, data := range games {
|
||||||
|
sum += data.TimesPlayed
|
||||||
|
}
|
||||||
|
result := sum / int32(len(games))
|
||||||
|
expected := int32(42)
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Average calculation with single game = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRandomGame(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{
|
||||||
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set seed for reproducible tests
|
||||||
|
rand.Seed(42)
|
||||||
|
|
||||||
|
result := games[rand.Intn(len(games))]
|
||||||
|
|
||||||
|
if result.SoundtrackName == "" {
|
||||||
|
t.Error("random game selection returned empty game")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, g := range games {
|
||||||
|
if g.SoundtrackName == result.SoundtrackName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("random game selection returned game not in list: %v", result.SoundtrackName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindGameByID(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{
|
||||||
|
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
|
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
|
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
games []repository.Soundtrack
|
||||||
|
gameID int32
|
||||||
|
expected repository.Soundtrack
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "existing game",
|
||||||
|
games: games,
|
||||||
|
gameID: 2,
|
||||||
|
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-existing game",
|
||||||
|
games: games,
|
||||||
|
gameID: 99,
|
||||||
|
expected: repository.Soundtrack{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var result repository.Soundtrack
|
||||||
|
for _, game := range tt.games {
|
||||||
|
if game.ID == tt.gameID {
|
||||||
|
result = game
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
||||||
|
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSoundtrackNames(t *testing.T) {
|
||||||
|
games := []repository.Soundtrack{
|
||||||
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for _, game := range games {
|
||||||
|
result = append(result, game.SoundtrackName)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range result {
|
||||||
|
if v != expected[i] {
|
||||||
|
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleSoundtrackNames(t *testing.T) {
|
||||||
|
games := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
|
// Test that shuffle doesn't lose any elements
|
||||||
|
// We can't test the order since it's random, but we can test length and contents
|
||||||
|
original := make([]string, len(games))
|
||||||
|
copy(original, games)
|
||||||
|
|
||||||
|
// Simple shuffle implementation for testing
|
||||||
|
for i := range games {
|
||||||
|
j := i // In real code this would be random
|
||||||
|
games[i], games[j] = games[j], games[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(games) != len(original) {
|
||||||
|
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(games))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all original elements are still present
|
||||||
|
for _, orig := range original {
|
||||||
|
found := false
|
||||||
|
for _, g := range games {
|
||||||
|
if g == orig {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GameWithSongs represents a game with its songs for statistics
|
||||||
|
type GameWithSongs struct {
|
||||||
|
SoundtrackID int32 `json:"game_id"`
|
||||||
|
SoundtrackName string `json:"game_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"game_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
||||||
|
Songs []SongInfoForStats `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SongInfoForStats represents a song with game info for statistics
|
||||||
|
type SongInfoForStats struct {
|
||||||
|
SoundtrackID int32 `json:"game_id"`
|
||||||
|
SoundtrackName string `json:"game_name"`
|
||||||
|
SongName string `json:"song_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
FileName *string `json:"file_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsSummary holds overall statistics
|
||||||
|
type StatisticsSummary struct {
|
||||||
|
TotalGames int64 `json:"total_games"`
|
||||||
|
PlayedGames int64 `json:"played_games"`
|
||||||
|
NeverPlayedGames int64 `json:"never_played_games"`
|
||||||
|
TotalGamePlays int64 `json:"total_game_plays"`
|
||||||
|
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||||
|
MaxGamePlays int64 `json:"max_game_plays"`
|
||||||
|
MinGamePlays int64 `json:"min_game_plays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsHandler manages statistics operations
|
||||||
|
type StatisticsHandler struct {
|
||||||
|
// Uses the global backend repo initialized via InitBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatisticsHandler creates a new StatisticsHandler
|
||||||
|
func NewStatisticsHandler() *StatisticsHandler {
|
||||||
|
return &StatisticsHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedGamesWithSongs returns the top N most played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
// Get raw results
|
||||||
|
rows, err := queries.GetMostPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GameWithSongs
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
// Parse JSON songs array
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
// Fallback: if JSON parsing fails, create empty song entries
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedGamesWithSongs returns the top N least played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedSongsWithGame returns the top N most played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetMostPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedSongsWithGame returns the top N least played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
|
func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetNeverPlayedGames(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: nil,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastPlayedGames returns the most recently played games
|
||||||
|
func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLastPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldestPlayedGames returns the least recently played games
|
||||||
|
func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetOldestPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
row, err := queries.GetStatisticsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StatisticsSummary{
|
||||||
|
TotalGames: int64(row.TotalSoundtracks),
|
||||||
|
PlayedGames: int64(row.PlayedSoundtracks),
|
||||||
|
NeverPlayedGames: int64(row.NeverPlayedSoundtracks),
|
||||||
|
TotalGamePlays: int64(row.TotalSoundtrackPlays),
|
||||||
|
AvgGamePlays: float64(row.AvgSoundtrackPlays),
|
||||||
|
MaxGamePlays: int64(row.MaxSoundtrackPlays),
|
||||||
|
MinGamePlays: int64(row.MinSoundtrackPlays),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log helper for statistics operations
|
||||||
|
func logStatisticsError(err error, operation string) {
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Statistics error",
|
||||||
|
zap.String("operation", operation),
|
||||||
|
zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,596 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"music-server/internal/db/repository"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/panjf2000/ants/v2"
|
||||||
|
|
||||||
|
"github.com/MShekow/directory-checksum/directory_checksum"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Syncing = false
|
||||||
|
var foldersSynced float32
|
||||||
|
var numberOfFoldersToSync float32
|
||||||
|
var start time.Time
|
||||||
|
var totalTime time.Duration
|
||||||
|
var timeSpent time.Duration
|
||||||
|
|
||||||
|
var allGames []repository.Soundtrack
|
||||||
|
var gamesBeforeSync []repository.Soundtrack
|
||||||
|
var gamesAfterSync []repository.Soundtrack
|
||||||
|
var gamesAdded []string
|
||||||
|
var gamesReAdded []string
|
||||||
|
var gamesChangedTitle map[string]string
|
||||||
|
var gamesChangedContent []string
|
||||||
|
var gamesRemoved []string
|
||||||
|
var catchedErrors []string
|
||||||
|
|
||||||
|
type brokenSong struct {
|
||||||
|
SoundtrackID int32
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
var brokenSongs []brokenSong
|
||||||
|
var pool *ants.Pool
|
||||||
|
var poolSong *ants.Pool
|
||||||
|
|
||||||
|
type SyncResponse struct {
|
||||||
|
GamesAdded []string `json:"games_added"`
|
||||||
|
GamesReAdded []string `json:"games_re_added"`
|
||||||
|
GamesChangedTitle map[string]string `json:"games_changed_title"`
|
||||||
|
GamesChangedContent []string `json:"games_changed_content"`
|
||||||
|
GamesRemoved []string `json:"games_removed"`
|
||||||
|
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
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotChanged GameStatus = iota
|
||||||
|
TitleChanged
|
||||||
|
GameChanged
|
||||||
|
NewGame
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusName = map[GameStatus]string{
|
||||||
|
NotChanged: "Not changed",
|
||||||
|
TitleChanged: "Title changed",
|
||||||
|
GameChanged: "Game changed",
|
||||||
|
NewGame: "New game",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs GameStatus) String() string {
|
||||||
|
return statusName[gs]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetDB() {
|
||||||
|
repo.ClearSongs(BackendCtx())
|
||||||
|
repo.ClearSoundtracks(BackendCtx())
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncProgress() ProgressResponse {
|
||||||
|
progress := int((foldersSynced / numberOfFoldersToSync) * 100)
|
||||||
|
currentTime := time.Now()
|
||||||
|
timeSpent = currentTime.Sub(start)
|
||||||
|
out := time.Time{}.Add(timeSpent)
|
||||||
|
logging.GetLogger().Debug("Sync progress",
|
||||||
|
zap.Int("progress_percent", progress),
|
||||||
|
zap.Int("folders_synced", int(foldersSynced)),
|
||||||
|
zap.Int("total_folders", int(numberOfFoldersToSync)),
|
||||||
|
zap.String("time_spent", out.Format("15:04:05.00000")))
|
||||||
|
return ProgressResponse{
|
||||||
|
Progress: fmt.Sprintf("%v", progress),
|
||||||
|
TimeSpent: out.Format("15:04:05"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncResult() SyncResponse {
|
||||||
|
logging.GetLogger().Info("Sync completed",
|
||||||
|
zap.Int("games_before", len(gamesBeforeSync)),
|
||||||
|
zap.Int("games_after", len(gamesAfterSync)))
|
||||||
|
|
||||||
|
if len(gamesAdded) > 0 {
|
||||||
|
logging.GetLogger().Debug("Games added", zap.Strings("games", gamesAdded))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gamesReAdded) > 0 {
|
||||||
|
logging.GetLogger().Debug("Games readded", zap.Strings("games", gamesReAdded))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gamesChangedTitle) > 0 {
|
||||||
|
logging.GetLogger().Debug("Games with changed title", zap.Any("changes", gamesChangedTitle))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gamesChangedContent) > 0 {
|
||||||
|
logging.GetLogger().Debug("Games with changed content", zap.Strings("games", gamesChangedContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
var gamesRemovedTemp []string
|
||||||
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
|
var found = false
|
||||||
|
for _, afterGame := range gamesAfterSync {
|
||||||
|
if beforeGame.SoundtrackName == afterGame.SoundtrackName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.SoundtrackName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, game := range gamesRemovedTemp {
|
||||||
|
var found bool = false
|
||||||
|
for key := range gamesChangedTitle {
|
||||||
|
if game == key {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
gamesRemoved = append(gamesRemoved, game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gamesRemoved) > 0 {
|
||||||
|
logging.GetLogger().Debug("Games removed", zap.Strings("games", gamesRemoved))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(catchedErrors) > 0 {
|
||||||
|
logging.GetLogger().Error("Errors caught during sync", zap.Strings("errors", catchedErrors))
|
||||||
|
}
|
||||||
|
|
||||||
|
out := time.Time{}.Add(totalTime)
|
||||||
|
logging.GetLogger().Info("Sync completed", zap.String("total_time", out.Format("15:04:05.00000")))
|
||||||
|
|
||||||
|
return SyncResponse{
|
||||||
|
GamesAdded: gamesAdded,
|
||||||
|
GamesReAdded: gamesReAdded,
|
||||||
|
GamesChangedTitle: gamesChangedTitle,
|
||||||
|
GamesChangedContent: gamesChangedContent,
|
||||||
|
GamesRemoved: gamesRemoved,
|
||||||
|
CatchedErrors: catchedErrors,
|
||||||
|
TotalTime: out.Format("15:04:05"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncSoundtracksNewFull() {
|
||||||
|
syncGamesNew(true)
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncSoundtracksNewOnlyChanges() {
|
||||||
|
syncGamesNew(false)
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGamesNew(full bool) {
|
||||||
|
musicPath := os.Getenv("MUSIC_PATH")
|
||||||
|
fmt.Printf("dir: %s\n", musicPath)
|
||||||
|
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
||||||
|
if !strings.HasSuffix(musicPath, "/") {
|
||||||
|
musicPath += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncWg sync.WaitGroup
|
||||||
|
|
||||||
|
initRepo()
|
||||||
|
start = time.Now()
|
||||||
|
foldersToSkip := []string{".sync", "characters", "dist", "old"}
|
||||||
|
logging.GetLogger().Debug("Folders to skip during sync", zap.Strings("folders", foldersToSkip))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
gamesAdded = nil
|
||||||
|
gamesReAdded = nil
|
||||||
|
gamesChangedTitle = nil
|
||||||
|
gamesChangedContent = nil
|
||||||
|
gamesRemoved = nil
|
||||||
|
catchedErrors = nil
|
||||||
|
brokenSongs = nil
|
||||||
|
|
||||||
|
gamesBeforeSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||||
|
handleError("FindAllSoundtracks Before", err, "")
|
||||||
|
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
|
||||||
|
|
||||||
|
allGames, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx())
|
||||||
|
handleError("GetAllSoundtracksIncludingDeleted", err, "")
|
||||||
|
err = repo.SetSoundtrackDeletionDate(BackendCtx())
|
||||||
|
handleError("SetSoundtrackDeletionDate", err, "")
|
||||||
|
|
||||||
|
directories, err := os.ReadDir(musicPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to read music directory", zap.String("path", musicPath), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
pool, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||||
|
poolSong, _ = ants.NewPool(10, ants.WithPreAlloc(true))
|
||||||
|
defer pool.Release()
|
||||||
|
defer poolSong.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.FindAllSoundtracks(BackendCtx())
|
||||||
|
handleError("FindAllSoundtracks After", err, "")
|
||||||
|
|
||||||
|
finished := time.Now()
|
||||||
|
totalTime = finished.Sub(start)
|
||||||
|
out := time.Time{}.Add(totalTime)
|
||||||
|
logging.GetLogger().Info("Sync completed", zap.Duration("total_time", totalTime), zap.String("formatted_time", out.Format("15:04:05.00000")))
|
||||||
|
|
||||||
|
Syncing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkBrokenSongsNew() {
|
||||||
|
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
||||||
|
handleError("FetchAllSongs", err, "")
|
||||||
|
var brokenWg sync.WaitGroup
|
||||||
|
poolBroken, _ := ants.NewPool(200, ants.WithPreAlloc(true))
|
||||||
|
defer poolBroken.Release()
|
||||||
|
|
||||||
|
brokenWg.Add(len(allSongs))
|
||||||
|
for _, song := range allSongs {
|
||||||
|
poolBroken.Submit(func() {
|
||||||
|
defer brokenWg.Done()
|
||||||
|
checkBrokenSongNew(song)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
brokenWg.Wait()
|
||||||
|
for _, bs := range brokenSongs {
|
||||||
|
err = repo.RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: bs.SoundtrackID, Path: bs.Path})
|
||||||
|
handleError("RemoveBrokenSong", err, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkBrokenSongNew(song repository.Song) {
|
||||||
|
//Check if file exists and open
|
||||||
|
openFile, err := os.Open(song.Path)
|
||||||
|
if err != nil {
|
||||||
|
//File not found
|
||||||
|
brokenSongs = append(brokenSongs, brokenSong{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
|
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
||||||
|
} else {
|
||||||
|
err = openFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to close file", zap.String("path", song.Path), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
||||||
|
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
||||||
|
logging.GetLogger().Debug("Syncing game", zap.String("game", file.Name()))
|
||||||
|
gameDir := baseDir + file.Name() + "/"
|
||||||
|
dirHash := getHashForDir(gameDir)
|
||||||
|
|
||||||
|
var status GameStatus = NewGame
|
||||||
|
var oldGame repository.Soundtrack
|
||||||
|
var id int32 = -1
|
||||||
|
|
||||||
|
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
|
||||||
|
|
||||||
|
for _, currentGame := range allGames {
|
||||||
|
oldGame = currentGame
|
||||||
|
//fmt.Printf("%s | %s\n", oldGame.SoundtrackName, oldGame.Hash)
|
||||||
|
if oldGame.SoundtrackName == file.Name() && oldGame.Hash == dirHash {
|
||||||
|
status = NotChanged
|
||||||
|
id = oldGame.ID
|
||||||
|
//fmt.Printf("Game not changed\n")
|
||||||
|
break
|
||||||
|
} else if oldGame.SoundtrackName == file.Name() && oldGame.Hash != dirHash {
|
||||||
|
status = GameChanged
|
||||||
|
id = oldGame.ID
|
||||||
|
//fmt.Printf("Game changed\n")
|
||||||
|
break
|
||||||
|
} else if oldGame.SoundtrackName != file.Name() && oldGame.Hash == dirHash {
|
||||||
|
status = TitleChanged
|
||||||
|
id = oldGame.ID
|
||||||
|
//fmt.Printf("SoundtrackName changed\n")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if full && status != NewGame {
|
||||||
|
status = TitleChanged
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(gameDir)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to read game directory", zap.String("path", gameDir), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case NewGame:
|
||||||
|
if id != -1 {
|
||||||
|
for _, entry := range entries {
|
||||||
|
fileInfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get file info", zap.String("error", err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id = getIdFromFileNew(fileInfo)
|
||||||
|
if id != -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash})
|
||||||
|
handleError("InsertSoundtrackWithExistingId", err, "")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Debug("Game already exists, removing old ID file",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("game_dir", gameDir))
|
||||||
|
fileName := gameDir + "/." + strconv.Itoa(int(id)) + ".id"
|
||||||
|
logging.GetLogger().Debug("Removing ID file", zap.String("filename", fileName))
|
||||||
|
|
||||||
|
err := os.Remove(fileName)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to remove ID file", zap.String("filename", fileName), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
newDirHash := getHashForDir(gameDir)
|
||||||
|
|
||||||
|
id = insertGameNew(file.Name(), gameDir, newDirHash)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = insertGameNew(file.Name(), gameDir, dirHash)
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("New game detected",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("game", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
gamesAdded = append(gamesAdded, file.Name())
|
||||||
|
newCheckSongs(entries, gameDir, id)
|
||||||
|
case GameChanged:
|
||||||
|
logging.GetLogger().Debug("Game changed",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("game", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
||||||
|
handleError("UpdateSoundtrackHash", err, "")
|
||||||
|
gamesChangedContent = append(gamesChangedContent, file.Name())
|
||||||
|
newCheckSongs(entries, gameDir, id)
|
||||||
|
case TitleChanged:
|
||||||
|
logging.GetLogger().Debug("Game title changed",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("oldName", oldGame.SoundtrackName),
|
||||||
|
zap.String("newName", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: gameDir, ID: id})
|
||||||
|
handleError("UpdateSoundtrackName", err, "")
|
||||||
|
newCheckSongs(entries, gameDir, id)
|
||||||
|
if gamesChangedTitle == nil {
|
||||||
|
gamesChangedTitle = make(map[string]string)
|
||||||
|
}
|
||||||
|
gamesChangedTitle[oldGame.SoundtrackName] = file.Name()
|
||||||
|
case NotChanged:
|
||||||
|
var found bool = false
|
||||||
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
|
if dirHash == beforeGame.Hash {
|
||||||
|
found = true
|
||||||
|
logging.GetLogger().Debug("Game not changed",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("newName", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
newCheckSongs(entries, gameDir, id)
|
||||||
|
gamesReAdded = append(gamesReAdded, file.Name())
|
||||||
|
logging.GetLogger().Debug("Game added again",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("newName", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Game sync status",
|
||||||
|
zap.Int32("id", id),
|
||||||
|
zap.String("game", file.Name()),
|
||||||
|
zap.String("hash", dirHash),
|
||||||
|
zap.String("status", status.String()))
|
||||||
|
err = repo.RemoveSoundtrackDeletionDate(BackendCtx(), id)
|
||||||
|
handleError("RemoveSoundtrackDeletionDate", err, "")
|
||||||
|
}
|
||||||
|
foldersSynced++
|
||||||
|
logging.GetLogger().Debug("Sync progress",
|
||||||
|
zap.Int("folders_synced", int(foldersSynced)),
|
||||||
|
zap.Int("total_folders", int(numberOfFoldersToSync)),
|
||||||
|
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertGameNew(name string, path string, hash string) int32 {
|
||||||
|
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
||||||
|
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash})
|
||||||
|
handleError("InsertSoundtrack", err, "")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Warn("ID collision detected, resetting sequence")
|
||||||
|
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
||||||
|
logging.GetLogger().Debug("Resetting game ID sequence")
|
||||||
|
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
||||||
|
handleError("ResetSoundtrackIdSeq", err, "")
|
||||||
|
id = insertGameNew(name, path, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
|
||||||
|
//hasher := md5.New()
|
||||||
|
var numberOfSongs int32
|
||||||
|
numberOfFiles := len(entries)
|
||||||
|
|
||||||
|
var songWg sync.WaitGroup
|
||||||
|
songWg.Add(numberOfFiles)
|
||||||
|
for _, entry := range entries {
|
||||||
|
poolSong.Submit(func() {
|
||||||
|
defer songWg.Done()
|
||||||
|
if newCheckSong(entry, gameDir, id) {
|
||||||
|
numberOfSongs++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
songWg.Wait()
|
||||||
|
return numberOfSongs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
||||||
|
fileInfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSong(fileInfo) {
|
||||||
|
path := gameDir + entry.Name()
|
||||||
|
|
||||||
|
songHash := getHashForFile(path)
|
||||||
|
//numberOfSongs++
|
||||||
|
|
||||||
|
fileName := entry.Name()
|
||||||
|
songName, _ := strings.CutSuffix(fileName, ".mp3")
|
||||||
|
|
||||||
|
song, err := repo.GetSongWithHash(BackendCtx(), songHash)
|
||||||
|
handleError("GetSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
if err == nil {
|
||||||
|
if song.SongName == songName && song.Path == path {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.GetLogger().Debug("Song changed",
|
||||||
|
zap.Int32("game_id", id),
|
||||||
|
zap.String("path", path),
|
||||||
|
zap.String("song_name", songName),
|
||||||
|
zap.String("song_hash", songHash))
|
||||||
|
|
||||||
|
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
|
handleError("CheckSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
|
if err != nil {
|
||||||
|
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||||
|
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
|
if count2 > 0 {
|
||||||
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||||
|
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
|
handleError("CheckSongWithHash 2", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//count, _ := repo.CheckSong(ctx, path)
|
||||||
|
if count > 0 {
|
||||||
|
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
||||||
|
handleError("UpdateSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
} else {
|
||||||
|
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||||
|
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
if count2 > 0 {
|
||||||
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||||
|
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
} else {
|
||||||
|
err = repo.AddSong(BackendCtx(), repository.AddSongParams{SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
|
||||||
|
handleError("AddSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else if isCoverImage(fileInfo) {
|
||||||
|
//TODO: Later add cover art image here in db
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(funcName string, err error, msg string) {
|
||||||
|
var compareError = errors.New("no rows in result set")
|
||||||
|
if err != nil {
|
||||||
|
if compareError.Error() != err.Error() {
|
||||||
|
logging.GetLogger().Error("Database error",
|
||||||
|
zap.String("function", funcName),
|
||||||
|
zap.String("error", err.Error()))
|
||||||
|
if msg != "" {
|
||||||
|
logging.GetLogger().Debug("Error context", zap.String("message", msg))
|
||||||
|
catchedErrors = append(catchedErrors, fmt.Sprintf("Func: %s\nError message: %s\nDebug message: %s", funcName, err, msg))
|
||||||
|
} else {
|
||||||
|
catchedErrors = append(catchedErrors, fmt.Sprintf("Func: %s\nError message: %s", funcName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHashForDir(gameDir string) string {
|
||||||
|
directory, _ := directory_checksum.ScanDirectory(gameDir, afero.NewOsFs())
|
||||||
|
hash, _ := directory.ComputeDirectoryChecksums()
|
||||||
|
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHashForFile(path string) string {
|
||||||
|
hasher := md5.New()
|
||||||
|
readFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to open file for hashing", zap.String("path", path), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
defer readFile.Close()
|
||||||
|
hasher.Reset()
|
||||||
|
_, err = io.Copy(hasher, readFile)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to hash file", zap.String("path", path), zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIdFromFileNew(file os.FileInfo) int32 {
|
||||||
|
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 int32(i)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
slice []string
|
||||||
|
search string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "element exists",
|
||||||
|
slice: []string{"a", "b", "c"},
|
||||||
|
search: "b",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "element does not exist",
|
||||||
|
slice: []string{"a", "b", "c"},
|
||||||
|
search: "d",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty slice",
|
||||||
|
slice: []string{},
|
||||||
|
search: "a",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "element at start",
|
||||||
|
slice: []string{"a", "b", "c"},
|
||||||
|
search: "a",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "element at end",
|
||||||
|
slice: []string{"a", "b", "c"},
|
||||||
|
search: "c",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := contains(tt.slice, tt.search)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("contains() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSong(t *testing.T) {
|
||||||
|
mockFileInfo := &mockFileInfoForSong{name: "test.mp3", isDir: false, size: 100}
|
||||||
|
|
||||||
|
result := isSong(mockFileInfo)
|
||||||
|
if !result {
|
||||||
|
t.Error("isSong() should return true for .mp3 file")
|
||||||
|
}
|
||||||
|
|
||||||
|
mockFileInfo2 := &mockFileInfoForSong{name: "test.txt", isDir: false, size: 100}
|
||||||
|
result = isSong(mockFileInfo2)
|
||||||
|
if result {
|
||||||
|
t.Error("isSong() should return false for .txt file")
|
||||||
|
}
|
||||||
|
|
||||||
|
mockFileInfo3 := &mockFileInfoForSong{name: "test", isDir: true, size: 100}
|
||||||
|
result = isSong(mockFileInfo3)
|
||||||
|
if result {
|
||||||
|
t.Error("isSong() should return false for directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsCoverImage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fileInfo fs.FileInfo
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cover.jpg",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "cover.jpg", isDir: false, size: 100},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cover.png",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "cover.png", isDir: false, size: 100},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "my_cover.jpg",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "my_cover.jpg", isDir: false, size: 100},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image.jpg",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "image.jpg", isDir: false, size: 100},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cover.txt",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "cover.txt", isDir: false, size: 100},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "directory",
|
||||||
|
fileInfo: &mockFileInfoForCover{name: "cover", isDir: true, size: 100},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := isCoverImage(tt.fileInfo)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("isCoverImage() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIdFromFileNew(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fileInfo os.FileInfo
|
||||||
|
expected int32
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid id file",
|
||||||
|
fileInfo: &mockFileInfoForId{name: ".123.id", isDir: false, size: 100},
|
||||||
|
expected: 123,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid id file (directory)",
|
||||||
|
fileInfo: &mockFileInfoForId{name: ".123.id", isDir: true, size: 100},
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid id file (no .id extension)",
|
||||||
|
fileInfo: &mockFileInfoForId{name: "123.txt", isDir: false, size: 100},
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid id file (not a number)",
|
||||||
|
fileInfo: &mockFileInfoForId{name: ".abc.id", isDir: false, size: 100},
|
||||||
|
expected: 0, // strconv.Atoi returns 0 for invalid numbers (error is ignored)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := getIdFromFileNew(tt.fileInfo)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("getIdFromFileNew() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock types for testing
|
||||||
|
type mockFileInfoForSong struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFileInfoForSong) Name() string { return m.name }
|
||||||
|
func (m *mockFileInfoForSong) Size() int64 { return m.size }
|
||||||
|
func (m *mockFileInfoForSong) Mode() os.FileMode { return 0 }
|
||||||
|
func (m *mockFileInfoForSong) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (m *mockFileInfoForSong) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockFileInfoForSong) Sys() interface{} { return nil }
|
||||||
|
|
||||||
|
type mockFileInfoForCover struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFileInfoForCover) Name() string { return m.name }
|
||||||
|
func (m *mockFileInfoForCover) Size() int64 { return m.size }
|
||||||
|
func (m *mockFileInfoForCover) Mode() os.FileMode { return 0 }
|
||||||
|
func (m *mockFileInfoForCover) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (m *mockFileInfoForCover) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockFileInfoForCover) Sys() interface{} { return nil }
|
||||||
|
|
||||||
|
type mockFileInfoForId struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFileInfoForId) Name() string { return m.name }
|
||||||
|
func (m *mockFileInfoForId) Size() int64 { return m.size }
|
||||||
|
func (m *mockFileInfoForId) Mode() os.FileMode { return 0 }
|
||||||
|
func (m *mockFileInfoForId) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (m *mockFileInfoForId) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockFileInfoForId) Sys() interface{} { return nil }
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
type VersionData struct {
|
||||||
|
Version string `json:"version" example:"1.0.0"`
|
||||||
|
Changelog []string `json:"changelog" example:"[\"Initial release\",\"Bug fixes\"]"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = []VersionData{
|
||||||
|
{
|
||||||
|
Version: "5.0.0-Beta",
|
||||||
|
Changelog: []string{
|
||||||
|
"#16 - Upgrade Echo framework from v4 to v5",
|
||||||
|
"#17 - Add Zap structured logging framework",
|
||||||
|
"#18 - Add OpenAPI/Swagger documentation",
|
||||||
|
"#19 - Replace Tailwind CSS with pure CSS",
|
||||||
|
"#20 - Change domain from sanplex.tech to sanplex.xyz",
|
||||||
|
"#21 - Refactor handlers into domain-specific files",
|
||||||
|
"#22 - Change VersionData Changelog from string to string array",
|
||||||
|
"#23 - Update all dependencies to latest versions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "4.5.0",
|
||||||
|
Changelog: []string{
|
||||||
|
"#1 - Created request to check newest version of the app",
|
||||||
|
"#2 - Added request to download the newest version of the app",
|
||||||
|
"#3 - Added request to check progress during sync",
|
||||||
|
"#4 - Now blocking all request while sync is in progress",
|
||||||
|
"#5 - Implemented ants for thread pooling",
|
||||||
|
"#6 - Changed the sync request to now only start the sync",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "4.0.0",
|
||||||
|
Changelog: []string{
|
||||||
|
"Changed framework from gin to Echo",
|
||||||
|
"Reorganized the code",
|
||||||
|
"Implemented sqlc",
|
||||||
|
"Added support to send character images from the server",
|
||||||
|
"Added function to create a new database of no one exists",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.2",
|
||||||
|
Changelog: []string{"Upgraded Go version and the version of all dependencies. Fixed som more bugs."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.1",
|
||||||
|
Changelog: []string{"Fixed some bugs with songs not found made the application crash. Now checking if song exists and if not, remove song from DB and find another one. Frontend is now decoupled from the backend."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "3.0",
|
||||||
|
Changelog: []string{"Changed routing framework from mux to Gin. Swagger doc is now included in the application. A fronted can now be hosted from the application."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.3.0",
|
||||||
|
Changelog: []string{"Images should not be included in the database, removes songs where the path doesn't work."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.2.0",
|
||||||
|
Changelog: []string{"Changed the structure of the whole application, should be no changes to functionality."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.4",
|
||||||
|
Changelog: []string{"Game list should now be sorted, a new endpoint with the game list in random order have been added."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.3",
|
||||||
|
Changelog: []string{"Added a check to see if song exists before returning it, if not a new song will be picked up."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.2",
|
||||||
|
Changelog: []string{"Added test server to swagger file."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.1",
|
||||||
|
Changelog: []string{"Fixed bug where wrong song was showed as currently played."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.1.0",
|
||||||
|
Changelog: []string{
|
||||||
|
"Added /addQue to add the last received song to the songQue.",
|
||||||
|
"Changed /rand and /rand/low to not add song to the que.",
|
||||||
|
"Changed /next to not call /rand when the end of the que is reached, instead the last song in the que will be resent.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.3",
|
||||||
|
Changelog: []string{"Another small change that should fix the caching problem."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.2",
|
||||||
|
Changelog: []string{"Hopefully fixed the caching problem with random."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.1",
|
||||||
|
Changelog: []string{"Fixed CORS"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "2.0.0",
|
||||||
|
Changelog: []string{"Rebuilt the application in Go."},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLatestVersion() VersionData {
|
||||||
|
return data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVersionHistory() []VersionData {
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database holds the database connection pool and context
|
||||||
|
type Database struct {
|
||||||
|
Pool *pgxpool.Pool
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new Database instance with connection pool
|
||||||
|
func NewDatabase(host, port, user, password, dbname string) (*Database, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
|
||||||
|
logging.GetLogger().Debug("Database connection info",
|
||||||
|
zap.String("host", host),
|
||||||
|
zap.String("port", port),
|
||||||
|
zap.String("dbname", dbname))
|
||||||
|
|
||||||
|
pool, err := pgxpool.New(ctx, psqlInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to connect to database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
var success string
|
||||||
|
err = pool.QueryRow(ctx, "select 'Successfully connected!'").Scan(&success)
|
||||||
|
if err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, fmt.Errorf("database query failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Database connected", zap.String("status", success))
|
||||||
|
|
||||||
|
return &Database{Pool: pool, Ctx: ctx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection pool
|
||||||
|
func (db *Database) Close() {
|
||||||
|
if db.Pool != nil {
|
||||||
|
logging.GetLogger().Info("Closing database connection")
|
||||||
|
db.Pool.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health checks the health of the database connection by pinging the database.
|
||||||
|
// It returns a map with keys indicating various health statistics.
|
||||||
|
func (db *Database) Health() map[string]string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
// Ping the database
|
||||||
|
err := db.Pool.Ping(ctx)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = err.Error()
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["status"] = "up"
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMigrations runs all pending database migrations to the latest version.
|
||||||
|
// Uses the existing pool to extract connection details.
|
||||||
|
func (db *Database) RunMigrations() error {
|
||||||
|
// Extract connection info from pool config
|
||||||
|
connConfig := db.Pool.Config().ConnConfig
|
||||||
|
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||||
|
connConfig.User,
|
||||||
|
connConfig.Password,
|
||||||
|
connConfig.Host,
|
||||||
|
connConfig.Port,
|
||||||
|
connConfig.Database)
|
||||||
|
|
||||||
|
logging.GetLogger().Debug("Migration info", zap.String("url", migrationURL))
|
||||||
|
|
||||||
|
sqlDb, err := sql.Open("postgres", migrationURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open database for migration: %w", err)
|
||||||
|
}
|
||||||
|
defer sqlDb.Close()
|
||||||
|
|
||||||
|
driver, err := postgres.WithInstance(sqlDb, &postgres.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration driver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := iofs.New(MigrationsFs, "migrations")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithInstance("iofs", files, "postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migrator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current version for logging
|
||||||
|
version, _, err := m.Version()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get migration version", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Migration version before", zap.Uint("version", version))
|
||||||
|
|
||||||
|
// Run all pending migrations to latest version
|
||||||
|
err = m.Up()
|
||||||
|
if err != nil {
|
||||||
|
if err == migrate.ErrNoChange {
|
||||||
|
logging.GetLogger().Info("Database already up to date")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("migration failed: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get new version after migration
|
||||||
|
versionAfter, _, _ := m.Version()
|
||||||
|
logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter))
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Migration completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: DEPRECATED - Remove these global variables once all code is migrated to use Database struct
|
||||||
|
// Use database.go's Database struct instead. These globals remain for backward compatibility
|
||||||
|
// with legacy code paths. New code should use the Database struct from database.go.
|
||||||
|
var Dbpool *pgxpool.Pool
|
||||||
|
var Ctx = context.Background()
|
||||||
|
|
||||||
|
//go:embed "migrations/*.sql"
|
||||||
|
var MigrationsFs embed.FS
|
||||||
|
|
||||||
|
func InitDB(host string, port string, user string, password string, dbname string) {
|
||||||
|
|
||||||
|
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s "+
|
||||||
|
"password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
|
||||||
|
logging.GetLogger().Debug("Database connection info", zap.String("host", host), zap.String("port", port), zap.String("dbname", dbname))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
Dbpool, err = pgxpool.New(Ctx, psqlInfo)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Unable to connect to database", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var success string
|
||||||
|
err = Dbpool.QueryRow(Ctx, "select 'Successfully connected!'").Scan(&success)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Database query failed", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Info("Database connected", zap.String("status", success))
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDb() {
|
||||||
|
logging.GetLogger().Info("Closing database connection")
|
||||||
|
Dbpool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetGameIdSeq() {
|
||||||
|
_, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to reset game ID sequence", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDb(host string, port string, user string, password string, dbname string) {
|
||||||
|
// Connect to the default postgres database to create new database
|
||||||
|
// In PostgreSQL, we need to connect to an existing database (postgres) to create a new one
|
||||||
|
conninfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable", host, port, user, password)
|
||||||
|
db, err := sql.Open("postgres", conninfo)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to connect for database creation", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
_, err = db.Exec("create database " + dbname)
|
||||||
|
if err != nil {
|
||||||
|
//handle the error
|
||||||
|
logging.GetLogger().Fatal("Failed to create database", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
logging.GetLogger().Info("Database created", zap.String("dbname", dbname))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Migrate_db(host string, port string, user string, password string, dbname string) {
|
||||||
|
migrationInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
||||||
|
user, password, host, port, dbname)
|
||||||
|
|
||||||
|
logging.GetLogger().Debug("Migration info", zap.String("url", migrationInfo))
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", migrationInfo)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to open database for migration", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to create migration driver", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
files, err := iofs.New(MigrationsFs, "migrations")
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to create migration files", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
m, err := migrate.NewWithInstance("iofs", files, "postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Fatal("Failed to create migrator", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
/*m, err := migrate.NewWithDatabaseInstance(
|
||||||
|
"file://./db/migrations/",
|
||||||
|
"postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Migration setup error", zap.String("error", err.Error()))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
version, _, err := m.Version()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get migration version", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Migration version before", zap.Uint("version", version))
|
||||||
|
|
||||||
|
//err = m.Force(1)
|
||||||
|
//err = m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run
|
||||||
|
//if err != nil {
|
||||||
|
// logging.GetLogger().Error("Force migration error", zap.String("error", err.Error()))
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Use Up() to apply all pending migrations instead of Migrate(2)
|
||||||
|
err = m.Up()
|
||||||
|
if err != nil {
|
||||||
|
if err == migrate.ErrNoChange {
|
||||||
|
logging.GetLogger().Info("Database already up to date")
|
||||||
|
} else {
|
||||||
|
logging.GetLogger().Error("Migration error", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
versionAfter, _, err := m.Version()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get migration version after", zap.String("error", err.Error()))
|
||||||
|
} else {
|
||||||
|
logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.GetLogger().Info("Migration completed")
|
||||||
|
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health checks the health of the database connection by pinging the database.
|
||||||
|
// It returns a map with keys indicating various health statistics.
|
||||||
|
func Health() map[string]string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
// Ping the database
|
||||||
|
//err := s.db.PingContext(ctx)
|
||||||
|
err := Dbpool.Ping(ctx)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = fmt.Sprintf("db down: %v", err)
|
||||||
|
logging.GetLogger().Fatal("Database health check failed", zap.String("error", err.Error()))
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database is up, add more statistics
|
||||||
|
stats["status"] = "up"
|
||||||
|
stats["message"] = "It's healthy"
|
||||||
|
|
||||||
|
// Get database stats (like open connections, in use, idle, etc.)
|
||||||
|
//dbStats := s.db.Stats()
|
||||||
|
dbStats := Dbpool.Stat()
|
||||||
|
//stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections)
|
||||||
|
stats["open_connections"] = strconv.Itoa(int(dbStats.NewConnsCount()))
|
||||||
|
//stats["in_use"] = strconv.Itoa(dbStats.InUse)
|
||||||
|
stats["in_use"] = strconv.Itoa(int(dbStats.AcquiredConns()))
|
||||||
|
//stats["idle"] = strconv.Itoa(dbStats.Idle)
|
||||||
|
stats["idle"] = strconv.Itoa(int(dbStats.IdleConns()))
|
||||||
|
//stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10)
|
||||||
|
stats["wait_count"] = strconv.FormatInt(dbStats.AcquireCount(), 10)
|
||||||
|
//stats["wait_duration"] = dbStats.WaitDuration.String()
|
||||||
|
stats["wait_duration"] = dbStats.AcquireDuration().String()
|
||||||
|
//stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10)
|
||||||
|
stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleDestroyCount(), 10)
|
||||||
|
//stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10)
|
||||||
|
stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeDestroyCount(), 10)
|
||||||
|
|
||||||
|
// Evaluate stats to provide a health message
|
||||||
|
if int(dbStats.NewConnsCount()) > 40 { // Assuming 50 is the max for this example
|
||||||
|
stats["message"] = "The database is experiencing heavy load."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.AcquireCount() > 1000 {
|
||||||
|
stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.MaxIdleDestroyCount() > int64(dbStats.NewConnsCount())/2 {
|
||||||
|
stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.MaxLifetimeDestroyCount() > int64(dbStats.NewConnsCount())/2 {
|
||||||
|
stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern."
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMigrationsStepByStep tests applying migrations incrementally
|
||||||
|
// Then adding data manually, then completing migrations
|
||||||
|
func TestMigrationsStepByStep(t *testing.T) {
|
||||||
|
host := os.Getenv("DB_HOST")
|
||||||
|
port := os.Getenv("DB_PORT")
|
||||||
|
user := os.Getenv("DB_USERNAME")
|
||||||
|
password := os.Getenv("DB_PASSWORD")
|
||||||
|
// Use a unique database name for this test
|
||||||
|
dbname := "music_server_migration_test"
|
||||||
|
|
||||||
|
if host == "" || port == "" || user == "" || password == "" {
|
||||||
|
t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up: drop database if it exists
|
||||||
|
cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
defer cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Create the database
|
||||||
|
createTestDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Step 1: Apply first 4 migrations (before soundtrack rename)
|
||||||
|
// This creates: game, song, vgmq, song_list tables
|
||||||
|
// And sessions table with indexes
|
||||||
|
t.Run("ApplyFirst4Migrations", func(t *testing.T) {
|
||||||
|
applyMigrations(t, host, port, user, password, dbname, 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 2: Add data manually to game and song tables
|
||||||
|
t.Run("AddManualData", func(t *testing.T) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Insert 5 games manually
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
gameName := fmt.Sprintf("Manual Game %d", i)
|
||||||
|
path := fmt.Sprintf("/manual/path/game%d", i)
|
||||||
|
hash := fmt.Sprintf("hash-%d", i)
|
||||||
|
|
||||||
|
_, err := db.Exec(`INSERT INTO game (game_name, path, hash, added)
|
||||||
|
VALUES ($1, $2, $3, NOW())`,
|
||||||
|
gameName, path, hash)
|
||||||
|
require.NoError(t, err, "Failed to insert game %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert songs for each game
|
||||||
|
songs := []struct {
|
||||||
|
gameID int
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{1, "Song A", "/path/a.mp3"},
|
||||||
|
{1, "Song B", "/path/b.mp3"},
|
||||||
|
{2, "Song C", "/path/c.mp3"},
|
||||||
|
{2, "Song D", "/path/d.mp3"},
|
||||||
|
{3, "Song E", "/path/e.mp3"},
|
||||||
|
{4, "Song F", "/path/f.mp3"},
|
||||||
|
{4, "Song G", "/path/g.mp3"},
|
||||||
|
{4, "Song H", "/path/h.mp3"},
|
||||||
|
{5, "Song I", "/path/i.mp3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range songs {
|
||||||
|
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
|
||||||
|
VALUES ($1, $2, $3, $4)`,
|
||||||
|
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
|
||||||
|
require.NoError(t, err, "Failed to insert song %s", s.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data was inserted
|
||||||
|
var gameCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM game").Scan(&gameCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, gameCount, "Expected 5 games")
|
||||||
|
|
||||||
|
var songCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 9, songCount, "Expected 9 songs")
|
||||||
|
|
||||||
|
t.Log("✓ Manually inserted 5 games with 9 songs")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||||
|
t.Run("ApplyMigration5", func(t *testing.T) {
|
||||||
|
// Apply the remaining migrations (just migration 5)
|
||||||
|
applyMigrations(t, host, port, user, password, dbname, 1)
|
||||||
|
|
||||||
|
// Verify tables were renamed
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check that soundtrack table exists
|
||||||
|
var soundtrackCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, soundtrackCount, "Expected 5 soundtracks after migration")
|
||||||
|
|
||||||
|
// Check that game table no longer exists
|
||||||
|
_, err = db.Exec("SELECT 1 FROM game LIMIT 1")
|
||||||
|
require.Error(t, err, "game table should not exist after migration")
|
||||||
|
|
||||||
|
// Check that song table has soundtrack_id column
|
||||||
|
var songCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 9, songCount, "Expected 9 songs after migration")
|
||||||
|
|
||||||
|
// Verify data integrity: soundtrack_name values
|
||||||
|
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
expectedNames := []string{"Manual Game 1", "Manual Game 2", "Manual Game 3", "Manual Game 4", "Manual Game 5"}
|
||||||
|
actualNames := make([]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
err := rows.Scan(&name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
actualNames = append(actualNames, name)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedNames, actualNames, "Soundtrack names should match original game names")
|
||||||
|
|
||||||
|
t.Log("✓ Migration 5 applied successfully, data preserved")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupDB drops the test database
|
||||||
|
func cleanupDB(t *testing.T, host, port, user, password, dbname string) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
||||||
|
host, port, user, password)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: could not connect to cleanup DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check if database exists before dropping
|
||||||
|
var exists int
|
||||||
|
err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
t.Logf("Warning: could not check if DB exists: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists == 1 {
|
||||||
|
_, err = db.Exec("DROP DATABASE " + dbname + " WITH (FORCE)")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: could not drop DB: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestDB creates a fresh test database
|
||||||
|
func createTestDB(t *testing.T, host, port, user, password, dbname string) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
||||||
|
host, port, user, password)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Drop if exists
|
||||||
|
cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Create database
|
||||||
|
_, err = db.Exec("CREATE DATABASE " + dbname)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Enable UUID extension if needed
|
||||||
|
connStrDB := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db2, err := sql.Open("postgres", connStrDB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db2.Close()
|
||||||
|
|
||||||
|
_, err = db2.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Note: uuid-ossp extension may not be available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyMigrations applies n migrations to the database using Go migrate library
|
||||||
|
func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) {
|
||||||
|
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
||||||
|
user, password, host, port, dbname)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", migrationURL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance(
|
||||||
|
"file://migrations",
|
||||||
|
"postgres", driver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get current version
|
||||||
|
version, _, err := m.Version()
|
||||||
|
if err != nil && err != migrate.ErrNilVersion {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if err == migrate.ErrNilVersion {
|
||||||
|
version = 0
|
||||||
|
}
|
||||||
|
t.Logf("Current migration version: %d", version)
|
||||||
|
|
||||||
|
// Apply exactly 'steps' migrations
|
||||||
|
if steps > 0 {
|
||||||
|
err = m.Steps(steps)
|
||||||
|
if err != nil && err != migrate.ErrNoChange {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
} else if steps < 0 {
|
||||||
|
err = m.Steps(steps)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new version
|
||||||
|
newVersion, _, err := m.Version()
|
||||||
|
if err != nil && err != migrate.ErrNilVersion {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if err == migrate.ErrNilVersion {
|
||||||
|
newVersion = 0
|
||||||
|
}
|
||||||
|
t.Logf("Migration version after applying %d steps: %d", steps, newVersion)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Alter table game
|
||||||
|
alter column times_played set null;
|
||||||
|
|
||||||
|
Alter table song
|
||||||
|
alter column times_played set null;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Alter table game
|
||||||
|
alter column times_played set not null;
|
||||||
|
|
||||||
|
Alter table song
|
||||||
|
alter column times_played set not null;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Drop indexes for sessions table
|
||||||
|
DROP INDEX IF EXISTS idx_sessions_expires;
|
||||||
|
DROP INDEX IF EXISTS idx_sessions_token;
|
||||||
|
DROP INDEX IF EXISTS idx_sessions_ip;
|
||||||
|
DROP INDEX IF EXISTS idx_sessions_created;
|
||||||
|
|
||||||
|
-- Drop sessions table
|
||||||
|
DROP TABLE IF EXISTS sessions;
|
||||||
|
|
||||||
|
-- Drop performance indexes for song_list
|
||||||
|
DROP INDEX IF EXISTS idx_song_list_match_date;
|
||||||
|
DROP INDEX IF EXISTS idx_song_list_match_id;
|
||||||
|
|
||||||
|
-- Drop performance indexes for song
|
||||||
|
DROP INDEX IF EXISTS idx_song_hash;
|
||||||
|
DROP INDEX IF EXISTS idx_song_path;
|
||||||
|
DROP INDEX IF EXISTS idx_song_game_id;
|
||||||
|
DROP INDEX IF EXISTS idx_song_game_id_song_name;
|
||||||
|
|
||||||
|
-- Drop performance indexes for game
|
||||||
|
DROP INDEX IF EXISTS idx_game_deleted;
|
||||||
|
DROP INDEX IF EXISTS idx_game_hash;
|
||||||
|
DROP INDEX IF EXISTS idx_game_path;
|
||||||
|
DROP INDEX IF EXISTS idx_game_name;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- PERFORMANCE INDEXES FOR EXISTING TABLES
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Game table indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_deleted ON game(deleted) WHERE deleted IS NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_hash ON game(hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_path ON game(path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_name ON game(game_name);
|
||||||
|
|
||||||
|
-- Song table indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_hash ON song(hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_game_id ON song(game_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_game_id_song_name ON song(game_id, song_name);
|
||||||
|
|
||||||
|
-- Song_list table indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_list_match_date ON song_list(match_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_list_match_id ON song_list(match_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- SESSIONS TABLE FOR TOKEN MANAGEMENT
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Create sessions table for tracking client tokens
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
token VARCHAR(64) PRIMARY KEY,
|
||||||
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
|
user_agent TEXT NOT NULL,
|
||||||
|
client_type VARCHAR(20) DEFAULT 'web',
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for fast lookup and cleanup
|
||||||
|
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
|
||||||
|
CREATE INDEX idx_sessions_token ON sessions(token);
|
||||||
|
CREATE INDEX idx_sessions_ip ON sessions(ip_address);
|
||||||
|
CREATE INDEX idx_sessions_created ON sessions(created_at);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- Revert: Rename soundtrack table back to game
|
||||||
|
ALTER TABLE soundtrack RENAME TO game;
|
||||||
|
|
||||||
|
-- Revert primary key sequence
|
||||||
|
ALTER SEQUENCE soundtrack_id_seq RENAME TO game_id_seq;
|
||||||
|
|
||||||
|
-- Revert columns in game table
|
||||||
|
ALTER TABLE game RENAME COLUMN soundtrack_name TO game_name;
|
||||||
|
|
||||||
|
-- Revert song table: rename soundtrack_id back to game_id
|
||||||
|
ALTER TABLE song RENAME COLUMN soundtrack_id TO game_id;
|
||||||
|
|
||||||
|
-- Revert song primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||||
|
ALTER TABLE song ADD PRIMARY KEY (game_id, path);
|
||||||
|
ALTER TABLE song RENAME CONSTRAINT song_pkey_soundtrack TO song_pkey;
|
||||||
|
|
||||||
|
-- Revert song_list table references
|
||||||
|
ALTER TABLE song_list RENAME COLUMN soundtrack_name TO game_name;
|
||||||
|
|
||||||
|
-- Revert foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_game_id_fkey
|
||||||
|
FOREIGN KEY (game_id) REFERENCES game(id);
|
||||||
|
|
||||||
|
-- Revert indexes
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_deleted RENAME TO idx_game_deleted;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_hash RENAME TO idx_game_hash;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_path RENAME TO idx_game_path;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_name RENAME TO idx_game_name;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_soundtrack_id RENAME TO idx_song_game_id;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_soundtrack_id_song_name RENAME TO idx_song_game_id_song_name;
|
||||||
|
ALTER INDEX IF EXISTS song_list_soundtrack_name_idx RENAME TO song_list_game_name_idx;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Rename game table to soundtrack
|
||||||
|
ALTER TABLE game RENAME TO soundtrack;
|
||||||
|
|
||||||
|
-- Rename primary key sequence
|
||||||
|
ALTER SEQUENCE game_id_seq RENAME TO soundtrack_id_seq;
|
||||||
|
|
||||||
|
-- Rename columns in soundtrack table
|
||||||
|
ALTER TABLE soundtrack RENAME COLUMN game_name TO soundtrack_name;
|
||||||
|
|
||||||
|
-- Update song table: rename game_id to soundtrack_id
|
||||||
|
ALTER TABLE song RENAME COLUMN game_id TO soundtrack_id;
|
||||||
|
|
||||||
|
-- Update song primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||||
|
ALTER TABLE song ADD PRIMARY KEY (soundtrack_id, path);
|
||||||
|
|
||||||
|
-- Update song_list table references
|
||||||
|
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||||
|
|
||||||
|
-- Rename foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_game_id_fkey;
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
|
|
||||||
|
-- Rename indexes
|
||||||
|
ALTER INDEX IF EXISTS idx_game_deleted RENAME TO idx_soundtrack_deleted;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_hash RENAME TO idx_soundtrack_hash;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_path RENAME TO idx_soundtrack_path;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_name RENAME TO idx_soundtrack_name;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_game_id RENAME TO idx_song_soundtrack_id;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_game_id_song_name RENAME TO idx_song_soundtrack_id_song_name;
|
||||||
|
ALTER INDEX IF EXISTS song_list_game_name_idx RENAME TO song_list_soundtrack_name_idx;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Rollback: Remove id column and restore composite PK
|
||||||
|
|
||||||
|
-- Step 1: Drop indexes created in up migration
|
||||||
|
DROP INDEX IF EXISTS idx_song_soundtrack_id;
|
||||||
|
DROP INDEX IF EXISTS idx_song_path;
|
||||||
|
|
||||||
|
-- Step 2: Drop foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
|
||||||
|
-- Step 3: Drop new primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||||
|
|
||||||
|
-- Step 4: Drop unique constraint on id
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_id_unique;
|
||||||
|
|
||||||
|
-- Step 5: Restore composite primary key
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (soundtrack_id, path);
|
||||||
|
|
||||||
|
-- Step 6: Drop the id column
|
||||||
|
ALTER TABLE song DROP COLUMN id;
|
||||||
|
|
||||||
|
-- Step 7: Recreate original foreign key (soundtrack_id references soundtrack.id)
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- Migration: Add id column to song table and change PK from composite to single column
|
||||||
|
-- This prepares the song table for eventual UUID migration
|
||||||
|
|
||||||
|
-- Step 1: Add new id column (nullable initially)
|
||||||
|
ALTER TABLE song ADD COLUMN id serial4;
|
||||||
|
|
||||||
|
-- Step 2: Create unique constraint on id (allows backfilling)
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_id_unique UNIQUE (id);
|
||||||
|
|
||||||
|
-- Step 3: Backfill existing rows with sequential IDs
|
||||||
|
-- Use DEFAULT which pulls from the sequence
|
||||||
|
UPDATE song SET id = DEFAULT WHERE id IS NULL;
|
||||||
|
|
||||||
|
-- Step 4: Verify all rows have an id
|
||||||
|
-- If this returns 0, backfill worked
|
||||||
|
-- SELECT COUNT(*) FROM song WHERE id IS NULL;
|
||||||
|
|
||||||
|
-- Step 5: Drop the composite primary key (soundtrack_id, path)
|
||||||
|
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||||
|
|
||||||
|
-- Step 6: Add new primary key on id column
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
-- Step 7: Ensure soundtrack_id remains a foreign key to soundtrack
|
||||||
|
-- First drop existing FK if it exists (from the rename migration)
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
|
||||||
|
-- Then recreate it
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
|
|
||||||
|
-- Step 8: Create index on soundtrack_id for query performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_id ON song(soundtrack_id);
|
||||||
|
|
||||||
|
-- Step 9: Create index on path for lookups (previously part of PK)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- name: CreateSession :one
|
||||||
|
INSERT INTO sessions (token, ip_address, user_agent, client_type, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING token, ip_address, user_agent, client_type, expires_at, created_at;
|
||||||
|
|
||||||
|
-- name: GetSession :one
|
||||||
|
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||||
|
FROM sessions
|
||||||
|
WHERE token = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: DeleteSession :exec
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE token = $1;
|
||||||
|
|
||||||
|
-- name: DeleteExpiredSessions :exec
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE expires_at < NOW();
|
||||||
|
|
||||||
|
-- name: ListSessions :many
|
||||||
|
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||||
|
FROM sessions
|
||||||
|
ORDER BY created_at DESC;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
-- name: ClearSongs :exec
|
-- name: ClearSongs :exec
|
||||||
DELETE FROM song;
|
DELETE FROM song;
|
||||||
|
|
||||||
-- name: ClearSongsByGameId :exec
|
-- name: ClearSongsBySoundtrackId :exec
|
||||||
DELETE FROM song WHERE game_id = $1;
|
DELETE FROM song WHERE soundtrack_id = $1;
|
||||||
|
|
||||||
-- name: AddSong :exec
|
-- name: AddSong :exec
|
||||||
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: CheckSong :one
|
-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE path = $1;
|
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||||
|
|
||||||
-- name: CheckSongWithHash :one
|
-- name: CheckSongWithHash :one
|
||||||
SELECT COUNT(*) FROM song WHERE hash = $1;
|
SELECT COUNT(*) FROM song WHERE hash = $1;
|
||||||
@@ -20,22 +20,25 @@ SELECT * FROM song WHERE hash = $1;
|
|||||||
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
||||||
|
|
||||||
-- name: AddHashToSong :exec
|
-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where path=$2;
|
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3;
|
||||||
|
|
||||||
-- name: FindSongsFromGame :many
|
-- name: FindSongsFromSoundtrack :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM song
|
FROM song
|
||||||
WHERE game_id = $1;
|
WHERE soundtrack_id = $1;
|
||||||
|
|
||||||
-- name: AddSongPlayed :exec
|
-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE game_id = $1 AND song_name = $2;
|
WHERE soundtrack_id = $1 AND song_name = $2;
|
||||||
|
|
||||||
-- name: FetchAllSongs :many
|
-- name: FetchAllSongs :many
|
||||||
SELECT * FROM song;
|
SELECT * FROM song;
|
||||||
|
|
||||||
|
-- name: GetSongById :one
|
||||||
|
SELECT * FROM song WHERE id = $1;
|
||||||
|
|
||||||
-- name: RemoveBrokenSong :exec
|
-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE path = $1;
|
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||||
|
|
||||||
-- name: RemoveBrokenSongs :exec
|
-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song where path = any (sqlc.slice('paths'));
|
DELETE FROM song WHERE id = ANY($1);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
-- name: InsertSongInList :exec
|
-- name: InsertSongInList :exec
|
||||||
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
|
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name)
|
||||||
VALUES ($1, $2, $3, $4, $5);
|
VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: GetSongList :many
|
-- name: GetSongList :many
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- name: ResetSoundtrackIdSeq :one
|
||||||
|
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);
|
||||||
|
|
||||||
|
-- name: GetSoundtrackNameById :one
|
||||||
|
SELECT soundtrack_name FROM soundtrack WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetSoundtrackById :one
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE id = $1
|
||||||
|
AND deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: SetSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: ClearSoundtracks :exec
|
||||||
|
DELETE FROM soundtrack;
|
||||||
|
|
||||||
|
-- name: UpdateSoundtrackName :exec
|
||||||
|
UPDATE soundtrack SET soundtrack_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: UpdateSoundtrackHash :exec
|
||||||
|
UPDATE soundtrack SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: RemoveSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=NULL WHERE id=$1;
|
||||||
|
|
||||||
|
-- name: GetIdBySoundtrackName :one
|
||||||
|
SELECT id FROM soundtrack WHERE soundtrack_name = $1;
|
||||||
|
|
||||||
|
-- name: InsertSoundtrack :one
|
||||||
|
INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
|
||||||
|
|
||||||
|
-- name: InsertSoundtrackWithExistingId :exec
|
||||||
|
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
||||||
|
|
||||||
|
-- name: FindAllSoundtracks :many
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
ORDER BY soundtrack_name;
|
||||||
|
|
||||||
|
-- name: GetAllSoundtracksIncludingDeleted :many
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
ORDER BY soundtrack_name;
|
||||||
|
|
||||||
|
-- name: AddSoundtrackPlayed :exec
|
||||||
|
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
-- Most played soundtracks with their songs
|
||||||
|
-- name: GetMostPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played DESC, g.soundtrack_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Least played soundtracks with their songs
|
||||||
|
-- name: GetLeastPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played ASC, g.soundtrack_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Most played songs with their soundtrack info
|
||||||
|
-- name: GetMostPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played DESC, s.song_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Least played songs with their soundtrack info
|
||||||
|
-- name: GetLeastPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played ASC, s.song_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Games that have never been played (times_played = 0)
|
||||||
|
-- name: GetNeverPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.added,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.times_played = 0
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
||||||
|
ORDER BY g.soundtrack_name;
|
||||||
|
|
||||||
|
-- Last played soundtracks (most recently played)
|
||||||
|
-- name: GetLastPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played DESC
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Oldest played soundtracks (least recently played, but has been played at least once)
|
||||||
|
-- name: GetOldestPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played ASC
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Get statistics summary
|
||||||
|
-- name: GetStatisticsSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||||
|
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||||
|
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||||
|
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||||
|
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL;
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
IpAddress string `json:"ip_address"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
ClientType *string `json:"client_type"`
|
||||||
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Song struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SongName string `json:"song_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
FileName *string `json:"file_name"`
|
||||||
|
ID pgtype.Int4 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongList struct {
|
||||||
|
MatchDate time.Time `json:"match_date"`
|
||||||
|
MatchID int32 `json:"match_id"`
|
||||||
|
SongNo int32 `json:"song_no"`
|
||||||
|
SoundtrackName *string `json:"soundtrack_name"`
|
||||||
|
SongName *string `json:"song_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Soundtrack struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
Added time.Time `json:"added"`
|
||||||
|
Deleted *time.Time `json:"deleted"`
|
||||||
|
LastChanged *time.Time `json:"last_changed"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
LastPlayed *time.Time `json:"last_played"`
|
||||||
|
NumberOfSongs int32 `json:"number_of_songs"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vgmq struct {
|
||||||
|
SongNo int32 `json:"song_no"`
|
||||||
|
Path *string `json:"path"`
|
||||||
|
Clue *string `json:"clue"`
|
||||||
|
Answered bool `json:"answered"`
|
||||||
|
Answer *string `json:"answer"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: session.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createSession = `-- name: CreateSession :one
|
||||||
|
INSERT INTO sessions (token, ip_address, user_agent, client_type, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING token, ip_address, user_agent, client_type, expires_at, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateSessionParams struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
IpAddress string `json:"ip_address"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
ClientType *string `json:"client_type"`
|
||||||
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createSession,
|
||||||
|
arg.Token,
|
||||||
|
arg.IpAddress,
|
||||||
|
arg.UserAgent,
|
||||||
|
arg.ClientType,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
var i Session
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Token,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.ClientType,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE expires_at < NOW()
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredSessions(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteExpiredSessions)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSession = `-- name: DeleteSession :exec
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteSession(ctx context.Context, token string) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteSession, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSession = `-- name: GetSession :one
|
||||||
|
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||||
|
FROM sessions
|
||||||
|
WHERE token = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSession(ctx context.Context, token string) (Session, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSession, token)
|
||||||
|
var i Session
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Token,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.ClientType,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listSessions = `-- name: ListSessions :many
|
||||||
|
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||||
|
FROM sessions
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listSessions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Session
|
||||||
|
for rows.Next() {
|
||||||
|
var i Session
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Token,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.ClientType,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
// 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
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const addHashToSong = `-- name: AddHashToSong :exec
|
const addHashToSong = `-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where path=$2
|
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddHashToSongParams struct {
|
type AddHashToSongParams struct {
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
|
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.SoundtrackID, arg.Path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSong = `-- name: AddSong :exec
|
const addSong = `-- name: AddSong :exec
|
||||||
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
|
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongParams struct {
|
type AddSongParams struct {
|
||||||
GameID int32 `json:"game_id"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
@@ -37,7 +40,7 @@ type AddSongParams struct {
|
|||||||
|
|
||||||
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSong,
|
_, err := q.db.Exec(ctx, addSong,
|
||||||
arg.GameID,
|
arg.SoundtrackID,
|
||||||
arg.SongName,
|
arg.SongName,
|
||||||
arg.Path,
|
arg.Path,
|
||||||
arg.FileName,
|
arg.FileName,
|
||||||
@@ -48,25 +51,30 @@ func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
|||||||
|
|
||||||
const addSongPlayed = `-- name: AddSongPlayed :exec
|
const addSongPlayed = `-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE game_id = $1 AND song_name = $2
|
WHERE soundtrack_id = $1 AND song_name = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongPlayedParams struct {
|
type AddSongPlayedParams struct {
|
||||||
GameID int32 `json:"game_id"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSongPlayed, arg.GameID, arg.SongName)
|
_, err := q.db.Exec(ctx, addSongPlayed, arg.SoundtrackID, arg.SongName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSong = `-- name: CheckSong :one
|
const checkSong = `-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE path = $1
|
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
|
type CheckSongParams struct {
|
||||||
row := q.db.QueryRow(ctx, checkSong, path)
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CheckSong(ctx context.Context, arg CheckSongParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, checkSong, arg.SoundtrackID, arg.Path)
|
||||||
var count int64
|
var count int64
|
||||||
err := row.Scan(&count)
|
err := row.Scan(&count)
|
||||||
return count, err
|
return count, err
|
||||||
@@ -92,17 +100,17 @@ func (q *Queries) ClearSongs(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSongsByGameId = `-- name: ClearSongsByGameId :exec
|
const clearSongsBySoundtrackId = `-- name: ClearSongsBySoundtrackId :exec
|
||||||
DELETE FROM song WHERE game_id = $1
|
DELETE FROM song WHERE soundtrack_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ClearSongsByGameId(ctx context.Context, gameID int32) error {
|
func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int32) error {
|
||||||
_, err := q.db.Exec(ctx, clearSongsByGameId, gameID)
|
_, err := q.db.Exec(ctx, clearSongsBySoundtrackId, soundtrackID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name FROM song
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||||
@@ -115,12 +123,13 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -132,14 +141,14 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const findSongsFromGame = `-- name: FindSongsFromGame :many
|
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id
|
||||||
FROM song
|
FROM song
|
||||||
WHERE game_id = $1
|
WHERE soundtrack_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song, error) {
|
func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int32) ([]Song, error) {
|
||||||
rows, err := q.db.Query(ctx, findSongsFromGame, gameID)
|
rows, err := q.db.Query(ctx, findSongsFromSoundtrack, soundtrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,12 +157,13 @@ func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song,
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -165,39 +175,64 @@ func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song,
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSongById = `-- name: GetSongById :one
|
||||||
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSongById, id)
|
||||||
|
var i Song
|
||||||
|
err := row.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SongName,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.Hash,
|
||||||
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name FROM song WHERE hash = $1
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE hash = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
||||||
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
||||||
var i Song
|
var i Song
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE path = $1
|
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
|
type RemoveBrokenSongParams struct {
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSong, path)
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) RemoveBrokenSong(ctx context.Context, arg RemoveBrokenSongParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, removeBrokenSong, arg.SoundtrackID, arg.Path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song where path = any ($1)
|
DELETE FROM song WHERE id = ANY($1)
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
|
func (q *Queries) RemoveBrokenSongs(ctx context.Context, id pgtype.Int4) error {
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSongs, paths)
|
_, err := q.db.Exec(ctx, removeBrokenSongs, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||