Compare commits
13 Commits
4033899a68
...
dbef39b828
| Author | SHA1 | Date | |
|---|---|---|---|
| dbef39b828 | |||
| 4e5bdc4ee2 | |||
| 0894d65ec5 | |||
| b0418b4f38 | |||
| 176848bb6d | |||
| fb387901cf | |||
| 0f29c33b1a | |||
| cec408187d | |||
| c60f40d7e3 | |||
| 2f407f6eef | |||
| 4c2db11cc5 | |||
| 06cbad708d | |||
| 89e884fae9 |
+458
-7
@@ -23,6 +23,385 @@ var doc = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/statistics/games/last-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the most recently played games",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get last played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/least-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N least played games with their songs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get least played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/most-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N most played games with their songs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get most played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/never-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns all games that have never been played (times_played = 0)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get never played games",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/oldest-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the least recently played games (that have been played at least once)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get oldest played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/songs/least-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N least played songs with their game info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get least played songs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/songs/most-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N most played songs with their game info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get most played songs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/summary": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns overall statistics about the music library",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get statistics summary",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/backend.StatisticsSummary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/token": {
|
"/api/v1/token": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Returns a new session token for API access",
|
"description": "Returns a new session token for API access",
|
||||||
@@ -455,7 +834,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games",
|
"summary": "Get all soundtracks",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -488,7 +867,7 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games random",
|
"summary": "Get all soundtracks random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -828,10 +1207,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync games with only changes",
|
"summary": "Sync soundtracks with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games",
|
"description": "Start syncing soundtracks",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -860,7 +1239,7 @@ var doc = `{
|
|||||||
"summary": "Sync all games fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games full",
|
"description": "Start syncing soundtracks full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -910,10 +1289,10 @@ var doc = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset games database",
|
"summary": "Reset soundtracks database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Games and songs are deleted from the database",
|
"description": "Soundtracks and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -990,6 +1369,78 @@ var doc = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"backend.GameWithSongs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_last_played": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_played": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"songs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend.SongInfoForStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"song_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"times_played": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend.StatisticsSummary": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avg_game_plays": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"max_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"min_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"never_played_games": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"played_games": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_games": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"backend.VersionData": {
|
"backend.VersionData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+458
-7
@@ -4,6 +4,385 @@
|
|||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/api/v1/statistics/games/last-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the most recently played games",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get last played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/least-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N least played games with their songs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get least played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/most-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N most played games with their songs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get most played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/never-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns all games that have never been played (times_played = 0)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get never played games",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/games/oldest-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the least recently played games (that have been played at least once)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get oldest played games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.GameWithSongs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/songs/least-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N least played songs with their game info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get least played songs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/songs/most-played": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the top N most played songs with their game info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get most played songs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results (default: 10)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/statistics/summary": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns overall statistics about the music library",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"summary": "Get statistics summary",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/backend.StatisticsSummary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/token": {
|
"/api/v1/token": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Returns a new session token for API access",
|
"description": "Returns a new session token for API access",
|
||||||
@@ -436,7 +815,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games",
|
"summary": "Get all soundtracks",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -469,7 +848,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"music"
|
"music"
|
||||||
],
|
],
|
||||||
"summary": "Get all games random",
|
"summary": "Get all soundtracks random",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -809,10 +1188,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Sync games with only changes",
|
"summary": "Sync soundtracks with only changes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games",
|
"description": "Start syncing soundtracks",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -841,7 +1220,7 @@
|
|||||||
"summary": "Sync all games fully",
|
"summary": "Sync all games fully",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Start syncing games full",
|
"description": "Start syncing soundtracks full",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -891,10 +1270,10 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"sync"
|
"sync"
|
||||||
],
|
],
|
||||||
"summary": "Reset games database",
|
"summary": "Reset soundtracks database",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Games and songs are deleted from the database",
|
"description": "Soundtracks and songs are deleted from the database",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -971,6 +1350,78 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"backend.GameWithSongs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_last_played": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_played": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"songs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/backend.SongInfoForStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend.SongInfoForStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"song_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"times_played": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend.StatisticsSummary": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avg_game_plays": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"max_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"min_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"never_played_games": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"played_games": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_game_plays": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_games": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"backend.VersionData": {
|
"backend.VersionData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+303
-7
@@ -1,4 +1,51 @@
|
|||||||
definitions:
|
definitions:
|
||||||
|
backend.GameWithSongs:
|
||||||
|
properties:
|
||||||
|
game_id:
|
||||||
|
type: integer
|
||||||
|
game_last_played:
|
||||||
|
type: string
|
||||||
|
game_name:
|
||||||
|
type: string
|
||||||
|
game_played:
|
||||||
|
type: integer
|
||||||
|
songs:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
backend.SongInfoForStats:
|
||||||
|
properties:
|
||||||
|
file_name:
|
||||||
|
type: string
|
||||||
|
game_id:
|
||||||
|
type: integer
|
||||||
|
game_name:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
song_name:
|
||||||
|
type: string
|
||||||
|
times_played:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
backend.StatisticsSummary:
|
||||||
|
properties:
|
||||||
|
avg_game_plays:
|
||||||
|
type: number
|
||||||
|
max_game_plays:
|
||||||
|
type: integer
|
||||||
|
min_game_plays:
|
||||||
|
type: integer
|
||||||
|
never_played_games:
|
||||||
|
type: integer
|
||||||
|
played_games:
|
||||||
|
type: integer
|
||||||
|
total_game_plays:
|
||||||
|
type: integer
|
||||||
|
total_games:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
backend.VersionData:
|
backend.VersionData:
|
||||||
properties:
|
properties:
|
||||||
changelog:
|
changelog:
|
||||||
@@ -30,6 +77,255 @@ definitions:
|
|||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
paths:
|
paths:
|
||||||
|
/api/v1/statistics/games/last-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the most recently played games
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get last played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/least-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N least played games with their songs
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get least played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/most-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N most played games with their songs
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get most played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/never-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all games that have never been played (times_played = 0)
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get never played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/games/oldest-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the least recently played games (that have been played
|
||||||
|
at least once)
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.GameWithSongs'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get oldest played games
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/songs/least-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N least played songs with their game info
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get least played songs
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/songs/most-played:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns the top N most played songs with their game info
|
||||||
|
parameters:
|
||||||
|
- description: 'Number of results (default: 10)'
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/backend.SongInfoForStats'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get most played songs
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
|
/api/v1/statistics/summary:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns overall statistics about the music library
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/backend.StatisticsSummary'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get statistics summary
|
||||||
|
tags:
|
||||||
|
- statistics
|
||||||
/api/v1/token:
|
/api/v1/token:
|
||||||
delete:
|
delete:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -325,7 +621,7 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all games
|
summary: Get all soundtracks
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/all/random:
|
/music/all/random:
|
||||||
@@ -347,7 +643,7 @@ paths:
|
|||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Get all games random
|
summary: Get all soundtracks random
|
||||||
tags:
|
tags:
|
||||||
- music
|
- music
|
||||||
/music/info:
|
/music/info:
|
||||||
@@ -561,14 +857,14 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing games
|
description: Start syncing soundtracks
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Sync games with only changes
|
summary: Sync soundtracks with only changes
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/sync/full:
|
/sync/full:
|
||||||
@@ -580,7 +876,7 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Start syncing games full
|
description: Start syncing soundtracks full
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
@@ -615,14 +911,14 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Games and songs are deleted from the database
|
description: Soundtracks and songs are deleted from the database
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
"423":
|
"423":
|
||||||
description: Syncing is in progress
|
description: Syncing is in progress
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
summary: Reset games database
|
summary: Reset soundtracks database
|
||||||
tags:
|
tags:
|
||||||
- sync
|
- sync
|
||||||
/version:
|
/version:
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
/* Pure CSS styles for Music Search */
|
/* Pure CSS styles for Music Search */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Light mode colors (default) */
|
||||||
|
--bg-primary: #f3f4f6;
|
||||||
|
--bg-secondary: #e5e7eb;
|
||||||
|
--bg-tertiary: #dcfce7;
|
||||||
|
--text-primary: #000;
|
||||||
|
--text-secondary: #374151;
|
||||||
|
--border-primary: #9ca3af;
|
||||||
|
--border-focus: #6b7280;
|
||||||
|
--accent-primary: #f97316;
|
||||||
|
--accent-hover: #ea580c;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* Dark mode colors matching frontend */
|
||||||
|
--bg-primary: #555;
|
||||||
|
--bg-secondary: #333;
|
||||||
|
--bg-tertiary: #2a2a2a;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #ff9c00;
|
||||||
|
--border-primary: #666;
|
||||||
|
--border-focus: #ff9c00;
|
||||||
|
--accent-primary: #ff9c00;
|
||||||
|
--accent-hover: #e68a00;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -10,7 +38,9 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: #f3f4f6;
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -29,15 +59,15 @@ main {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid #9ca3af;
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #e5e7eb;
|
background-color: var(--bg-secondary);
|
||||||
color: #000;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_term:focus {
|
#search_term:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #6b7280;
|
border-color: var(--border-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear {
|
#clear {
|
||||||
@@ -45,23 +75,48 @@ main {
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #f97316;
|
background-color: var(--accent-primary);
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#clear:hover {
|
#clear:hover {
|
||||||
background-color: #ea580c;
|
background-color: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
#games-container {
|
#games-container {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode toggle */
|
||||||
|
#dark-mode-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dark-mode-toggle:hover {
|
||||||
|
background-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Game result cards */
|
/* Game result cards */
|
||||||
.bg-green-100 {
|
.bg-green-100 {
|
||||||
background-color: #dcfce7;
|
background-color: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-4 {
|
.p-4 {
|
||||||
@@ -69,7 +124,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-md {
|
.shadow-md {
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ func FindGameWebHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func search(searchText string) {
|
func search(searchText string) {
|
||||||
games_added = nil
|
games_added = nil
|
||||||
games := backend.GetAllGames()
|
games := backend.GetAllSoundtracks()
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
if is_match_exact(searchText, game) {
|
if is_match_exact(searchText, game) {
|
||||||
add_game(game)
|
add_game(game)
|
||||||
|
|||||||
+23
-1
@@ -2,6 +2,7 @@ package web
|
|||||||
|
|
||||||
templ HelloForm() {
|
templ HelloForm() {
|
||||||
@Base() {
|
@Base() {
|
||||||
|
<button id="dark-mode-toggle">🌙</button>
|
||||||
<div id="search-container">
|
<div id="search-container">
|
||||||
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
|
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
|
||||||
<button type="button" id="clear" name="clear">Clear</button>
|
<button type="button" id="clear" name="clear">Clear</button>
|
||||||
@@ -12,8 +13,29 @@ templ HelloForm() {
|
|||||||
if (document.readyState == 'complete') {
|
if (document.readyState == 'complete') {
|
||||||
htmx.ajax('POST', '/find', '#games-container');
|
htmx.ajax('POST', '/find', '#games-container');
|
||||||
document.getElementById("search_term").focus();
|
document.getElementById("search_term").focus();
|
||||||
|
|
||||||
|
// Initialize dark mode from localStorage (default to dark)
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
document.getElementById('dark-mode-toggle').textContent = '☀️';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dark mode toggle functionality
|
||||||
|
document.getElementById("dark-mode-toggle").addEventListener("click", function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
|
// Update toggle button text
|
||||||
|
this.textContent = newTheme === 'dark' ? '☀️' : '🌙';
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("clear").addEventListener("click", function (event) {
|
document.getElementById("clear").addEventListener("click", function (event) {
|
||||||
document.getElementById("search_term").value = "";
|
document.getElementById("search_term").value = "";
|
||||||
htmx.ajax('POST', '/find', '#games-container');
|
htmx.ajax('POST', '/find', '#games-container');
|
||||||
@@ -26,7 +48,7 @@ templ HelloForm() {
|
|||||||
templ FoundGames(games []string) {
|
templ FoundGames(games []string) {
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||||
<p>{ game }</p>
|
<p class="game-text">{ game }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-29
@@ -22,7 +22,7 @@ type SongInfo struct {
|
|||||||
|
|
||||||
var currentSong = -1
|
var currentSong = -1
|
||||||
|
|
||||||
var gamesNew []repository.Game
|
var gamesNew []repository.Soundtrack
|
||||||
|
|
||||||
var songQueNew []repository.Song
|
var songQueNew []repository.Song
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ func initRepo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllGames() []repository.Game {
|
func getAllGames() []repository.Soundtrack {
|
||||||
if len(gamesNew) == 0 {
|
if len(gamesNew) == 0 {
|
||||||
initRepo()
|
initRepo()
|
||||||
gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
|
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||||
}
|
}
|
||||||
return gamesNew
|
return gamesNew
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ func Reset() {
|
|||||||
songQueNew = nil
|
songQueNew = nil
|
||||||
currentSong = -1
|
currentSong = -1
|
||||||
initRepo()
|
initRepo()
|
||||||
gamesNew, _ = BackendRepo().FindAllGames(BackendCtx())
|
gamesNew, _ = BackendRepo().FindAllSoundtracks(BackendCtx())
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddLatestToQue() {
|
func AddLatestToQue() {
|
||||||
@@ -77,8 +77,8 @@ func AddLatestPlayed() {
|
|||||||
currentSongData := songQueNew[currentSong]
|
currentSongData := songQueNew[currentSong]
|
||||||
|
|
||||||
initRepo()
|
initRepo()
|
||||||
BackendRepo().AddGamePlayed(BackendCtx(), currentSongData.GameID)
|
BackendRepo().AddSoundtrackPlayed(BackendCtx(), currentSongData.SoundtrackID)
|
||||||
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: currentSongData.GameID, SongName: currentSongData.SongName})
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: currentSongData.SoundtrackID, SongName: currentSongData.SongName})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetPlayed(songNumber int) {
|
func SetPlayed(songNumber int) {
|
||||||
@@ -87,8 +87,8 @@ func SetPlayed(songNumber int) {
|
|||||||
}
|
}
|
||||||
songData := songQueNew[songNumber]
|
songData := songQueNew[songNumber]
|
||||||
initRepo()
|
initRepo()
|
||||||
BackendRepo().AddGamePlayed(BackendCtx(), songData.GameID)
|
BackendRepo().AddSoundtrackPlayed(BackendCtx(), songData.SoundtrackID)
|
||||||
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{GameID: songData.GameID, SongName: songData.SongName})
|
BackendRepo().AddSongPlayed(BackendCtx(), repository.AddSongPlayedParams{SoundtrackID: songData.SoundtrackID, SongName: songData.SongName})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomSong() string {
|
func GetRandomSong() string {
|
||||||
@@ -105,7 +105,7 @@ func GetRandomSong() string {
|
|||||||
func GetRandomSongLowChance() string {
|
func GetRandomSongLowChance() string {
|
||||||
getAllGames()
|
getAllGames()
|
||||||
|
|
||||||
var listOfGames []repository.Game
|
var listOfGames []repository.Soundtrack
|
||||||
|
|
||||||
var averagePlayed = getAveragePlayed()
|
var averagePlayed = getAveragePlayed()
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ func GetRandomSongClassic() string {
|
|||||||
|
|
||||||
var listOfAllSongs []repository.Song
|
var listOfAllSongs []repository.Song
|
||||||
for _, game := range gamesNew {
|
for _, game := range gamesNew {
|
||||||
songList, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
|
songList, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||||
listOfAllSongs = append(listOfAllSongs, songList...)
|
listOfAllSongs = append(listOfAllSongs, songList...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,13 +139,13 @@ func GetRandomSongClassic() string {
|
|||||||
var song repository.Song
|
var song repository.Song
|
||||||
for !songFound {
|
for !songFound {
|
||||||
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
song = listOfAllSongs[rand.Intn(len(listOfAllSongs))]
|
||||||
gameData, err := BackendRepo().GetGameById(BackendCtx(), song.GameID)
|
gameData, err := BackendRepo().GetSoundtrackById(BackendCtx(), song.SoundtrackID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("game", gameData.GameName),
|
zap.String("game", gameData.SoundtrackName),
|
||||||
zap.String("filename", *song.FileName))
|
zap.String("filename", *song.FileName))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,10 @@ func GetRandomSongClassic() string {
|
|||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
|
if err != nil || (song.FileName != nil && gameData.Path+*song.FileName != song.Path) {
|
||||||
//File not found
|
//File not found
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("game", gameData.GameName),
|
zap.String("game", gameData.SoundtrackName),
|
||||||
zap.String("filename", *song.FileName))
|
zap.String("filename", *song.FileName))
|
||||||
} else {
|
} else {
|
||||||
songFound = true
|
songFound = true
|
||||||
@@ -180,7 +180,7 @@ func GetSongInfo() SongInfo {
|
|||||||
currentGameData := getCurrentGame(currentSongData)
|
currentGameData := getCurrentGame(currentSongData)
|
||||||
|
|
||||||
return SongInfo{
|
return SongInfo{
|
||||||
Game: currentGameData.GameName,
|
Game: currentGameData.SoundtrackName,
|
||||||
GamePlayed: currentGameData.TimesPlayed,
|
GamePlayed: currentGameData.TimesPlayed,
|
||||||
Song: currentSongData.SongName,
|
Song: currentSongData.SongName,
|
||||||
SongPlayed: currentSongData.TimesPlayed,
|
SongPlayed: currentSongData.TimesPlayed,
|
||||||
@@ -195,7 +195,7 @@ func GetPlayedSongs() []SongInfo {
|
|||||||
for i, song := range songQueNew {
|
for i, song := range songQueNew {
|
||||||
gameData := getCurrentGame(song)
|
gameData := getCurrentGame(song)
|
||||||
songList = append(songList, SongInfo{
|
songList = append(songList, SongInfo{
|
||||||
Game: gameData.GameName,
|
Game: gameData.SoundtrackName,
|
||||||
GamePlayed: gameData.TimesPlayed,
|
GamePlayed: gameData.TimesPlayed,
|
||||||
Song: song.SongName,
|
Song: song.SongName,
|
||||||
SongPlayed: song.TimesPlayed,
|
SongPlayed: song.TimesPlayed,
|
||||||
@@ -217,22 +217,22 @@ func GetSong(song string) string {
|
|||||||
return songData.Path
|
return songData.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllGames() []string {
|
func GetAllSoundtracks() []string {
|
||||||
getAllGames()
|
getAllGames()
|
||||||
|
|
||||||
var jsonArray []string
|
var jsonArray []string
|
||||||
for _, game := range gamesNew {
|
for _, game := range gamesNew {
|
||||||
jsonArray = append(jsonArray, game.GameName)
|
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||||
}
|
}
|
||||||
return jsonArray
|
return jsonArray
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllGamesRandom() []string {
|
func GetAllSoundtracksRandom() []string {
|
||||||
getAllGames()
|
getAllGames()
|
||||||
|
|
||||||
var jsonArray []string
|
var jsonArray []string
|
||||||
for _, game := range gamesNew {
|
for _, game := range gamesNew {
|
||||||
jsonArray = append(jsonArray, game.GameName)
|
jsonArray = append(jsonArray, game.SoundtrackName)
|
||||||
}
|
}
|
||||||
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
rand.Shuffle(len(jsonArray), func(i, j int) { jsonArray[i], jsonArray[j] = jsonArray[j], jsonArray[i] })
|
||||||
return jsonArray
|
return jsonArray
|
||||||
@@ -266,12 +266,12 @@ func GetPreviousSong() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSongFromList(games []repository.Game) repository.Song {
|
func getSongFromList(games []repository.Soundtrack) repository.Song {
|
||||||
songFound := false
|
songFound := false
|
||||||
var song repository.Song
|
var song repository.Song
|
||||||
for !songFound {
|
for !songFound {
|
||||||
game := getRandomGame(games)
|
game := getRandomGame(games)
|
||||||
songs, _ := BackendRepo().FindSongsFromGame(BackendCtx(), game.ID)
|
songs, _ := BackendRepo().FindSongsFromSoundtrack(BackendCtx(), game.ID)
|
||||||
if len(songs) == 0 {
|
if len(songs) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -282,10 +282,10 @@ func getSongFromList(games []repository.Game) repository.Song {
|
|||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
if err != nil || (song.FileName != nil && game.Path+*song.FileName != song.Path) || (song.FileName != nil && strings.HasSuffix(*song.FileName, ".wav")) {
|
||||||
//File not found
|
//File not found
|
||||||
BackendRepo().RemoveBrokenSong(BackendCtx(), song.Path)
|
BackendRepo().RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
logging.GetLogger().Warn("Song not found, removed from database",
|
logging.GetLogger().Warn("Song not found, removed from database",
|
||||||
zap.String("song", song.SongName),
|
zap.String("song", song.SongName),
|
||||||
zap.String("game", game.GameName),
|
zap.String("game", game.SoundtrackName),
|
||||||
zap.Any("filename", song.FileName))
|
zap.Any("filename", song.FileName))
|
||||||
} else {
|
} else {
|
||||||
songFound = true
|
songFound = true
|
||||||
@@ -299,13 +299,13 @@ func getSongFromList(games []repository.Game) repository.Song {
|
|||||||
return song
|
return song
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentGame(currentSongData repository.Song) repository.Game {
|
func getCurrentGame(currentSongData repository.Song) repository.Soundtrack {
|
||||||
for _, game := range gamesNew {
|
for _, game := range gamesNew {
|
||||||
if game.ID == currentSongData.GameID {
|
if game.ID == currentSongData.SoundtrackID {
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return repository.Game{}
|
return repository.Soundtrack{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAveragePlayed() int32 {
|
func getAveragePlayed() int32 {
|
||||||
@@ -317,6 +317,6 @@ func getAveragePlayed() int32 {
|
|||||||
return sum / int32(len(gamesNew))
|
return sum / int32(len(gamesNew))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRandomGame(listOfGames []repository.Game) repository.Game {
|
func getRandomGame(listOfGames []repository.Soundtrack) repository.Soundtrack {
|
||||||
return listOfGames[rand.Intn(len(listOfGames))]
|
return listOfGames[rand.Intn(len(listOfGames))]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
|
|
||||||
// Test the average calculation logic directly without database access
|
// Test the average calculation logic directly without database access
|
||||||
func TestCalculateAverage(t *testing.T) {
|
func TestCalculateAverage(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
var sum int32
|
var sum int32
|
||||||
@@ -28,7 +28,7 @@ func TestCalculateAverage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateAverageEmpty(t *testing.T) {
|
func TestCalculateAverageEmpty(t *testing.T) {
|
||||||
games := []repository.Game{}
|
games := []repository.Soundtrack{}
|
||||||
|
|
||||||
if len(games) == 0 {
|
if len(games) == 0 {
|
||||||
result := int32(0)
|
result := int32(0)
|
||||||
@@ -52,8 +52,8 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateAverageSingle(t *testing.T) {
|
func TestCalculateAverageSingle(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 42},
|
{SoundtrackName: "Game1", TimesPlayed: 42},
|
||||||
}
|
}
|
||||||
|
|
||||||
var sum int32
|
var sum int32
|
||||||
@@ -69,10 +69,10 @@ func TestCalculateAverageSingle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRandomGame(t *testing.T) {
|
func TestGetRandomGame(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seed for reproducible tests
|
// Set seed for reproducible tests
|
||||||
@@ -80,93 +80,93 @@ func TestGetRandomGame(t *testing.T) {
|
|||||||
|
|
||||||
result := games[rand.Intn(len(games))]
|
result := games[rand.Intn(len(games))]
|
||||||
|
|
||||||
if result.GameName == "" {
|
if result.SoundtrackName == "" {
|
||||||
t.Error("random game selection returned empty game")
|
t.Error("random game selection returned empty game")
|
||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, g := range games {
|
for _, g := range games {
|
||||||
if g.GameName == result.GameName {
|
if g.SoundtrackName == result.SoundtrackName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("random game selection returned game not in list: %v", result.GameName)
|
t.Errorf("random game selection returned game not in list: %v", result.SoundtrackName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindGameByID(t *testing.T) {
|
func TestFindGameByID(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
games []repository.Game
|
games []repository.Soundtrack
|
||||||
gameID int32
|
gameID int32
|
||||||
expected repository.Game
|
expected repository.Soundtrack
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "existing game",
|
name: "existing game",
|
||||||
games: games,
|
games: games,
|
||||||
gameID: 2,
|
gameID: 2,
|
||||||
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-existing game",
|
name: "non-existing game",
|
||||||
games: games,
|
games: games,
|
||||||
gameID: 99,
|
gameID: 99,
|
||||||
expected: repository.Game{},
|
expected: repository.Soundtrack{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var result repository.Game
|
var result repository.Soundtrack
|
||||||
for _, game := range tt.games {
|
for _, game := range tt.games {
|
||||||
if game.ID == tt.gameID {
|
if game.ID == tt.gameID {
|
||||||
result = game
|
result = game
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result.ID != tt.expected.ID || result.GameName != tt.expected.GameName {
|
if result.ID != tt.expected.ID || result.SoundtrackName != tt.expected.SoundtrackName {
|
||||||
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
t.Errorf("findGameByID() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractGameNames(t *testing.T) {
|
func TestExtractSoundtrackNames(t *testing.T) {
|
||||||
games := []repository.Game{
|
games := []repository.Soundtrack{
|
||||||
{GameName: "Game1", TimesPlayed: 10},
|
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||||
{GameName: "Game2", TimesPlayed: 20},
|
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||||
{GameName: "Game3", TimesPlayed: 30},
|
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
for _, game := range games {
|
for _, game := range games {
|
||||||
result = append(result, game.GameName)
|
result = append(result, game.SoundtrackName)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{"Game1", "Game2", "Game3"}
|
expected := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
if len(result) != len(expected) {
|
if len(result) != len(expected) {
|
||||||
t.Errorf("extractGameNames() length = %d, want %d", len(result), len(expected))
|
t.Errorf("extractSoundtrackNames() length = %d, want %d", len(result), len(expected))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, v := range result {
|
for i, v := range result {
|
||||||
if v != expected[i] {
|
if v != expected[i] {
|
||||||
t.Errorf("extractGameNames()[%d] = %v, want %v", i, v, expected[i])
|
t.Errorf("extractSoundtrackNames()[%d] = %v, want %v", i, v, expected[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShuffleGameNames(t *testing.T) {
|
func TestShuffleSoundtrackNames(t *testing.T) {
|
||||||
games := []string{"Game1", "Game2", "Game3"}
|
games := []string{"Game1", "Game2", "Game3"}
|
||||||
|
|
||||||
// Test that shuffle doesn't lose any elements
|
// Test that shuffle doesn't lose any elements
|
||||||
@@ -181,7 +181,7 @@ func TestShuffleGameNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(games) != len(original) {
|
if len(games) != len(original) {
|
||||||
t.Errorf("shuffleGameNames() changed length from %d to %d", len(original), len(games))
|
t.Errorf("shuffleSoundtrackNames() changed length from %d to %d", len(original), len(games))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,9 +195,7 @@ func TestShuffleGameNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("shuffleGameNames() lost element: %v", orig)
|
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GameWithSongs represents a game with its songs for statistics
|
||||||
|
type GameWithSongs struct {
|
||||||
|
SoundtrackID int32 `json:"game_id"`
|
||||||
|
SoundtrackName string `json:"game_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"game_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
||||||
|
Songs []SongInfoForStats `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SongInfoForStats represents a song with game info for statistics
|
||||||
|
type SongInfoForStats struct {
|
||||||
|
SoundtrackID int32 `json:"game_id"`
|
||||||
|
SoundtrackName string `json:"game_name"`
|
||||||
|
SongName string `json:"song_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
FileName *string `json:"file_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsSummary holds overall statistics
|
||||||
|
type StatisticsSummary struct {
|
||||||
|
TotalGames int64 `json:"total_games"`
|
||||||
|
PlayedGames int64 `json:"played_games"`
|
||||||
|
NeverPlayedGames int64 `json:"never_played_games"`
|
||||||
|
TotalGamePlays int64 `json:"total_game_plays"`
|
||||||
|
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||||
|
MaxGamePlays int64 `json:"max_game_plays"`
|
||||||
|
MinGamePlays int64 `json:"min_game_plays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsHandler manages statistics operations
|
||||||
|
type StatisticsHandler struct {
|
||||||
|
// Uses the global backend repo initialized via InitBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatisticsHandler creates a new StatisticsHandler
|
||||||
|
func NewStatisticsHandler() *StatisticsHandler {
|
||||||
|
return &StatisticsHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedGamesWithSongs returns the top N most played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
// Get raw results
|
||||||
|
rows, err := queries.GetMostPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GameWithSongs
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
// Parse JSON songs array
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
// Fallback: if JSON parsing fails, create empty song entries
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedGamesWithSongs returns the top N least played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedSongsWithGame returns the top N most played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetMostPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedSongsWithGame returns the top N least played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
|
func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetNeverPlayedGames(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: nil,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastPlayedGames returns the most recently played games
|
||||||
|
func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLastPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldestPlayedGames returns the least recently played games
|
||||||
|
func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetOldestPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
SoundtrackID: row.SoundtrackID,
|
||||||
|
SoundtrackName: row.SoundtrackName,
|
||||||
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
||||||
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
row, err := queries.GetStatisticsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StatisticsSummary{
|
||||||
|
TotalGames: int64(row.TotalSoundtracks),
|
||||||
|
PlayedGames: int64(row.PlayedSoundtracks),
|
||||||
|
NeverPlayedGames: int64(row.NeverPlayedSoundtracks),
|
||||||
|
TotalGamePlays: int64(row.TotalSoundtrackPlays),
|
||||||
|
AvgGamePlays: float64(row.AvgSoundtrackPlays),
|
||||||
|
MaxGamePlays: int64(row.MaxSoundtrackPlays),
|
||||||
|
MinGamePlays: int64(row.MinSoundtrackPlays),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log helper for statistics operations
|
||||||
|
func logStatisticsError(err error, operation string) {
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Statistics error",
|
||||||
|
zap.String("operation", operation),
|
||||||
|
zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-54
@@ -30,16 +30,22 @@ var start time.Time
|
|||||||
var totalTime time.Duration
|
var totalTime time.Duration
|
||||||
var timeSpent time.Duration
|
var timeSpent time.Duration
|
||||||
|
|
||||||
var allGames []repository.Game
|
var allGames []repository.Soundtrack
|
||||||
var gamesBeforeSync []repository.Game
|
var gamesBeforeSync []repository.Soundtrack
|
||||||
var gamesAfterSync []repository.Game
|
var gamesAfterSync []repository.Soundtrack
|
||||||
var gamesAdded []string
|
var gamesAdded []string
|
||||||
var gamesReAdded []string
|
var gamesReAdded []string
|
||||||
var gamesChangedTitle map[string]string
|
var gamesChangedTitle map[string]string
|
||||||
var gamesChangedContent []string
|
var gamesChangedContent []string
|
||||||
var gamesRemoved []string
|
var gamesRemoved []string
|
||||||
var catchedErrors []string
|
var catchedErrors []string
|
||||||
var brokenSongs []string
|
|
||||||
|
type brokenSong struct {
|
||||||
|
SoundtrackID int32
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
var brokenSongs []brokenSong
|
||||||
var pool *ants.Pool
|
var pool *ants.Pool
|
||||||
var poolSong *ants.Pool
|
var poolSong *ants.Pool
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ func (gs GameStatus) String() string {
|
|||||||
|
|
||||||
func ResetDB() {
|
func ResetDB() {
|
||||||
repo.ClearSongs(BackendCtx())
|
repo.ClearSongs(BackendCtx())
|
||||||
repo.ClearGames(BackendCtx())
|
repo.ClearSoundtracks(BackendCtx())
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncProgress() ProgressResponse {
|
func SyncProgress() ProgressResponse {
|
||||||
@@ -124,13 +130,13 @@ func SyncResult() SyncResponse {
|
|||||||
for _, beforeGame := range gamesBeforeSync {
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
var found = false
|
var found = false
|
||||||
for _, afterGame := range gamesAfterSync {
|
for _, afterGame := range gamesAfterSync {
|
||||||
if beforeGame.GameName == afterGame.GameName {
|
if beforeGame.SoundtrackName == afterGame.SoundtrackName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.GameName)
|
gamesRemovedTemp = append(gamesRemovedTemp, beforeGame.SoundtrackName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +175,12 @@ func SyncResult() SyncResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncGamesNewFull() {
|
func SyncSoundtracksNewFull() {
|
||||||
syncGamesNew(true)
|
syncGamesNew(true)
|
||||||
Reset()
|
Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncGamesNewOnlyChanges() {
|
func SyncSoundtracksNewOnlyChanges() {
|
||||||
syncGamesNew(false)
|
syncGamesNew(false)
|
||||||
Reset()
|
Reset()
|
||||||
}
|
}
|
||||||
@@ -203,14 +209,14 @@ func syncGamesNew(full bool) {
|
|||||||
catchedErrors = nil
|
catchedErrors = nil
|
||||||
brokenSongs = nil
|
brokenSongs = nil
|
||||||
|
|
||||||
gamesBeforeSync, err = repo.FindAllGames(BackendCtx())
|
gamesBeforeSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||||
handleError("FindAllGames Before", err, "")
|
handleError("FindAllSoundtracks Before", err, "")
|
||||||
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
|
logging.GetLogger().Info("Starting sync", zap.Int("games_before", len(gamesBeforeSync)))
|
||||||
|
|
||||||
allGames, err = repo.GetAllGamesIncludingDeleted(BackendCtx())
|
allGames, err = repo.GetAllSoundtracksIncludingDeleted(BackendCtx())
|
||||||
handleError("GetAllGamesIncludingDeleted", err, "")
|
handleError("GetAllSoundtracksIncludingDeleted", err, "")
|
||||||
err = repo.SetGameDeletionDate(BackendCtx())
|
err = repo.SetSoundtrackDeletionDate(BackendCtx())
|
||||||
handleError("SetGameDeletionDate", err, "")
|
handleError("SetSoundtrackDeletionDate", err, "")
|
||||||
|
|
||||||
directories, err := os.ReadDir(musicPath)
|
directories, err := os.ReadDir(musicPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,8 +239,8 @@ func syncGamesNew(full bool) {
|
|||||||
syncWg.Wait()
|
syncWg.Wait()
|
||||||
checkBrokenSongsNew()
|
checkBrokenSongsNew()
|
||||||
|
|
||||||
gamesAfterSync, err = repo.FindAllGames(BackendCtx())
|
gamesAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||||
handleError("FindAllGames After", err, "")
|
handleError("FindAllSoundtracks After", err, "")
|
||||||
|
|
||||||
finished := time.Now()
|
finished := time.Now()
|
||||||
totalTime = finished.Sub(start)
|
totalTime = finished.Sub(start)
|
||||||
@@ -259,8 +265,10 @@ func checkBrokenSongsNew() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
brokenWg.Wait()
|
brokenWg.Wait()
|
||||||
err = repo.RemoveBrokenSongs(BackendCtx(), brokenSongs)
|
for _, bs := range brokenSongs {
|
||||||
handleError("RemoveBrokenSongs", err, "")
|
err = repo.RemoveBrokenSong(BackendCtx(), repository.RemoveBrokenSongParams{SoundtrackID: bs.SoundtrackID, Path: bs.Path})
|
||||||
|
handleError("RemoveBrokenSong", err, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBrokenSongNew(song repository.Song) {
|
func checkBrokenSongNew(song repository.Song) {
|
||||||
@@ -268,7 +276,7 @@ func checkBrokenSongNew(song repository.Song) {
|
|||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//File not found
|
//File not found
|
||||||
brokenSongs = append(brokenSongs, song.Path)
|
brokenSongs = append(brokenSongs, brokenSong{SoundtrackID: song.SoundtrackID, Path: song.Path})
|
||||||
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
logging.GetLogger().Warn("Broken song found", zap.String("path", song.Path))
|
||||||
} else {
|
} else {
|
||||||
err = openFile.Close()
|
err = openFile.Close()
|
||||||
@@ -285,28 +293,28 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
dirHash := getHashForDir(gameDir)
|
dirHash := getHashForDir(gameDir)
|
||||||
|
|
||||||
var status GameStatus = NewGame
|
var status GameStatus = NewGame
|
||||||
var oldGame repository.Game
|
var oldGame repository.Soundtrack
|
||||||
var id int32 = -1
|
var id int32 = -1
|
||||||
|
|
||||||
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
|
//fmt.Printf("Games before: %d\n", len(gamesBeforeSync))
|
||||||
|
|
||||||
for _, currentGame := range allGames {
|
for _, currentGame := range allGames {
|
||||||
oldGame = currentGame
|
oldGame = currentGame
|
||||||
//fmt.Printf("%s | %s\n", oldGame.GameName, oldGame.Hash)
|
//fmt.Printf("%s | %s\n", oldGame.SoundtrackName, oldGame.Hash)
|
||||||
if oldGame.GameName == file.Name() && oldGame.Hash == dirHash {
|
if oldGame.SoundtrackName == file.Name() && oldGame.Hash == dirHash {
|
||||||
status = NotChanged
|
status = NotChanged
|
||||||
id = oldGame.ID
|
id = oldGame.ID
|
||||||
//fmt.Printf("Game not changed\n")
|
//fmt.Printf("Game not changed\n")
|
||||||
break
|
break
|
||||||
} else if oldGame.GameName == file.Name() && oldGame.Hash != dirHash {
|
} else if oldGame.SoundtrackName == file.Name() && oldGame.Hash != dirHash {
|
||||||
status = GameChanged
|
status = GameChanged
|
||||||
id = oldGame.ID
|
id = oldGame.ID
|
||||||
//fmt.Printf("Game changed\n")
|
//fmt.Printf("Game changed\n")
|
||||||
break
|
break
|
||||||
} else if oldGame.GameName != file.Name() && oldGame.Hash == dirHash {
|
} else if oldGame.SoundtrackName != file.Name() && oldGame.Hash == dirHash {
|
||||||
status = TitleChanged
|
status = TitleChanged
|
||||||
id = oldGame.ID
|
id = oldGame.ID
|
||||||
//fmt.Printf("GameName changed\n")
|
//fmt.Printf("SoundtrackName changed\n")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,8 +340,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = repo.InsertGameWithExistingId(BackendCtx(), repository.InsertGameWithExistingIdParams{ID: id, GameName: file.Name(), Path: gameDir, Hash: dirHash})
|
err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash})
|
||||||
handleError("InsertGameWithExistingId", err, "")
|
handleError("InsertSoundtrackWithExistingId", err, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Debug("Game already exists, removing old ID file",
|
logging.GetLogger().Debug("Game already exists, removing old ID file",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
@@ -366,24 +374,24 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
zap.String("game", file.Name()),
|
zap.String("game", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.UpdateGameHash(BackendCtx(), repository.UpdateGameHashParams{Hash: dirHash, ID: id})
|
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
||||||
handleError("UpdateGameHash", err, "")
|
handleError("UpdateSoundtrackHash", err, "")
|
||||||
gamesChangedContent = append(gamesChangedContent, file.Name())
|
gamesChangedContent = append(gamesChangedContent, file.Name())
|
||||||
newCheckSongs(entries, gameDir, id)
|
newCheckSongs(entries, gameDir, id)
|
||||||
case TitleChanged:
|
case TitleChanged:
|
||||||
logging.GetLogger().Debug("Game title changed",
|
logging.GetLogger().Debug("Game title changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("oldName", oldGame.GameName),
|
zap.String("oldName", oldGame.SoundtrackName),
|
||||||
zap.String("newName", file.Name()),
|
zap.String("newName", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.UpdateGameName(BackendCtx(), repository.UpdateGameNameParams{Name: file.Name(), Path: gameDir, ID: id})
|
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: gameDir, ID: id})
|
||||||
handleError("UpdateGameName", err, "")
|
handleError("UpdateSoundtrackName", err, "")
|
||||||
newCheckSongs(entries, gameDir, id)
|
newCheckSongs(entries, gameDir, id)
|
||||||
if gamesChangedTitle == nil {
|
if gamesChangedTitle == nil {
|
||||||
gamesChangedTitle = make(map[string]string)
|
gamesChangedTitle = make(map[string]string)
|
||||||
}
|
}
|
||||||
gamesChangedTitle[oldGame.GameName] = file.Name()
|
gamesChangedTitle[oldGame.SoundtrackName] = file.Name()
|
||||||
case NotChanged:
|
case NotChanged:
|
||||||
var found bool = false
|
var found bool = false
|
||||||
for _, beforeGame := range gamesBeforeSync {
|
for _, beforeGame := range gamesBeforeSync {
|
||||||
@@ -412,8 +420,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
zap.String("game", file.Name()),
|
zap.String("game", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.RemoveDeletionDate(BackendCtx(), id)
|
err = repo.RemoveSoundtrackDeletionDate(BackendCtx(), id)
|
||||||
handleError("RemoveDeletionDate", err, "")
|
handleError("RemoveSoundtrackDeletionDate", err, "")
|
||||||
}
|
}
|
||||||
foldersSynced++
|
foldersSynced++
|
||||||
logging.GetLogger().Debug("Sync progress",
|
logging.GetLogger().Debug("Sync progress",
|
||||||
@@ -424,14 +432,14 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full
|
|||||||
|
|
||||||
func insertGameNew(name string, path string, hash string) int32 {
|
func insertGameNew(name string, path string, hash string) int32 {
|
||||||
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
||||||
id, err := repo.InsertGame(BackendCtx(), repository.InsertGameParams{GameName: name, Path: path, Hash: hash})
|
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash})
|
||||||
handleError("InsertGame", err, "")
|
handleError("InsertSoundtrack", err, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Warn("ID collision detected, resetting sequence")
|
logging.GetLogger().Warn("ID collision detected, resetting sequence")
|
||||||
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
if strings.HasPrefix(err.Error(), duplicateError.Error()) {
|
||||||
logging.GetLogger().Debug("Resetting game ID sequence")
|
logging.GetLogger().Debug("Resetting game ID sequence")
|
||||||
_, err = repo.ResetGameIdSeq(BackendCtx())
|
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
||||||
handleError("ResetGameIdSeq", err, "")
|
handleError("ResetSoundtrackIdSeq", err, "")
|
||||||
id = insertGameNew(name, path, hash)
|
id = insertGameNew(name, path, hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,7 +483,7 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
|||||||
songName, _ := strings.CutSuffix(fileName, ".mp3")
|
songName, _ := strings.CutSuffix(fileName, ".mp3")
|
||||||
|
|
||||||
song, err := repo.GetSongWithHash(BackendCtx(), songHash)
|
song, err := repo.GetSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("GetSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("GetSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if song.SongName == songName && song.Path == path {
|
if song.SongName == songName && song.Path == path {
|
||||||
return false
|
return false
|
||||||
@@ -488,31 +496,31 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool {
|
|||||||
zap.String("song_hash", songHash))
|
zap.String("song_hash", songHash))
|
||||||
|
|
||||||
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
count, err := repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("CheckSongWithHash", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
handleError("CheckSongWithHash", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
count2, err := repo.CheckSong(BackendCtx(), path)
|
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||||
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s\n", id, path, entry.Name(), songHash))
|
||||||
if count2 > 0 {
|
if count2 > 0 {
|
||||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||||
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
count, err = repo.CheckSongWithHash(BackendCtx(), songHash)
|
||||||
handleError("CheckSongWithHash 2", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("CheckSongWithHash 2", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//count, _ := repo.CheckSong(ctx, path)
|
//count, _ := repo.CheckSong(ctx, path)
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
err = repo.UpdateSong(BackendCtx(), repository.UpdateSongParams{SongName: songName, FileName: &fileName, Path: path, Hash: songHash})
|
||||||
handleError("UpdateSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("UpdateSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
} else {
|
} else {
|
||||||
count2, err := repo.CheckSong(BackendCtx(), path)
|
count2, err := repo.CheckSong(BackendCtx(), repository.CheckSongParams{SoundtrackID: id, Path: path})
|
||||||
handleError("CheckSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("CheckSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
if count2 > 0 {
|
if count2 > 0 {
|
||||||
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, Path: path})
|
err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path})
|
||||||
handleError("AddHashToSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
} else {
|
} else {
|
||||||
err = repo.AddSong(BackendCtx(), repository.AddSongParams{GameID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
|
err = repo.AddSong(BackendCtx(), repository.AddSongParams{SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash})
|
||||||
handleError("AddSong", err, fmt.Sprintf("GameID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
handleError("AddSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"music-server/internal/logging"
|
"music-server/internal/logging"
|
||||||
|
|
||||||
@@ -59,6 +60,26 @@ func (db *Database) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Health checks the health of the database connection by pinging the database.
|
||||||
|
// It returns a map with keys indicating various health statistics.
|
||||||
|
func (db *Database) Health() map[string]string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
// Ping the database
|
||||||
|
err := db.Pool.Ping(ctx)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = err.Error()
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["status"] = "up"
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
// RunMigrations runs all pending database migrations to the latest version.
|
// RunMigrations runs all pending database migrations to the latest version.
|
||||||
// Uses the existing pool to extract connection details.
|
// Uses the existing pool to extract connection details.
|
||||||
func (db *Database) RunMigrations() error {
|
func (db *Database) RunMigrations() error {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: DEPRECATED - Remove these global variables once all code is migrated to use Database struct
|
||||||
|
// Use database.go's Database struct instead. These globals remain for backward compatibility
|
||||||
|
// with legacy code paths. New code should use the Database struct from database.go.
|
||||||
var Dbpool *pgxpool.Pool
|
var Dbpool *pgxpool.Pool
|
||||||
var Ctx = context.Background()
|
var Ctx = context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMigrationsStepByStep tests applying migrations incrementally
|
||||||
|
// Then adding data manually, then completing migrations
|
||||||
|
func TestMigrationsStepByStep(t *testing.T) {
|
||||||
|
host := os.Getenv("DB_HOST")
|
||||||
|
port := os.Getenv("DB_PORT")
|
||||||
|
user := os.Getenv("DB_USERNAME")
|
||||||
|
password := os.Getenv("DB_PASSWORD")
|
||||||
|
// Use a unique database name for this test
|
||||||
|
dbname := "music_server_migration_test"
|
||||||
|
|
||||||
|
if host == "" || port == "" || user == "" || password == "" {
|
||||||
|
t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up: drop database if it exists
|
||||||
|
cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
defer cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Create the database
|
||||||
|
createTestDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Step 1: Apply first 4 migrations (before soundtrack rename)
|
||||||
|
// This creates: game, song, vgmq, song_list tables
|
||||||
|
// And sessions table with indexes
|
||||||
|
t.Run("ApplyFirst4Migrations", func(t *testing.T) {
|
||||||
|
applyMigrations(t, host, port, user, password, dbname, 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 2: Add data manually to game and song tables
|
||||||
|
t.Run("AddManualData", func(t *testing.T) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Insert 5 games manually
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
gameName := fmt.Sprintf("Manual Game %d", i)
|
||||||
|
path := fmt.Sprintf("/manual/path/game%d", i)
|
||||||
|
hash := fmt.Sprintf("hash-%d", i)
|
||||||
|
|
||||||
|
_, err := db.Exec(`INSERT INTO game (game_name, path, hash, added)
|
||||||
|
VALUES ($1, $2, $3, NOW())`,
|
||||||
|
gameName, path, hash)
|
||||||
|
require.NoError(t, err, "Failed to insert game %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert songs for each game
|
||||||
|
songs := []struct {
|
||||||
|
gameID int
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{1, "Song A", "/path/a.mp3"},
|
||||||
|
{1, "Song B", "/path/b.mp3"},
|
||||||
|
{2, "Song C", "/path/c.mp3"},
|
||||||
|
{2, "Song D", "/path/d.mp3"},
|
||||||
|
{3, "Song E", "/path/e.mp3"},
|
||||||
|
{4, "Song F", "/path/f.mp3"},
|
||||||
|
{4, "Song G", "/path/g.mp3"},
|
||||||
|
{4, "Song H", "/path/h.mp3"},
|
||||||
|
{5, "Song I", "/path/i.mp3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range songs {
|
||||||
|
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path, hash)
|
||||||
|
VALUES ($1, $2, $3, $4)`,
|
||||||
|
s.gameID, s.name, s.path, fmt.Sprintf("song-hash-%s", s.name))
|
||||||
|
require.NoError(t, err, "Failed to insert song %s", s.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data was inserted
|
||||||
|
var gameCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM game").Scan(&gameCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, gameCount, "Expected 5 games")
|
||||||
|
|
||||||
|
var songCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 9, songCount, "Expected 9 songs")
|
||||||
|
|
||||||
|
t.Log("✓ Manually inserted 5 games with 9 songs")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||||
|
t.Run("ApplyMigration5", func(t *testing.T) {
|
||||||
|
// Apply the remaining migrations (just migration 5)
|
||||||
|
applyMigrations(t, host, port, user, password, dbname, 1)
|
||||||
|
|
||||||
|
// Verify tables were renamed
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check that soundtrack table exists
|
||||||
|
var soundtrackCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 5, soundtrackCount, "Expected 5 soundtracks after migration")
|
||||||
|
|
||||||
|
// Check that game table no longer exists
|
||||||
|
_, err = db.Exec("SELECT 1 FROM game LIMIT 1")
|
||||||
|
require.Error(t, err, "game table should not exist after migration")
|
||||||
|
|
||||||
|
// Check that song table has soundtrack_id column
|
||||||
|
var songCount int
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 9, songCount, "Expected 9 songs after migration")
|
||||||
|
|
||||||
|
// Verify data integrity: soundtrack_name values
|
||||||
|
rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
expectedNames := []string{"Manual Game 1", "Manual Game 2", "Manual Game 3", "Manual Game 4", "Manual Game 5"}
|
||||||
|
actualNames := make([]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
err := rows.Scan(&name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
actualNames = append(actualNames, name)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedNames, actualNames, "Soundtrack names should match original game names")
|
||||||
|
|
||||||
|
t.Log("✓ Migration 5 applied successfully, data preserved")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupDB drops the test database
|
||||||
|
func cleanupDB(t *testing.T, host, port, user, password, dbname string) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
||||||
|
host, port, user, password)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: could not connect to cleanup DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check if database exists before dropping
|
||||||
|
var exists int
|
||||||
|
err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
t.Logf("Warning: could not check if DB exists: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists == 1 {
|
||||||
|
_, err = db.Exec("DROP DATABASE " + dbname + " WITH (FORCE)")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: could not drop DB: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestDB creates a fresh test database
|
||||||
|
func createTestDB(t *testing.T, host, port, user, password, dbname string) {
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable",
|
||||||
|
host, port, user, password)
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Drop if exists
|
||||||
|
cleanupDB(t, host, port, user, password, dbname)
|
||||||
|
|
||||||
|
// Create database
|
||||||
|
_, err = db.Exec("CREATE DATABASE " + dbname)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Enable UUID extension if needed
|
||||||
|
connStrDB := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, dbname)
|
||||||
|
db2, err := sql.Open("postgres", connStrDB)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db2.Close()
|
||||||
|
|
||||||
|
_, err = db2.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Note: uuid-ossp extension may not be available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyMigrations applies n migrations to the database using Go migrate library
|
||||||
|
func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) {
|
||||||
|
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
||||||
|
user, password, host, port, dbname)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", migrationURL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance(
|
||||||
|
"file://migrations",
|
||||||
|
"postgres", driver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get current version
|
||||||
|
version, _, err := m.Version()
|
||||||
|
if err != nil && err != migrate.ErrNilVersion {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if err == migrate.ErrNilVersion {
|
||||||
|
version = 0
|
||||||
|
}
|
||||||
|
t.Logf("Current migration version: %d", version)
|
||||||
|
|
||||||
|
// Apply exactly 'steps' migrations
|
||||||
|
if steps > 0 {
|
||||||
|
err = m.Steps(steps)
|
||||||
|
if err != nil && err != migrate.ErrNoChange {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
} else if steps < 0 {
|
||||||
|
err = m.Steps(steps)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new version
|
||||||
|
newVersion, _, err := m.Version()
|
||||||
|
if err != nil && err != migrate.ErrNilVersion {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if err == migrate.ErrNilVersion {
|
||||||
|
newVersion = 0
|
||||||
|
}
|
||||||
|
t.Logf("Migration version after applying %d steps: %d", steps, newVersion)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- Revert: Rename soundtrack table back to game
|
||||||
|
ALTER TABLE soundtrack RENAME TO game;
|
||||||
|
|
||||||
|
-- Revert primary key sequence
|
||||||
|
ALTER SEQUENCE soundtrack_id_seq RENAME TO game_id_seq;
|
||||||
|
|
||||||
|
-- Revert columns in game table
|
||||||
|
ALTER TABLE game RENAME COLUMN soundtrack_name TO game_name;
|
||||||
|
|
||||||
|
-- Revert song table: rename soundtrack_id back to game_id
|
||||||
|
ALTER TABLE song RENAME COLUMN soundtrack_id TO game_id;
|
||||||
|
|
||||||
|
-- Revert song primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||||
|
ALTER TABLE song ADD PRIMARY KEY (game_id, path);
|
||||||
|
ALTER TABLE song RENAME CONSTRAINT song_pkey_soundtrack TO song_pkey;
|
||||||
|
|
||||||
|
-- Revert song_list table references
|
||||||
|
ALTER TABLE song_list RENAME COLUMN soundtrack_name TO game_name;
|
||||||
|
|
||||||
|
-- Revert foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_game_id_fkey
|
||||||
|
FOREIGN KEY (game_id) REFERENCES game(id);
|
||||||
|
|
||||||
|
-- Revert indexes
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_deleted RENAME TO idx_game_deleted;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_hash RENAME TO idx_game_hash;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_path RENAME TO idx_game_path;
|
||||||
|
ALTER INDEX IF EXISTS idx_soundtrack_name RENAME TO idx_game_name;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_soundtrack_id RENAME TO idx_song_game_id;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_soundtrack_id_song_name RENAME TO idx_song_game_id_song_name;
|
||||||
|
ALTER INDEX IF EXISTS song_list_soundtrack_name_idx RENAME TO song_list_game_name_idx;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Rename game table to soundtrack
|
||||||
|
ALTER TABLE game RENAME TO soundtrack;
|
||||||
|
|
||||||
|
-- Rename primary key sequence
|
||||||
|
ALTER SEQUENCE game_id_seq RENAME TO soundtrack_id_seq;
|
||||||
|
|
||||||
|
-- Rename columns in soundtrack table
|
||||||
|
ALTER TABLE soundtrack RENAME COLUMN game_name TO soundtrack_name;
|
||||||
|
|
||||||
|
-- Update song table: rename game_id to soundtrack_id
|
||||||
|
ALTER TABLE song RENAME COLUMN game_id TO soundtrack_id;
|
||||||
|
|
||||||
|
-- Update song primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_pkey;
|
||||||
|
ALTER TABLE song ADD PRIMARY KEY (soundtrack_id, path);
|
||||||
|
|
||||||
|
-- Update song_list table references
|
||||||
|
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||||
|
|
||||||
|
-- Rename foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_game_id_fkey;
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
|
|
||||||
|
-- Rename indexes
|
||||||
|
ALTER INDEX IF EXISTS idx_game_deleted RENAME TO idx_soundtrack_deleted;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_hash RENAME TO idx_soundtrack_hash;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_path RENAME TO idx_soundtrack_path;
|
||||||
|
ALTER INDEX IF EXISTS idx_game_name RENAME TO idx_soundtrack_name;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_game_id RENAME TO idx_song_soundtrack_id;
|
||||||
|
ALTER INDEX IF EXISTS idx_song_game_id_song_name RENAME TO idx_song_soundtrack_id_song_name;
|
||||||
|
ALTER INDEX IF EXISTS song_list_game_name_idx RENAME TO song_list_soundtrack_name_idx;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Rollback: Remove id column and restore composite PK
|
||||||
|
|
||||||
|
-- Step 1: Drop indexes created in up migration
|
||||||
|
DROP INDEX IF EXISTS idx_song_soundtrack_id;
|
||||||
|
DROP INDEX IF EXISTS idx_song_path;
|
||||||
|
|
||||||
|
-- Step 2: Drop foreign key constraint
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
|
||||||
|
-- Step 3: Drop new primary key
|
||||||
|
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||||
|
|
||||||
|
-- Step 4: Drop unique constraint on id
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_id_unique;
|
||||||
|
|
||||||
|
-- Step 5: Restore composite primary key
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (soundtrack_id, path);
|
||||||
|
|
||||||
|
-- Step 6: Drop the id column
|
||||||
|
ALTER TABLE song DROP COLUMN id;
|
||||||
|
|
||||||
|
-- Step 7: Recreate original foreign key (soundtrack_id references soundtrack.id)
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- Migration: Add id column to song table and change PK from composite to single column
|
||||||
|
-- This prepares the song table for eventual UUID migration
|
||||||
|
|
||||||
|
-- Step 1: Add new id column (nullable initially)
|
||||||
|
ALTER TABLE song ADD COLUMN id serial4;
|
||||||
|
|
||||||
|
-- Step 2: Create unique constraint on id (allows backfilling)
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_id_unique UNIQUE (id);
|
||||||
|
|
||||||
|
-- Step 3: Backfill existing rows with sequential IDs
|
||||||
|
-- Use DEFAULT which pulls from the sequence
|
||||||
|
UPDATE song SET id = DEFAULT WHERE id IS NULL;
|
||||||
|
|
||||||
|
-- Step 4: Verify all rows have an id
|
||||||
|
-- If this returns 0, backfill worked
|
||||||
|
-- SELECT COUNT(*) FROM song WHERE id IS NULL;
|
||||||
|
|
||||||
|
-- Step 5: Drop the composite primary key (soundtrack_id, path)
|
||||||
|
ALTER TABLE song DROP CONSTRAINT song_pkey;
|
||||||
|
|
||||||
|
-- Step 6: Add new primary key on id column
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
-- Step 7: Ensure soundtrack_id remains a foreign key to soundtrack
|
||||||
|
-- First drop existing FK if it exists (from the rename migration)
|
||||||
|
ALTER TABLE song DROP CONSTRAINT IF EXISTS song_soundtrack_id_fkey;
|
||||||
|
|
||||||
|
-- Then recreate it
|
||||||
|
ALTER TABLE song ADD CONSTRAINT song_soundtrack_id_fkey
|
||||||
|
FOREIGN KEY (soundtrack_id) REFERENCES soundtrack(id);
|
||||||
|
|
||||||
|
-- Step 8: Create index on soundtrack_id for query performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_soundtrack_id ON song(soundtrack_id);
|
||||||
|
|
||||||
|
-- Step 9: Create index on path for lookups (previously part of PK)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
-- name: ResetGameIdSeq :one
|
|
||||||
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);
|
|
||||||
|
|
||||||
-- name: GetGameNameById :one
|
|
||||||
SELECT game_name FROM game WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetGameById :one
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: SetGameDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=now() WHERE deleted IS NULL;
|
|
||||||
|
|
||||||
-- name: ClearGames :exec
|
|
||||||
DELETE FROM game;
|
|
||||||
|
|
||||||
-- name: UpdateGameName :exec
|
|
||||||
UPDATE game SET game_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: UpdateGameHash :exec
|
|
||||||
UPDATE game SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
|
||||||
|
|
||||||
-- name: RemoveDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=NULL WHERE id=$1;
|
|
||||||
|
|
||||||
-- name: GetIdByGameName :one
|
|
||||||
SELECT id FROM game WHERE game_name = $1;
|
|
||||||
|
|
||||||
-- name: InsertGame :one
|
|
||||||
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
|
|
||||||
|
|
||||||
-- name: InsertGameWithExistingId :exec
|
|
||||||
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
|
||||||
|
|
||||||
-- name: FindAllGames :many
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY game_name;
|
|
||||||
|
|
||||||
-- name: GetAllGamesIncludingDeleted :many
|
|
||||||
SELECT *
|
|
||||||
FROM game
|
|
||||||
ORDER BY game_name;
|
|
||||||
|
|
||||||
-- name: AddGamePlayed :exec
|
|
||||||
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
-- name: ClearSongs :exec
|
-- name: ClearSongs :exec
|
||||||
DELETE FROM song;
|
DELETE FROM song;
|
||||||
|
|
||||||
-- name: ClearSongsByGameId :exec
|
-- name: ClearSongsBySoundtrackId :exec
|
||||||
DELETE FROM song WHERE game_id = $1;
|
DELETE FROM song WHERE soundtrack_id = $1;
|
||||||
|
|
||||||
-- name: AddSong :exec
|
-- name: AddSong :exec
|
||||||
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: CheckSong :one
|
-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE path = $1;
|
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||||
|
|
||||||
-- name: CheckSongWithHash :one
|
-- name: CheckSongWithHash :one
|
||||||
SELECT COUNT(*) FROM song WHERE hash = $1;
|
SELECT COUNT(*) FROM song WHERE hash = $1;
|
||||||
@@ -20,22 +20,25 @@ SELECT * FROM song WHERE hash = $1;
|
|||||||
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
UPDATE song SET song_name=$1, file_name=$2, path=$3 where hash=$4;
|
||||||
|
|
||||||
-- name: AddHashToSong :exec
|
-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where path=$2;
|
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3;
|
||||||
|
|
||||||
-- name: FindSongsFromGame :many
|
-- name: FindSongsFromSoundtrack :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM song
|
FROM song
|
||||||
WHERE game_id = $1;
|
WHERE soundtrack_id = $1;
|
||||||
|
|
||||||
-- name: AddSongPlayed :exec
|
-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE game_id = $1 AND song_name = $2;
|
WHERE soundtrack_id = $1 AND song_name = $2;
|
||||||
|
|
||||||
-- name: FetchAllSongs :many
|
-- name: FetchAllSongs :many
|
||||||
SELECT * FROM song;
|
SELECT * FROM song;
|
||||||
|
|
||||||
|
-- name: GetSongById :one
|
||||||
|
SELECT * FROM song WHERE id = $1;
|
||||||
|
|
||||||
-- name: RemoveBrokenSong :exec
|
-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE path = $1;
|
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2;
|
||||||
|
|
||||||
-- name: RemoveBrokenSongs :exec
|
-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song where path = any (sqlc.slice('paths'));
|
DELETE FROM song WHERE id = ANY($1);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- name: InsertSongInList :exec
|
-- name: InsertSongInList :exec
|
||||||
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
|
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name)
|
||||||
VALUES ($1, $2, $3, $4, $5);
|
VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
-- name: GetSongList :many
|
-- name: GetSongList :many
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- name: ResetSoundtrackIdSeq :one
|
||||||
|
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1);
|
||||||
|
|
||||||
|
-- name: GetSoundtrackNameById :one
|
||||||
|
SELECT soundtrack_name FROM soundtrack WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetSoundtrackById :one
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE id = $1
|
||||||
|
AND deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: SetSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL;
|
||||||
|
|
||||||
|
-- name: ClearSoundtracks :exec
|
||||||
|
DELETE FROM soundtrack;
|
||||||
|
|
||||||
|
-- name: UpdateSoundtrackName :exec
|
||||||
|
UPDATE soundtrack SET soundtrack_name=sqlc.arg(name), path=sqlc.arg(path), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: UpdateSoundtrackHash :exec
|
||||||
|
UPDATE soundtrack SET hash=sqlc.arg(hash), last_changed=now() WHERE id=sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: RemoveSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=NULL WHERE id=$1;
|
||||||
|
|
||||||
|
-- name: GetIdBySoundtrackName :one
|
||||||
|
SELECT id FROM soundtrack WHERE soundtrack_name = $1;
|
||||||
|
|
||||||
|
-- name: InsertSoundtrack :one
|
||||||
|
INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id;
|
||||||
|
|
||||||
|
-- name: InsertSoundtrackWithExistingId :exec
|
||||||
|
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now());
|
||||||
|
|
||||||
|
-- name: FindAllSoundtracks :many
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
ORDER BY soundtrack_name;
|
||||||
|
|
||||||
|
-- name: GetAllSoundtracksIncludingDeleted :many
|
||||||
|
SELECT *
|
||||||
|
FROM soundtrack
|
||||||
|
ORDER BY soundtrack_name;
|
||||||
|
|
||||||
|
-- name: AddSoundtrackPlayed :exec
|
||||||
|
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
-- Most played soundtracks with their songs
|
||||||
|
-- name: GetMostPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played DESC, g.soundtrack_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Least played soundtracks with their songs
|
||||||
|
-- name: GetLeastPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played ASC, g.soundtrack_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Most played songs with their soundtrack info
|
||||||
|
-- name: GetMostPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played DESC, s.song_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Least played songs with their soundtrack info
|
||||||
|
-- name: GetLeastPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played ASC, s.song_name
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Games that have never been played (times_played = 0)
|
||||||
|
-- name: GetNeverPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.added,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.times_played = 0
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
||||||
|
ORDER BY g.soundtrack_name;
|
||||||
|
|
||||||
|
-- Last played soundtracks (most recently played)
|
||||||
|
-- name: GetLastPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played DESC
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Oldest played soundtracks (least recently played, but has been played at least once)
|
||||||
|
-- name: GetOldestPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played ASC
|
||||||
|
LIMIT $1;
|
||||||
|
|
||||||
|
-- Get statistics summary
|
||||||
|
-- name: GetStatisticsSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||||
|
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||||
|
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||||
|
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||||
|
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL;
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: game.sql
|
|
||||||
|
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const addGamePlayed = `-- name: AddGamePlayed :exec
|
|
||||||
UPDATE game SET times_played = times_played + 1, last_played = now() WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) AddGamePlayed(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, addGamePlayed, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearGames = `-- name: ClearGames :exec
|
|
||||||
DELETE FROM game
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ClearGames(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, clearGames)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const findAllGames = `-- name: FindAllGames :many
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
WHERE deleted IS NULL
|
|
||||||
ORDER BY game_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) FindAllGames(ctx context.Context) ([]Game, error) {
|
|
||||||
rows, err := q.db.Query(ctx, findAllGames)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Game
|
|
||||||
for rows.Next() {
|
|
||||||
var i Game
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllGamesIncludingDeleted = `-- name: GetAllGamesIncludingDeleted :many
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
ORDER BY game_name
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetAllGamesIncludingDeleted(ctx context.Context) ([]Game, error) {
|
|
||||||
rows, err := q.db.Query(ctx, getAllGamesIncludingDeleted)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Game
|
|
||||||
for rows.Next() {
|
|
||||||
var i Game
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGameById = `-- name: GetGameById :one
|
|
||||||
SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
|
||||||
FROM game
|
|
||||||
WHERE id = $1
|
|
||||||
AND deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetGameById(ctx context.Context, id int32) (Game, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getGameById, id)
|
|
||||||
var i Game
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.GameName,
|
|
||||||
&i.Added,
|
|
||||||
&i.Deleted,
|
|
||||||
&i.LastChanged,
|
|
||||||
&i.Path,
|
|
||||||
&i.TimesPlayed,
|
|
||||||
&i.LastPlayed,
|
|
||||||
&i.NumberOfSongs,
|
|
||||||
&i.Hash,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGameNameById = `-- name: GetGameNameById :one
|
|
||||||
SELECT game_name FROM game WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetGameNameById(ctx context.Context, id int32) (string, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getGameNameById, id)
|
|
||||||
var game_name string
|
|
||||||
err := row.Scan(&game_name)
|
|
||||||
return game_name, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIdByGameName = `-- name: GetIdByGameName :one
|
|
||||||
SELECT id FROM game WHERE game_name = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetIdByGameName(ctx context.Context, gameName string) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getIdByGameName, gameName)
|
|
||||||
var id int32
|
|
||||||
err := row.Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertGame = `-- name: InsertGame :one
|
|
||||||
INSERT INTO game (game_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertGameParams struct {
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertGame(ctx context.Context, arg InsertGameParams) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, insertGame, arg.GameName, arg.Path, arg.Hash)
|
|
||||||
var id int32
|
|
||||||
err := row.Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertGameWithExistingId = `-- name: InsertGameWithExistingId :exec
|
|
||||||
INSERT INTO game (id, game_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertGameWithExistingIdParams struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertGameWithExistingId(ctx context.Context, arg InsertGameWithExistingIdParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, insertGameWithExistingId,
|
|
||||||
arg.ID,
|
|
||||||
arg.GameName,
|
|
||||||
arg.Path,
|
|
||||||
arg.Hash,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeDeletionDate = `-- name: RemoveDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=NULL WHERE id=$1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) RemoveDeletionDate(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.Exec(ctx, removeDeletionDate, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetGameIdSeq = `-- name: ResetGameIdSeq :one
|
|
||||||
SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1)
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ResetGameIdSeq(ctx context.Context) (int64, error) {
|
|
||||||
row := q.db.QueryRow(ctx, resetGameIdSeq)
|
|
||||||
var setval int64
|
|
||||||
err := row.Scan(&setval)
|
|
||||||
return setval, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const setGameDeletionDate = `-- name: SetGameDeletionDate :exec
|
|
||||||
UPDATE game SET deleted=now() WHERE deleted IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) SetGameDeletionDate(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, setGameDeletionDate)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGameHash = `-- name: UpdateGameHash :exec
|
|
||||||
UPDATE game SET hash=$1, last_changed=now() WHERE id=$2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateGameHashParams struct {
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateGameHash(ctx context.Context, arg UpdateGameHashParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateGameHash, arg.Hash, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGameName = `-- name: UpdateGameName :exec
|
|
||||||
UPDATE game SET game_name=$1, path=$2, last_changed=now() WHERE id=$3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateGameNameParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateGameName(ctx context.Context, arg UpdateGameNameParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, updateGameName, arg.Name, arg.Path, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -10,19 +10,6 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Game struct {
|
|
||||||
ID int32 `json:"id"`
|
|
||||||
GameName string `json:"game_name"`
|
|
||||||
Added time.Time `json:"added"`
|
|
||||||
Deleted *time.Time `json:"deleted"`
|
|
||||||
LastChanged *time.Time `json:"last_changed"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
TimesPlayed int32 `json:"times_played"`
|
|
||||||
LastPlayed *time.Time `json:"last_played"`
|
|
||||||
NumberOfSongs int32 `json:"number_of_songs"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
IpAddress string `json:"ip_address"`
|
IpAddress string `json:"ip_address"`
|
||||||
@@ -33,20 +20,34 @@ type Session struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Song struct {
|
type Song struct {
|
||||||
GameID int32 `json:"game_id"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TimesPlayed int32 `json:"times_played"`
|
TimesPlayed int32 `json:"times_played"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
|
ID pgtype.Int4 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SongList struct {
|
type SongList struct {
|
||||||
MatchDate time.Time `json:"match_date"`
|
MatchDate time.Time `json:"match_date"`
|
||||||
MatchID int32 `json:"match_id"`
|
MatchID int32 `json:"match_id"`
|
||||||
SongNo int32 `json:"song_no"`
|
SongNo int32 `json:"song_no"`
|
||||||
GameName *string `json:"game_name"`
|
SoundtrackName *string `json:"soundtrack_name"`
|
||||||
SongName *string `json:"song_name"`
|
SongName *string `json:"song_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Soundtrack struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
Added time.Time `json:"added"`
|
||||||
|
Deleted *time.Time `json:"deleted"`
|
||||||
|
LastChanged *time.Time `json:"last_changed"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
LastPlayed *time.Time `json:"last_played"`
|
||||||
|
NumberOfSongs int32 `json:"number_of_songs"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Vgmq struct {
|
type Vgmq struct {
|
||||||
|
|||||||
@@ -7,37 +7,40 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const addHashToSong = `-- name: AddHashToSong :exec
|
const addHashToSong = `-- name: AddHashToSong :exec
|
||||||
UPDATE song SET hash=$1 where path=$2
|
UPDATE song SET hash=$1 where soundtrack_id = $2 AND path = $3
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddHashToSongParams struct {
|
type AddHashToSongParams struct {
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Path string `json:"path"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.Path)
|
_, err := q.db.Exec(ctx, addHashToSong, arg.Hash, arg.SoundtrackID, arg.Path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSong = `-- name: AddSong :exec
|
const addSong = `-- name: AddSong :exec
|
||||||
INSERT INTO song(game_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
|
INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5)
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongParams struct {
|
type AddSongParams struct {
|
||||||
GameID int32 `json:"game_id"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
FileName *string `json:"file_name"`
|
FileName *string `json:"file_name"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSong,
|
_, err := q.db.Exec(ctx, addSong,
|
||||||
arg.GameID,
|
arg.SoundtrackID,
|
||||||
arg.SongName,
|
arg.SongName,
|
||||||
arg.Path,
|
arg.Path,
|
||||||
arg.FileName,
|
arg.FileName,
|
||||||
@@ -48,25 +51,30 @@ func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error {
|
|||||||
|
|
||||||
const addSongPlayed = `-- name: AddSongPlayed :exec
|
const addSongPlayed = `-- name: AddSongPlayed :exec
|
||||||
UPDATE song SET times_played = times_played + 1
|
UPDATE song SET times_played = times_played + 1
|
||||||
WHERE game_id = $1 AND song_name = $2
|
WHERE soundtrack_id = $1 AND song_name = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddSongPlayedParams struct {
|
type AddSongPlayedParams struct {
|
||||||
GameID int32 `json:"game_id"`
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
SongName string `json:"song_name"`
|
SongName string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
func (q *Queries) AddSongPlayed(ctx context.Context, arg AddSongPlayedParams) error {
|
||||||
_, err := q.db.Exec(ctx, addSongPlayed, arg.GameID, arg.SongName)
|
_, err := q.db.Exec(ctx, addSongPlayed, arg.SoundtrackID, arg.SongName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSong = `-- name: CheckSong :one
|
const checkSong = `-- name: CheckSong :one
|
||||||
SELECT COUNT(*) FROM song WHERE path = $1
|
SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CheckSong(ctx context.Context, path string) (int64, error) {
|
type CheckSongParams struct {
|
||||||
row := q.db.QueryRow(ctx, checkSong, path)
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CheckSong(ctx context.Context, arg CheckSongParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, checkSong, arg.SoundtrackID, arg.Path)
|
||||||
var count int64
|
var count int64
|
||||||
err := row.Scan(&count)
|
err := row.Scan(&count)
|
||||||
return count, err
|
return count, err
|
||||||
@@ -92,17 +100,17 @@ func (q *Queries) ClearSongs(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSongsByGameId = `-- name: ClearSongsByGameId :exec
|
const clearSongsBySoundtrackId = `-- name: ClearSongsBySoundtrackId :exec
|
||||||
DELETE FROM song WHERE game_id = $1
|
DELETE FROM song WHERE soundtrack_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ClearSongsByGameId(ctx context.Context, gameID int32) error {
|
func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int32) error {
|
||||||
_, err := q.db.Exec(ctx, clearSongsByGameId, gameID)
|
_, err := q.db.Exec(ctx, clearSongsBySoundtrackId, soundtrackID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name FROM song
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||||
@@ -115,12 +123,13 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -132,14 +141,14 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const findSongsFromGame = `-- name: FindSongsFromGame :many
|
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id
|
||||||
FROM song
|
FROM song
|
||||||
WHERE game_id = $1
|
WHERE soundtrack_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song, error) {
|
func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int32) ([]Song, error) {
|
||||||
rows, err := q.db.Query(ctx, findSongsFromGame, gameID)
|
rows, err := q.db.Query(ctx, findSongsFromSoundtrack, soundtrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,12 +157,13 @@ func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song,
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Song
|
var i Song
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -165,39 +175,64 @@ func (q *Queries) FindSongsFromGame(ctx context.Context, gameID int32) ([]Song,
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSongById = `-- name: GetSongById :one
|
||||||
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSongById, id)
|
||||||
|
var i Song
|
||||||
|
err := row.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SongName,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.Hash,
|
||||||
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||||
SELECT game_id, song_name, path, times_played, hash, file_name FROM song WHERE hash = $1
|
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id FROM song WHERE hash = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error) {
|
||||||
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
row := q.db.QueryRow(ctx, getSongWithHash, hash)
|
||||||
var i Song
|
var i Song
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.GameID,
|
&i.SoundtrackID,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
&i.Path,
|
&i.Path,
|
||||||
&i.TimesPlayed,
|
&i.TimesPlayed,
|
||||||
&i.Hash,
|
&i.Hash,
|
||||||
&i.FileName,
|
&i.FileName,
|
||||||
|
&i.ID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
const removeBrokenSong = `-- name: RemoveBrokenSong :exec
|
||||||
DELETE FROM song WHERE path = $1
|
DELETE FROM song WHERE soundtrack_id = $1 AND path = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSong(ctx context.Context, path string) error {
|
type RemoveBrokenSongParams struct {
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSong, path)
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) RemoveBrokenSong(ctx context.Context, arg RemoveBrokenSongParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, removeBrokenSong, arg.SoundtrackID, arg.Path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
const removeBrokenSongs = `-- name: RemoveBrokenSongs :exec
|
||||||
DELETE FROM song where path = any ($1)
|
DELETE FROM song WHERE id = ANY($1)
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) RemoveBrokenSongs(ctx context.Context, paths []string) error {
|
func (q *Queries) RemoveBrokenSongs(ctx context.Context, id pgtype.Int4) error {
|
||||||
_, err := q.db.Exec(ctx, removeBrokenSongs, paths)
|
_, err := q.db.Exec(ctx, removeBrokenSongs, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getSongList = `-- name: GetSongList :many
|
const getSongList = `-- name: GetSongList :many
|
||||||
SELECT match_date, match_id, song_no, game_name, song_name
|
SELECT match_date, match_id, song_no, soundtrack_name, song_name
|
||||||
FROM song_list
|
FROM song_list
|
||||||
WHERE match_date = $1
|
WHERE match_date = $1
|
||||||
ORDER BY song_no DESC
|
ORDER BY song_no DESC
|
||||||
@@ -30,7 +30,7 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
|
|||||||
&i.MatchDate,
|
&i.MatchDate,
|
||||||
&i.MatchID,
|
&i.MatchID,
|
||||||
&i.SongNo,
|
&i.SongNo,
|
||||||
&i.GameName,
|
&i.SoundtrackName,
|
||||||
&i.SongName,
|
&i.SongName,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -44,16 +44,16 @@ func (q *Queries) GetSongList(ctx context.Context, matchDate time.Time) ([]SongL
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertSongInList = `-- name: InsertSongInList :exec
|
const insertSongInList = `-- name: InsertSongInList :exec
|
||||||
INSERT INTO song_list (match_date, match_id, song_no, game_name, song_name)
|
INSERT INTO song_list (match_date, match_id, song_no, soundtrack_name, song_name)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertSongInListParams struct {
|
type InsertSongInListParams struct {
|
||||||
MatchDate time.Time `json:"match_date"`
|
MatchDate time.Time `json:"match_date"`
|
||||||
MatchID int32 `json:"match_id"`
|
MatchID int32 `json:"match_id"`
|
||||||
SongNo int32 `json:"song_no"`
|
SongNo int32 `json:"song_no"`
|
||||||
GameName *string `json:"game_name"`
|
SoundtrackName *string `json:"soundtrack_name"`
|
||||||
SongName *string `json:"song_name"`
|
SongName *string `json:"song_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListParams) error {
|
func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListParams) error {
|
||||||
@@ -61,7 +61,7 @@ func (q *Queries) InsertSongInList(ctx context.Context, arg InsertSongInListPara
|
|||||||
arg.MatchDate,
|
arg.MatchDate,
|
||||||
arg.MatchID,
|
arg.MatchID,
|
||||||
arg.SongNo,
|
arg.SongNo,
|
||||||
arg.GameName,
|
arg.SoundtrackName,
|
||||||
arg.SongName,
|
arg.SongName,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: soundtrack.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const addSoundtrackPlayed = `-- name: AddSoundtrackPlayed :exec
|
||||||
|
UPDATE soundtrack SET times_played = times_played + 1, last_played = now() WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) AddSoundtrackPlayed(ctx context.Context, id int32) error {
|
||||||
|
_, err := q.db.Exec(ctx, addSoundtrackPlayed, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSoundtracks = `-- name: ClearSoundtracks :exec
|
||||||
|
DELETE FROM soundtrack
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ClearSoundtracks(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, clearSoundtracks)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
|
||||||
|
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
ORDER BY soundtrack_name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error) {
|
||||||
|
rows, err := q.db.Query(ctx, findAllSoundtracks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Soundtrack
|
||||||
|
for rows.Next() {
|
||||||
|
var i Soundtrack
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
|
||||||
|
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM soundtrack
|
||||||
|
ORDER BY soundtrack_name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soundtrack, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getAllSoundtracksIncludingDeleted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Soundtrack
|
||||||
|
for rows.Next() {
|
||||||
|
var i Soundtrack
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIdBySoundtrackName = `-- name: GetIdBySoundtrackName :one
|
||||||
|
SELECT id FROM soundtrack WHERE soundtrack_name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName string) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getIdBySoundtrackName, soundtrackName)
|
||||||
|
var id int32
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSoundtrackById = `-- name: GetSoundtrackById :one
|
||||||
|
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE id = $1
|
||||||
|
AND deleted IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSoundtrackById, id)
|
||||||
|
var i Soundtrack
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.Added,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.LastChanged,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.LastPlayed,
|
||||||
|
&i.NumberOfSongs,
|
||||||
|
&i.Hash,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSoundtrackNameById = `-- name: GetSoundtrackNameById :one
|
||||||
|
SELECT soundtrack_name FROM soundtrack WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSoundtrackNameById(ctx context.Context, id int32) (string, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSoundtrackNameById, id)
|
||||||
|
var soundtrack_name string
|
||||||
|
err := row.Scan(&soundtrack_name)
|
||||||
|
return soundtrack_name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertSoundtrack = `-- name: InsertSoundtrack :one
|
||||||
|
INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertSoundtrackParams struct {
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertSoundtrack(ctx context.Context, arg InsertSoundtrackParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, insertSoundtrack, arg.SoundtrackName, arg.Path, arg.Hash)
|
||||||
|
var id int32
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertSoundtrackWithExistingId = `-- name: InsertSoundtrackWithExistingId :exec
|
||||||
|
INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now())
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertSoundtrackWithExistingIdParams struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertSoundtrackWithExistingId(ctx context.Context, arg InsertSoundtrackWithExistingIdParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, insertSoundtrackWithExistingId,
|
||||||
|
arg.ID,
|
||||||
|
arg.SoundtrackName,
|
||||||
|
arg.Path,
|
||||||
|
arg.Hash,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSoundtrackDeletionDate = `-- name: RemoveSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=NULL WHERE id=$1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) RemoveSoundtrackDeletionDate(ctx context.Context, id int32) error {
|
||||||
|
_, err := q.db.Exec(ctx, removeSoundtrackDeletionDate, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSoundtrackIdSeq = `-- name: ResetSoundtrackIdSeq :one
|
||||||
|
SELECT setval('soundtrack_id_seq', (SELECT MAX(id) FROM soundtrack)+1)
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ResetSoundtrackIdSeq(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, resetSoundtrackIdSeq)
|
||||||
|
var setval int64
|
||||||
|
err := row.Scan(&setval)
|
||||||
|
return setval, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSoundtrackDeletionDate = `-- name: SetSoundtrackDeletionDate :exec
|
||||||
|
UPDATE soundtrack SET deleted=now() WHERE deleted IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SetSoundtrackDeletionDate(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, setSoundtrackDeletionDate)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSoundtrackHash = `-- name: UpdateSoundtrackHash :exec
|
||||||
|
UPDATE soundtrack SET hash=$1, last_changed=now() WHERE id=$2
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateSoundtrackHashParams struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSoundtrackHash(ctx context.Context, arg UpdateSoundtrackHashParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateSoundtrackHash, arg.Hash, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSoundtrackName = `-- name: UpdateSoundtrackName :exec
|
||||||
|
UPDATE soundtrack SET soundtrack_name=$1, path=$2, last_changed=now() WHERE id=$3
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateSoundtrackNameParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSoundtrackName(ctx context.Context, arg UpdateSoundtrackNameParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateSoundtrackName, arg.Name, arg.Path, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: statistics.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getLastPlayedGames = `-- name: GetLastPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played DESC
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetLastPlayedGamesRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last played soundtracks (most recently played)
|
||||||
|
func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getLastPlayedGames, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetLastPlayedGamesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetLastPlayedGamesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SoundtrackPlayed,
|
||||||
|
&i.SoundtrackLastPlayed,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLeastPlayedGamesWithSongs = `-- name: GetLeastPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played ASC, g.soundtrack_name
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetLeastPlayedGamesWithSongsRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Least played soundtracks with their songs
|
||||||
|
func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetLeastPlayedGamesWithSongsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetLeastPlayedGamesWithSongsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SoundtrackPlayed,
|
||||||
|
&i.SoundtrackLastPlayed,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLeastPlayedSongsWithGame = `-- name: GetLeastPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played ASC, s.song_name
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetLeastPlayedSongsWithGameRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SongName string `json:"song_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
FileName *string `json:"file_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Least played songs with their soundtrack info
|
||||||
|
func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetLeastPlayedSongsWithGameRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetLeastPlayedSongsWithGameRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SongName,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.FileName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMostPlayedGamesWithSongs = `-- name: GetMostPlayedGamesWithSongs :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played,
|
||||||
|
'file_name', s.file_name
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.times_played DESC, g.soundtrack_name
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetMostPlayedGamesWithSongsRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most played soundtracks with their songs
|
||||||
|
func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetMostPlayedGamesWithSongsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetMostPlayedGamesWithSongsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SoundtrackPlayed,
|
||||||
|
&i.SoundtrackLastPlayed,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMostPlayedSongsWithGame = `-- name: GetMostPlayedSongsWithGame :many
|
||||||
|
SELECT
|
||||||
|
s.soundtrack_id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
s.song_name,
|
||||||
|
s.path,
|
||||||
|
s.times_played,
|
||||||
|
s.file_name
|
||||||
|
FROM song s
|
||||||
|
JOIN soundtrack g ON s.soundtrack_id = g.id
|
||||||
|
WHERE g.deleted IS NULL
|
||||||
|
ORDER BY s.times_played DESC, s.song_name
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetMostPlayedSongsWithGameRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SongName string `json:"song_name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TimesPlayed int32 `json:"times_played"`
|
||||||
|
FileName *string `json:"file_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most played songs with their soundtrack info
|
||||||
|
func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetMostPlayedSongsWithGameRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetMostPlayedSongsWithGameRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SongName,
|
||||||
|
&i.Path,
|
||||||
|
&i.TimesPlayed,
|
||||||
|
&i.FileName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNeverPlayedGames = `-- name: GetNeverPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.added,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.times_played = 0
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.added
|
||||||
|
ORDER BY g.soundtrack_name
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetNeverPlayedGamesRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||||
|
Added time.Time `json:"added"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Games that have never been played (times_played = 0)
|
||||||
|
func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGamesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getNeverPlayedGames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetNeverPlayedGamesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetNeverPlayedGamesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SoundtrackPlayed,
|
||||||
|
&i.Added,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOldestPlayedGames = `-- name: GetOldestPlayedGames :many
|
||||||
|
SELECT
|
||||||
|
g.id as soundtrack_id,
|
||||||
|
g.soundtrack_name,
|
||||||
|
g.times_played as soundtrack_played,
|
||||||
|
g.last_played as soundtrack_last_played,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'song_name', s.song_name,
|
||||||
|
'path', s.path,
|
||||||
|
'times_played', s.times_played
|
||||||
|
)
|
||||||
|
) as songs
|
||||||
|
FROM soundtrack g
|
||||||
|
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||||
|
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
|
||||||
|
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||||
|
ORDER BY g.last_played ASC
|
||||||
|
LIMIT $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetOldestPlayedGamesRow struct {
|
||||||
|
SoundtrackID int32 `json:"soundtrack_id"`
|
||||||
|
SoundtrackName string `json:"soundtrack_name"`
|
||||||
|
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||||
|
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
||||||
|
Songs []byte `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oldest played soundtracks (least recently played, but has been played at least once)
|
||||||
|
func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getOldestPlayedGames, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetOldestPlayedGamesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetOldestPlayedGamesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.SoundtrackID,
|
||||||
|
&i.SoundtrackName,
|
||||||
|
&i.SoundtrackPlayed,
|
||||||
|
&i.SoundtrackLastPlayed,
|
||||||
|
&i.Songs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END), 0)::bigint as played_soundtracks,
|
||||||
|
COALESCE(SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END), 0)::bigint as never_played_soundtracks,
|
||||||
|
COALESCE(SUM(times_played), 0)::bigint as total_soundtrack_plays,
|
||||||
|
COALESCE(AVG(times_played), 0)::float as avg_soundtrack_plays,
|
||||||
|
COALESCE(MAX(times_played), 0)::bigint as max_soundtrack_plays,
|
||||||
|
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
||||||
|
FROM soundtrack
|
||||||
|
WHERE deleted IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetStatisticsSummaryRow struct {
|
||||||
|
TotalSoundtracks int64 `json:"total_soundtracks"`
|
||||||
|
PlayedSoundtracks int64 `json:"played_soundtracks"`
|
||||||
|
NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"`
|
||||||
|
TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"`
|
||||||
|
AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"`
|
||||||
|
MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"`
|
||||||
|
MinSoundtrackPlays int64 `json:"min_soundtrack_plays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get statistics summary
|
||||||
|
func (q *Queries) GetStatisticsSummary(ctx context.Context) (GetStatisticsSummaryRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getStatisticsSummary)
|
||||||
|
var i GetStatisticsSummaryRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TotalSoundtracks,
|
||||||
|
&i.PlayedSoundtracks,
|
||||||
|
&i.NeverPlayedSoundtracks,
|
||||||
|
&i.TotalSoundtrackPlays,
|
||||||
|
&i.AvgSoundtrackPlays,
|
||||||
|
&i.MaxSoundtrackPlays,
|
||||||
|
&i.MinSoundtrackPlays,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
+42
-10
@@ -1,6 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -16,6 +17,8 @@ var (
|
|||||||
testDBUser string
|
testDBUser string
|
||||||
testDBPassword string
|
testDBPassword string
|
||||||
testDBName string
|
testDBName string
|
||||||
|
// TestDatabase is the database instance for tests
|
||||||
|
TestDatabase *Database
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSetupDB initializes the test database using existing functions
|
// TestSetupDB initializes the test database using existing functions
|
||||||
@@ -44,9 +47,28 @@ func TestSetupDB(t *testing.T) {
|
|||||||
// Create the database first (testuser is a superuser in the container)
|
// Create the database first (testuser is a superuser in the container)
|
||||||
createTestDatabase(host, port, dbname, user, password)
|
createTestDatabase(host, port, dbname, user, password)
|
||||||
|
|
||||||
// Now run migrations using the existing function
|
// Create database instance and run migrations
|
||||||
Migrate_db(host, port, user, password, dbname)
|
var err error
|
||||||
InitDB(host, port, user, password, dbname)
|
TestDatabase, err = NewDatabase(host, port, user, password, dbname)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize test database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any existing schema to ensure clean state
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err = TestDatabase.Pool.Exec(ctx, "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: Could not clean schema: %v", err)
|
||||||
|
// Continue anyway, migrations might still work
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
if err := TestDatabase.RunMigrations(); err != nil {
|
||||||
|
// Clean up on failure to prevent nil pointer issues in other tests
|
||||||
|
TestDatabase.Close()
|
||||||
|
TestDatabase = nil
|
||||||
|
t.Fatalf("Failed to run migrations: %v", err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,33 +108,43 @@ func createTestDatabase(host, port, dbname, user, password string) {
|
|||||||
// "closed pool" errors when tests run sequentially
|
// "closed pool" errors when tests run sequentially
|
||||||
func TestTearDownDB(t *testing.T) {
|
func TestTearDownDB(t *testing.T) {
|
||||||
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
||||||
|
// Note: We also don't nil TestDatabase to allow reuse across tests
|
||||||
|
// if TestDatabase != nil {
|
||||||
|
// TestDatabase.Close()
|
||||||
|
// TestDatabase = nil
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestClearDatabase clears all data from the test database
|
// TestClearDatabase clears all data from the test database
|
||||||
// Useful for running tests with a clean slate
|
// Useful for running tests with a clean slate
|
||||||
func TestClearDatabase(t *testing.T) {
|
func TestClearDatabase(t *testing.T) {
|
||||||
if Dbpool == nil {
|
if TestDatabase == nil || TestDatabase.Pool == nil {
|
||||||
t.Skip("Database not initialized")
|
t.Skip("Database not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all tables in reverse order to respect foreign keys
|
// Clear all tables in reverse order to respect foreign keys
|
||||||
// Note: This assumes the tables exist and have the expected structure
|
// Note: This assumes the tables exist and have the expected structure
|
||||||
|
// After migration 000005, game table was renamed to soundtrack
|
||||||
tables := []string{
|
tables := []string{
|
||||||
"song_list",
|
"song_list",
|
||||||
"song",
|
"song",
|
||||||
"game",
|
"soundtrack",
|
||||||
|
"vgmq",
|
||||||
|
"sessions",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
_, err := Dbpool.Exec(Ctx, "TRUNCATE TABLE "+table+" CASCADE")
|
_, err := TestDatabase.Pool.Exec(ctx, "TRUNCATE TABLE "+table+" CASCADE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Failed to truncate table %s: %v", table, err)
|
t.Logf("Failed to truncate table %s: %v", table, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset sequences
|
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
||||||
_, err := Dbpool.Exec(Ctx, "SELECT setval('game_id_seq', 1, false)")
|
var seqErr error
|
||||||
if err != nil {
|
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
||||||
t.Logf("Failed to reset game_id_seq: %v", err)
|
if seqErr != nil {
|
||||||
|
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HealthHandler struct {
|
type HealthHandler struct {
|
||||||
|
db *db.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthHandler() *HealthHandler {
|
func NewHealthHandler(database *db.Database) *HealthHandler {
|
||||||
return &HealthHandler{}
|
return &HealthHandler{db: database}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheck godoc
|
// HealthCheck godoc
|
||||||
@@ -24,5 +25,5 @@ func NewHealthHandler() *HealthHandler {
|
|||||||
// @Success 200 {string} string "OK"
|
// @Success 200 {string} string "OK"
|
||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
func (h *HealthHandler) HealthCheck(ctx *echo.Context) error {
|
||||||
return ctx.JSON(http.StatusOK, db.Health())
|
return ctx.JSON(http.StatusOK, h.db.Health())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"music-server/internal/db"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHealthCheck verifies the health endpoint returns database status
|
// TestHealthCheck verifies the health endpoint returns database status
|
||||||
func TestHealthCheck(t *testing.T) {
|
func TestHealthCheck(t *testing.T) {
|
||||||
// Setup database
|
|
||||||
db.TestSetupDB(t)
|
|
||||||
defer db.TestTearDownDB(t)
|
|
||||||
|
|
||||||
e := StartTestServer(t)
|
e := StartTestServer(t)
|
||||||
|
// No explicit teardown - handled by StartTestServer's sync.Once
|
||||||
|
|
||||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||||
assert.Equal(t, http.StatusOK, resp.Code)
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeprecationMiddleware adds deprecation warning to responses
|
||||||
|
// for old endpoints that are being phased out in favor of /api/v1/*
|
||||||
|
func DeprecationMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c *echo.Context) error {
|
||||||
|
// Add deprecation warning header
|
||||||
|
c.Response().Header().Add("Warning", `299 - "Deprecated: This endpoint is deprecated. Use /api/v1/ endpoints instead."`)
|
||||||
|
c.Response().Header().Add("Deprecation", "true")
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -229,8 +229,8 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
|||||||
return ctx.Stream(http.StatusOK, "audio/mpeg", file)
|
return ctx.Stream(http.StatusOK, "audio/mpeg", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllGames godoc
|
// GetAllSoundtracks godoc
|
||||||
// @Summary Get all games
|
// @Summary Get all soundtracks
|
||||||
// @Description Returns a list of all games in order
|
// @Description Returns a list of all games in order
|
||||||
// @Tags music
|
// @Tags music
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -238,17 +238,17 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
|||||||
// @Success 200 {array} map[string]interface{}
|
// @Success 200 {array} map[string]interface{}
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/order [get]
|
// @Router /music/all/order [get]
|
||||||
func (m *MusicHandler) GetAllGames(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
if backend.Syncing {
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
logging.GetLogger().Info("Syncing is in progress")
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
}
|
}
|
||||||
gameList := backend.GetAllGames()
|
soundtrackList := backend.GetAllSoundtracks()
|
||||||
return ctx.JSON(http.StatusOK, gameList)
|
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllGamesRandom godoc
|
// GetAllSoundtracksRandom godoc
|
||||||
// @Summary Get all games random
|
// @Summary Get all soundtracks random
|
||||||
// @Description Returns a list of all games in random order
|
// @Description Returns a list of all games in random order
|
||||||
// @Tags music
|
// @Tags music
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -256,13 +256,13 @@ func (m *MusicHandler) GetAllGames(ctx *echo.Context) error {
|
|||||||
// @Success 200 {array} map[string]interface{}
|
// @Success 200 {array} map[string]interface{}
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/random [get]
|
// @Router /music/all/random [get]
|
||||||
func (m *MusicHandler) GetAllGamesRandom(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
if backend.Syncing {
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
logging.GetLogger().Info("Syncing is in progress")
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
}
|
}
|
||||||
gameList := backend.GetAllGamesRandom()
|
soundtrackList := backend.GetAllSoundtracksRandom()
|
||||||
return ctx.JSON(http.StatusOK, gameList)
|
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutPlayed godoc
|
// PutPlayed godoc
|
||||||
|
|||||||
+71
-38
@@ -58,59 +58,64 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// Swagger UI
|
// Swagger UI
|
||||||
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
||||||
|
|
||||||
health := NewHealthHandler()
|
// ============================================
|
||||||
e.GET("/health", health.HealthCheck)
|
// Legacy Endpoints (Deprecated - use /api/v1/ instead)
|
||||||
|
// ============================================
|
||||||
|
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||||
|
|
||||||
|
health := NewHealthHandler(s.db)
|
||||||
|
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
||||||
|
|
||||||
version := NewVersionHandler()
|
version := NewVersionHandler()
|
||||||
e.GET("/version", version.GetLatestVersion)
|
e.GET("/version", deprecatedMiddleware(version.GetLatestVersion))
|
||||||
e.GET("/version/history", version.GetVersionHistory)
|
e.GET("/version/history", deprecatedMiddleware(version.GetVersionHistory))
|
||||||
|
|
||||||
character := NewCharacterHandler()
|
character := NewCharacterHandler()
|
||||||
e.GET("/character", character.GetCharacter)
|
e.GET("/character", deprecatedMiddleware(character.GetCharacter))
|
||||||
e.GET("/characters", character.GetCharacterList)
|
e.GET("/characters", deprecatedMiddleware(character.GetCharacterList))
|
||||||
|
|
||||||
download := NewDownloadHandler()
|
download := NewDownloadHandler()
|
||||||
e.GET("/download", download.checkLatest)
|
e.GET("/download", deprecatedMiddleware(download.checkLatest))
|
||||||
e.GET("/download/list", download.listAssetsOfLatest)
|
e.GET("/download/list", deprecatedMiddleware(download.listAssetsOfLatest))
|
||||||
e.GET("/download/windows", download.downloadLatestWindows)
|
e.GET("/download/windows", deprecatedMiddleware(download.downloadLatestWindows))
|
||||||
e.GET("/download/linux", download.downloadLatestLinux)
|
e.GET("/download/linux", deprecatedMiddleware(download.downloadLatestLinux))
|
||||||
|
|
||||||
sync := NewSyncHandler()
|
sync := NewSyncHandler()
|
||||||
syncGroup := e.Group("/sync")
|
syncGroup := e.Group("/sync")
|
||||||
syncGroup.GET("", sync.SyncGamesNewOnlyChanges)
|
syncGroup.GET("", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||||
syncGroup.GET("/progress", sync.SyncProgress)
|
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
||||||
syncGroup.GET("/new", sync.SyncGamesNewOnlyChanges)
|
syncGroup.GET("/new", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||||
syncGroup.GET("/full", sync.SyncGamesNewFull)
|
syncGroup.GET("/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
||||||
syncGroup.GET("/new/full", sync.SyncGamesNewFull)
|
syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
||||||
syncGroup.GET("/quick", sync.SyncGamesNewOnlyChanges)
|
syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
||||||
syncGroup.GET("/reset", sync.ResetGames)
|
syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetDB))
|
||||||
|
|
||||||
music := NewMusicHandler()
|
music := NewMusicHandler()
|
||||||
musicGroup := e.Group("/music")
|
musicGroup := e.Group("/music")
|
||||||
musicGroup.GET("", music.GetSong)
|
musicGroup.GET("", deprecatedMiddleware(music.GetSong))
|
||||||
musicGroup.GET("/soundTest", music.GetSoundCheckSong)
|
musicGroup.GET("/soundTest", deprecatedMiddleware(music.GetSoundCheckSong))
|
||||||
musicGroup.GET("/reset", music.ResetMusic)
|
musicGroup.GET("/reset", deprecatedMiddleware(music.ResetMusic))
|
||||||
musicGroup.GET("/rand", music.GetRandomSong)
|
musicGroup.GET("/rand", deprecatedMiddleware(music.GetRandomSong))
|
||||||
musicGroup.GET("/rand/low", music.GetRandomSongLowChance)
|
musicGroup.GET("/rand/low", deprecatedMiddleware(music.GetRandomSongLowChance))
|
||||||
musicGroup.GET("/rand/classic", music.GetRandomSongClassic)
|
musicGroup.GET("/rand/classic", deprecatedMiddleware(music.GetRandomSongClassic))
|
||||||
musicGroup.GET("/info", music.GetSongInfo)
|
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
||||||
musicGroup.GET("/list", music.GetPlayedSongs)
|
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
||||||
musicGroup.GET("/next", music.GetNextSong)
|
musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong))
|
||||||
musicGroup.GET("/previous", music.GetPreviousSong)
|
musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong))
|
||||||
musicGroup.GET("/all", music.GetAllGamesRandom)
|
musicGroup.GET("/all", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
||||||
musicGroup.GET("/all/order", music.GetAllGames)
|
musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllSoundtracks))
|
||||||
musicGroup.GET("/all/random", music.GetAllGamesRandom)
|
musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
||||||
musicGroup.PUT("/played", music.PutPlayed)
|
musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed))
|
||||||
musicGroup.GET("/addQue", music.AddLatestToQue)
|
musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue))
|
||||||
musicGroup.GET("/addPlayed", music.AddLatestPlayed)
|
musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// API v1 Routes with Token Authentication
|
// API v1 Routes with Token Authentication
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Create /api/v1 group
|
// Create /api/v1 group
|
||||||
apiV1 := e.Group("/api/v1")
|
apiV1 := e.Group("/api/v1")
|
||||||
|
|
||||||
// Public endpoints - no token required
|
// Public endpoints - no token required
|
||||||
apiV1.POST("/token", func(c *echo.Context) error {
|
apiV1.POST("/token", func(c *echo.Context) error {
|
||||||
return s.tokenHandler.CreateTokenHandler(c)
|
return s.tokenHandler.CreateTokenHandler(c)
|
||||||
@@ -126,10 +131,38 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// Create token auth middleware with pool access
|
// Create token auth middleware with pool access
|
||||||
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
||||||
|
|
||||||
// Protected group with token authentication - will be used by VGMQ and Statistics API
|
// Protected group with token authentication
|
||||||
_ = apiV1.Group("", tokenAuthMiddleware)
|
protectedV1 := apiV1.Group("", tokenAuthMiddleware)
|
||||||
|
|
||||||
// Note: Future protected endpoints (VGMQ, Statistics) will be added here
|
// Statistics API endpoints (protected by token auth)
|
||||||
|
statistics := s.statisticsHandler
|
||||||
|
protectedV1.GET("/statistics/games/most-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetMostPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/least-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLeastPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/never-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetNeverPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/last-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLastPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/oldest-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetOldestPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/songs/most-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetMostPlayedSongs(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/songs/least-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLeastPlayedSongs(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/summary", func(c *echo.Context) error {
|
||||||
|
return statistics.GetStatisticsSummary(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Future: VGMQ endpoints will be added to protectedV1 group
|
||||||
|
_ = protectedV1 // Use the variable to avoid unused variable error
|
||||||
|
|
||||||
routes := e.Router().Routes()
|
routes := e.Router().Routes()
|
||||||
sort.Slice(routes, func(i, j int) bool {
|
sort.Slice(routes, func(i, j int) bool {
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
port int
|
port int
|
||||||
db *db.Database
|
db *db.Database
|
||||||
tokenHandler *TokenHandler
|
tokenHandler *TokenHandler
|
||||||
httpServer *http.Server
|
statisticsHandler *StatisticsHandler
|
||||||
|
httpServer *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -67,12 +68,16 @@ func NewServerInstance() *Server {
|
|||||||
|
|
||||||
// Initialize token handler with database pool
|
// Initialize token handler with database pool
|
||||||
tokenHandler := NewTokenHandler(database.Pool)
|
tokenHandler := NewTokenHandler(database.Pool)
|
||||||
|
|
||||||
|
// Initialize statistics handler
|
||||||
|
statisticsHandler := NewStatisticsHandler()
|
||||||
|
|
||||||
// Create the server instance
|
// Create the server instance
|
||||||
appServer := &Server{
|
appServer := &Server{
|
||||||
port: port,
|
port: port,
|
||||||
db: database,
|
db: database,
|
||||||
tokenHandler: tokenHandler,
|
tokenHandler: tokenHandler,
|
||||||
|
statisticsHandler: statisticsHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the HTTP server
|
// Create the HTTP server
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatisticsHandler handles statistics-related HTTP requests
|
||||||
|
type StatisticsHandler struct {
|
||||||
|
statsBackend *backend.StatisticsHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatisticsHandler creates a new StatisticsHandler
|
||||||
|
func NewStatisticsHandler() *StatisticsHandler {
|
||||||
|
return &StatisticsHandler{
|
||||||
|
statsBackend: backend.NewStatisticsHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedGames returns top N most played games with songs
|
||||||
|
// GET /api/v1/statistics/games/most-played
|
||||||
|
//
|
||||||
|
// @Summary Get most played games
|
||||||
|
// @Description Returns the top N most played games with their songs
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/most-played [get]
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10 // default
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
// Cap at 100 for performance
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetMostPlayedGamesWithSongs(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get most played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedGames returns top N least played games with songs
|
||||||
|
// GET /api/v1/statistics/games/least-played
|
||||||
|
//
|
||||||
|
// @Summary Get least played games
|
||||||
|
// @Description Returns the top N least played games with their songs
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/least-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetLeastPlayedGamesWithSongs(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get least played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedSongs returns top N most played songs with game info
|
||||||
|
// GET /api/v1/statistics/songs/most-played
|
||||||
|
//
|
||||||
|
// @Summary Get most played songs
|
||||||
|
// @Description Returns the top N most played songs with their game info
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.SongInfoForStats
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/songs/most-played [get]
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedSongs(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs, err := h.statsBackend.GetMostPlayedSongsWithGame(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get most played songs", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedSongs returns top N least played songs with game info
|
||||||
|
// GET /api/v1/statistics/songs/least-played
|
||||||
|
//
|
||||||
|
// @Summary Get least played songs
|
||||||
|
// @Description Returns the top N least played songs with their game info
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.SongInfoForStats
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/songs/least-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedSongs(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs, err := h.statsBackend.GetLeastPlayedSongsWithGame(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get least played songs", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
|
// GET /api/v1/statistics/games/never-played
|
||||||
|
//
|
||||||
|
// @Summary Get never played games
|
||||||
|
// @Description Returns all games that have never been played (times_played = 0)
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/never-played [get]
|
||||||
|
func (h *StatisticsHandler) GetNeverPlayedGames(ctx *echo.Context) error {
|
||||||
|
games, err := h.statsBackend.GetNeverPlayedGames()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get never played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastPlayedGames returns most recently played games
|
||||||
|
// GET /api/v1/statistics/games/last-played
|
||||||
|
//
|
||||||
|
// @Summary Get last played games
|
||||||
|
// @Description Returns the most recently played games
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/last-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLastPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetLastPlayedGames(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get last played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldestPlayedGames returns least recently played games
|
||||||
|
// GET /api/v1/statistics/games/oldest-played
|
||||||
|
//
|
||||||
|
// @Summary Get oldest played games
|
||||||
|
// @Description Returns the least recently played games (that have been played at least once)
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/oldest-played [get]
|
||||||
|
func (h *StatisticsHandler) GetOldestPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetOldestPlayedGames(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get oldest played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
// GET /api/v1/statistics/summary
|
||||||
|
//
|
||||||
|
// @Summary Get statistics summary
|
||||||
|
// @Description Returns overall statistics about the music library
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} backend.StatisticsSummary
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/summary [get]
|
||||||
|
func (h *StatisticsHandler) GetStatisticsSummary(ctx *echo.Context) error {
|
||||||
|
summary, err := h.statsBackend.GetStatisticsSummary()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get statistics summary", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, summary)
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/db"
|
||||||
|
"music-server/internal/db/repository"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestStatisticsEndpoints tests the statistics API endpoints
|
||||||
|
func TestStatisticsEndpoints(t *testing.T) {
|
||||||
|
// Skip if test database not configured
|
||||||
|
e := StartTestServer(t)
|
||||||
|
if e == nil {
|
||||||
|
t.Skip("Test database not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token first
|
||||||
|
token := getTestToken(t, e)
|
||||||
|
if token == "" {
|
||||||
|
t.Skip("Could not get test token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test /api/v1/statistics/summary
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
var summary backend.StatisticsSummary
|
||||||
|
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPartialMigrationThenSyncThenComplete tests migration workflow
|
||||||
|
// Note: This test requires the database to be in a specific state
|
||||||
|
// It tests: partial migration → data insert → sync → complete migration
|
||||||
|
func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||||
|
// This test is complex and requires careful setup
|
||||||
|
// For now, we test the final state: all migrations + sync
|
||||||
|
|
||||||
|
e := StartTestServer(t)
|
||||||
|
if e == nil {
|
||||||
|
t.Skip("Test database not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token
|
||||||
|
token := getTestToken(t, e)
|
||||||
|
if token == "" {
|
||||||
|
t.Skip("Could not get test token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert test data manually (5 soundtracks with songs)
|
||||||
|
insertTestData(t)
|
||||||
|
|
||||||
|
// Run sync to ensure data is properly loaded
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/sync/new", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
// Wait for sync to complete
|
||||||
|
if !waitForSyncCompletion(t, e, 60) {
|
||||||
|
t.Error("Sync did not complete within timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data via statistics endpoint
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/statistics/summary", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
var summary backend.StatisticsSummary
|
||||||
|
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// After sync with /sync/new, only soundtracks matching filesystem remain
|
||||||
|
// testMusic has 3 games
|
||||||
|
require.Equal(t, int64(3), summary.TotalGames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertTestData inserts 5 test soundtracks with songs into the database
|
||||||
|
func insertTestData(t *testing.T) {
|
||||||
|
if db.TestDatabase == nil || db.TestDatabase.Pool == nil {
|
||||||
|
t.Skip("Test database not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
queries := repository.New(db.TestDatabase.Pool)
|
||||||
|
|
||||||
|
// Insert 5 soundtracks
|
||||||
|
soundtracks := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{"Test Soundtrack 1", "/path/to/soundtrack1"},
|
||||||
|
{"Test Soundtrack 2", "/path/to/soundtrack2"},
|
||||||
|
{"Test Soundtrack 3", "/path/to/soundtrack3"},
|
||||||
|
{"Test Soundtrack 4", "/path/to/soundtrack4"},
|
||||||
|
{"Test Soundtrack 5", "/path/to/soundtrack5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range soundtracks {
|
||||||
|
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
||||||
|
SoundtrackName: st.name,
|
||||||
|
Path: st.path,
|
||||||
|
Hash: "test-hash-" + st.name,
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Failed to insert soundtrack: %s", st.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get soundtrack IDs
|
||||||
|
soundtrackIDs, err := queries.FindAllSoundtracks(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.GreaterOrEqual(t, len(soundtrackIDs), 5)
|
||||||
|
|
||||||
|
// Insert songs for each soundtrack
|
||||||
|
songData := []struct {
|
||||||
|
soundtrackID int32
|
||||||
|
songs []string
|
||||||
|
}{
|
||||||
|
{soundtrackIDs[0].ID, []string{"Song A", "Song B"}},
|
||||||
|
{soundtrackIDs[1].ID, []string{"Song C", "Song D"}},
|
||||||
|
{soundtrackIDs[2].ID, []string{"Song E"}},
|
||||||
|
{soundtrackIDs[3].ID, []string{"Song F", "Song G", "Song H"}},
|
||||||
|
{soundtrackIDs[4].ID, []string{"Song I"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sd := range songData {
|
||||||
|
for _, songName := range sd.songs {
|
||||||
|
err := queries.AddSong(ctx, repository.AddSongParams{
|
||||||
|
SoundtrackID: sd.soundtrackID,
|
||||||
|
SongName: songName,
|
||||||
|
Path: "/path/to/" + songName + ".mp3",
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Failed to insert song: %s", songName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Inserted %d soundtracks with %d total songs", len(soundtracks), 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTestToken gets a valid token for testing
|
||||||
|
func getTestToken(t *testing.T, e *echo.Echo) string {
|
||||||
|
reqBody := `{"client_type": "test"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/token", strings.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Logf("Failed to get token: %s", rec.Body.String())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return resp.Token
|
||||||
|
}
|
||||||
@@ -34,61 +34,61 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
|
|||||||
return ctx.JSON(http.StatusOK, response)
|
return ctx.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncGamesNewOnlyChanges godoc
|
// SyncSoundtracksNewOnlyChanges godoc
|
||||||
// @Summary Sync games with only changes
|
// @Summary Sync soundtracks with only changes
|
||||||
// @Description Starts syncing games with only new changes
|
// @Description Starts syncing games with only new changes
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Start syncing games"
|
// @Success 200 {string} string "Start syncing soundtracks"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync [get]
|
// @Router /sync [get]
|
||||||
func (s *SyncHandler) SyncGamesNewOnlyChanges(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
if backend.Syncing {
|
||||||
logging.GetLogger().Warn("Syncing is already in progress")
|
logging.GetLogger().Warn("Syncing is already in progress")
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
}
|
}
|
||||||
logging.GetLogger().Info("Starting sync with only changes")
|
logging.GetLogger().Info("Starting sync with only changes")
|
||||||
backend.Syncing = true
|
backend.Syncing = true
|
||||||
go backend.SyncGamesNewOnlyChanges()
|
go backend.SyncSoundtracksNewOnlyChanges()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing games")
|
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncGamesNewFull godoc
|
// SyncSoundtracksNewFull godoc
|
||||||
// @Summary Sync all games fully
|
// @Summary Sync all games fully
|
||||||
// @Description Starts a full sync of all games
|
// @Description Starts a full sync of all games
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Start syncing games full"
|
// @Success 200 {string} string "Start syncing soundtracks full"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/full [get]
|
// @Router /sync/full [get]
|
||||||
func (s *SyncHandler) SyncGamesNewFull(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
if backend.Syncing {
|
||||||
logging.GetLogger().Warn("Syncing is already in progress")
|
logging.GetLogger().Warn("Syncing is already in progress")
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
}
|
}
|
||||||
logging.GetLogger().Info("Starting full sync")
|
logging.GetLogger().Info("Starting full sync")
|
||||||
backend.Syncing = true
|
backend.Syncing = true
|
||||||
go backend.SyncGamesNewFull()
|
go backend.SyncSoundtracksNewFull()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing games full")
|
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetGames godoc
|
// ResetDB godoc
|
||||||
// @Summary Reset games database
|
// @Summary Reset soundtracks database
|
||||||
// @Description Resets the games database by deleting all games and songs
|
// @Description Resets the games database by deleting all games and songs
|
||||||
// @Tags sync
|
// @Tags sync
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "Games and songs are deleted from the database"
|
// @Success 200 {string} string "Soundtracks and songs are deleted from the database"
|
||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/reset [get]
|
// @Router /sync/reset [get]
|
||||||
func (s *SyncHandler) ResetGames(ctx *echo.Context) error {
|
func (s *SyncHandler) ResetDB(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
if backend.Syncing {
|
||||||
logging.GetLogger().Warn("Cannot reset - syncing is in progress")
|
logging.GetLogger().Warn("Cannot reset - syncing is in progress")
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
}
|
}
|
||||||
logging.GetLogger().Info("Resetting games database")
|
logging.GetLogger().Info("Resetting soundtracks database")
|
||||||
backend.ResetDB()
|
backend.ResetDB()
|
||||||
return ctx.JSON(http.StatusOK, "Games and songs are deleted from the database")
|
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
|||||||
|
|
||||||
// Before sync - should have no games
|
// Before sync - should have no games
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
|
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
beforeCount := len(gamesBefore)
|
beforeCount := len(gamesBefore)
|
||||||
t.Logf("Games before sync: %d", beforeCount)
|
t.Logf("Games before sync: %d", beforeCount)
|
||||||
@@ -92,7 +92,7 @@ func TestSyncPopulatesDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// After sync - should have games
|
// After sync - should have games
|
||||||
gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
|
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
afterCount := len(gamesAfter)
|
afterCount := len(gamesAfter)
|
||||||
t.Logf("Games after sync: %d", afterCount)
|
t.Logf("Games after sync: %d", afterCount)
|
||||||
@@ -113,7 +113,7 @@ func TestSyncMakesDifference(t *testing.T) {
|
|||||||
|
|
||||||
// Before sync - should have no games
|
// Before sync - should have no games
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
gamesBefore, err := repo.FindAllGames(backend.BackendCtx())
|
gamesBefore, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync")
|
assert.Equal(t, 0, len(gamesBefore), "Should have no games before sync")
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ func TestSyncMakesDifference(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// After sync - should have games
|
// After sync - should have games
|
||||||
gamesAfter, err := repo.FindAllGames(backend.BackendCtx())
|
gamesAfter, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, len(gamesAfter) > 0, "Should have games after sync")
|
assert.True(t, len(gamesAfter) > 0, "Should have games after sync")
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
|||||||
|
|
||||||
// Get initial count
|
// Get initial count
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
|
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
beforeCount := len(gamesBefore)
|
beforeCount := len(gamesBefore)
|
||||||
|
|
||||||
// Run incremental sync (should not change count if nothing changed)
|
// Run incremental sync (should not change count if nothing changed)
|
||||||
@@ -211,7 +211,7 @@ func TestSyncGamesNewOnlyChanges(t *testing.T) {
|
|||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Count should be the same
|
// Count should be the same
|
||||||
gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
|
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
afterCount := len(gamesAfter)
|
afterCount := len(gamesAfter)
|
||||||
|
|
||||||
// Note: This might not be exactly equal due to timing, but should be close
|
// Note: This might not be exactly equal due to timing, but should be close
|
||||||
@@ -228,7 +228,7 @@ func TestResetGames(t *testing.T) {
|
|||||||
|
|
||||||
// First ensure we have data
|
// First ensure we have data
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
gamesBefore, _ := repo.FindAllGames(backend.BackendCtx())
|
gamesBefore, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
beforeCount := len(gamesBefore)
|
beforeCount := len(gamesBefore)
|
||||||
|
|
||||||
if beforeCount == 0 {
|
if beforeCount == 0 {
|
||||||
@@ -238,7 +238,7 @@ func TestResetGames(t *testing.T) {
|
|||||||
t.Error("Sync did not complete within timeout")
|
t.Error("Sync did not complete within timeout")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gamesBefore, _ = repo.FindAllGames(backend.BackendCtx())
|
gamesBefore, _ = repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
beforeCount = len(gamesBefore)
|
beforeCount = len(gamesBefore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ func TestResetGames(t *testing.T) {
|
|||||||
// Note: reset might take a moment to propagate
|
// Note: reset might take a moment to propagate
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
gamesAfter, _ := repo.FindAllGames(backend.BackendCtx())
|
gamesAfter, _ := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
afterCount := len(gamesAfter)
|
afterCount := len(gamesAfter)
|
||||||
|
|
||||||
t.Logf("Games after reset: %d", afterCount)
|
t.Logf("Games after reset: %d", afterCount)
|
||||||
@@ -282,7 +282,7 @@ func TestSyncGamesNewFull(t *testing.T) {
|
|||||||
|
|
||||||
// Verify database is populated
|
// Verify database is populated
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
games, err := repo.FindAllGames(backend.BackendCtx())
|
games, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, len(games) > 0, "Database should be populated after full sync")
|
assert.True(t, len(games) > 0, "Database should be populated after full sync")
|
||||||
t.Logf("Full sync populated %d games", len(games))
|
t.Logf("Full sync populated %d games", len(games))
|
||||||
|
|||||||
@@ -50,20 +50,18 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
|||||||
|
|
||||||
// Initialize database for tests
|
// Initialize database for tests
|
||||||
db.TestSetupDB(t)
|
db.TestSetupDB(t)
|
||||||
|
|
||||||
// Initialize backend with the global Dbpool
|
// Initialize backend with test database pool
|
||||||
// This ensures BackendRepo() and BackendCtx() are available
|
// This ensures BackendRepo() and BackendCtx() are available
|
||||||
if db.Dbpool != nil {
|
if db.TestDatabase != nil && db.TestDatabase.Pool != nil {
|
||||||
backend.InitBackend(db.Dbpool)
|
backend.InitBackend(db.TestDatabase.Pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Server instance and get its routes
|
// Create a Server instance and get its routes
|
||||||
s := &Server{
|
s := &Server{
|
||||||
db: &db.Database{
|
db: db.TestDatabase,
|
||||||
Pool: db.Dbpool,
|
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||||
Ctx: db.Ctx,
|
statisticsHandler: NewStatisticsHandler(),
|
||||||
},
|
|
||||||
tokenHandler: NewTokenHandler(db.Dbpool),
|
|
||||||
}
|
}
|
||||||
handler := s.RegisterRoutes()
|
handler := s.RegisterRoutes()
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
// ensureSyncRan ensures that sync has been run before testing music endpoints
|
// ensureSyncRan ensures that sync has been run before testing music endpoints
|
||||||
func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
func ensureSyncRan(t *testing.T, e *echo.Echo) {
|
||||||
repo := repository.New(backend.BackendPool())
|
repo := repository.New(backend.BackendPool())
|
||||||
games, err := repo.FindAllGames(backend.BackendCtx())
|
games, err := repo.FindAllSoundtracks(backend.BackendCtx())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
if len(games) == 0 {
|
if len(games) == 0 {
|
||||||
|
|||||||
@@ -84,8 +84,13 @@ build-run: build
|
|||||||
@go run cmd/main.go
|
@go run cmd/main.go
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
@echo "Testing..."
|
@echo "Starting test database container..."
|
||||||
@go test ./... -v
|
@podman-compose -f compose.test.yaml up -d
|
||||||
|
@sleep 10
|
||||||
|
@echo "Running integration tests..."
|
||||||
|
@just test-integration
|
||||||
|
@echo "Stopping test database container..."
|
||||||
|
@just test-integration-down
|
||||||
|
|
||||||
# Clean the binary
|
# Clean the binary
|
||||||
clean:
|
clean:
|
||||||
@@ -105,7 +110,9 @@ podman-down:
|
|||||||
# Run integration tests with podman
|
# Run integration tests with podman
|
||||||
# Starts a test PostgreSQL container, runs tests, then cleans up
|
# Starts a test PostgreSQL container, runs tests, then cleans up
|
||||||
test-integration:
|
test-integration:
|
||||||
@echo "Starting test database container..."
|
@echo "Cleaning old test database..."
|
||||||
|
@podman-compose -f compose.test.yaml down -v
|
||||||
|
@echo "Starting fresh test database container..."
|
||||||
@podman-compose -f compose.test.yaml up -d
|
@podman-compose -f compose.test.yaml up -d
|
||||||
@sleep 10
|
@sleep 10
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
|
|||||||
Reference in New Issue
Block a user