'float', 'playtime_minutes' => 'int', 'completion_percentage' => 'int', 'is_installed' => 'bool', 'is_favorite' => 'bool', 'release_date' => 'date', 'last_played_at' => 'datetime', 'platform_achievements' => '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() { return new Source($this->pdo); } public function markAsPlayed(int $minutes = 60): bool { $this->playtime_minutes += $minutes; $this->last_played_at = date('Y-m-d H:i:s'); return $this->update($this->id, [ 'playtime_minutes' => $this->playtime_minutes, 'last_played_at' => $this->last_played_at ]); } public function toggleFavorite(): bool { return $this->update($this->id, [ 'is_favorite' => !$this->is_favorite ]); } public function toggleInstalled(): bool { return $this->update($this->id, [ 'is_installed' => !$this->is_installed ]); } public function updateCompletion(int $percentage): bool { return $this->update($this->id, [ 'completion_percentage' => min(100, max(0, $percentage)) ]); } public function updateRating(float $rating): bool { return $this->update($this->id, [ 'rating' => min(10.0, max(0.0, $rating)) ]); } public static function getStats(\PDO $pdo): array { $stmt = $pdo->query(" SELECT COUNT(*) as total_games, SUM(playtime_minutes) as total_playtime, AVG(rating) as avg_rating, COUNT(CASE WHEN is_favorite = 1 THEN 1 END) as favorite_games, COUNT(CASE WHEN is_installed = 1 THEN 1 END) as installed_games, AVG(completion_percentage) as avg_completion FROM games "); return $stmt->fetch(\PDO::FETCH_ASSOC); } public static function getRecent(\PDO $pdo, int $limit = 10): array { $stmt = $pdo->prepare(" SELECT g.*, s.display_name as source_name FROM games g JOIN sources s ON g.source_id = s.id WHERE g.last_played_at IS NOT NULL ORDER BY g.last_played_at DESC LIMIT :limit "); $stmt->execute(['limit' => $limit]); return $stmt->fetchAll(\PDO::FETCH_ASSOC); } /** * Generate a game key for grouping games across platforms */ public static function generateGameKey(string $title, string $platform = null): string { // Normalize title for consistent grouping $normalized = strtolower(trim($title)); $normalized = preg_replace('/[^\w\s]/', '', $normalized); // Remove special characters $normalized = preg_replace('/\s+/', ' ', $normalized); // Normalize whitespace $normalized = trim($normalized); // Add platform to make it unique if provided if ($platform) { $normalized .= ' ' . strtolower($platform); } return $normalized; } /** * Find all platform versions of a game */ public function getPlatformVersions(): array { if (!$this->game_key) { return []; } $stmt = $this->pdo->prepare(" SELECT g.*, s.display_name as source_name FROM games g JOIN sources s ON g.source_id = s.id WHERE g.game_key = :game_key ORDER BY g.platform, g.source_id "); $stmt->execute(['game_key' => $this->game_key]); return $stmt->fetchAll(\PDO::FETCH_ASSOC); } /** * Get games grouped by title for display */ public static function getGroupedGames(\PDO $pdo, int $limit = 50): array { $stmt = $pdo->prepare(" SELECT game_key, title, COUNT(*) as platform_count, GROUP_CONCAT(DISTINCT platform ORDER BY platform) as platforms, GROUP_CONCAT(DISTINCT source_id ORDER BY source_id) as source_ids, MAX(image_url) as image_url, MAX(last_played_at) as last_played_at, SUM(playtime_minutes) as total_playtime, MAX(completion_percentage) as max_completion, GROUP_CONCAT(DISTINCT genre ORDER BY genre) as genres FROM games WHERE game_key IS NOT NULL GROUP BY game_key, title ORDER BY last_played_at DESC, total_playtime DESC LIMIT :limit "); $stmt->execute(['limit' => $limit]); $games = $stmt->fetchAll(\PDO::FETCH_ASSOC); // Enhance each game with platform details foreach ($games as &$game) { $game['platforms'] = array_unique(explode(',', $game['platforms'])); $game['source_ids'] = array_unique(explode(',', $game['source_ids'])); $game['genres'] = array_unique(array_filter(explode(',', $game['genres']))); } return $games; } /** * Update platform-specific achievements */ public function updatePlatformAchievements(array $achievements): bool { return $this->update($this->id, [ 'platform_achievements' => json_encode($achievements) ]); } /** * Update platform-specific statistics */ public function updatePlatformStats(array $stats): bool { return $this->update($this->id, [ 'platform_stats' => json_encode($stats) ]); } /** * Get platform-specific achievements */ public function getPlatformAchievements(): array { return $this->platform_achievements ?? []; } /** * Get platform-specific statistics */ public function getPlatformStats(): array { return $this->platform_stats ?? []; } /** * Get total count of games for pagination */ public static function getTotalCount(\PDO $pdo, string $search = ''): int { $sql = "SELECT COUNT(*) as count FROM games"; $params = []; if (!empty($search)) { $sql .= " WHERE title LIKE :search"; $params['search'] = "%{$search}%"; } $stmt = $pdo->prepare($sql); $stmt->execute($params); return (int) $stmt->fetch()['count']; } /** * Get grouped games with pagination and search support */ public static function getGroupedGamesWithPagination(\PDO $pdo, int $page, int $perPage, string $search = ''): array { $offset = ($page - 1) * $perPage; $sql = " SELECT game_key, title, COUNT(*) as platform_count, GROUP_CONCAT(DISTINCT platform ORDER BY platform) as platforms, GROUP_CONCAT(DISTINCT source_id ORDER BY source_id) as source_ids, MAX(image_url) as image_url, MAX(last_played_at) as last_played_at, SUM(playtime_minutes) as total_playtime, MAX(completion_percentage) as max_completion, GROUP_CONCAT(DISTINCT genre ORDER BY genre) as genres FROM games WHERE game_key IS NOT NULL "; $params = []; if (!empty($search)) { $sql .= " AND title LIKE :search"; $params['search'] = "%{$search}%"; } $sql .= " GROUP BY game_key, title ORDER BY last_played_at DESC, total_playtime DESC LIMIT :limit OFFSET :offset"; $stmt = $pdo->prepare($sql); $stmt->bindValue(':limit', $perPage, \PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT); foreach ($params as $key => $value) { $stmt->bindValue(":{$key}", $value); } $stmt->execute(); $games = $stmt->fetchAll(\PDO::FETCH_ASSOC); // Enhance each game with platform details foreach ($games as &$game) { $game['platforms'] = array_unique(explode(',', $game['platforms'])); $game['source_ids'] = array_unique(explode(',', $game['source_ids'])); $game['genres'] = array_unique(array_filter(explode(',', $game['genres']))); } 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); } }