Feature/statistics api #26
+458
-7
@@ -23,6 +23,385 @@ var doc = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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": {
|
||||
"post": {
|
||||
"description": "Returns a new session token for API access",
|
||||
@@ -455,7 +834,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -488,7 +867,7 @@ var doc = `{
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -828,10 +1207,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -860,7 +1239,7 @@ var doc = `{
|
||||
"summary": "Sync all games fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -910,10 +1289,10 @@ var doc = `{
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -990,6 +1369,78 @@ var doc = `{
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+458
-7
@@ -4,6 +4,385 @@
|
||||
"contact": {}
|
||||
},
|
||||
"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": {
|
||||
"post": {
|
||||
"description": "Returns a new session token for API access",
|
||||
@@ -436,7 +815,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games",
|
||||
"summary": "Get all soundtracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -469,7 +848,7 @@
|
||||
"tags": [
|
||||
"music"
|
||||
],
|
||||
"summary": "Get all games random",
|
||||
"summary": "Get all soundtracks random",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -809,10 +1188,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Sync games with only changes",
|
||||
"summary": "Sync soundtracks with only changes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games",
|
||||
"description": "Start syncing soundtracks",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -841,7 +1220,7 @@
|
||||
"summary": "Sync all games fully",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Start syncing games full",
|
||||
"description": "Start syncing soundtracks full",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -891,10 +1270,10 @@
|
||||
"tags": [
|
||||
"sync"
|
||||
],
|
||||
"summary": "Reset games database",
|
||||
"summary": "Reset soundtracks database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Games and songs are deleted from the database",
|
||||
"description": "Soundtracks and songs are deleted from the database",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -971,6 +1350,78 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+303
-7
@@ -1,4 +1,51 @@
|
||||
definitions:
|
||||
backend.GameWithSongs:
|
||||
properties:
|
||||
game_id:
|
||||
type: integer
|
||||
game_last_played:
|
||||
type: string
|
||||
game_name:
|
||||
type: string
|
||||
game_played:
|
||||
type: integer
|
||||
songs:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
type: object
|
||||
backend.SongInfoForStats:
|
||||
properties:
|
||||
file_name:
|
||||
type: string
|
||||
game_id:
|
||||
type: integer
|
||||
game_name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
song_name:
|
||||
type: string
|
||||
times_played:
|
||||
type: integer
|
||||
type: object
|
||||
backend.StatisticsSummary:
|
||||
properties:
|
||||
avg_game_plays:
|
||||
type: number
|
||||
max_game_plays:
|
||||
type: integer
|
||||
min_game_plays:
|
||||
type: integer
|
||||
never_played_games:
|
||||
type: integer
|
||||
played_games:
|
||||
type: integer
|
||||
total_game_plays:
|
||||
type: integer
|
||||
total_games:
|
||||
type: integer
|
||||
type: object
|
||||
backend.VersionData:
|
||||
properties:
|
||||
changelog:
|
||||
@@ -30,6 +77,255 @@ definitions:
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
/api/v1/statistics/games/last-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the most recently played games
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get last played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/least-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N least played games with their songs
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get least played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/most-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N most played games with their songs
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get most played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/never-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns all games that have never been played (times_played = 0)
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get never played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/games/oldest-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the least recently played games (that have been played
|
||||
at least once)
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.GameWithSongs'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get oldest played games
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/songs/least-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N least played songs with their game info
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get least played songs
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/songs/most-played:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the top N most played songs with their game info
|
||||
parameters:
|
||||
- description: 'Number of results (default: 10)'
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/backend.SongInfoForStats'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get most played songs
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/statistics/summary:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns overall statistics about the music library
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/backend.StatisticsSummary'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get statistics summary
|
||||
tags:
|
||||
- statistics
|
||||
/api/v1/token:
|
||||
delete:
|
||||
consumes:
|
||||
@@ -325,7 +621,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games
|
||||
summary: Get all soundtracks
|
||||
tags:
|
||||
- music
|
||||
/music/all/random:
|
||||
@@ -347,7 +643,7 @@ paths:
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Get all games random
|
||||
summary: Get all soundtracks random
|
||||
tags:
|
||||
- music
|
||||
/music/info:
|
||||
@@ -561,14 +857,14 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games
|
||||
description: Start syncing soundtracks
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Sync games with only changes
|
||||
summary: Sync soundtracks with only changes
|
||||
tags:
|
||||
- sync
|
||||
/sync/full:
|
||||
@@ -580,7 +876,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Start syncing games full
|
||||
description: Start syncing soundtracks full
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
@@ -615,14 +911,14 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Games and songs are deleted from the database
|
||||
description: Soundtracks and songs are deleted from the database
|
||||
schema:
|
||||
type: string
|
||||
"423":
|
||||
description: Syncing is in progress
|
||||
schema:
|
||||
type: string
|
||||
summary: Reset games database
|
||||
summary: Reset soundtracks database
|
||||
tags:
|
||||
- sync
|
||||
/version:
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
/* Pure CSS styles for Music Search */
|
||||
|
||||
:root {
|
||||
/* Light mode colors (default) */
|
||||
--bg-primary: #f3f4f6;
|
||||
--bg-secondary: #e5e7eb;
|
||||
--bg-tertiary: #dcfce7;
|
||||
--text-primary: #000;
|
||||
--text-secondary: #374151;
|
||||
--border-primary: #9ca3af;
|
||||
--border-focus: #6b7280;
|
||||
--accent-primary: #f97316;
|
||||
--accent-hover: #ea580c;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark mode colors matching frontend */
|
||||
--bg-primary: #555;
|
||||
--bg-secondary: #333;
|
||||
--bg-tertiary: #2a2a2a;
|
||||
--text-primary: #fff;
|
||||
--text-secondary: #ff9c00;
|
||||
--border-primary: #666;
|
||||
--border-focus: #ff9c00;
|
||||
--accent-primary: #ff9c00;
|
||||
--accent-hover: #e68a00;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -10,7 +38,9 @@ html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
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 {
|
||||
@@ -29,15 +59,15 @@ main {
|
||||
max-width: 600px;
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #9ca3af;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 0.5rem;
|
||||
background-color: #e5e7eb;
|
||||
color: #000;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#search_term:focus {
|
||||
outline: none;
|
||||
border-color: #6b7280;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
#clear {
|
||||
@@ -45,23 +75,48 @@ main {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f97316;
|
||||
color: #fff;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#clear:hover {
|
||||
background-color: #ea580c;
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
#games-container {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.game-text {
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Dark mode toggle */
|
||||
#dark-mode-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#dark-mode-toggle:hover {
|
||||
background-color: var(--border-primary);
|
||||
}
|
||||
|
||||
/* Game result cards */
|
||||
.bg-green-100 {
|
||||
background-color: #dcfce7;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
@@ -69,7 +124,7 @@ main {
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
+23
-1
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
templ HelloForm() {
|
||||
@Base() {
|
||||
<button id="dark-mode-toggle">🌙</button>
|
||||
<div id="search-container">
|
||||
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#games-container"/>
|
||||
<button type="button" id="clear" name="clear">Clear</button>
|
||||
@@ -12,8 +13,29 @@ templ HelloForm() {
|
||||
if (document.readyState == 'complete') {
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
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("search_term").value = "";
|
||||
htmx.ajax('POST', '/find', '#games-container');
|
||||
@@ -26,7 +48,7 @@ templ HelloForm() {
|
||||
templ FoundGames(games []string) {
|
||||
for _, game := range games {
|
||||
<div class="bg-green-100 p-4 shadow-md rounded-lg mt-6">
|
||||
<p>{ game }</p>
|
||||
<p class="game-text">{ game }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
|
||||
// Test the average calculation logic directly without database access
|
||||
func TestCalculateAverage(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
@@ -28,7 +28,7 @@ func TestCalculateAverage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageEmpty(t *testing.T) {
|
||||
games := []repository.Game{}
|
||||
games := []repository.Soundtrack{}
|
||||
|
||||
if len(games) == 0 {
|
||||
result := int32(0)
|
||||
@@ -52,8 +52,8 @@ func TestCalculateAverageEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateAverageSingle(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 42},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 42},
|
||||
}
|
||||
|
||||
var sum int32
|
||||
@@ -69,10 +69,10 @@ func TestCalculateAverageSingle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRandomGame(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
// Set seed for reproducible tests
|
||||
@@ -80,93 +80,93 @@ func TestGetRandomGame(t *testing.T) {
|
||||
|
||||
result := games[rand.Intn(len(games))]
|
||||
|
||||
if result.GameName == "" {
|
||||
if result.SoundtrackName == "" {
|
||||
t.Error("random game selection returned empty game")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, g := range games {
|
||||
if g.GameName == result.GameName {
|
||||
if g.SoundtrackName == result.SoundtrackName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
games := []repository.Game{
|
||||
{ID: 1, GameName: "Game1", TimesPlayed: 10},
|
||||
{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
{ID: 3, GameName: "Game3", TimesPlayed: 30},
|
||||
games := []repository.Soundtrack{
|
||||
{ID: 1, SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{ID: 3, SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
games []repository.Game
|
||||
games []repository.Soundtrack
|
||||
gameID int32
|
||||
expected repository.Game
|
||||
expected repository.Soundtrack
|
||||
}{
|
||||
{
|
||||
name: "existing game",
|
||||
games: games,
|
||||
gameID: 2,
|
||||
expected: repository.Game{ID: 2, GameName: "Game2", TimesPlayed: 20},
|
||||
expected: repository.Soundtrack{ID: 2, SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
},
|
||||
{
|
||||
name: "non-existing game",
|
||||
games: games,
|
||||
gameID: 99,
|
||||
expected: repository.Game{},
|
||||
expected: repository.Soundtrack{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result repository.Game
|
||||
var result repository.Soundtrack
|
||||
for _, game := range tt.games {
|
||||
if game.ID == tt.gameID {
|
||||
result = game
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGameNames(t *testing.T) {
|
||||
games := []repository.Game{
|
||||
{GameName: "Game1", TimesPlayed: 10},
|
||||
{GameName: "Game2", TimesPlayed: 20},
|
||||
{GameName: "Game3", TimesPlayed: 30},
|
||||
func TestExtractSoundtrackNames(t *testing.T) {
|
||||
games := []repository.Soundtrack{
|
||||
{SoundtrackName: "Game1", TimesPlayed: 10},
|
||||
{SoundtrackName: "Game2", TimesPlayed: 20},
|
||||
{SoundtrackName: "Game3", TimesPlayed: 30},
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, game := range games {
|
||||
result = append(result, game.GameName)
|
||||
result = append(result, game.SoundtrackName)
|
||||
}
|
||||
|
||||
expected := []string{"Game1", "Game2", "Game3"}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
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"}
|
||||
|
||||
// Test that shuffle doesn't lose any elements
|
||||
@@ -181,7 +181,7 @@ func TestShuffleGameNames(t *testing.T) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -195,9 +195,7 @@ func TestShuffleGameNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("shuffleGameNames() lost element: %v", orig)
|
||||
t.Errorf("shuffleSoundtrackNames() lost element: %v", orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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.
|
||||
// Uses the existing pool to extract connection details.
|
||||
func (db *Database) RunMigrations() error {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -80,9 +79,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range songs {
|
||||
_, err := db.Exec(`INSERT INTO song (game_id, song_name, path)
|
||||
VALUES ($1, $2, $3)`,
|
||||
s.gameID, s.name, s.path)
|
||||
_, 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)
|
||||
}
|
||||
|
||||
@@ -95,9 +94,9 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs")
|
||||
require.Equal(t, 9, songCount, "Expected 9 songs")
|
||||
|
||||
t.Log("✓ Manually inserted 5 games with 8 songs")
|
||||
t.Log("✓ Manually inserted 5 games with 9 songs")
|
||||
})
|
||||
|
||||
// Step 3: Apply migration 5 (rename game→soundtrack)
|
||||
@@ -126,7 +125,7 @@ func TestMigrationsStepByStep(t *testing.T) {
|
||||
var songCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, songCount, "Expected 8 songs after migration")
|
||||
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")
|
||||
@@ -215,13 +214,18 @@ func applyMigrations(t *testing.T, host, port, user, password, dbname string, st
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://internal/db/migrations",
|
||||
"file://migrations",
|
||||
"postgres", driver)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get current version
|
||||
version, _, err := m.Version()
|
||||
require.NoError(t, err)
|
||||
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
|
||||
@@ -237,6 +241,11 @@ func applyMigrations(t *testing.T, host, port, user, password, dbname string, st
|
||||
|
||||
// Get new version
|
||||
newVersion, _, err := m.Version()
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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);
|
||||
ALTER TABLE song RENAME CONSTRAINT song_pkey TO song_pkey_soundtrack;
|
||||
|
||||
-- Update song_list table references
|
||||
ALTER TABLE song_list RENAME COLUMN game_name TO soundtrack_name;
|
||||
|
||||
@@ -138,8 +138,8 @@ LIMIT $1;
|
||||
-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_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,
|
||||
|
||||
@@ -10,23 +10,6 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type IDMigrationStatus struct {
|
||||
TableName string `json:"table_name"`
|
||||
TotalRows int32 `json:"total_rows"`
|
||||
MigratedRows int32 `json:"migrated_rows"`
|
||||
Completed bool `json:"completed"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string `json:"token"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
ClientType *string `json:"client_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string `json:"token"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
@@ -44,7 +27,6 @@ type Song struct {
|
||||
Hash string `json:"hash"`
|
||||
FileName *string `json:"file_name"`
|
||||
ID pgtype.Int4 `json:"id"`
|
||||
Uuid pgtype.UUID `json:"uuid"`
|
||||
}
|
||||
|
||||
type SongList struct {
|
||||
@@ -56,17 +38,16 @@ type SongList struct {
|
||||
}
|
||||
|
||||
type Soundtrack struct {
|
||||
ID int32 `json:"id"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
Added time.Time `json:"added"`
|
||||
Deleted *time.Time `json:"deleted"`
|
||||
LastChanged *time.Time `json:"last_changed"`
|
||||
Path string `json:"path"`
|
||||
TimesPlayed int32 `json:"times_played"`
|
||||
LastPlayed *time.Time `json:"last_played"`
|
||||
NumberOfSongs int32 `json:"number_of_songs"`
|
||||
Hash string `json:"hash"`
|
||||
Uuid pgtype.UUID `json:"uuid"`
|
||||
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 {
|
||||
|
||||
@@ -110,7 +110,7 @@ func (q *Queries) ClearSongsBySoundtrackId(ctx context.Context, soundtrackID int
|
||||
}
|
||||
|
||||
const fetchAllSongs = `-- name: FetchAllSongs :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid 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) {
|
||||
@@ -130,7 +130,6 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -143,7 +142,7 @@ func (q *Queries) FetchAllSongs(ctx context.Context) ([]Song, error) {
|
||||
}
|
||||
|
||||
const findSongsFromSoundtrack = `-- name: FindSongsFromSoundtrack :many
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id
|
||||
FROM song
|
||||
WHERE soundtrack_id = $1
|
||||
`
|
||||
@@ -165,7 +164,6 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -178,7 +176,7 @@ func (q *Queries) FindSongsFromSoundtrack(ctx context.Context, soundtrackID int3
|
||||
}
|
||||
|
||||
const getSongById = `-- name: GetSongById :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid FROM song WHERE id = $1
|
||||
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) {
|
||||
@@ -192,13 +190,12 @@ func (q *Queries) GetSongById(ctx context.Context, id pgtype.Int4) (Song, error)
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSongWithHash = `-- name: GetSongWithHash :one
|
||||
SELECT soundtrack_id, song_name, path, times_played, hash, file_name, id, uuid 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) {
|
||||
@@ -212,7 +209,6 @@ func (q *Queries) GetSongWithHash(ctx context.Context, hash string) (Song, error
|
||||
&i.Hash,
|
||||
&i.FileName,
|
||||
&i.ID,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (q *Queries) ClearSoundtracks(ctx context.Context) error {
|
||||
}
|
||||
|
||||
const findAllSoundtracks = `-- name: FindAllSoundtracks :many
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
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
|
||||
@@ -54,7 +54,6 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,7 +66,7 @@ func (q *Queries) FindAllSoundtracks(ctx context.Context) ([]Soundtrack, error)
|
||||
}
|
||||
|
||||
const getAllSoundtracksIncludingDeleted = `-- name: GetAllSoundtracksIncludingDeleted :many
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash
|
||||
FROM soundtrack
|
||||
ORDER BY soundtrack_name
|
||||
`
|
||||
@@ -92,7 +91,6 @@ func (q *Queries) GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soun
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -116,7 +114,7 @@ func (q *Queries) GetIdBySoundtrackName(ctx context.Context, soundtrackName stri
|
||||
}
|
||||
|
||||
const getSoundtrackById = `-- name: GetSoundtrackById :one
|
||||
SELECT id, soundtrack_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs, hash, uuid
|
||||
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
|
||||
@@ -136,7 +134,6 @@ func (q *Queries) GetSoundtrackById(ctx context.Context, id int32) (Soundtrack,
|
||||
&i.LastPlayed,
|
||||
&i.NumberOfSongs,
|
||||
&i.Hash,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -398,8 +398,8 @@ func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetO
|
||||
const getStatisticsSummary = `-- name: GetStatisticsSummary :one
|
||||
SELECT
|
||||
COUNT(*) as total_soundtracks,
|
||||
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_soundtracks,
|
||||
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_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,
|
||||
|
||||
@@ -54,8 +54,19 @@ func TestSetupDB(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -97,10 +108,11 @@ func createTestDatabase(host, port, dbname, user, password string) {
|
||||
// "closed pool" errors when tests run sequentially
|
||||
func TestTearDownDB(t *testing.T) {
|
||||
// CloseDb() // Disabled to prevent pool closure between sequential tests
|
||||
if TestDatabase != nil {
|
||||
TestDatabase.Close()
|
||||
TestDatabase = nil
|
||||
}
|
||||
// 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
|
||||
@@ -112,10 +124,13 @@ func TestClearDatabase(t *testing.T) {
|
||||
|
||||
// Clear all tables in reverse order to respect foreign keys
|
||||
// Note: This assumes the tables exist and have the expected structure
|
||||
// After migration 000005, game table was renamed to soundtrack
|
||||
tables := []string{
|
||||
"song_list",
|
||||
"song",
|
||||
"game",
|
||||
"soundtrack",
|
||||
"vgmq",
|
||||
"sessions",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -126,9 +141,10 @@ func TestClearDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset sequences
|
||||
_, err := TestDatabase.Pool.Exec(ctx, "SELECT setval('game_id_seq', 1, false)")
|
||||
if err != nil {
|
||||
t.Logf("Failed to reset game_id_seq: %v", err)
|
||||
// Reset sequences (renamed from game_id_seq to soundtrack_id_seq in migration 000005)
|
||||
var seqErr error
|
||||
_, seqErr = TestDatabase.Pool.Exec(ctx, "SELECT setval('soundtrack_id_seq', 1, false)")
|
||||
if seqErr != nil {
|
||||
t.Logf("Failed to reset soundtrack_id_seq: %v", seqErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{}
|
||||
func NewHealthHandler(database *db.Database) *HealthHandler {
|
||||
return &HealthHandler{db: database}
|
||||
}
|
||||
|
||||
// HealthCheck godoc
|
||||
@@ -24,5 +25,5 @@ func NewHealthHandler() *HealthHandler {
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Router /health [get]
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"music-server/internal/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestHealthCheck verifies the health endpoint returns database status
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
// Setup database
|
||||
db.TestSetupDB(t)
|
||||
defer db.TestTearDownDB(t)
|
||||
|
||||
e := StartTestServer(t)
|
||||
// No explicit teardown - handled by StartTestServer's sync.Once
|
||||
|
||||
resp := MakeTestRequest(t, e, "GET", "/health")
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
@@ -63,7 +63,7 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
// ============================================
|
||||
deprecatedMiddleware := middleware.DeprecationMiddleware
|
||||
|
||||
health := NewHealthHandler()
|
||||
health := NewHealthHandler(s.db)
|
||||
e.GET("/health", deprecatedMiddleware(health.HealthCheck))
|
||||
|
||||
version := NewVersionHandler()
|
||||
@@ -164,33 +164,6 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
// Future: VGMQ endpoints will be added to protectedV1 group
|
||||
_ = protectedV1 // Use the variable to avoid unused variable error
|
||||
|
||||
// ============================================
|
||||
// API v1 Routes with Token Authentication
|
||||
// ============================================
|
||||
|
||||
// Create /api/v1 group
|
||||
apiV1 := e.Group("/api/v1")
|
||||
|
||||
// Public endpoints - no token required
|
||||
apiV1.POST("/token", func(c *echo.Context) error {
|
||||
return s.tokenHandler.CreateTokenHandler(c)
|
||||
})
|
||||
apiV1.DELETE("/token", func(c *echo.Context) error {
|
||||
return s.tokenHandler.DeleteTokenHandler(c)
|
||||
})
|
||||
apiV1.POST("/token/cleanup", func(c *echo.Context) error {
|
||||
return s.tokenHandler.CleanupExpiredSessionsHandler(c)
|
||||
})
|
||||
|
||||
// Protected endpoints - require valid token
|
||||
// Create token auth middleware with pool access
|
||||
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
||||
|
||||
// Protected group with token authentication - will be used by VGMQ and Statistics API
|
||||
_ = apiV1.Group("", tokenAuthMiddleware)
|
||||
|
||||
// Note: Future protected endpoints (VGMQ, Statistics) will be added here
|
||||
|
||||
routes := e.Router().Routes()
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Path < routes[j].Path
|
||||
|
||||
@@ -73,6 +73,11 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
|
||||
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)
|
||||
@@ -85,9 +90,9 @@ func TestPartialMigrationThenSyncThenComplete(t *testing.T) {
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We inserted 5 soundtracks, so total should be at least 5
|
||||
// (there might be existing data)
|
||||
require.GreaterOrEqual(t, summary.TotalGames, int64(5))
|
||||
// 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
|
||||
@@ -115,8 +120,8 @@ func insertTestData(t *testing.T) {
|
||||
for _, st := range soundtracks {
|
||||
_, err := queries.InsertSoundtrack(ctx, repository.InsertSoundtrackParams{
|
||||
SoundtrackName: st.name,
|
||||
Path: st.path,
|
||||
Hash: "test-hash-" + st.name,
|
||||
Path: st.path,
|
||||
Hash: "test-hash-" + st.name,
|
||||
})
|
||||
require.NoError(t, err, "Failed to insert soundtrack: %s", st.name)
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ func StartTestServer(t *testing.T) *echo.Echo {
|
||||
|
||||
// Create a Server instance and get its routes
|
||||
s := &Server{
|
||||
db: db.TestDatabase,
|
||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||
db: db.TestDatabase,
|
||||
tokenHandler: NewTokenHandler(db.TestDatabase.Pool),
|
||||
statisticsHandler: NewStatisticsHandler(),
|
||||
}
|
||||
handler := s.RegisterRoutes()
|
||||
|
||||
|
||||
@@ -84,8 +84,13 @@ build-run: build
|
||||
@go run cmd/main.go
|
||||
|
||||
test: build
|
||||
@echo "Testing..."
|
||||
@go test ./... -v
|
||||
@echo "Starting test database container..."
|
||||
@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:
|
||||
@@ -105,7 +110,9 @@ podman-down:
|
||||
# Run integration tests with podman
|
||||
# Starts a test PostgreSQL container, runs tests, then cleans up
|
||||
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
|
||||
@sleep 10
|
||||
@echo "Running integration tests..."
|
||||
|
||||
Reference in New Issue
Block a user