mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
1299 lines
48 KiB
PHP
1299 lines
48 KiB
PHP
<?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;
|
|
use PDO;
|
|
|
|
class PlayniteController extends Controller
|
|
{
|
|
|
|
private \PDO $pdo;
|
|
private PlayniteImportService $importService;
|
|
|
|
private string $logFile;
|
|
|
|
public function __construct(\PDO $pdo)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->importService = new PlayniteImportService($pdo);
|
|
|
|
// Set up log file
|
|
$logDir = __DIR__ . '/../../../logs';
|
|
if (!is_dir($logDir)) {
|
|
mkdir($logDir, 0755, true);
|
|
}
|
|
$this->logFile = $logDir . '/playnite_api_' . date('Y-m-d') . '.log';
|
|
|
|
// Create media directory if it doesn't exist
|
|
$mediaDir = __DIR__ . '/../../../public/media/playnite';
|
|
if (!is_dir($mediaDir)) {
|
|
mkdir($mediaDir, 0755, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log a message to the API log file
|
|
*/
|
|
private function log(string $message, array $context = []): void
|
|
{
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$logLine = "[$timestamp] $message";
|
|
|
|
if (!empty($context)) {
|
|
$logLine .= "\n" . json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
$logLine .= "\n" . str_repeat('-', 80) . "\n";
|
|
|
|
error_log($logLine, 3, $this->logFile);
|
|
}
|
|
|
|
/**
|
|
* Get the raw request body
|
|
*/
|
|
private function getRawBody(Request $request): string
|
|
{
|
|
$body = $request->getBody();
|
|
$body->rewind();
|
|
$content = $body->getContents();
|
|
$body->rewind(); // Reset the stream position for potential later use
|
|
return $content;
|
|
}
|
|
|
|
|
|
/**
|
|
* @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)
|
|
{
|
|
$requestId = uniqid('req_');
|
|
$startTime = microtime(true);
|
|
|
|
// Log request details
|
|
$rawBody = $this->getRawBody($request);
|
|
$parsedBody = $request->getParsedBody();
|
|
|
|
$this->log("[$requestId] Received Playnite API request", [
|
|
'method' => $request->getMethod(),
|
|
'uri' => (string)$request->getUri(),
|
|
'headers' => $request->getHeaders(),
|
|
'raw_body' => $rawBody,
|
|
'parsed_body' => $parsedBody,
|
|
'content_type' => $request->getHeaderLine('Content-Type'),
|
|
'client_ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'
|
|
]);
|
|
|
|
try {
|
|
// Validate request
|
|
if (!isset($parsedBody['games']) || !is_array($parsedBody['games'])) {
|
|
$error = 'Games data is required';
|
|
$this->log("[$requestId] Validation failed: $error");
|
|
|
|
return $this->jsonResponse($response->withStatus(400), [
|
|
'success' => false,
|
|
'error' => $error,
|
|
'request_id' => $requestId
|
|
]);
|
|
}
|
|
|
|
$this->log("[$requestId] Starting import of " . count($parsedBody['games']) . " games");
|
|
|
|
// Process the import with detailed logging
|
|
$importResult = $this->importService->importGames(
|
|
$parsedBody['games'],
|
|
true, // Always update existing
|
|
function($message) use ($requestId) {
|
|
$this->log("[$requestId] $message");
|
|
}
|
|
);
|
|
|
|
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
|
|
|
$this->log("[$requestId] Import completed in {$executionTime}ms", [
|
|
'imported' => $importResult['imported'] ?? 0,
|
|
'updated' => $importResult['updated'] ?? 0,
|
|
'skipped' => $importResult['skipped'] ?? 0,
|
|
'errors' => count($importResult['errors'] ?? []),
|
|
'execution_time_ms' => $executionTime
|
|
]);
|
|
|
|
return $this->jsonResponse($response, [
|
|
'success' => true,
|
|
'request_id' => $requestId,
|
|
'result' => $importResult,
|
|
'execution_time_ms' => $executionTime
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
$errorDetails = [
|
|
'error' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'request_body' => $rawBody,
|
|
'execution_time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
];
|
|
|
|
$this->log("[$requestId] Exception during import", $errorDetails);
|
|
|
|
// Don't expose sensitive error details in production
|
|
$errorResponse = [
|
|
'success' => false,
|
|
'error' => 'An error occurred while processing your request',
|
|
'request_id' => $requestId
|
|
];
|
|
|
|
// Include more details in development
|
|
if (getenv('APP_ENV') === 'development') {
|
|
$errorResponse['debug'] = [
|
|
'message' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine()
|
|
];
|
|
}
|
|
|
|
return $this->jsonResponse($response->withStatus(500), $errorResponse);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @OA\Post(
|
|
* path="/playnite/media",
|
|
* summary="Update game media from Playnite",
|
|
* tags={"Playnite"},
|
|
* operationId="updateMedia",
|
|
* @OA\RequestBody(
|
|
* required=true,
|
|
* @OA\JsonContent(
|
|
* required={"game_id", "plugin_id"},
|
|
* @OA\Property(property="game_id", type="string", example="1486920"),
|
|
* @OA\Property(property="plugin_id", type="string", example="cb91dfc9-b977-43bf-8e70-55f46e410fab"),
|
|
* @OA\Property(property="cover_image", type="string", nullable=true),
|
|
* @OA\Property(property="background_image", type="string", nullable=true),
|
|
* @OA\Property(property="icon", type="string", nullable=true)
|
|
* )
|
|
* ),
|
|
* @OA\Response(
|
|
* response=200,
|
|
* description="Media updated successfully"
|
|
* ),
|
|
* @OA\Response(
|
|
* response=400,
|
|
* description="Invalid input"
|
|
* )
|
|
* )
|
|
*/
|
|
public function updateMedia(Request $request, Response $response, $args)
|
|
{
|
|
$requestId = uniqid('req_');
|
|
$startTime = microtime(true);
|
|
|
|
// Log request details
|
|
$rawBody = $this->getRawBody($request);
|
|
$data = json_decode($rawBody, true) ?: [];
|
|
|
|
$this->log("[$requestId] Received Playnite media update request", [
|
|
'method' => $request->getMethod(),
|
|
'uri' => (string)$request->getUri(),
|
|
'headers' => $request->getHeaders(),
|
|
//'data' => $data,
|
|
'client_ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'
|
|
]);
|
|
|
|
$this->log("[$requestId]: " . $data['name']);
|
|
try {
|
|
// Validate required fields
|
|
if (empty($data['game_id'])) {
|
|
$error = 'game_id and plugin_id are required';
|
|
$this->log("[$requestId] Validation failed: $error");
|
|
|
|
return $this->jsonResponse($response->withStatus(400), [
|
|
'success' => false,
|
|
'error' => $error,
|
|
'request_id' => $requestId
|
|
]);
|
|
}
|
|
|
|
$gameId = $data['game_id'];
|
|
$pluginId = 1;
|
|
|
|
// Process media files if provided
|
|
$mediaUpdates = [];
|
|
|
|
// Check if a transaction is already active
|
|
$isInTransaction = $this->pdo->inTransaction();
|
|
|
|
try {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->beginTransaction();
|
|
}
|
|
|
|
// Double-check if the game exists within the transaction
|
|
$gameExists = $this->gameExists($gameId);
|
|
$this->log("[$requestId] Game {$gameId} exists check: " . ($gameExists ? 'true' : 'false'));
|
|
|
|
if (!$gameExists) {
|
|
$this->log("[$requestId] Game {$gameId} not found, creating new game record");
|
|
|
|
// Add the game_id and plugin_id to the data array for the createGame method
|
|
$data['game_id'] = $gameId;
|
|
$data['plugin_id'] = $pluginId;
|
|
|
|
if ($this->createGame($data)) {
|
|
$this->log("[$requestId] Successfully created game {$gameId}");
|
|
} else {
|
|
throw new \Exception("Failed to create game {$gameId}");
|
|
}
|
|
}
|
|
|
|
// Update the game record in the database if we have any media to update
|
|
if (!empty($mediaUpdates)) {
|
|
$this->updateGameMedia($gameId, $pluginId, $mediaUpdates);
|
|
$this->log("[$requestId] Updated media for game {$gameId}", $mediaUpdates);
|
|
} else {
|
|
$this->log("[$requestId] No media updates found for game {$gameId}");
|
|
}
|
|
|
|
if (!$isInTransaction) {
|
|
$this->pdo->commit();
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $e; // Re-throw to be caught by the outer try-catch
|
|
}
|
|
|
|
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
|
|
|
$this->log("[$requestId] Media update completed in {$executionTime}ms", [
|
|
'game_id' => $gameId,
|
|
'plugin_id' => $pluginId,
|
|
'updates' => array_keys($mediaUpdates),
|
|
'execution_time_ms' => $executionTime
|
|
]);
|
|
|
|
return $this->jsonResponse($response, [
|
|
'success' => true,
|
|
'request_id' => $requestId,
|
|
'updates' => $mediaUpdates,
|
|
'execution_time_ms' => $executionTime
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
$errorDetails = [
|
|
'error' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'request_body' => $rawBody,
|
|
'execution_time_ms' => round((microtime(true) - $startTime) * 1000, 2)
|
|
];
|
|
|
|
$this->log("[$requestId] Exception during media update", $errorDetails);
|
|
|
|
$errorResponse = [
|
|
'success' => false,
|
|
'error' => 'An error occurred while processing your request',
|
|
'request_id' => $requestId
|
|
];
|
|
|
|
if (getenv('APP_ENV') === 'development') {
|
|
$errorResponse['debug'] = [
|
|
'message' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine()
|
|
];
|
|
}
|
|
|
|
return $this->jsonResponse($response->withStatus(500), $errorResponse);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update game media in the database
|
|
*/
|
|
/**
|
|
* Check if a game exists in the database
|
|
*/
|
|
private function gameExists(string $gameId): bool
|
|
{
|
|
//try {
|
|
$db = $this->pdo;
|
|
|
|
// Debug: Log the SQL query and parameters
|
|
$sql = "SELECT COUNT(*) as count FROM games WHERE platform_game_id = :game_id";
|
|
$this->log("Checking if game exists - SQL: $sql, game_id: $gameId");
|
|
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute([':game_id' => $gameId]);
|
|
|
|
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
$exists = $result && $result['count'] > 0;
|
|
|
|
$this->log(sprintf(
|
|
'Game ID %s %s in database',
|
|
$gameId,
|
|
$exists ? 'exists' : 'does not exist'
|
|
));
|
|
|
|
return $exists;
|
|
|
|
//} catch (\Exception $e) {
|
|
// $this->log("Error checking if game exists: " . $e->getMessage());
|
|
// return false;
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Create a new game with basic information
|
|
*/
|
|
private function createGame(array $data): bool
|
|
{
|
|
$isInTransaction = $this->pdo->inTransaction();
|
|
|
|
//try {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->beginTransaction();
|
|
}
|
|
$this->log(sprintf(
|
|
'Game ID %s',
|
|
$data['name']
|
|
));
|
|
// Get or create source
|
|
/*$source = $this->findOrCreateSource($data);
|
|
|
|
$this->log(sprintf(
|
|
'Source %s',
|
|
$source
|
|
));
|
|
*/
|
|
|
|
$cover_image = end(explode('\\', $data['cover_image']));
|
|
$background_image = end(explode('\\', $data['background_image']));
|
|
|
|
|
|
// Prepare game data according to the database schema
|
|
$gameData = [
|
|
'title' => $data['name'] ?? 'Unknown Game',
|
|
'description' => $this->cleanHtml($data['description'] ?? ''),
|
|
'game_key' => $this->generateGameKey($data['name']),
|
|
'platform_game_id' => $data['game_id'] ?? null,
|
|
'platform' => !empty($data['platforms']) ? $data['platforms'][0]['name'] : 'PC',
|
|
|
|
'image_url' => $cover_image,
|
|
'banner_url' => $background_image,
|
|
'playtime_minutes' => !empty($data['playtime']) ? floor($data['playtime'] / 60) : 0,
|
|
/* 'genre' => !empty($data['genres']) ? implode(', ', array_column($data['genres'], 'name')) : null,
|
|
'developer' => !empty($data['developers']) ? $data['developers'][0]['name'] : null,
|
|
'publisher' => !empty($data['publishers']) ? $data['publishers'][0]['name'] : null,
|
|
'release_date' => !empty($data['release_date']['ReleaseDate'])
|
|
? date('Y-m-d', strtotime($data['release_date']['ReleaseDate']))
|
|
: null,
|
|
'platform' => !empty($data['platforms']) ? $data['platforms'][0]['name'] : 'PC',
|
|
'image_url' => $data['cover_image'] ?? null,
|
|
'banner_url' => $data['background_image'] ?? null,
|
|
'rating' => $this->normalizeRating($data['user_score_rating'] ?? $data['critic_score_rating'] ?? null),*/
|
|
//'is_installed' => $this->toBoolean($data['is_installed'] ?? false),
|
|
//'is_favorite' => $this->toBoolean($data['favorite'] ?? false),
|
|
'source_id' => 1,
|
|
'last_played_at' => !empty($data['last_activity'])
|
|
? date('Y-m-d H:i:s', strtotime($data['last_activity']))
|
|
: null,
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
|
|
// Enhanced ratings
|
|
'rating' => $this->normalizeRating($data['critic_score'] ?? null),
|
|
'critic_score' => $data['critic_score'] ?? null,
|
|
'community_score' => $data['community_score'] ?? null,
|
|
'user_score' => $data['user_score'] ?? null,
|
|
|
|
// Timestamps
|
|
'added_at' => isset($data['added']) ? date('Y-m-d H:i:s', strtotime($data['added'])) : null,
|
|
'modified_at' => isset($data['modified']) ? date('Y-m-d H:i:s', strtotime($data['modified'])) : null,
|
|
|
|
// Playnite metadata
|
|
'metadata' => json_encode([
|
|
'playnite_id' => $data['Id'] ?? $data['playnite_id'] ?? null,
|
|
'version' => $data['Version'] ?? $data['version'] ?? null,
|
|
'hidden' => $this->toBoolean($data['Hidden'] ?? $data['hidden'] ?? false),
|
|
'notes' => $data['Notes'] ?? $data['notes'] ?? null,
|
|
'manual' => $data['Manual'] ?? $data['manual'] ?? null,
|
|
'pre_script' => $data['PreScript'] ?? $data['pre_script'] ?? null,
|
|
'post_script' => $data['PostScript'] ?? $data['post_script'] ?? null,
|
|
'game_started_script' => $data['GameStartedScript'] ?? $data['game_started_script'] ?? null,
|
|
'use_global_scripts' => [
|
|
'pre' => $this->toBoolean($data['UseGlobalPreScript'] ?? $data['use_global_pre_script'] ?? true),
|
|
'post' => $this->toBoolean($data['UseGlobalPostScript'] ?? $data['use_global_post_script'] ?? true),
|
|
'game_started' => $this->toBoolean($data['UseGlobalGameStartedScript'] ?? $data['use_global_game_started_script'] ?? true)
|
|
]
|
|
])
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
//throw new \Exception('Game created');
|
|
/*
|
|
$this->log($data['name'] . " Received Playnite media update request", [
|
|
'data' => $gameData,
|
|
]);
|
|
|
|
*/
|
|
// Store all related data in metadata
|
|
$metadata = [];
|
|
|
|
if (!empty($data['genres']) && is_array($data['genres'])) {
|
|
$metadata['genres'] = array_column($data['genres'], 'name');
|
|
}
|
|
|
|
if (!empty($data['platforms']) && is_array($data['platforms'])) {
|
|
$metadata['platforms'] = array_column($data['platforms'], 'name');
|
|
}
|
|
|
|
if (!empty($data['developers']) && is_array($data['developers'])) {
|
|
$metadata['developers'] = array_column($data['developers'], 'name');
|
|
}
|
|
|
|
if (!empty($data['publishers']) && is_array($data['publishers'])) {
|
|
$metadata['publishers'] = array_column($data['publishers'], 'name');
|
|
}
|
|
|
|
if (!empty($data['tags']) && is_array($data['tags'])) {
|
|
$metadata['tags'] = array_column($data['tags'], 'name');
|
|
}
|
|
|
|
if (!empty($data['features']) && is_array($data['features'])) {
|
|
$metadata['features'] = array_column($data['features'], 'name');
|
|
}
|
|
|
|
if (!empty($metadata)) {
|
|
$gameData['metadata'] = json_encode($metadata, JSON_PRETTY_PRINT);
|
|
}
|
|
$this->log(sprintf(
|
|
'Metadata %s',
|
|
$metadata
|
|
));
|
|
|
|
// Insert game
|
|
$columns = implode(', ', array_keys($gameData));
|
|
$placeholders = ':' . implode(', :', array_keys($gameData));
|
|
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT INTO games ($columns) VALUES ($placeholders)"
|
|
);
|
|
|
|
$stmt->execute($gameData);
|
|
$gameId = $this->pdo->lastInsertId();
|
|
|
|
// All related data is already stored in metadata
|
|
|
|
if (!$isInTransaction) {
|
|
$this->pdo->commit();
|
|
}
|
|
return true;
|
|
|
|
/*} catch (\Exception $e) {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
$this->log("Error creating game: " . $e->getMessage());
|
|
throw $e; // Re-throw to be handled by the caller
|
|
}*/
|
|
}
|
|
|
|
/**
|
|
* Handle game genres
|
|
*/
|
|
private function handleGameGenres(int $gameId, array $genres): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT IGNORE INTO game_genres (game_id, genre_id)
|
|
SELECT :game_id, id FROM genres WHERE name = :name"
|
|
);
|
|
|
|
foreach ($genres as $genre) {
|
|
$stmt->execute([
|
|
':game_id' => $gameId,
|
|
':name' => $genre['name']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle game platforms
|
|
*/
|
|
private function handleGamePlatforms(int $gameId, array $platforms): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT IGNORE INTO game_platforms (game_id, platform_id)
|
|
SELECT :game_id, id FROM platforms WHERE name = :name"
|
|
);
|
|
|
|
foreach ($platforms as $platform) {
|
|
$stmt->execute([
|
|
':game_id' => $gameId,
|
|
':name' => $platform['name']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle game developers
|
|
*/
|
|
private function handleGameDevelopers(int $gameId, array $developers): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT IGNORE INTO game_developers (game_id, developer_id)
|
|
SELECT :game_id, id FROM developers WHERE name = :name"
|
|
);
|
|
|
|
foreach ($developers as $developer) {
|
|
$stmt->execute([
|
|
':game_id' => $gameId,
|
|
':name' => $developer['name']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle game publishers
|
|
*/
|
|
private function handleGamePublishers(int $gameId, array $publishers): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT IGNORE INTO game_publishers (game_id, publisher_id)
|
|
SELECT :game_id, id FROM publishers WHERE name = :name"
|
|
);
|
|
|
|
foreach ($publishers as $publisher) {
|
|
$stmt->execute([
|
|
':game_id' => $gameId,
|
|
':name' => $publisher['name']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle game tags
|
|
*/
|
|
private function handleGameTags(int $gameId, array $tags): void
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT IGNORE INTO game_tags (game_id, tag_id)
|
|
SELECT :game_id, id FROM tags WHERE name = :name"
|
|
);
|
|
|
|
foreach ($tags as $tag) {
|
|
$stmt->execute([
|
|
':game_id' => $gameId,
|
|
':name' => $tag['name']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update game media in the database
|
|
*/
|
|
private function updateGameMedia(string $gameId, string $pluginId, array $updates): void
|
|
{
|
|
if (empty($updates)) {
|
|
return;
|
|
}
|
|
|
|
//try {
|
|
$setClauses = [];
|
|
$params = [':game_id' => $gameId, ':plugin_id' => $pluginId];
|
|
|
|
foreach ($updates as $field => $value) {
|
|
$param = ":{$field}";
|
|
$setClauses[] = "{$field} = {$param}";
|
|
$params[$param] = $value;
|
|
}
|
|
|
|
$sql = "UPDATE games SET " . implode(', ', $setClauses) .
|
|
" WHERE platform_game_id = :game_id AND platform_plugin_id = :plugin_id";
|
|
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
//} catch (\Exception $e) {
|
|
// $this->log("Error updating game media in database: " . $e->getMessage());
|
|
// throw $e;
|
|
//}
|
|
}
|
|
|
|
/**
|
|
* @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)
|
|
{
|
|
// Initialize results array
|
|
$results = [
|
|
'uploaded' => 0,
|
|
'errors' => []
|
|
];
|
|
|
|
// Log raw request body
|
|
$rawBody = (string)$request->getBody();
|
|
$this->log("[/playnite/image/base64] Raw request received. Length: " . strlen($rawBody) . " bytes");
|
|
|
|
// Log headers
|
|
$headers = $request->getHeaders();
|
|
$this->log("[/playnite/image/base64] Headers: " . json_encode($headers, JSON_PRETTY_PRINT));
|
|
|
|
// Get parsed body
|
|
$data = $request->getParsedBody();
|
|
|
|
// Log parsed data structure
|
|
$this->log("[/playnite/image/base64] Parsed request data: " . json_encode([
|
|
'has_games' => isset($data['games']) ? 'yes' : 'no',
|
|
'has_direct_fields' => isset($data['name']) ? 'yes' : 'no',
|
|
'data_keys' => array_keys($data)
|
|
], JSON_PRETTY_PRINT));
|
|
|
|
// Handle image uploads based on the format expected by the plugin
|
|
if (isset($data['name']) && isset($data['cover'])) {
|
|
// Handle single game upload
|
|
$this->log('[/playnite/image/base64] Processing single game upload');
|
|
$this->log('[/playnite/image/base64] Upload data: ' . json_encode([
|
|
'has_name' => isset($data['name']) ? 'yes' : 'no',
|
|
'has_cover' => isset($data['cover']) ? 'yes, ' . strlen($data['cover']) . ' chars' : 'no',
|
|
'has_icon' => isset($data['icon']) ? 'yes, ' . strlen($data['icon']) . ' chars' : 'no',
|
|
'has_background' => isset($data['background']) ? 'yes, ' . strlen($data['background']) . ' chars' : 'no'
|
|
]));
|
|
|
|
$result = $this->handleImageUpload($data);
|
|
}
|
|
return $this->jsonResponse($response, [
|
|
'success' => true,
|
|
'result' => $result
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle individual image upload
|
|
*/
|
|
private function handleImageUpload(array $gameData): bool
|
|
{
|
|
$name = $gameData['name'] ?? 'Unnamed Game';
|
|
$gameId = $gameData['name'] ?? uniqid('game_', true);
|
|
$pluginId = $gameData['plugin_id'] ?? 'default';
|
|
|
|
//$this->log("[/playnite/image/base64] Game data: " . json_encode($gameData));
|
|
|
|
$this->log("[/playnite/image/base64] Processing images for game: {$name} (ID: {$gameId})");
|
|
|
|
// Define media types and their corresponding data
|
|
$mediaTypes = [
|
|
'cover' => [
|
|
'data' => $gameData['cover'] ?? null,
|
|
'filename' => end(explode('\\', $gameData['coverName'] ?? null)),
|
|
'field' => 'cover_image'
|
|
],
|
|
//'icon' => [
|
|
// 'data' => $gameData['icon'] ?? null,
|
|
// 'filename' => $gameData['iconName'] ?? null,
|
|
// 'field' => 'icon_image'
|
|
//],
|
|
'background' => [
|
|
'data' => $gameData['background'] ?? null,
|
|
'filename' => end(explode('\\', $gameData['backgroundName'] ?? null)),
|
|
'field' => 'background_image'
|
|
]
|
|
];
|
|
|
|
$updates = [];
|
|
$mediaDir = __DIR__ . '/../../../storage/images/playnite';
|
|
|
|
// Ensure media directory exists
|
|
if (!is_dir($mediaDir)) {
|
|
if (!mkdir($mediaDir, 0777, true) && !is_dir($mediaDir)) {
|
|
throw new \RuntimeException("Failed to create directory: {$mediaDir}");
|
|
}
|
|
}
|
|
|
|
foreach ($mediaTypes as $type => $media) {
|
|
$base64Data = $media['data'];
|
|
$fileName = $media['filename'];
|
|
|
|
if (empty($base64Data) || empty($fileName)) {
|
|
$this->log("[/playnite/image/base64] No {$type} image data or filename provided for game ID: {$gameId}");
|
|
continue;
|
|
}
|
|
|
|
// Check if the data is URL-encoded
|
|
if (strpos($base64Data, 'data:image/') === 0) {
|
|
// Standard base64 data URL
|
|
if (!preg_match('/^data:image\/(jpeg|png|gif|webp);base64,/', $base64Data, $matches)) {
|
|
$this->log("[/playnite/image/base64] Invalid {$type} image format for game ID: {$gameId}");
|
|
continue;
|
|
}
|
|
$extension = $matches[1];
|
|
$base64Image = explode(',', $base64Data, 2)[1] ?? '';
|
|
} else {
|
|
// Raw base64 data
|
|
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
|
$base64Image = $base64Data;
|
|
|
|
// If no valid extension found, try to determine from base64 data
|
|
if (empty($extension)) {
|
|
$signature = substr($base64Data, 0, 20);
|
|
if (strpos($signature, '/9j/') === 0) {
|
|
$extension = 'jpg';
|
|
} elseif (strpos($signature, 'iVBORw') === 0) {
|
|
$extension = 'png';
|
|
} elseif (strpos($signature, 'R0lGOD') === 0) {
|
|
$extension = 'gif';
|
|
} elseif (strpos($signature, 'UklGR') === 0) {
|
|
$extension = 'webp';
|
|
} else {
|
|
$extension = 'bin'; // Default extension if can't determine
|
|
}
|
|
// Update filename with determined extension
|
|
$fileName = $media['filename'] . '.' . $extension;
|
|
}
|
|
}
|
|
|
|
$filePath = "{$mediaDir}/{$fileName}";
|
|
|
|
// Check if file already exists and has content
|
|
if (file_exists($filePath) && filesize($filePath) > 0) {
|
|
$this->log("[/playnite/image/base64] Skipping {$type} image for game ID: {$gameId} - file already exists at {$filePath}");
|
|
$updates[$media['field']] = "/storage/images/playnite/{$fileName}";
|
|
continue;
|
|
}
|
|
|
|
// Extract base64 data
|
|
$imageData = base64_decode($base64Image, true);
|
|
|
|
if ($imageData === false) {
|
|
$this->log("[/playnite/image/base64] Failed to decode {$type} image for game ID: {$gameId}");
|
|
continue;
|
|
}
|
|
|
|
// Save the image file
|
|
if (file_put_contents($filePath, $imageData) === false) {
|
|
$this->log("[/playnite/image/base64] Failed to save {$type} image for game ID: {$gameId}");
|
|
continue;
|
|
}
|
|
|
|
// Add to updates for database
|
|
$updates[$media['field']] = "/storage/images/playnite/{$fileName}";
|
|
$this->log("[/playnite/image/base64] Successfully saved {$type} image for game ID: {$gameId} at {$filePath}");
|
|
|
|
// Log the first 100 characters of the image data for debugging
|
|
$sampleData = substr($base64Image, 0, 100);
|
|
$this->log("[/playnite/image/base64] Sample image data (first 100 chars): {$sampleData}...");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
$displayName = $game['Source']['Name'] ?? $game['source'] ?? 'Playnite';
|
|
$sourceId = $game['Source']['Id'] ?? $game['source_id'] ?? null;
|
|
$sourceName = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $displayName));
|
|
|
|
// Try to find existing source by name or ID
|
|
$stmt = $this->pdo->prepare("SELECT id, name, display_name FROM sources WHERE name = :name OR id = :source_id");
|
|
$stmt->execute([
|
|
'name' => $sourceName,
|
|
'source_id' => $sourceId
|
|
]);
|
|
|
|
$source = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
if (!$source) {
|
|
// Check if we're already in a transaction
|
|
$isInTransaction = $this->pdo->inTransaction();
|
|
|
|
try {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->beginTransaction();
|
|
}
|
|
|
|
// Create new source with required fields
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO sources (name, display_name, created_at, updated_at)
|
|
VALUES (:name, :display_name, NOW(), NOW())
|
|
") or die(print_r($this->pdo->errorInfo(), true));
|
|
|
|
$stmt->execute([
|
|
'name' => $sourceName,
|
|
'display_name' => $displayName
|
|
]);
|
|
|
|
$source = [
|
|
'id' => $this->pdo->lastInsertId(),
|
|
'name' => $sourceName,
|
|
'display_name' => $displayName
|
|
];
|
|
|
|
if (!$isInTransaction) {
|
|
$this->pdo->commit();
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
if (!$isInTransaction) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
$this->log("Error in findOrCreateSource: " . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Extract file extension from URL
|
|
*/
|
|
private function getExtensionFromUrl(string $url): ?string
|
|
{
|
|
$path = parse_url($url, PHP_URL_PATH);
|
|
if ($path === false || $path === null) {
|
|
return null;
|
|
}
|
|
|
|
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
|
if (empty($extension)) {
|
|
return null;
|
|
}
|
|
|
|
// Remove query string if present
|
|
$extension = strtok($extension, '?');
|
|
|
|
// Return only the first part if there are multiple dots
|
|
$parts = explode('.', $extension);
|
|
return strtolower($parts[0]);
|
|
}
|
|
|
|
/**
|
|
* 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 various input types to boolean
|
|
*/
|
|
private function toBoolean($value): bool
|
|
{
|
|
if ($value === '' || $value === null) {
|
|
return false;
|
|
}
|
|
if (is_string($value)) {
|
|
$value = strtolower($value);
|
|
if ($value === 'false' || $value === 'no' || $value === '0' || $value === 'off') {
|
|
return false;
|
|
}
|
|
}
|
|
return (bool) $value;
|
|
}
|
|
}
|