Stuff i guess ?

This commit is contained in:
Lars Behrends
2025-10-31 00:24:17 +01:00
parent db0fd4e728
commit 04140786a7
40 changed files with 5411 additions and 525 deletions

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Controllers\Api;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Controllers\Controller;
use App\Services\AuthService;
class AuthController extends Controller
{
private AuthService $authService;
public function __construct(AuthService $authService)
{
$this->authService = $authService;
}
/**
* Check if user is authenticated (API endpoint)
*/
public function checkAuth(Request $request, Response $response, $args)
{
try {
if (!$this->authService->isLoggedIn()) {
return $this->jsonResponse($response->withStatus(401), [
'error' => '401 Forbidden'
]);
}
$user = $this->authService->getCurrentUser();
if (!$user) {
return $this->jsonResponse($response->withStatus(401), [
'error' => '401 Forbidden'
]);
}
return $this->jsonResponse($response, [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'is_admin' => $this->authService->isAdmin()
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => 'Authentication check failed'
]);
}
}
}

View File

@@ -0,0 +1,569 @@
<?php
namespace App\Controllers\Api;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Controllers\Controller;
use App\Models\Game;
use App\Services\PlayniteImportService;
class PlayniteController extends Controller
{
private \PDO $pdo;
private PlayniteImportService $importService;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
$this->importService = new PlayniteImportService($pdo);
}
/**
* @OA\Post(
* path="/playnite/insert",
* summary="Insert or update games from Playnite",
* tags={"Playnite"},
* operationId="insertGames",
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"games"},
* @OA\Property(
* property="games",
* type="array",
* @OA\Items(type="object")
* ),
* @OA\Property(
* property="update_existing",
* type="boolean",
* default=true,
* description="Whether to update existing games"
* )
* )
* ),
* @OA\Response(
* response=200,
* description="Games successfully imported/updated",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean"),
* @OA\Property(property="result", type="object")
* )
* ),
* @OA\Response(
* response=400,
* description="Invalid input"
* ),
* @OA\Response(
* response=500,
* description="Server error"
* )
* )
*
* @param Request $request
* @param Response $response
* @param array $args
* @return Response
*/
public function insertGames(Request $request, Response $response, $args)
{
$data = $request->getParsedBody();
if (!isset($data['games']) || !is_array($data['games'])) {
return $this->jsonResponse($response->withStatus(400), [
'error' => 'Games data is required'
]);
}
try {
$importResult = $this->importService->importGames($data['games'], true);
return $this->jsonResponse($response, [
'success' => true,
'result' => $importResult
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => $e->getMessage()
]);
}
}
/**
* @OA\Put(
* path="/playnite/update",
* summary="Update existing games from Playnite",
* tags={"Playnite"},
* operationId="updateGames",
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"games"},
* @OA\Property(
* property="games",
* type="array",
* @OA\Items(type="object")
* )
* )
* ),
* @OA\Response(
* response=200,
* description="Games successfully updated",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean"),
* @OA\Property(property="result", type="object")
* )
* ),
* @OA\Response(
* response=400,
* description="Invalid input"
* ),
* @OA\Response(
* response=500,
* description="Server error"
* )
* )
*
* @param Request $request
* @param Response $response
* @param array $args
* @return Response
*/
public function updateGames(Request $request, Response $response, $args)
{
$data = $request->getParsedBody();
if (!isset($data['games']) || !is_array($data['games'])) {
return $this->jsonResponse($response->withStatus(400), [
'error' => 'Games data is required'
]);
}
try {
$importResult = $this->importService->importGames($data['games'], true);
return $this->jsonResponse($response, [
'success' => true,
'result' => $importResult
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => $e->getMessage()
]);
}
}
/**
* @OA\Delete(
* path="/playnite/delete",
* summary="Delete games from Playnite",
* tags={"Playnite"},
* operationId="deleteGames",
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"games"},
* @OA\Property(
* property="games",
* type="array",
* @OA\Items(type="object")
* )
* )
* ),
* @OA\Response(
* response=200,
* description="Games successfully deleted",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean"),
* @OA\Property(property="result", type="object",
* @OA\Property(property="deleted", type="integer"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"))
* )
* )
* ),
* @OA\Response(
* response=400,
* description="Invalid input"
* ),
* @OA\Response(
* response=500,
* description="Server error"
* )
* )
*
* @param Request $request
* @param Response $response
* @param array $args
* @return Response
*/
public function deleteGames(Request $request, Response $response, $args)
{
$data = $request->getParsedBody();
if (!isset($data['games']) || !is_array($data['games'])) {
return $this->jsonResponse($response->withStatus(400), [
'error' => 'Games data is required'
]);
}
try {
$results = [
'deleted' => 0,
'errors' => []
];
foreach ($data['games'] as $gameData) {
try {
// Find the game by platform_game_id and source_id
$existingGame = $this->findExistingGame($gameData);
if ($existingGame) {
$this->deleteGame($existingGame['id']);
$results['deleted']++;
}
} catch (\Exception $e) {
$results['errors'][] = "Failed to delete {$gameData['title']}: " . $e->getMessage();
}
}
return $this->jsonResponse($response, [
'success' => true,
'result' => $results
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => $e->getMessage()
]);
}
}
/**
* @OA\Post(
* path="/playnite/upload-images",
* summary="Upload game images from Playnite",
* tags={"Playnite"},
* operationId="uploadImages",
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* oneOf={
* @OA\Schema(
* @OA\Property(property="name", type="string"),
* @OA\Property(property="cover", type="string", format="byte"),
* @OA\Property(property="icon", type="string", format="byte"),
* @OA\Property(property="background", type="string", format="byte")
* ),
* @OA\Schema(
* @OA\Property(
* property="games",
* type="array",
* @OA\Items(
* @OA\Property(property="name", type="string"),
* @OA\Property(property="cover", type="string", format="byte"),
* @OA\Property(property="icon", type="string", format="byte"),
* @OA\Property(property="background", type="string", format="byte")
* )
* )
* )
* }
* )
* ),
* @OA\Response(
* response=200,
* description="Images successfully uploaded",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean"),
* @OA\Property(property="result", type="object",
* @OA\Property(property="uploaded", type="integer"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"))
* )
* )
* ),
* @OA\Response(
* response=500,
* description="Server error"
* )
* )
*
* @param Request $request
* @param Response $response
* @param array $args
* @return Response
*/
public function uploadImages(Request $request, Response $response, $args)
{
$data = $request->getParsedBody();
try {
$results = [
'uploaded' => 0,
'errors' => []
];
// Handle image uploads based on the format expected by the plugin
if (isset($data['name']) && isset($data['cover'])) {
// Single game image upload
$result = $this->handleImageUpload($data);
if ($result) {
$results['uploaded']++;
}
} elseif (isset($data['games']) && is_array($data['games'])) {
// Multiple games with images
foreach ($data['games'] as $gameData) {
$result = $this->handleImageUpload($gameData);
if ($result) {
$results['uploaded']++;
}
}
}
return $this->jsonResponse($response, [
'success' => true,
'result' => $results
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => $e->getMessage()
]);
}
}
/**
* Handle individual image upload
*/
private function handleImageUpload(array $gameData): bool
{
try {
// For now, we'll just validate the data format
// In a real implementation, you might want to save the images to disk
// and update the game records with the image paths
$name = $gameData['name'] ?? '';
$cover = $gameData['cover'] ?? '';
$icon = $gameData['icon'] ?? '';
$background = $gameData['background'] ?? '';
// Validate base64 images
if ($cover && !preg_match('/^data:image\/(jpeg|png|gif|webp);base64,/', $cover)) {
throw new \Exception("Invalid cover image format");
}
// Here you would typically:
// 1. Decode base64 images
// 2. Save them to the filesystem
// 3. Update the game record with the image paths
return true;
} catch (\Exception $e) {
error_log("Image upload failed for game {$name}: " . $e->getMessage());
return false;
}
}
/**
* Find existing game by platform_game_id and source_id
*/
private function findExistingGame(array $gameData): ?array
{
$stmt = $this->pdo->prepare("
SELECT id, title, platform_game_id, source_id
FROM games
WHERE platform_game_id = :platform_game_id AND source_id = :source_id
");
$stmt->execute([
'platform_game_id' => $gameData['platform_game_id'],
'source_id' => $gameData['source_id']
]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
/**
* Delete game
*/
private function deleteGame(int $gameId): void
{
$stmt = $this->pdo->prepare("DELETE FROM games WHERE id = :id");
$stmt->execute(['id' => $gameId]);
}
/**
* Transform Playnite game data to internal format
*/
private function transformPlayniteGame(array $game): array
{
// Find or create source
$source = $this->findOrCreateSource($game);
// Transform the game data similar to PlayniteImportService
return [
'title' => $game['Name'] ?? $game['name'] ?? '',
'game_key' => $this->generateGameKey($game['Name'] ?? $game['name'] ?? ''),
'description' => $this->cleanHtml($game['Description'] ?? $game['description'] ?? ''),
'platform_game_id' => $game['GameId'] ?? $game['game_id'] ?? '',
'platform' => $this->extractPlatformFromPlaynite($game),
'source_id' => $source['id'],
// Rich media
'background_image' => $game['BackgroundImage'] ?? $game['background'] ?? null,
'cover_image' => $game['CoverImage'] ?? $game['cover'] ?? null,
'icon' => $game['Icon'] ?? $game['icon'] ?? null,
// Play statistics
'playtime_minutes' => $this->parsePlaytime($game['Playtime'] ?? $game['playtime'] ?? 0),
'play_count' => $game['PlayCount'] ?? $game['play_count'] ?? 0,
// Enhanced ratings
'rating' => $this->normalizeRating($game['CriticScore'] ?? $game['critic_score'] ?? null),
'critic_score' => $game['CriticScore'] ?? $game['critic_score'] ?? null,
'community_score' => $game['CommunityScore'] ?? $game['community_score'] ?? null,
'user_score' => $game['UserScore'] ?? $game['user_score'] ?? null,
// Timestamps
'added_at' => isset($game['Added']) ? date('Y-m-d H:i:s', strtotime($game['Added'])) : null,
'modified_at' => isset($game['Modified']) ? date('Y-m-d H:i:s', strtotime($game['Modified'])) : null,
'last_played_at' => isset($game['LastActivity']) ? date('Y-m-d H:i:s', strtotime($game['LastActivity'])) : null,
// Playnite metadata
'metadata' => json_encode([
'playnite_id' => $game['Id'] ?? $game['playnite_id'] ?? null,
'version' => $game['Version'] ?? $game['version'] ?? null,
'hidden' => $this->toBoolean($game['Hidden'] ?? $game['hidden'] ?? false),
'notes' => $game['Notes'] ?? $game['notes'] ?? null,
'manual' => $game['Manual'] ?? $game['manual'] ?? null,
'pre_script' => $game['PreScript'] ?? $game['pre_script'] ?? null,
'post_script' => $game['PostScript'] ?? $game['post_script'] ?? null,
'game_started_script' => $game['GameStartedScript'] ?? $game['game_started_script'] ?? null,
'use_global_scripts' => [
'pre' => $this->toBoolean($game['UseGlobalPreScript'] ?? $game['use_global_pre_script'] ?? true),
'post' => $this->toBoolean($game['UseGlobalPostScript'] ?? $game['use_global_post_script'] ?? true),
'game_started' => $this->toBoolean($game['UseGlobalGameStartedScript'] ?? $game['use_global_game_started_script'] ?? true)
]
])
];
}
/**
* Find or create a source for the game
*/
private function findOrCreateSource(array $game): array
{
$sourceName = $game['Source']['Name'] ?? $game['source'] ?? 'Playnite';
$sourceId = $game['Source']['Id'] ?? $game['source_id'] ?? null;
// Try to find existing source
$stmt = $this->pdo->prepare("SELECT id, display_name FROM sources WHERE display_name = :name OR id = :source_id");
$stmt->execute([
'name' => $sourceName,
'source_id' => $sourceId
]);
$source = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$source) {
// Create new source
$stmt = $this->pdo->prepare("INSERT INTO sources (display_name, created_at, updated_at) VALUES (:name, NOW(), NOW())");
$stmt->execute(['name' => $sourceName]);
$source = ['id' => $this->pdo->lastInsertId(), 'display_name' => $sourceName];
}
return $source;
}
/**
* Generate a consistent game key for grouping
*/
private function generateGameKey(string $title): string
{
return \App\Models\Game::generateGameKey($title);
}
/**
* Extract platform from Playnite data
*/
private function extractPlatformFromPlaynite(array $game): string
{
if (isset($game['Platforms']) && is_array($game['Platforms'])) {
$platformNames = array_map(function($platform) {
return $platform['Name'] ?? 'Unknown';
}, $game['Platforms']);
return implode(', ', $platformNames);
}
if (isset($game['Platform']) && is_array($game['Platform'])) {
return $game['Platform']['Name'] ?? 'PC';
}
return $game['platform'] ?? 'PC';
}
/**
* Parse playtime from Playnite format (usually in seconds)
*/
private function parsePlaytime($playtime): int
{
if (is_numeric($playtime)) {
return (int)($playtime / 60); // Convert seconds to minutes
}
return 0;
}
/**
* Normalize rating to 0-10 scale
*/
private function normalizeRating($rating): ?float
{
if (is_numeric($rating)) {
$rating = (float)$rating;
// If rating is 0-100 scale, convert to 0-10
if ($rating > 10) {
return $rating / 10;
}
return $rating;
}
return null;
}
/**
* Clean HTML from description
*/
private function cleanHtml(?string $html): ?string
{
if (!$html) {
return null;
}
// Remove HTML tags but keep basic formatting
$text = strip_tags($html);
// Decode HTML entities
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
// Clean up extra whitespace
$text = preg_replace('/\s+/', ' ', $text);
return trim($text);
}
/**
* Convert a value to boolean
*/
private function toBoolean($value): bool
{
if ($value === null || $value === false || $value === 0 || $value === '0') {
return false;
}
if ($value === true || $value === 1 || $value === '1') {
return true;
}
if (is_string($value)) {
return !empty(trim($value));
}
return (bool) $value;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @OA\OpenApi(
* @OA\Info(
* title="Playnite API",
* version="1.0.0",
* description="API for managing games and media from Playnite",
* @OA\Contact(
* email="support@example.com"
* ),
* @OA\License(
* name="Apache 2.0",
* url="http://www.apache.org/licenses/LICENSE-2.0.html"
* )
* ),
* @OA\Server(
* url="/api",
* description="API Server"
* ),
* @OA\Components(
* @OA\Schema(
* schema="Error",
* type="object",
* @OA\Property(property="success", type="boolean", example=false),
* @OA\Property(property="error", type="string", example="Error message")
* ),
* @OA\Schema(
* schema="Success",
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="result", type="object")
* )
* )
* )
*
* @OA\Tag(
* name="Playnite",
* description="Endpoints for Playnite integration"
* )
*/