impoort stuff!

This commit is contained in:
Lars Behrends
2025-10-24 16:04:34 +02:00
parent 73d8441787
commit 218d0c28c0
17 changed files with 3043 additions and 277 deletions

View File

@@ -3,18 +3,19 @@
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
abstract class Controller
{
protected $view;
public function __construct(Twig $view)
protected $auth;
public function __construct(Twig $view, $auth = null)
{
$this->view = $view;
$this->auth = $auth;
}
protected function json(Response $response, $data, int $status = 200): Response
{
$response->getBody()->write(json_encode($data));
@@ -22,4 +23,122 @@ abstract class Controller
->withHeader('Content-Type', 'application/json')
->withStatus($status);
}
protected function jsonResponse(Response $response, $data, int $status = 200): Response
{
return $this->json($response, $data, $status);
}
protected function withRedirect(Response $response, string $url): Response
{
return $response->withStatus(302)->withHeader('Location', $url);
}
protected function generateCSRFToken(): string
{
if ($this->auth && method_exists($this->auth, 'generateCSRFToken')) {
return $this->auth->generateCSRFToken();
}
// Fallback for when auth service is not available
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
protected function verifyCSRFToken(string $token): bool
{
if ($this->auth && method_exists($this->auth, 'verifyCSRFToken')) {
return $this->auth->verifyCSRFToken($token);
}
// Fallback for when auth service is not available
return isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $token;
}
protected function getRoutePath(string $routeName, array $data = [], array $queryParams = []): string
{
// Simple implementation matching the path_for function in templates
$basePath = '';
// Handle common route patterns
switch ($routeName) {
case 'home':
$basePath = '/';
break;
case 'games.index':
$basePath = '/media/games';
break;
case 'games.show':
$basePath = '/media/games/' . ($data['game_key'] ?? '');
break;
case 'movies.index':
$basePath = '/media/movies';
break;
case 'tvshows.index':
$basePath = '/media/tv-shows';
break;
case 'music.index':
$basePath = '/media/music';
break;
case 'admin.index':
$basePath = '/admin';
break;
case 'admin.playnite.import':
$basePath = '/admin/playnite/import';
break;
case 'admin.playnite.upload':
$basePath = '/admin/playnite/import';
break;
case 'admin.settings':
$basePath = '/admin/settings';
break;
case 'admin.sources':
$basePath = '/admin/sources';
break;
case 'admin.sync':
$basePath = '/admin/sync/' . ($data['id'] ?? '');
break;
case 'auth.login':
$basePath = '/login';
break;
case 'auth.logout':
$basePath = '/logout';
break;
case 'movies.show':
$basePath = '/media/movies/' . ($data['id'] ?? '');
break;
case 'tvshows.show':
$basePath = '/media/tv-shows/' . ($data['id'] ?? '');
break;
case 'music.show':
$basePath = '/media/music/' . ($data['id'] ?? '');
break;
case 'adult.index':
$basePath = '/media/adult';
break;
case 'adult.show':
$basePath = '/media/adult/' . ($data['id'] ?? '');
break;
case 'actors.index':
$basePath = '/media/actors';
break;
case 'actors.show':
$basePath = '/media/actors/' . ($data['id'] ?? '');
break;
case 'search.index':
$basePath = '/search';
break;
default:
$basePath = '/' . str_replace('.', '/', $routeName);
}
// Add query parameters
if (!empty($queryParams)) {
$basePath .= '?' . http_build_query($queryParams);
}
return $basePath;
}
}

View File

@@ -0,0 +1,239 @@
<?php
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Services\PlayniteImportService;
use Slim\Views\Twig;
class PlayniteImportController extends Controller
{
private \PDO $pdo;
private PlayniteImportService $importService;
protected $auth;
public function __construct(\PDO $pdo, Twig $view, $auth = null)
{
parent::__construct($view, $auth);
$this->pdo = $pdo;
$this->importService = new PlayniteImportService($pdo);
$this->auth = $auth;
}
/**
* Show the import form
*/
public function showImport(Request $request, Response $response, $args)
{
return $this->view->render($response, 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'csrf_token' => $this->generateCSRFToken()
]);
}
/**
* Handle file upload and preview
*/
public function upload(Request $request, Response $response, $args)
{
$data = $request->getParsedBody();
$csrfToken = $data['csrf_token'] ?? '';
// Verify CSRF token
if (!$this->verifyCSRFToken($csrfToken)) {
return $this->view->render($response->withStatus(400), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'Invalid CSRF token',
'csrf_token' => $this->generateCSRFToken()
]);
}
$uploadedFiles = $request->getUploadedFiles();
if (empty($uploadedFiles['playnite_file'])) {
return $this->view->render($response->withStatus(400), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'No file uploaded',
'csrf_token' => $this->generateCSRFToken()
]);
}
$file = $uploadedFiles['playnite_file'];
// Validate file
if ($file->getError() !== UPLOAD_ERR_OK) {
return $this->view->render($response->withStatus(400), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'Upload error: ' . $this->getUploadErrorMessage($file->getError()),
'csrf_token' => $this->generateCSRFToken()
]);
}
// Check file type
$filename = $file->getClientFilename();
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($extension, ['json'])) {
return $this->view->render($response->withStatus(400), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'Only JSON files are supported',
'csrf_token' => $this->generateCSRFToken()
]);
}
// Move uploaded file to temp location
$tempPath = sys_get_temp_dir() . '/playnite_import_' . uniqid() . '.json';
$file->moveTo($tempPath);
try {
// Parse and validate the file
$result = $this->importService->parsePlayniteFile($tempPath);
// Store the temp file path and results in session for the confirmation step
$_SESSION['playnite_import'] = [
'temp_file' => $tempPath,
'preview_data' => $result
];
return $this->view->render($response, 'admin/playnite/preview.twig', [
'title' => 'Preview Playnite Import',
'preview' => $result,
'filename' => $filename
]);
} catch (\Exception $e) {
// Clean up temp file
if (file_exists($tempPath)) {
unlink($tempPath);
}
return $this->view->render($response->withStatus(400), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'Error parsing file: ' . $e->getMessage(),
'csrf_token' => $this->generateCSRFToken()
]);
}
}
/**
* Confirm and execute the import
*/
public function confirm(Request $request, Response $response, $args)
{
if (!isset($_SESSION['playnite_import'])) {
return $response->withRedirect($this->getRoutePath('admin.playnite.import'));
}
$importData = $_SESSION['playnite_import'];
$tempPath = $importData['temp_file'];
$previewData = $importData['preview_data'];
// Get import options from form
$queryParams = $request->getQueryParams();
$updateExisting = ($queryParams['update_existing'] ?? 'false') === 'true';
try {
// Execute the import
$importResult = $this->importService->importGames($previewData['games'], $updateExisting);
// Clean up temp file
if (file_exists($tempPath)) {
unlink($tempPath);
}
// Clear session data
unset($_SESSION['playnite_import']);
return $this->view->render($response, 'admin/playnite/result.twig', [
'title' => 'Import Complete',
'import_result' => $importResult,
'preview_data' => $previewData
]);
} catch (\Exception $e) {
// Clean up temp file
if (file_exists($tempPath)) {
unlink($tempPath);
}
// Clear session data
unset($_SESSION['playnite_import']);
return $this->view->render($response->withStatus(500), 'admin/playnite/import.twig', [
'title' => 'Import Playnite Games',
'error' => 'Import failed: ' . $e->getMessage(),
'csrf_token' => $this->generateCSRFToken()
]);
}
}
/**
* Cancel the import (cleanup)
*/
public function cancel(Request $request, Response $response, $args)
{
if (isset($_SESSION['playnite_import'])) {
$tempPath = $_SESSION['playnite_import']['temp_file'];
if (file_exists($tempPath)) {
unlink($tempPath);
}
unset($_SESSION['playnite_import']);
}
return $response->withRedirect($this->getRoutePath('admin.playnite.import'));
}
/**
* API endpoint for programmatic imports
*/
public function apiImport(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'], $data['update_existing'] ?? true);
return $this->jsonResponse($response, [
'success' => true,
'result' => $importResult
]);
} catch (\Exception $e) {
return $this->jsonResponse($response->withStatus(500), [
'error' => $e->getMessage()
]);
}
}
/**
* Get upload error message
*/
private function getUploadErrorMessage(int $errorCode): string
{
switch ($errorCode) {
case UPLOAD_ERR_INI_SIZE:
return 'File too large (exceeds server limit)';
case UPLOAD_ERR_FORM_SIZE:
return 'File too large (exceeds form limit)';
case UPLOAD_ERR_PARTIAL:
return 'File only partially uploaded';
case UPLOAD_ERR_NO_FILE:
return 'No file uploaded';
case UPLOAD_ERR_NO_TMP_DIR:
return 'No temporary directory';
case UPLOAD_ERR_CANT_WRITE:
return 'Cannot write to disk';
case UPLOAD_ERR_EXTENSION:
return 'File upload stopped by extension';
default:
return 'Unknown upload error';
}
}
}

View File

@@ -25,9 +25,27 @@ class Game extends Model
'is_favorite',
'metadata',
'platform_achievements',
'platform_stats',
'source_id',
'last_played_at'
'background_image',
'cover_image',
'icon',
'genres_json',
'developers_json',
'publishers_json',
'tags_json',
'features_json',
'links_json',
'series_json',
'age_ratings_json',
'play_count',
'install_size',
'completion_status',
'critic_score',
'community_score',
'user_score',
'is_custom_game',
'installation_status',
'added_at',
'modified_at'
];
protected array $casts = [
@@ -39,7 +57,23 @@ class Game extends Model
'release_date' => 'date',
'last_played_at' => 'datetime',
'platform_achievements' => 'array',
'platform_stats' => 'array'
'critic_score' => 'int',
'community_score' => 'int',
'user_score' => 'int',
'play_count' => 'int',
'install_size' => 'int',
'installation_status' => 'int',
'is_custom_game' => 'bool',
'added_at' => 'datetime',
'modified_at' => 'datetime',
'genres_json' => 'array',
'developers_json' => 'array',
'publishers_json' => 'array',
'tags_json' => 'array',
'features_json' => 'array',
'links_json' => 'array',
'series_json' => 'array',
'age_ratings_json' => 'array'
];
public function source()
@@ -295,4 +329,110 @@ class Game extends Model
return $games;
}
/**
* Get Playnite-specific genres
*/
public function getGenres(): array
{
return $this->genres_json ?? [];
}
/**
* Get Playnite-specific developers
*/
public function getDevelopers(): array
{
return $this->developers_json ?? [];
}
/**
* Get Playnite-specific publishers
*/
public function getPublishers(): array
{
return $this->publishers_json ?? [];
}
/**
* Get Playnite-specific tags
*/
public function getTags(): array
{
return $this->tags_json ?? [];
}
/**
* Get Playnite-specific features
*/
public function getFeatures(): array
{
return $this->features_json ?? [];
}
/**
* Get Playnite-specific links
*/
public function getLinks(): array
{
return $this->links_json ?? [];
}
/**
* Get Playnite-specific series
*/
public function getSeries(): array
{
return $this->series_json ?? [];
}
/**
* Get Playnite-specific age ratings
*/
public function getAgeRatings(): array
{
return $this->age_ratings_json ?? [];
}
/**
* Get formatted install size
*/
public function getFormattedInstallSize(): string
{
if (!$this->install_size) {
return 'Unknown';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = $this->install_size;
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
/**
* Get Steam store URL if available
*/
public function getSteamUrl(): ?string
{
if (!$this->steam_app_id) {
return null;
}
return "https://store.steampowered.com/app/{$this->steam_app_id}";
}
/**
* Check if game has rich Playnite data
*/
public function hasPlayniteData(): bool
{
return !empty($this->genres_json) || !empty($this->tags_json) ||
!empty($this->links_json) || !empty($this->background_image);
}
}

View File

@@ -37,19 +37,32 @@ class TvShow extends Model
];
/**
* Get all actors associated with this TV show
* Remove an actor from this TV show
*/
public function actors()
public function removeActor(int $actorId): bool
{
$stmt = $this->pdo->prepare("
SELECT a.*
FROM actors a
JOIN actor_tv_show ats ON a.id = ats.actor_id
WHERE ats.tv_show_id = :tv_show_id
ORDER BY a.name ASC
DELETE FROM actor_tv_show
WHERE tv_show_id = :tv_show_id AND actor_id = :actor_id
");
$stmt->execute(['tv_show_id' => $this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
return $stmt->execute([
'tv_show_id' => $this->id,
'actor_id' => $actorId
]);
}
/**
* Update the cast field with actor names
*/
public function updateCastField(): bool
{
$actors = $this->actors();
$actorNames = array_column($actors, 'name');
$castString = implode(', ', $actorNames);
return $this->update($this->id, [
'cast' => $castString
]);
}
/**
@@ -151,35 +164,51 @@ class TvShow extends Model
}
public function getSeasonsWithEpisodes(): array
{
$stmt = $this->pdo->prepare("
SELECT s.*,
COUNT(e.id) as episode_count,
SUM(CASE WHEN e.watched = 1 THEN 1 ELSE 0 END) as watched_episodes
FROM seasons s
LEFT JOIN episodes e ON s.id = e.season_id
WHERE s.tv_show_id = :tv_show_id
GROUP BY s.id
ORDER BY s.season_number ASC
");
$stmt->execute(['tv_show_id' => $this->id]);
$seasons = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Get episodes for each season
foreach ($seasons as &$season) {
{
// Get all episodes for this TV show, grouped by season
$stmt = $this->pdo->prepare("
SELECT e.*,
(SELECT COUNT(*) FROM user_episodes WHERE episode_id = e.id AND watched = 1) as watch_count
FROM episodes e
WHERE e.season_id = :season_id
ORDER BY e.episode_number ASC
SELECT season_number,
COUNT(*) as episode_count,
SUM(CASE WHEN watched = 1 THEN 1 ELSE 0 END) as watched_episodes
FROM tv_episodes
WHERE tv_show_id = :tv_show_id
GROUP BY season_number
ORDER BY season_number ASC
");
$stmt->execute(['season_id' => $season['id']]);
$season['episodes'] = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
$stmt->execute(['tv_show_id' => $this->id]);
$seasonStats = $stmt->fetchAll(\PDO::FETCH_ASSOC);
return $seasons;
}
$seasons = [];
// For each season, get the episodes and create a season object
foreach ($seasonStats as $stat) {
$seasonNumber = $stat['season_number'];
// Get episodes for this season
$stmt = $this->pdo->prepare("
SELECT e.*
FROM tv_episodes e
WHERE e.tv_show_id = :tv_show_id AND e.season_number = :season_number
ORDER BY e.episode_number ASC
");
$stmt->execute([
'tv_show_id' => $this->id,
'season_number' => $seasonNumber
]);
$episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Create a season object (simulating the old seasons table structure)
$seasons[] = [
'id' => null, // No seasons table, so no ID
'season_number' => $seasonNumber,
'episode_count' => (int)$stat['episode_count'],
'watched_episodes' => (int)$stat['watched_episodes'],
'episodes' => $episodes
];
}
return $seasons;
}
/**
* Get similar TV shows based on genres

View File

@@ -0,0 +1,374 @@
<?php
namespace App\Services;
use App\Models\Game;
use App\Models\Source;
class PlayniteImportService
{
private \PDO $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Parse and validate a Playnite export file
*/
public function parsePlayniteFile(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \Exception("Playnite export file not found: {$filePath}");
}
$jsonContent = file_get_contents($filePath);
if ($jsonContent === false) {
throw new \Exception("Failed to read Playnite export file: {$filePath}");
}
$games = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception("Invalid JSON in Playnite export file: " . json_last_error_msg());
}
if (!is_array($games)) {
throw new \Exception("Playnite export file must contain an array of games");
}
return $this->validateAndTransformGames($games);
}
/**
* Validate and transform Playnite games data
*/
private function validateAndTransformGames(array $games): array
{
$transformedGames = [];
$errors = [];
$warnings = [];
foreach ($games as $index => $game) {
try {
$transformedGame = $this->transformPlayniteGame($game, $index + 1);
if ($transformedGame) {
$transformedGames[] = $transformedGame;
}
} catch (\Exception $e) {
$errors[] = "Game at index {$index}: " . $e->getMessage();
}
}
return [
'games' => $transformedGames,
'errors' => $errors,
'warnings' => $warnings,
'total' => count($games),
'valid' => count($transformedGames),
'invalid' => count($errors)
];
}
/**
* Transform a single Playnite game to our internal format
*/
private function transformPlayniteGame(array $game, int $index): array
{
// Validate required fields
if (empty($game['Name'])) {
throw new \Exception("Missing game name");
}
if (empty($game['GameId'])) {
throw new \Exception("Missing GameId");
}
// Find or create source
$source = $this->findOrCreateSource($game);
// Transform the game data
$transformed = [
'title' => $game['Name'],
'game_key' => $this->generateGameKey($game['Name'], $this->extractPlatformFromPlaynite($game)),
'description' => $this->cleanHtml($game['Description'] ?? ''),
'platform_game_id' => $game['GameId'],
'platform' => $this->extractPlatformFromPlaynite($game),
'source_id' => $source['id'],
// Rich media
'background_image' => $game['BackgroundImage'] ?? null,
'cover_image' => $game['CoverImage'] ?? null,
'icon' => $game['Icon'] ?? null,
// Multiple entities as JSON
'genres_json' => json_encode($game['Genres'] ?? []),
'developers_json' => json_encode($game['Developers'] ?? []),
'publishers_json' => json_encode($game['Publishers'] ?? []),
'tags_json' => json_encode($game['Tags'] ?? []),
'features_json' => json_encode($game['Features'] ?? []),
'links_json' => json_encode($game['Links'] ?? []),
'series_json' => json_encode($game['Series'] ?? []),
'age_ratings_json' => json_encode($game['AgeRatings'] ?? []),
// Play statistics
'playtime_minutes' => $this->parsePlaytime($game['Playtime'] ?? 0),
'play_count' => $game['PlayCount'] ?? 0,
'install_size' => $this->parseInstallSize($game['InstallSize'] ?? null),
'completion_status' => $game['CompletionStatus']['Name'] ?? null,
// Enhanced ratings
'rating' => $this->normalizeRating($game['CriticScore'] ?? null),
'critic_score' => $game['CriticScore'] ?? null,
'community_score' => $game['CommunityScore'] ?? null,
'user_score' => $game['UserScore'] ?? null,
// Legacy single-value fields (take first from arrays if available)
'genre' => $this->getFirstItemName($game['Genres'] ?? []),
'developer' => $this->getFirstItemName($game['Developers'] ?? []),
'publisher' => $this->getFirstItemName($game['Publishers'] ?? []),
// Platform-specific data
'steam_app_id' => $this->extractSteamAppId($game),
// Playnite-specific metadata
'is_installed' => $game['IsInstalled'] ?? false,
'is_favorite' => $game['Favorite'] ?? false,
'is_custom_game' => $game['IsCustomGame'] ?? false,
'installation_status' => $game['InstallationStatus'] ?? 0,
// 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,
'release_date' => isset($game['ReleaseDate']['ReleaseDate']) ? date('Y-m-d', strtotime($game['ReleaseDate']['ReleaseDate'])) : null,
// Playnite metadata
'metadata' => json_encode([
'playnite_id' => $game['Id'] ?? null,
'version' => $game['Version'] ?? null,
'hidden' => $game['Hidden'] ?? false,
'notes' => $game['Notes'] ?? null,
'manual' => $game['Manual'] ?? null,
'pre_script' => $game['PreScript'] ?? null,
'post_script' => $game['PostScript'] ?? null,
'game_started_script' => $game['GameStartedScript'] ?? null,
'use_global_scripts' => [
'pre' => $game['UseGlobalPreScript'] ?? true,
'post' => $game['UseGlobalPostScript'] ?? true,
'game_started' => $game['UseGlobalGameStartedScript'] ?? true
]
])
];
return $transformed;
}
/**
* Find or create a source for the game
*/
private function findOrCreateSource(array $game): array
{
$sourceName = $game['Source']['Name'] ?? 'Playnite';
$sourceId = $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 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);
}
return 'PC'; // Default platform
}
/**
* Extract Steam App ID from game links or metadata
*/
private function extractSteamAppId(array $game): ?string
{
if (isset($game['Links']) && is_array($game['Links'])) {
foreach ($game['Links'] as $link) {
if (isset($link['Name']) && strtolower($link['Name']) === 'steam' &&
preg_match('/\/app\/(\d+)/', $link['Url'], $matches)) {
return $matches[1];
}
}
}
return null;
}
/**
* 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;
}
/**
* Parse install size
*/
private function parseInstallSize($installSize): ?int
{
if (is_numeric($installSize)) {
return (int)$installSize;
}
return null;
}
/**
* 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);
}
/**
* Get first item name from an array of objects
*/
private function getFirstItemName(array $items): ?string
{
if (empty($items)) {
return null;
}
$first = reset($items);
return $first['Name'] ?? null;
}
/**
* Import games to database
*/
public function importGames(array $games, bool $updateExisting = true): array
{
$results = [
'imported' => 0,
'updated' => 0,
'skipped' => 0,
'errors' => []
];
foreach ($games as $gameData) {
try {
$existingGame = $this->findExistingGame($gameData);
if ($existingGame && $updateExisting) {
$this->updateGame($existingGame['id'], $gameData);
$results['updated']++;
} elseif (!$existingGame) {
$this->insertGame($gameData);
$results['imported']++;
} else {
$results['skipped']++;
}
} catch (\Exception $e) {
$results['errors'][] = "Failed to import {$gameData['title']}: " . $e->getMessage();
}
}
return $results;
}
/**
* 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;
}
/**
* Insert new game
*/
private function insertGame(array $gameData): void
{
// Use the Game model's create method which respects fillable fields
$gameModel = new Game($this->pdo);
$gameModel->create($gameData);
}
/**
* Update existing game
*/
private function updateGame(int $gameId, array $gameData): void
{
// Use the Game model's update method which respects fillable fields
$gameModel = new Game($this->pdo);
$gameModel->update($gameId, $gameData);
}
}