Compare commits

...

10 Commits

Author SHA1 Message Date
6bebd5c51c Started added gitea actions 2025-05-08 07:51:36 +02:00
8e2d22b899 Updated .gitignore and added justfile 2025-01-09 14:34:43 +01:00
5577070b8d Changed addPlayed 2024-12-31 13:25:46 +01:00
59ff51393f Removed logging 2024-12-30 18:01:12 +01:00
26c5e6e4ef Small change on search page 2024-12-29 09:43:20 +01:00
a5f8e1b2ba Added search
Added a search page and fixed som error with migration
2024-12-26 14:55:47 +01:00
2a537d2398 Fixed error with docker 2024-12-19 12:55:17 +01:00
5ab19e16e5 Added a new sync that uses hash
Added a new sync that uses hash and sqlc for the queries. Added db
migration. Started adding a config file.
2024-12-19 12:11:20 +01:00
8fa93d580d Added concurrent sync, added search page. Other small changes 2024-10-04 20:19:40 +02:00
fafa044c9b Removed som logging during sync 2024-08-17 19:19:57 +02:00
58 changed files with 7268 additions and 145 deletions

34
.gitea/gitea-ci.yaml Normal file
View File

@@ -0,0 +1,34 @@
#
# .gitea/gitea-ci.yaml
#
name: Build And Test
run-name: ${{ gitea.actor }} is runs ci pipeline
on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
publish:
runs-on: ubuntu-latest
needs: build
if: gitea.ref == 'refs/heads/main'
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.tech/sansan"]
http = true
insecure = true
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: "gitea.sanplex.tech/sansan/MusicServer:${{gitea.sha}},gitea.sanplex.tech/sansan/MusicServer:latest"

7
.gitignore vendored
View File

@@ -1 +1,8 @@
tmp tmp
.DS_Store
.idea
*templ.go
conf.yaml
output.css
compose.yaml
tailwindcss

View File

@@ -1,19 +1,22 @@
FROM golang:1.22.2-alpine as build_go FROM golang:1.23-alpine as build_go
RUN apk add --no-cache curl
COPY go.* /music-server/ WORKDIR /app
COPY ./cmd/*.go /music-server/cmd/
COPY ./cmd/swagger /music-server/cmd/swagger
COPY ./pkg /music-server/pkg/
#WORKDIR /music-server/ COPY go.mod go.sum ./
#RUN go mod download RUN go mod download
WORKDIR /music-server/cmd COPY . .
RUN go install github.com/a-h/templ/cmd/templ@latest && \
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 /music-server/MusicServer RUN go build -o main cmd/main.go
# Stage 2, distribution container # Stage 2, distribution container
FROM golang:1.16-alpine FROM golang:1.23-alpine
EXPOSE 8080 EXPOSE 8080
VOLUME /sorted VOLUME /sorted
VOLUME /frontend VOLUME /frontend
@@ -24,7 +27,7 @@ ENV DB_USERNAME ""
ENV DB_PASSWORD "" ENV DB_PASSWORD ""
ENV DB_NAME "" ENV DB_NAME ""
COPY --from=build_go /music-server/MusicServer . COPY --from=build_go /app/main .
COPY ./songs/ ./songs/ COPY ./songs/ ./songs/
CMD ./MusicServer CMD ./main

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,34 @@
[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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,34 @@
[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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,34 @@
[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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,34 @@
[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

View File

@@ -0,0 +1,213 @@
/**************************************************************************/
/* 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);

200
cmd/search/index.html Normal file
View File

@@ -0,0 +1,200 @@
<!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>

BIN
cmd/search/index.icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,34 @@
[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

908
cmd/search/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"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"}

View File

@@ -0,0 +1,41 @@
<!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>

BIN
cmd/search/index.pck Normal file

Binary file not shown.

BIN
cmd/search/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,34 @@
[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

View File

@@ -0,0 +1,166 @@
// 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)));
}
});
});

BIN
cmd/search/index.wasm Normal file

Binary file not shown.

View File

@@ -0,0 +1,20 @@
@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;
}

3476
cmd/web/assets/js/htmx.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

18
cmd/web/base.templ Normal file
View File

@@ -0,0 +1,18 @@
package web
templ Base() {
<!DOCTYPE html>
<html lang="en" class="h-screen">
<head>
<meta charset="utf-8"/>
<title>Music Search</title>
<link href="assets/css/output.css" rel="stylesheet"/>
<script src="assets/js/htmx.min.js"></script>
</head>
<body class="bg-gray-100">
<main class="mx-auto p-4">
{ children... }
</main>
</body>
</html>
}

6
cmd/web/efs.go Normal file
View File

@@ -0,0 +1,6 @@
package web
import "embed"
//go:embed "assets"
var Files embed.FS

117
cmd/web/hello.go Normal file
View File

@@ -0,0 +1,117 @@
package web
import (
"log"
"music-server/pkg/server"
"net/http"
"regexp"
"strings"
)
var games_added []string
func FindGameWebHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
}
search_term := r.FormValue("search_term")
search(search_term)
component := FoundGames(games_added)
err = component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Fatalf("Error rendering in FindGameWebHandler: %e", err)
}
}
func search(searchText string) {
games_added = nil
games := server.GetAllGames()
for _, game := range games {
if is_match_exact(searchText, game) {
add_game(game)
}
}
for _, game := range games {
if is_match_contains(clean_term(searchText), clean_term(game)) {
add_game(game)
}
}
for _, game := range games {
if is_match_regex(clean_term(searchText), clean_term(game)) {
add_game(game)
}
}
}
func is_match_exact(search_term string, game_name string) bool {
search_term = strings.ToLower(search_term)
game_name = strings.ToLower(game_name)
if search_term == "" {
return true
} else if strings.Contains(game_name, search_term) {
return true
} else {
return false
}
}
func is_match_contains(search_term string, game_name string) bool {
if search_term == "" {
return true
} else if strings.Contains(game_name, search_term) {
return true
} else {
return false
}
}
func is_match_regex(search_term string, game_name string) bool {
if search_term == "" {
return true
} else if compile_regex(search_term).MatchString(game_name) {
return true
} else {
return false
}
}
func add_game(game string) {
if !check_if_game_exists(game) {
games_added = append(games_added, game)
}
}
func check_if_game_exists(gameName string) bool {
game_exists := false
for _, child := range games_added {
if child == gameName {
game_exists = true
}
}
return game_exists
}
func compile_regex(search_term string) *regexp.Regexp {
regText := ".*"
for _, letter := range search_term {
regText += string(letter) + ".*"
}
r, _ := regexp.Compile(regText)
return r
}
func clean_term(term string) string {
term = strings.ReplaceAll(term, " ", "")
term = strings.ReplaceAll(term, "é", "e")
term = strings.ReplaceAll(term, "+", "plus")
term = strings.ReplaceAll(term, "&", "and")
term = strings.ReplaceAll(term, "'n", "and")
return strings.ToLower(term)
}

32
cmd/web/hello.templ Normal file
View File

@@ -0,0 +1,32 @@
package web
templ HelloForm() {
@Base() {
<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"/>
<button type="button" class="bg-orange-500 hover:bg-orange-700 text-white py-2 px-4 rounded" id="clear" name="clear">Clear</button>
</div>
<div id="games-container"></div>
<script>
document.addEventListener('readystatechange', () => {
if (document.readyState == 'complete') {
htmx.ajax('POST', '/find', '#games-container');
document.getElementById("search_term").focus();
}
});
document.getElementById("clear").addEventListener("click", function (event) {
document.getElementById("search_term").value = "";
htmx.ajax('POST', '/find', '#games-container');
document.getElementById("search_term").focus();
});
</script>
}
}
templ FoundGames(games []string) {
for _, game := range games {
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
<p>{ game }</p>
</div>
}
}

6
db/efs.go Normal file
View File

@@ -0,0 +1,6 @@
package newDb
import "embed"
//go:embed "migrations/*.sql"
var MigrationsFs embed.FS

View File

@@ -0,0 +1,4 @@
DROP TABLE game;
DROP TABLE song;
DROP TABLE song_list;
DROP TABLE vgmq;

View File

@@ -0,0 +1,43 @@
CREATE TABLE game (
id serial4 NOT NULL,
game_name varchar NOT NULL,
added timestamp NOT NULL,
deleted timestamp NULL,
last_changed timestamp NULL,
"path" varchar NOT NULL,
times_played int4 DEFAULT 0 NULL,
last_played timestamp NULL,
number_of_songs int4 NULL,
hash varchar NULL,
CONSTRAINT game_pkey PRIMARY KEY (id)
);
CREATE TABLE song (
game_id int4 NOT NULL,
song_name varchar NOT NULL,
"path" varchar NOT NULL,
times_played int4 DEFAULT 0 NULL,
hash varchar NULL,
file_name varchar NULL,
CONSTRAINT song_pkey PRIMARY KEY (game_id, path)
);
CREATE TABLE vgmq (
song_no int4 NOT NULL,
"path" varchar(50) NULL,
clue varchar(200) NULL,
answered bool DEFAULT false NOT NULL,
answer varchar(50) NULL,
CONSTRAINT vgmq_pk PRIMARY KEY (song_no)
);
CREATE UNIQUE INDEX vgmq_song_no_uindex ON vgmq USING btree (song_no);
CREATE TABLE song_list (
match_date date NOT NULL,
match_id int4 NOT NULL,
song_no int4 NOT NULL,
game_name varchar(50) NULL,
song_name varchar(50) NULL,
CONSTRAINT song_list_pkey PRIMARY KEY (match_date, match_id, song_no)
);
CREATE INDEX song_list_game_name_idx ON song_list USING btree (game_name);

View File

@@ -0,0 +1,3 @@
Alter table game
alter column number_of_songs set null,
alter column hash set null;

View File

@@ -0,0 +1,19 @@
BEGIN;
UPDATE game
SET number_of_songs = 0
WHERE number_of_songs IS NULL;
UPDATE game
SET hash = ''
WHERE hash IS NULL;
UPDATE song
SET hash = ''
WHERE hash IS NULL;
COMMIT;
BEGIN;
Alter table game
alter column number_of_songs set not null,
alter column number_of_songs set default 0,
ALTER COLUMN hash SET NOT NULL;
ALTER TABLE song
ALTER COLUMN hash SET NOT NULL;
COMMIT;

49
db/queries/game.sql Normal file
View File

@@ -0,0 +1,49 @@
-- name: ResetGameIdSeq :one
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);
-- name: GetGameNameById :one
SELECT game_name FROM game WHERE id = $1;
-- name: GetGameById :one
SELECT *
FROM game
WHERE id = $1
AND deleted IS NULL;
-- name: SetGameDeletionDate :exec
UPDATE game SET deleted=now() WHERE deleted IS NULL;
-- name: ClearGames :exec
DELETE FROM game;
-- name: UpdateGameName :exec
UPDATE game SET game_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
-- name: UpdateGameHash :exec
UPDATE game SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
-- name: RemoveDeletionDate :exec
UPDATE game SET deleted=NULL WHERE id=$1;
-- name: GetIdByGameName :one
SELECT id FROM game WHERE game_name = $1;
-- name: InsertGame :one
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
-- name: InsertGameWithExistingId :exec
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
-- name: FindAllGames :many
SELECT *
FROM game
WHERE deleted IS NULL
ORDER BY game_name;
-- name: GetAllGamesIncludingDeleted :many
SELECT *
FROM game
ORDER BY game_name;
-- name: AddGamePlayed :exec
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1;

41
db/queries/song.sql Normal file
View File

@@ -0,0 +1,41 @@
-- name: ClearSongs :exec
DELETE FROM song;
-- name: ClearSongsByGameId :exec
DELETE FROM song WHERE game_id = $1;
-- name: AddSong :exec
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
-- name: CheckSong :one
SELECT COUNT(*) FROM song WHERE path = $1;
-- name: CheckSongWithHash :one
SELECT COUNT(*) FROM song WHERE hash = $1;
-- name: GetSongWithHash :one
SELECT * FROM song WHERE hash = $1;
-- name: UpdateSong :exec
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
-- name: AddHashToSong :exec
UPDATE song SET hash=$1 where path=$2;
-- name: FindSongsFromGame :many
SELECT *
FROM song
WHERE game_id = $1;
-- name: AddSongPlayed :exec
UPDATE song SET times_played = times_played + 1
WHERE game_id = $1 AND song_name = $2;
-- name: FetchAllSongs :many
SELECT * FROM song;
-- name: RemoveBrokenSong :exec
DELETE FROM song WHERE path = $1;
-- name: RemoveBrokenSongs :exec
DELETE FROM song where path = any (sqlc.slice('paths'));

9
db/queries/song_list.sql Normal file
View File

@@ -0,0 +1,9 @@
-- name: InsertSongInList :exec
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
VALUES ($1, $2, $3, $4, $5);
-- name: GetSongList :many
SELECT *
FROM song_list
WHERE match_date = $1
ORDER BY song_no DESC;

0
db/queries/vgmq.sql Normal file
View File

32
db/repository/db.go Normal file
View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package repository
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

246
db/repository/game.sql.go Normal file
View File

@@ -0,0 +1,246 @@
// 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
}

47
db/repository/models.go Normal file
View File

@@ -0,0 +1,47 @@
// 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"`
}

223
db/repository/song.sql.go Normal file
View File

@@ -0,0 +1,223 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: song.sql
package repository
import (
"context"
)
const addHashToSong = `-- name: AddHashToSong :exec
UPDATE song SET hash=$1 where path=$2
`
type AddHashToSongParams struct {
Hash string `json:"hash"`
Path string `json:"path"`
}
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
return err
}
const addSong = `-- name: AddSong :exec
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
`
type AddSongParams struct {
GameID int32 `json:"game_id"`
SongName string `json:"song_name"`
Path string `json:"path"`
FileName *string `json:"file_name"`
Hash string `json:"hash"`
}
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
_, err := q.db.Exec(ctx, addSong,
arg.GameID,
arg.SongName,
arg.Path,
arg.FileName,
arg.Hash,
)
return err
}
const addSongPlayed = `-- name: AddSongPlayed :exec
UPDATE song SET times_played = times_played + 1
WHERE game_id = $1 AND song_name = $2
`
type AddSongPlayedParams struct {
GameID int32 `json:"game_id"`
SongName string `json:"song_name"`
}
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
_, err := q.db.Exec(ctx, addSongPlayed, arg.GameID, arg.SongName)
return err
}
const checkSong = `-- name: CheckSong :one
SELECT COUNT(*) FROM song WHERE path = $1
`
func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
row := q.db.QueryRow(ctx, checkSong, path)
var count int64
err := row.Scan(&count)
return count, err
}
const checkSongWithHash = `-- name: CheckSongWithHash :one
SELECT COUNT(*) FROM song WHERE hash = $1
`
func (q *Queries) CheckSongWithHash(ctx context.Context, hash string) (int64, error) {
row := q.db.QueryRow(ctx, checkSongWithHash, hash)
var count int64
err := row.Scan(&count)
return count, err
}
const clearSongs = `-- name: ClearSongs :exec
DELETE FROM song
`
func (q *Queries) ClearSongs(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearSongs)
return err
}
const clearSongsByGameId = `-- name: ClearSongsByGameId :exec
DELETE FROM song WHERE game_id = $1
`
func (q *Queries) ClearSongsByGameId(ctx context.Context, gameID int32) error {
_, err := q.db.Exec(ctx, clearSongsByGameId, gameID)
return err
}
const fetchAllSongs = `-- name: FetchAllSongs :many
SELECT game_id, song_name, path, times_played, hash, file_name FROM song
`
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
rows, err := q.db.Query(ctx, fetchAllSongs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Song
for rows.Next() {
var i Song
if err := rows.Scan(
&i.GameID,
&i.SongName,
&i.Path,
&i.TimesPlayed,
&i.Hash,
&i.FileName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const findSongsFromGame = `-- name: FindSongsFromGame :many
SELECT game_id, song_name, path, times_played, hash, file_name
FROM song
WHERE game_id = $1
`
func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song, error) {
rows, err := q.db.Query(ctx, findSongsFromGame, gameID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Song
for rows.Next() {
var i Song
if err := rows.Scan(
&i.GameID,
&i.SongName,
&i.Path,
&i.TimesPlayed,
&i.Hash,
&i.FileName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSongWithHash = `-- name: GetSongWithHash :one
SELECT game_id, song_name, path, times_played, hash, file_name FROM song WHERE hash = $1
`
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
row := q.db.QueryRow(ctx, getSongWithHash, hash)
var i Song
err := row.Scan(
&i.GameID,
&i.SongName,
&i.Path,
&i.TimesPlayed,
&i.Hash,
&i.FileName,
)
return i, err
}
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
DELETE FROM song WHERE path = $1
`
func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
_, err := q.db.Exec(ctx, removeBrokenSong, path)
return err
}
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
DELETE FROM song where path = any ($1)
`
func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
_, err := q.db.Exec(ctx, removeBrokenSongs, paths)
return err
}
const updateSong = `-- name: UpdateSong :exec
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4
`
type UpdateSongParams struct {
SongName string `json:"song_name"`
FileName *string `json:"file_name"`
Path string `json:"path"`
Hash string `json:"hash"`
}
func (q *Queries) UpdateSong(ctx context.Context, arg UpdateSongParams) error {
_, err := q.db.Exec(ctx, updateSong,
arg.SongName,
arg.FileName,
arg.Path,
arg.Hash,
)
return err
}

View File

@@ -0,0 +1,68 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: song_list.sql
package repository
import (
"context"
"time"
)
const getSongList = `-- name: GetSongList :many
SELECT match_date, match_id, song_no, game_name, song_name
FROM song_list
WHERE match_date = $1
ORDER BY song_no DESC
`
func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongList, error) {
rows, err := q.db.Query(ctx, getSongList, matchDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SongList
for rows.Next() {
var i SongList
if err := rows.Scan(
&i.MatchDate,
&i.MatchID,
&i.SongNo,
&i.GameName,
&i.SongName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertSongInList = `-- name: InsertSongInList :exec
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
VALUES ($1, $2, $3, $4, $5)
`
type InsertSongInListParams 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"`
}
func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListParams) error {
_, err := q.db.Exec(ctx, insertSongInList,
arg.MatchDate,
arg.MatchID,
arg.SongNo,
arg.GameName,
arg.SongName,
)
return err
}

26
go.mod
View File

@@ -3,10 +3,16 @@ module music-server
go 1.22.2 go 1.22.2
require ( require (
github.com/MShekow/directory-checksum v1.4.6
github.com/a-h/templ v0.2.793
github.com/gin-contrib/static v1.1.2 github.com/gin-contrib/static v1.1.2
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/jackc/pgtype v1.14.3 github.com/jackc/pgtype v1.14.3
github.com/jackc/pgx/v5 v5.5.5 github.com/jackc/pgx/v5 v5.5.5
github.com/lib/pq v1.10.9
github.com/spf13/afero v1.11.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
@@ -14,15 +20,20 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.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-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
@@ -32,11 +43,14 @@ require (
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/sys v0.25.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect golang.org/x/text v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect google.golang.org/protobuf v1.34.2 // indirect
) )

98
go.sum
View File

@@ -1,5 +1,13 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/MShekow/directory-checksum v1.4.6 h1:2fhlCYbpjEN1iH9S0tdmEM0p1wvNT9x5x0rIchGI7nE=
github.com/MShekow/directory-checksum v1.4.6/go.mod h1:bMfFBkaIlNk7O9VgEi8D2X7Q2Jfk3c7d67z3t6cpIi4=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
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.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -15,6 +23,18 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=
github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs=
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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
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 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -23,8 +43,14 @@ github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0Nglqm
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw= 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 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -37,10 +63,19 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 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 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.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@@ -93,7 +128,6 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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 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.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/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 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
@@ -111,15 +145,17 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 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-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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -127,19 +163,31 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@@ -148,6 +196,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -171,10 +221,20 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -197,8 +257,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -213,12 +273,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -237,8 +298,8 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -254,8 +315,8 @@ 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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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-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-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-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -271,10 +332,9 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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-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=

78
justfile Normal file
View File

@@ -0,0 +1,78 @@
# Build the application
all: build test
templ-install:
@if ! command -v templ > /dev/null; then \
read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
go install github.com/a-h/templ/cmd/templ@latest; \
if [ ! -x "$$(command -v templ)" ]; then \
echo "templ installation failed. Exiting..."; \
exit 1; \
fi; \
else \
echo "You chose not to install templ. Exiting..."; \
exit 1; \
fi; \
fi
tailwind:
@if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-x64 -o tailwindcss; fi
@chmod +x tailwindcss
build:
@echo "Building..."
@templ generate
@./tailwindcss -i cmd/web/assets/css/input.css -o cmd/web/assets/css/output.css
@go build -o main cmd/main.go
run:
@templ generate
@go run cmd/main.go
test: build
@echo "Testing..."
@go test ./... -v
# Clean the binary
clean:
@echo "Cleaning..."
@rm -f main
# Create DB container
docker-run:
@if docker compose up --build 2>/dev/null; then \
: ; \
else \
echo "Falling back to Docker Compose V1"; \
docker-compose up --build; \
fi
# Shutdown DB container
docker-down:
@if docker compose down 2>/dev/null; then \
: ; \
else \
echo "Falling back to Docker Compose V1"; \
docker-compose down; \
fi
watch-templ:
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
# Live Reload
watch:
@if command -v air > /dev/null; then \
air; \
echo "Watching...";\
else \
read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
go install github.com/air-verse/air@latest; \
air; \
echo "Watching...";\
else \
echo "You chose not to install air. Exiting..."; \
exit 1; \
fi; \
fi

View File

@@ -1,9 +1,10 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"music-server/pkg/server" "music-server/pkg/server"
"net/http" "net/http"
"github.com/gin-gonic/gin"
) )
type Sync struct { type Sync struct {
@@ -19,6 +20,18 @@ func (s *Sync) SyncGames(ctx *gin.Context) {
ctx.JSON(http.StatusOK, "Games are synced") ctx.JSON(http.StatusOK, "Games are synced")
} }
func (s *Sync) SyncGamesQuick(ctx *gin.Context) {
server.SyncGamesQuick()
server.Reset()
ctx.JSON(http.StatusOK, "Games are synced")
}
func (s *Sync) SyncGamesNew(ctx *gin.Context) {
response := server.SyncGamesNew()
server.Reset()
ctx.JSON(http.StatusOK, response)
}
func (s *Sync) ResetGames(ctx *gin.Context) { func (s *Sync) ResetGames(ctx *gin.Context) {
server.ResetDB() server.ResetDB()
ctx.JSON(http.StatusOK, "Games and songs are deleted from the database") ctx.JSON(http.StatusOK, "Games and songs are deleted from the database")

View File

@@ -3,17 +3,29 @@ package conf
import ( import (
"embed" "embed"
"fmt" "fmt"
"github.com/gin-contrib/static" "io/fs"
"github.com/gin-gonic/gin"
"log" "log"
"music-server/cmd/web"
"music-server/pkg/api" "music-server/pkg/api"
"music-server/pkg/db" "music-server/pkg/db"
"music-server/pkg/helpers" "music-server/pkg/helpers"
"music-server/pkg/server"
"net/http"
"os" "os"
"strconv" "strconv"
"github.com/a-h/templ"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
) )
func SetupDb() { func SetupDb() {
/*err := server.ReadConf("conf.yaml")
if err != nil {
log.Fatal(err)
}*/
// Get the value of an Environment Variable // Get the value of an Environment Variable
host := os.Getenv("DB_HOST") host := os.Getenv("DB_HOST")
dbPort, dbPortErr := strconv.Atoi(os.Getenv("DB_PORT")) dbPort, dbPortErr := strconv.Atoi(os.Getenv("DB_PORT"))
@@ -34,16 +46,35 @@ func SetupDb() {
dbPort = 5432 dbPort = 5432
} }
if username == "" { if username == "" {
username = "sebastian" username = "postgres"
} }
if password == "" { if password == "" {
password = "950100" password = "postgres"
} }
if dbName == "" { if dbName == "" {
dbName = "music_dev_local" dbName = "music_test_local"
} }
db.Migrate_db(host, dbPort, username, password, dbName)
db.InitDB(host, dbPort, username, password, dbName) db.InitDB(host, dbPort, username, password, dbName)
var dir string
if host != "localhost" {
dir = "/sorted/"
} else {
dir = "/Users/sebastian/ResilioSync/Sorterat_test/"
}
server.Conf = &server.Config{
Host: host,
Port: dbPort,
User: username,
Password: password,
Dbname: dbName,
Path: dir,
}
} }
func CloseDb() { func CloseDb() {
@@ -59,6 +90,8 @@ func SetupRestServer(swagger embed.FS) {
syncGroup := router.Group("/sync") syncGroup := router.Group("/sync")
{ {
syncGroup.GET("", sync.SyncGames) syncGroup.GET("", sync.SyncGames)
syncGroup.GET("/new", sync.SyncGamesNew)
syncGroup.GET("/quick", sync.SyncGamesQuick)
syncGroup.GET("/reset", sync.ResetGames) syncGroup.GET("/reset", sync.ResetGames)
} }
@@ -85,11 +118,21 @@ func SetupRestServer(swagger embed.FS) {
index := api.NewIndex() index := api.NewIndex()
router.GET("/version", index.GetVersion) router.GET("/version", index.GetVersion)
router.GET("/test", index.GetDBTest) router.GET("/health", index.GetDBTest)
router.StaticFS("/swagger", helpers.EmbedFolder(swagger, "swagger", false)) router.StaticFS("/swagger", helpers.EmbedFolder(swagger, "swagger", false))
router.Use(static.Serve("/", static.LocalFile("/frontend", true))) router.Use(static.Serve("/", static.LocalFile("/frontend", true)))
router.Use(static.Serve("/new", static.LocalFile("/newFrontend", true))) router.Use(static.Serve("/new", static.LocalFile("/newFrontend", true)))
staticFiles, _ := fs.Sub(web.Files, "assets")
router.StaticFS("/assets", http.FS(staticFiles))
router.GET("/search", func(c *gin.Context) {
templ.Handler(web.HelloForm()).ServeHTTP(c.Writer, c.Request)
})
router.POST("/find", func(c *gin.Context) {
web.FindGameWebHandler(c.Writer, c.Request)
})
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"

View File

@@ -2,12 +2,23 @@ package db
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"github.com/jackc/pgx/v5" "log"
newDb "music-server/db"
"os" "os"
"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"
) )
var pool *pgx.Conn var Dbpool *pgxpool.Pool
var Ctx = context.Background()
func InitDB(host string, port int, user string, password string, dbname string) { func InitDB(host string, port int, user string, password string, dbname string) {
@@ -18,14 +29,14 @@ func InitDB(host string, port int, user string, password string, dbname string)
fmt.Println(psqlInfo) fmt.Println(psqlInfo)
var err error var err error
pool, err = pgx.Connect(context.Background(), psqlInfo) Dbpool, err = pgxpool.New(Ctx, psqlInfo)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1) os.Exit(1)
} }
var success string var success string
err = pool.QueryRow(context.Background(), "select 'Successfully connected!'").Scan(&success) err = Dbpool.QueryRow(Ctx, "select 'Successfully connected!'").Scan(&success)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -35,14 +46,11 @@ func InitDB(host string, port int, user string, password string, dbname string)
} }
func CloseDb() { func CloseDb() {
err := pool.Close(context.Background()) Dbpool.Close()
if err != nil {
return
}
} }
func Testf() { func Testf() {
rows, dbErr := pool.Query(context.Background(), "select game_name from game") rows, dbErr := Dbpool.Query(Ctx, "select game_name from game")
if dbErr != nil { if dbErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr) _, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr)
os.Exit(1) os.Exit(1)
@@ -58,8 +66,68 @@ func Testf() {
} }
func resetGameIdSeq() { func resetGameIdSeq() {
_, err := pool.Query(context.Background(), "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);") _, err := Dbpool.Query(Ctx, "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }
} }
func Migrate_db(host string, port int, user string, password string, dbname string) {
migrationInfo := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
user, password, host, port, dbname)
fmt.Println("Migration Info: ", migrationInfo)
db, err := sql.Open("postgres", migrationInfo)
if err != nil {
log.Println(err)
}
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
log.Println(err)
}
files, err := iofs.New(newDb.MigrationsFs, "migrations")
if err != nil {
log.Fatal(err)
}
m, err := migrate.NewWithInstance("iofs", files, "postgres", driver)
if err != nil {
log.Fatal(err)
}
/*m, err := migrate.NewWithDatabaseInstance(
"file://./db/migrations/",
"postgres", driver)
if err != nil {
log.Println(err)
}*/
version, _, err := m.Version()
if err != nil {
log.Println("Migration version err: ", err)
}
fmt.Println("Migration version before: ", 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 {
log.Println("Force err: ", err)
}
err = m.Migrate(2)
//err = m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run
if err != nil {
log.Println("Migration err: ", err)
}
versionAfter, _, err := m.Version()
if err != nil {
log.Println("Migration version err: ", err)
}
fmt.Println("Migration version after: ", versionAfter)
fmt.Println("Migration done")
db.Close()
}

View File

@@ -1,7 +1,6 @@
package db package db
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"music-server/pkg/models" "music-server/pkg/models"
@@ -13,10 +12,12 @@ import (
func GetGameName(gameId int) string { func GetGameName(gameId int) string {
var gameName = "" var gameName = ""
err := pool.QueryRow(context.Background(), err := Dbpool.QueryRow(Ctx,
"SELECT game_name FROM game WHERE id = $1", gameId).Scan(&gameName) "SELECT game_name FROM game WHERE id = $1", gameId).Scan(&gameName)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return "" return ""
} }
return gameName return gameName
@@ -27,11 +28,13 @@ func GetGameById(gameId int) (models.GameData, error) {
var numberOfSongs pgtype.Int4 var numberOfSongs pgtype.Int4
var gameName, path string var gameName, path string
var added, deleted, lastChanged, lastPlayed pgtype.Timestamp var added, deleted, lastChanged, lastPlayed pgtype.Timestamp
err := pool.QueryRow(context.Background(), err := Dbpool.QueryRow(Ctx,
"SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs "+ "SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs "+
"FROM game WHERE id = $1 AND deleted IS NULL ", gameId).Scan(&id, &gameName, &added, &deleted, &lastChanged, &path, &timesPlayed, &lastPlayed, &numberOfSongs) "FROM game WHERE id = $1 AND deleted IS NULL ", gameId).Scan(&id, &gameName, &added, &deleted, &lastChanged, &path, &timesPlayed, &lastPlayed, &numberOfSongs)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return models.GameData{}, errors.New("Game not found") return models.GameData{}, errors.New("Game not found")
} }
return models.GameData{ return models.GameData{
@@ -48,7 +51,7 @@ func GetGameById(gameId int) (models.GameData, error) {
} }
func SetGameDeletionDate() { func SetGameDeletionDate() {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE game SET deleted=$1 WHERE deleted IS NULL", time.Now()) "UPDATE game SET deleted=$1 WHERE deleted IS NULL", time.Now())
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
@@ -56,14 +59,14 @@ func SetGameDeletionDate() {
} }
func ClearGames() { func ClearGames() {
_, err := pool.Exec(context.Background(), "DELETE FROM game") _, err := Dbpool.Exec(Ctx, "DELETE FROM game")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }
} }
func UpdateGameName(id int, name string, path string) { func UpdateGameName(id int, name string, path string) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE game SET game_name=$1, path=$2, last_changed=$3 WHERE id=$4", "UPDATE game SET game_name=$1, path=$2, last_changed=$3 WHERE id=$4",
name, path, time.Now(), id) name, path, time.Now(), id)
if err != nil { if err != nil {
@@ -72,7 +75,7 @@ func UpdateGameName(id int, name string, path string) {
} }
func RemoveDeletionDate(id int) { func RemoveDeletionDate(id int) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE game SET deleted=null WHERE id=$1", id) "UPDATE game SET deleted=null WHERE id=$1", id)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
@@ -81,10 +84,12 @@ func RemoveDeletionDate(id int) {
func GetIdByGameName(name string) int { func GetIdByGameName(name string) int {
var gameId = -1 var gameId = -1
err := pool.QueryRow(context.Background(), err := Dbpool.QueryRow(Ctx,
"SELECT id FROM game WHERE game_name = $1", name).Scan(&gameId) "SELECT id FROM game WHERE game_name = $1", name).Scan(&gameId)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return -1 return -1
} }
return gameId return gameId
@@ -92,17 +97,21 @@ func GetIdByGameName(name string) int {
func InsertGame(name string, path string) int { func InsertGame(name string, path string) int {
gameId := -1 gameId := -1
err := pool.QueryRow(context.Background(), err := Dbpool.QueryRow(Ctx,
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id", "INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId) name, path, time.Now()).Scan(&gameId)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
resetGameIdSeq() resetGameIdSeq()
err2 := pool.QueryRow(context.Background(), err2 := Dbpool.QueryRow(Ctx,
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id", "INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId) name, path, time.Now()).Scan(&gameId)
if err2 != nil { if err2 != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err2.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return -1 return -1
} }
} }
@@ -110,7 +119,7 @@ func InsertGame(name string, path string) int {
} }
func InsertGameWithExistingId(id int, name string, path string) { func InsertGameWithExistingId(id int, name string, path string) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"INSERT INTO game(id, game_name, path, added) VALUES ($1, $2, $3, $4)", "INSERT INTO game(id, game_name, path, added) VALUES ($1, $2, $3, $4)",
id, name, path, time.Now()) id, name, path, time.Now())
if err != nil { if err != nil {
@@ -119,12 +128,14 @@ func InsertGameWithExistingId(id int, name string, path string) {
} }
func FindAllGames() []models.GameData { func FindAllGames() []models.GameData {
rows, err := pool.Query(context.Background(), rows, err := Dbpool.Query(Ctx,
"SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs "+ "SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs "+
"FROM game WHERE deleted IS NULL "+ "FROM game WHERE deleted IS NULL "+
"ORDER BY game_name") "ORDER BY game_name")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
var gameList []models.GameData var gameList []models.GameData
for rows.Next() { for rows.Next() {
@@ -134,7 +145,9 @@ func FindAllGames() []models.GameData {
var added, deleted, lastChanged, lastPlayed pgtype.Timestamp var added, deleted, lastChanged, lastPlayed pgtype.Timestamp
err := rows.Scan(&id, &gameName, &added, &deleted, &lastChanged, &path, &timesPlayed, &lastPlayed, &numberOfSongs) err := rows.Scan(&id, &gameName, &added, &deleted, &lastChanged, &path, &timesPlayed, &lastPlayed, &numberOfSongs)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
gameList = append(gameList, models.GameData{ gameList = append(gameList, models.GameData{
Id: id, Id: id,
@@ -152,7 +165,7 @@ func FindAllGames() []models.GameData {
} }
func AddGamePlayed(id int) { func AddGamePlayed(id int) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE game SET times_played=times_played+1, last_played=now() WHERE id=$1", id) "UPDATE game SET times_played=times_played+1, last_played=now() WHERE id=$1", id)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)

View File

@@ -1,21 +1,23 @@
package db package db
import ( import (
"context" "errors"
"fmt" "fmt"
"music-server/pkg/models" "music-server/pkg/models"
"os" "os"
"strings" "strings"
) )
var compareError = errors.New("no rows in result set")
func ClearSongs(gameId int) { func ClearSongs(gameId int) {
if gameId == -1 { if gameId == -1 {
_, err := pool.Exec(context.Background(), "DELETE FROM song") _, err := Dbpool.Exec(Ctx, "DELETE FROM song")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }
} else { } else {
_, err := pool.Exec(context.Background(), "DELETE FROM song where game_id=$1", gameId) _, err := Dbpool.Exec(Ctx, "DELETE FROM song where game_id=$1", gameId)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }
@@ -23,7 +25,7 @@ func ClearSongs(gameId int) {
} }
func AddSong(song models.SongData) { func AddSong(song models.SongData) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"INSERT INTO song(game_id, song_name, path, file_name) VALUES ($1, $2, $3, $4)", "INSERT INTO song(game_id, song_name, path, file_name) VALUES ($1, $2, $3, $4)",
song.GameId, song.SongName, song.Path, song.FileName) song.GameId, song.SongName, song.Path, song.FileName)
if err != nil { if err != nil {
@@ -33,17 +35,18 @@ func AddSong(song models.SongData) {
func CheckSong(songPath string) bool { func CheckSong(songPath string) bool {
var path string var path string
err := pool.QueryRow(context.Background(), err := Dbpool.QueryRow(Ctx,
"SELECT path FROM song WHERE path = $1", songPath).Scan(&path) "SELECT path FROM song WHERE path = $1", songPath).Scan(&path)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
println("CheckSong path", path)
return path != "" return path != ""
} }
func UpdateSong(songName string, fileName string, path string) { func UpdateSong(songName string, fileName string, path string) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE song SET song_name=$1, file_name=$2 WHERE path = $3", "UPDATE song SET song_name=$1, file_name=$2 WHERE path = $3",
songName, fileName, path) songName, fileName, path)
if err != nil { if err != nil {
@@ -52,10 +55,12 @@ func UpdateSong(songName string, fileName string, path string) {
} }
func FindSongsFromGame(id int) []models.SongData { func FindSongsFromGame(id int) []models.SongData {
rows, err := pool.Query(context.Background(), rows, err := Dbpool.Query(Ctx,
"SELECT song_name, path, file_name, times_played FROM song WHERE game_id = $1", id) "SELECT song_name, path, file_name, times_played FROM song WHERE game_id = $1", id)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return nil return nil
} }
@@ -68,7 +73,9 @@ func FindSongsFromGame(id int) []models.SongData {
err := rows.Scan(&songName, &path, &fileName, &timesPlayed) err := rows.Scan(&songName, &path, &fileName, &timesPlayed)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
songDataList = append(songDataList, models.SongData{ songDataList = append(songDataList, models.SongData{
@@ -83,7 +90,7 @@ func FindSongsFromGame(id int) []models.SongData {
} }
func AddSongPlayed(id int, name string) { func AddSongPlayed(id int, name string) {
_, err := pool.Exec(context.Background(), _, err := Dbpool.Exec(Ctx,
"UPDATE song SET times_played=times_played+1 WHERE game_id=$1 AND song_name=$2", id, name) "UPDATE song SET times_played=times_played+1 WHERE game_id=$1 AND song_name=$2", id, name)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
@@ -91,10 +98,12 @@ func AddSongPlayed(id int, name string) {
} }
func FetchAllSongs() []models.SongData { func FetchAllSongs() []models.SongData {
rows, err := pool.Query(context.Background(), rows, err := Dbpool.Query(Ctx,
"SELECT song_name, path FROM song") "SELECT song_name, path FROM song")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
return nil return nil
} }
@@ -105,7 +114,9 @@ func FetchAllSongs() []models.SongData {
err := rows.Scan(&songName, &path) err := rows.Scan(&songName, &path)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
songDataList = append(songDataList, models.SongData{ songDataList = append(songDataList, models.SongData{
@@ -117,7 +128,7 @@ func FetchAllSongs() []models.SongData {
} }
func RemoveBrokenSong(song models.SongData) { func RemoveBrokenSong(song models.SongData) {
_, err := pool.Exec(context.Background(), "DELETE FROM song where path=$1", song.Path) _, err := Dbpool.Exec(Ctx, "DELETE FROM song where path=$1", song.Path)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }
@@ -130,7 +141,7 @@ func RemoveBrokenSongs(songs []models.SongData) {
} }
joined = strings.TrimSuffix(joined, ",") joined = strings.TrimSuffix(joined, ",")
_, err := pool.Exec(context.Background(), "DELETE FROM song where path in ($1)", joined) _, err := Dbpool.Exec(Ctx, "DELETE FROM song where path in ($1)", joined)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err) _, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
} }

View File

@@ -1,7 +1,6 @@
package db package db
import ( import (
"context"
"fmt" "fmt"
"music-server/pkg/models" "music-server/pkg/models"
"os" "os"
@@ -9,7 +8,7 @@ import (
) )
func InsertSongInList(song models.SongListData) { func InsertSongInList(song models.SongListData) {
err := pool.QueryRow(context.Background(), _, err := Dbpool.Exec(Ctx,
`INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name) VALUES ($1, $2, $3, $4, $5)`, `INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name) VALUES ($1, $2, $3, $4, $5)`,
song.MatchDate, song.MatchId, song.SongNo, song.GameName, song.SongName) song.MatchDate, song.MatchId, song.SongNo, song.GameName, song.SongName)
if err != nil { if err != nil {
@@ -18,12 +17,14 @@ func InsertSongInList(song models.SongListData) {
} }
func GetSongList(matchId int) []models.SongListData { func GetSongList(matchId int) []models.SongListData {
rows, err := pool.Query(context.Background(), rows, err := Dbpool.Query(Ctx,
"SELECT match_date, match_id, song_no, game_name, song_name "+ "SELECT match_date, match_id, song_no, game_name, song_name "+
"FROM song_list WHERE match_date = $1"+ "FROM song_list WHERE match_date = $1"+
"ORDER BY song_no DESC", matchId) "ORDER BY song_no DESC", matchId)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err) if compareError.Error() != err.Error() {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
} }
var songList []models.SongListData var songList []models.SongListData
for rows.Next() { for rows.Next() {

View File

@@ -3,14 +3,15 @@ package helpers
import ( import (
"embed" "embed"
"fmt" "fmt"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"io" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
) )
func SetCorsAndNoCacheHeaders() gin.HandlerFunc { func SetCorsAndNoCacheHeaders() gin.HandlerFunc {

34
pkg/server/config.go Normal file
View File

@@ -0,0 +1,34 @@
package server
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Host string
Port int
User string
Password string
Dbname string
Path string
}
var Conf *Config
func ReadConf(filename string) error {
buf, err := os.ReadFile(filename)
if err != nil {
return err
}
c := &Config{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return fmt.Errorf("in file %q: %w", filename, err)
}
Conf = c
return err
}

View File

@@ -7,6 +7,7 @@ import (
"music-server/pkg/models" "music-server/pkg/models"
"os" "os"
"strconv" "strconv"
"strings"
) )
var currentSong = -1 var currentSong = -1
@@ -38,15 +39,31 @@ func AddLatestToQue() {
} }
func AddLatestPlayed() { func AddLatestPlayed() {
if lastFetched.Path != "" { if len(songQue) == 0 {
db.AddGamePlayed(lastFetched.GameId) return
} }
var currentSongData = songQue[currentSong]
db.AddGamePlayed(currentSongData.GameId)
db.AddSongPlayed(currentSongData.GameId, currentSongData.SongName)
}
func SetPlayed(songNumber int) {
if len(songQue) == 0 || songNumber >= len(songQue) {
return
}
var songData = songQue[songNumber]
db.AddGamePlayed(songData.GameId)
db.AddSongPlayed(songData.GameId, songData.SongName)
} }
func GetRandomSong() string { func GetRandomSong() string {
if games == nil || len(games) == 0 { if len(games) == 0 {
games = db.FindAllGames() games = db.FindAllGames()
} }
if len(games) == 0 {
return ""
}
song := getSongFromList(games) song := getSongFromList(games)
lastFetched = song lastFetched = song
@@ -54,7 +71,7 @@ func GetRandomSong() string {
} }
func GetRandomSongLowChance() string { func GetRandomSongLowChance() string {
if games == nil || len(games) == 0 { if len(games) == 0 {
games = db.FindAllGames() games = db.FindAllGames()
} }
@@ -190,15 +207,6 @@ func GetAllGamesRandom() []string {
return jsonArray return jsonArray
} }
func SetPlayed(songNumber int) {
if songQue == nil || len(songQue) == 0 || songNumber >= len(songQue) {
return
}
var songData = songQue[songNumber]
db.AddGamePlayed(songData.GameId)
db.AddSongPlayed(songData.GameId, songData.SongName)
}
func GetNextSong() string { func GetNextSong() string {
if songQue == nil { if songQue == nil {
return "" return ""
@@ -232,9 +240,9 @@ func getSongFromList(games []models.GameData) models.SongData {
var song models.SongData var song models.SongData
for !songFound { for !songFound {
game := getRandomGame(games) game := getRandomGame(games)
log.Println("game = ", game) //log.Println("game = ", game)
songs := db.FindSongsFromGame(game.Id) songs := db.FindSongsFromGame(game.Id)
log.Println("songs = ", songs) //log.Println("songs = ", songs)
if len(songs) == 0 { if len(songs) == 0 {
continue continue
} }
@@ -243,10 +251,10 @@ func getSongFromList(games []models.GameData) models.SongData {
//Check if file exists and open //Check if file exists and open
openFile, err := os.Open(song.Path) openFile, err := os.Open(song.Path)
log.Println("game.Path+song.FileName: ", game.Path+song.FileName) //log.Println("game.Path+song.FileName: ", game.Path+song.FileName)
log.Println("song.Path: ", song.Path) //log.Println("song.Path: ", song.Path)
log.Println("game.Path+song.FileName != song.Path: ", game.Path+song.FileName != song.Path) //log.Println("game.Path+song.FileName != song.Path: ", game.Path+song.FileName != song.Path)
if err != nil || game.Path+song.FileName != song.Path { if err != nil || game.Path+song.FileName != song.Path || strings.HasSuffix(song.FileName, ".wav") {
//File not found //File not found
db.RemoveBrokenSong(song) db.RemoveBrokenSong(song)
log.Println("Song not found, song '" + song.SongName + "' deleted from game '" + game.GameName + "' FileName: " + song.FileName) log.Println("Song not found, song '" + song.SongName + "' deleted from game '" + game.GameName + "' FileName: " + song.FileName)

View File

@@ -1,25 +1,80 @@
package server package server
import ( import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"log" "log"
"music-server/db/repository"
"music-server/pkg/db" "music-server/pkg/db"
"music-server/pkg/models" "music-server/pkg/models"
"os" "os"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/MShekow/directory-checksum/directory_checksum"
"github.com/spf13/afero"
) )
// SyncGames TODO: Make sync concurrent var allGames []repository.Game
var gamesBeforeSync []repository.Game
var gamesAfterSync []repository.Game
var gamesAdded []string
var gamesReAdded []string
var gamesChangedTitle map[string]string
var gamesChangedContent []string
var gamesRemoved []string
var catchedErrors []string
var brokenSongs []string
type Response 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"`
}
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]
}
var syncWg sync.WaitGroup
var repo *repository.Queries
var wg sync.WaitGroup
func SyncGames() { func SyncGames() {
start := time.Now()
host := os.Getenv("DB_HOST") host := os.Getenv("DB_HOST")
var dir string var dir string
if host != "" { if host != "" {
dir = "/sorted/" dir = "/sorted/"
} else { } else {
dir = "/Users/sebastian/Resilio Sync/Sorterat_test/" dir = "/Users/sebastian/ResilioSync/Sorterat_test/"
} }
fmt.Printf("dir: %s\n", dir) fmt.Printf("dir: %s\n", dir)
foldersToSkip := []string{".sync", "dist", "old"} foldersToSkip := []string{".sync", "dist", "old"}
@@ -33,33 +88,77 @@ func SyncGames() {
} }
for _, file := range files { for _, file := range files {
if file.IsDir() && !contains(foldersToSkip, file.Name()) { syncGame(file, foldersToSkip, dir)
fmt.Println(file.Name()) }
path := dir + file.Name() + "/" finished := time.Now()
fmt.Println(path) totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
}
entries, err := os.ReadDir(path) func SyncGamesQuick() {
start := time.Now()
host := os.Getenv("DB_HOST")
var dir string
if host != "" {
dir = "/sorted/"
} else {
dir = "/Users/sebastian/ResilioSync/Sorterat_test/"
}
fmt.Printf("dir: %s\n", dir)
foldersToSkip := []string{".sync", "dist", "old"}
fmt.Println(foldersToSkip)
db.SetGameDeletionDate()
checkBrokenSongs()
files, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
wg.Add(1)
go func() {
defer wg.Done()
syncGame(file, foldersToSkip, dir)
}()
}
wg.Wait()
finished := time.Now()
totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
}
func syncGame(file os.DirEntry, foldersToSkip []string, dir string) {
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
fmt.Println(file.Name())
path := dir + file.Name() + "/"
fmt.Println(path)
entries, err := os.ReadDir(path)
if err != nil {
log.Println(err)
}
id := -1
for _, entry := range entries {
fileInfo, err := entry.Info()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
id := -1 id = getIdFromFile(fileInfo)
for _, entry := range entries { if id != -1 {
fileInfo, err := entry.Info() break
if err != nil {
log.Println(err)
}
id = getIdFromFile(fileInfo)
if id != -1 {
break
}
}
if id == -1 {
addNewGame(file.Name(), path)
} else {
checkIfChanged(id, file.Name(), path)
checkSongs(path, id)
} }
} }
if id == -1 {
addNewGame(file.Name(), path)
} else {
checkIfChanged(id, file.Name(), path)
checkSongs(path, id)
}
} }
} }
@@ -98,7 +197,10 @@ func checkIfChanged(id int, name string, path string) {
func addNewGame(name string, path string) { func addNewGame(name string, path string) {
newId := db.GetIdByGameName(name) newId := db.GetIdByGameName(name)
if newId == -1 { if newId != -1 {
checkBrokenSongs()
db.RemoveDeletionDate(newId)
} else {
newId = db.InsertGame(name, path) newId = db.InsertGame(name, path)
} }
@@ -127,12 +229,9 @@ func checkSongs(gameDir string, gameId int) {
path := gameDir + entry.Name() path := gameDir + entry.Name()
fileName := entry.Name() fileName := entry.Name()
songName, _ := strings.CutSuffix(fileName, ".mp3") songName, _ := strings.CutSuffix(fileName, ".mp3")
println("path", path)
if db.CheckSong(path) { if db.CheckSong(path) {
println("db.CheckSong(path)", true)
db.UpdateSong(songName, fileName, path) db.UpdateSong(songName, fileName, path)
} else { } else {
println("db.CheckSong(path)", false)
db.AddSong(models.SongData{GameId: gameId, SongName: songName, Path: path, FileName: fileName}) db.AddSong(models.SongData{GameId: gameId, SongName: songName, Path: path, FileName: fileName})
} }
} else if isCoverImage(entry) { } else if isCoverImage(entry) {
@@ -162,8 +261,404 @@ func checkBrokenSongs() {
db.RemoveBrokenSongs(brokenSongs) db.RemoveBrokenSongs(brokenSongs)
} }
func SyncGamesNew() Response {
repo = repository.New(db.Dbpool)
start := time.Now()
fmt.Printf("dir: %s\n", Conf.Path)
foldersToSkip := []string{".sync", "dist", "old"}
fmt.Println(foldersToSkip)
var err error
gamesBeforeSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames Before", err, "")
fmt.Printf("Games Before: %d\n", len(gamesBeforeSync))
allGames, err = repo.GetAllGamesIncludingDeleted(db.Ctx)
handleError("GetAllGamesIncludingDeleted", err, "")
err = repo.SetGameDeletionDate(db.Ctx)
handleError("SetGameDeletionDate", err, "")
directories, err := os.ReadDir(Conf.Path)
if err != nil {
log.Fatal(err)
}
syncWg.Add(len(directories))
for _, dir := range directories {
go func() {
defer syncWg.Done()
syncGameNew(dir, foldersToSkip, Conf.Path)
}()
}
syncWg.Wait()
checkBrokenSongsNew()
gamesAfterSync, err = repo.FindAllGames(db.Ctx)
handleError("FindAllGames After", err, "")
fmt.Printf("\nGames Before: %d\n", len(gamesBeforeSync))
fmt.Printf("Games After: %d\n", len(gamesAfterSync))
fmt.Printf("\nGames added: \n")
for _, game := range gamesAdded {
fmt.Printf("%s\n", game)
}
fmt.Printf("\nGames readded: \n")
for _, game := range gamesReAdded {
fmt.Printf("%s\n", game)
}
fmt.Printf("\nGames with changed title: \n")
for key, value := range gamesChangedTitle {
fmt.Printf("The game: %s changed title to: %s\n", key, value)
}
fmt.Printf("\nGames with changed content: \n")
for _, game := range gamesChangedContent {
fmt.Printf("%s\n", game)
}
fmt.Printf("\n\n")
var gamesRemovedTemp []string
for _, beforeGame := range gamesBeforeSync {
var found bool = false
for _, afterGame := range gamesAfterSync {
if beforeGame.GameName == afterGame.GameName {
found = true
//fmt.Printf("Game: %s, Found: %v break\n", beforeGame.GameName, found)
break
}
}
if !found {
//fmt.Printf("Game: %s, Found: %v\n", beforeGame.GameName, found)
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.GameName)
}
}
for _, game := range gamesRemovedTemp {
var found bool = false
for key, _ := range gamesChangedTitle {
if game == key {
found = true
//fmt.Printf("Game: %s, Found: %v break2\n", game, found)
break
}
}
if !found {
gamesRemoved = append(gamesRemoved, game)
}
}
fmt.Printf("\nGames removed: \n")
for _, game := range gamesRemoved {
fmt.Printf("%s\n", game)
}
fmt.Printf("\nErrors catched: \n")
for _, error := range catchedErrors {
fmt.Printf("%s\n", error)
}
finished := time.Now()
totalTime := finished.Sub(start)
out := time.Time{}.Add(totalTime)
fmt.Printf("\nTotal time: %v\n", totalTime)
fmt.Printf("Total time: %v\n", out.Format("15:04:05.00000"))
return Response{
GamesAdded: gamesAdded,
GamesReAdded: gamesReAdded,
GamesChangedTitle: gamesChangedTitle,
GamesChangedContent: gamesChangedContent,
GamesRemoved: gamesRemoved,
CatchedErrors: catchedErrors,
}
}
func checkBrokenSongsNew() {
allSongs, err := repo.FetchAllSongs(db.Ctx)
handleError("FetchAllSongs", err, "")
var brokenWg sync.WaitGroup
brokenWg.Add(len(allSongs))
for _, song := range allSongs {
go func() {
defer brokenWg.Done()
checkBrokenSongNew(song)
}()
}
brokenWg.Wait()
err = repo.RemoveBrokenSongs(db.Ctx, brokenSongs)
handleError("RemoveBrokenSongs", 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, song.Path)
fmt.Printf("song broken: %v\n", song.Path)
} else {
err = openFile.Close()
if err != nil {
log.Println(err)
}
}
}
func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string) {
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
gameDir := baseDir + file.Name() + "/"
dirHash := getHashForDir(gameDir)
var status GameStatus = NewGame
var oldGame repository.Game
var id int32 = -1
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
for _, currentGame := range allGames {
oldGame = currentGame
//fmt.Printf("%s | %s\n", oldGame.GameName, oldGame.Hash)
if oldGame.GameName == file.Name() && oldGame.Hash == dirHash {
status = NotChanged
id = oldGame.ID
//fmt.Printf("Game not changed\n")
break
} else if oldGame.GameName == file.Name() && oldGame.Hash != dirHash {
status = GameChanged
id = oldGame.ID
//fmt.Printf("Game changed\n")
break
} else if oldGame.GameName != file.Name() && oldGame.Hash == dirHash {
status = TitleChanged
id = oldGame.ID
//fmt.Printf("GameName changed\n")
break
}
}
entries, err := os.ReadDir(gameDir)
if err != nil {
log.Println(err)
}
switch status {
case NewGame:
fmt.Printf("\n\nID: %d | GameName: %s | GameHash: %s | Status: %s\n", id, file.Name(), dirHash, status)
if id != -1 {
for _, entry := range entries {
fileInfo, err := entry.Info()
if err != nil {
log.Println(err)
}
id = getIdFromFileNew(fileInfo)
if id != -1 {
break
}
}
err = repo.InsertGameWithExistingId(db.Ctx, repository.InsertGameWithExistingIdParams{ID: id, GameName: file.Name(), Path: gameDir, Hash: dirHash})
handleError("InsertGameWithExistingId", err, "")
if err != nil {
fmt.Printf("id = %v\n", id)
fileName := gameDir + "/." + strconv.Itoa(int(id)) + ".id"
fmt.Printf("fileName = %v\n", fileName)
err := os.Remove(fileName)
if err != nil {
fmt.Printf("%s\n", err)
}
newDirHash := getHashForDir(gameDir)
id = insertGameNew(file.Name(), gameDir, newDirHash)
}
} else {
id = insertGameNew(file.Name(), gameDir, dirHash)
}
gamesAdded = append(gamesAdded, file.Name())
newCheckSongs(entries, gameDir, id)
case GameChanged:
fmt.Printf("\n\nID: %d | GameName: %s | GameHash: %s | Status: %s\n", id, file.Name(), dirHash, status)
err = repo.UpdateGameHash(db.Ctx, repository.UpdateGameHashParams{Hash: dirHash, ID: id})
handleError("UpdateGameHash", err, "")
gamesChangedContent = append(gamesChangedContent, file.Name())
newCheckSongs(entries, gameDir, id)
case TitleChanged:
fmt.Printf("\n\nID: %d | GameName: %s | GameHash: %s | Status: %s\n", id, file.Name(), dirHash, status)
//println("TitleChanged")
err = repo.UpdateGameName(db.Ctx, repository.UpdateGameNameParams{Name: file.Name(), Path: gameDir, ID: id})
handleError("UpdateGameName", err, "")
newCheckSongs(entries, gameDir, id)
if gamesChangedTitle == nil {
gamesChangedTitle = make(map[string]string)
}
gamesChangedTitle[oldGame.GameName] = file.Name()
case NotChanged:
//println("NotChanged")
var found bool = false
for _, beforeGame := range gamesBeforeSync {
if dirHash == beforeGame.Hash {
found = true
//fmt.Printf("Game %s | %s | %s | %s | %v\n", beforeGame.GameName, beforeGame.Hash, dirHash, status, beforeGame.Deleted)
}
}
if !found {
newCheckSongs(entries, gameDir, id)
gamesReAdded = append(gamesReAdded, file.Name())
}
}
err = repo.RemoveDeletionDate(db.Ctx, id)
handleError("RemoveDeletionDate", err, "")
}
}
func insertGameNew(name string, path string, hash string) int32 {
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
id, err := repo.InsertGame(db.Ctx, repository.InsertGameParams{GameName: name, Path: path, Hash: hash})
handleError("InsertGame", err, "")
if err != nil {
fmt.Printf("Handle id busy\n")
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
fmt.Printf("Handeling this id\n")
_, err = repo.ResetGameIdSeq(db.Ctx)
handleError("ResetGameIdSeq", err, "")
id = insertGameNew(name, path, hash)
}
}
return id
}
func newCheckSongs(entries []os.DirEntry, gameDir string, id int32) int32 {
//hasher := md5.New()
var numberOfSongs int32
var songWg sync.WaitGroup
songWg.Add(len(entries))
for _, entry := range entries {
go func() {
defer songWg.Done()
newCheckSong(entry, gameDir, id)
}()
}
songWg.Wait()
return numberOfSongs
}
func newCheckSong(entry os.DirEntry, gameDir string, id int32) {
fileInfo, err := entry.Info()
if err != nil {
log.Println(err)
}
if isSong(fileInfo) {
path := gameDir + entry.Name()
songHash := getHashForFile(path)
//numberOfSongs++
fileName := entry.Name()
songName, _ := strings.CutSuffix(fileName, ".mp3")
song, err := repo.GetSongWithHash(db.Ctx, songHash)
handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if err == nil {
if song.SongName == songName && song.Path == path {
return
}
}
fmt.Printf("Song Changed\n")
fmt.Printf("Path: %s | SongHash: %s\n", path, songHash)
count, err := repo.CheckSongWithHash(db.Ctx, songHash)
handleError("CheckSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if err != nil {
count2, err := repo.CheckSong(db.Ctx, path)
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if count2 > 0 {
err = repo.AddHashToSong(db.Ctx, repository.AddHashToSongParams{Hash: songHash, Path: path})
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
count, err = repo.CheckSongWithHash(db.Ctx, songHash)
handleError("CheckSongWithHash 2", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
}
}
//count, _ := repo.CheckSong(ctx, path)
if count > 0 {
err = repo.UpdateSong(db.Ctx, repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
handleError("UpdateSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
} else {
count2, err := repo.CheckSong(db.Ctx, path)
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
if count2 > 0 {
err = repo.AddHashToSong(db.Ctx, repository.AddHashToSongParams{Hash: songHash, Path: path})
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
} else {
err = repo.AddSong(db.Ctx, repository.AddSongParams{GameID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
handleError("AddSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
}
}
} else if isCoverImage(fileInfo) {
//TODO: Later add cover art image here in db
}
}
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() {
fmt.Printf("%s Error: %s\n", funcName, err)
if msg != "" {
fmt.Printf("%s\n", msg)
catchedErrors = append(catchedErrors, fmt.Sprintf("Func: %s\nError message: %s\nDebug message: %s\n", funcName, err, msg))
} else {
catchedErrors = append(catchedErrors, fmt.Sprintf("Func: %s\nError message: %s\n", funcName, err))
}
}
}
}
func getHashForDir(gameDir string) string {
directory, _ := directory_checksum.ScanDirectory(gameDir, afero.NewOsFs())
hash, _ := directory.ComputeDirectoryChecksums()
//fmt.Printf("Hash: |%s|\n", hash)
return hash
}
func getHashForFile(path string) string {
hasher := md5.New()
readFile, err := os.Open(path)
if err != nil {
panic(err)
}
defer readFile.Close()
hasher.Reset()
_, err = io.Copy(hasher, readFile)
if err != nil {
log.Fatal(err)
}
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 { func isSong(entry fs.FileInfo) bool {
return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".mp3") || strings.HasSuffix(entry.Name(), ".wav") return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".mp3")
} }
func isCoverImage(entry fs.FileInfo) bool { func isCoverImage(entry fs.FileInfo) bool {

37
sqlc.yaml Normal file
View File

@@ -0,0 +1,37 @@
version: "2"
sql:
- engine: "postgresql"
queries: "./db/queries"
schema: "./db/migrations"
gen:
go:
emit_json_tags: true
package: "repository"
out: "db/repository"
sql_package: "pgx/v5"
overrides:
- db_type: "pg_catalog.timestamp"
go_type:
import: "time"
type: "Time"
- db_type: "pg_catalog.timestamp"
nullable: true
go_type:
pointer: true
import: "time"
type: "Time"
- db_type: "pg_catalog.varchar"
nullable: true
go_type:
pointer: true
type: "string"
- db_type: "int4"
nullable: true
go_type:
pointer: true
type: "int32"
- db_type: "date"
nullable: false
go_type:
import: "time"
type: "Time"

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./cmd/web/**/*.html", "./cmd/web/**/*.templ",
],
theme: {
extend: {},
},
plugins: [],
}