Remove obsolete test scripts and add new API controllers for dashboard and game management

- Deleted test scripts: test_jellyfin_execution.php, test_stash.php, test_xbvr.php, test_xbvr_sync.php, vite.config.js
- Added DashboardController for fetching dashboard statistics and recent activity
- Added GameController for managing games, including fetching all games, game details, and games by category
- Introduced various check scripts to validate database structures and data integrity for adult videos, games, gender data, posters, and TV show actors
This commit is contained in:
Lars Behrends
2026-01-18 01:42:03 +01:00
parent b728b0c72d
commit eb1ec1153d
29 changed files with 2685 additions and 2454 deletions

3
.gitignore vendored
View File

@@ -149,3 +149,6 @@ composer.lock
/public/public/images/backdrops /public/public/images/backdrops
/public/public/images/posters /public/public/images/posters
/storage/images /storage/images
/frontend/

View File

@@ -8,6 +8,35 @@ use App\Controllers\Controller;
class BaseApiController extends Controller class BaseApiController extends Controller
{ {
protected function getPdo(): \PDO
{
// Get PDO from the container - this assumes PDO is registered in the DI container
global $container;
if ($container && $container->has('pdo')) {
return $container->get('pdo');
}
// Fallback to creating a new PDO connection
$host = $_ENV['DB_HOST'] ?? 'localhost';
$dbname = $_ENV['DB_NAME'] ?? 'medialib';
$username = $_ENV['DB_USER'] ?? 'root';
$password = $_ENV['DB_PASS'] ?? '';
try {
return new \PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password,
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (\PDOException $e) {
throw new \Exception('Database connection failed: ' . $e->getMessage());
}
}
protected function success(Response $response, $data = null, int $status = 200): Response protected function success(Response $response, $data = null, int $status = 200): Response
{ {
$responseData = ['success' => true]; $responseData = ['success' => true];

View File

@@ -0,0 +1,206 @@
<?php
namespace App\Controllers\Api;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Controllers\Api\ApiController;
class DashboardController extends ApiController
{
private \PDO $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Get dashboard statistics
*/
public function getStats(Request $request, Response $response): Response
{
try {
$stats = [];
// Get movies count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM movies");
$stmt->execute();
$moviesCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'Total Movies',
'value' => number_format($moviesCount),
'icon' => 'FilmIcon',
'color' => 'bg-blue-500',
'href' => '/movies'
];
// Get TV shows count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM tv_shows");
$stmt->execute();
$tvShowsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'TV Shows',
'value' => number_format($tvShowsCount),
'icon' => 'TvIcon',
'color' => 'bg-purple-500',
'href' => '/tvshows'
];
// Get games count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM games");
$stmt->execute();
$gamesCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'Games',
'value' => number_format($gamesCount),
'icon' => 'GamepadIcon',
'color' => 'bg-green-500',
'href' => '/games'
];
// Get music albums count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM music_albums");
$stmt->execute();
$albumsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'Music Albums',
'value' => number_format($albumsCount),
'icon' => 'MusicalNoteIcon',
'color' => 'bg-pink-500',
'href' => '/music'
];
// Get adult videos count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM adult_videos");
$stmt->execute();
$adultCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'Adult Videos',
'value' => number_format($adultCount),
'icon' => 'LockClosedIcon',
'color' => 'bg-red-500',
'href' => '/adult'
];
// Get actors count
$stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM actors");
$stmt->execute();
$actorsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
$stats[] = [
'name' => 'Actors',
'value' => number_format($actorsCount),
'icon' => 'UserIcon',
'color' => 'bg-indigo-500',
'href' => '/actors'
];
return $this->success($response, $stats);
} catch (\Exception $e) {
return $this->error($response, 'Failed to fetch dashboard stats', 500);
}
}
/**
* Get recent activity
*/
public function getRecentActivity(Request $request, Response $response): Response
{
try {
$activities = [];
// Get recent movies (last 5)
$stmt = $this->pdo->prepare("
SELECT 'Added movie' as action, title as item,
DATE_FORMAT(created_at, '%b %d, %Y') as time,
'movie' as type
FROM movies
ORDER BY created_at DESC
LIMIT 5
");
$stmt->execute();
$recentMovies = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$activities = array_merge($activities, $recentMovies);
// Get recent TV shows (last 5)
$stmt = $this->pdo->prepare("
SELECT 'Added TV show' as action, title as item,
DATE_FORMAT(created_at, '%b %d, %Y') as time,
'tvshow' as type
FROM tv_shows
ORDER BY created_at DESC
LIMIT 5
");
$stmt->execute();
$recentTvShows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$activities = array_merge($activities, $recentTvShows);
// Get recent games (last 5)
$stmt = $this->pdo->prepare("
SELECT 'Added game' as action, title as item,
DATE_FORMAT(created_at, '%b %d, %Y') as time,
'game' as type
FROM games
ORDER BY created_at DESC
LIMIT 5
");
$stmt->execute();
$recentGames = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$activities = array_merge($activities, $recentGames);
// Get recent music albums (last 5)
$stmt = $this->pdo->prepare("
SELECT 'Added album' as action, title as item,
DATE_FORMAT(created_at, '%b %d, %Y') as time,
'music' as type
FROM music_albums
ORDER BY created_at DESC
LIMIT 5
");
$stmt->execute();
$recentAlbums = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$activities = array_merge($activities, $recentAlbums);
// Sort all activities by time (most recent first)
usort($activities, function($a, $b) {
return strtotime($b['time']) - strtotime($a['time']);
});
// Take only the 10 most recent activities
$activities = array_slice($activities, 0, 10);
// Format time to be more relative
foreach ($activities as &$activity) {
$activity['id'] = uniqid();
$activity['time'] = $this->formatRelativeTime($activity['time']);
}
return $this->success($response, $activities);
} catch (\Exception $e) {
return $this->error($response, 'Failed to fetch recent activity', 500);
}
}
/**
* Format time to relative format (simplified version)
*/
private function formatRelativeTime($dateString): string
{
$date = strtotime($dateString);
$now = time();
$diff = $now - $date;
if ($diff < 3600) {
$minutes = floor($diff / 60);
return $minutes <= 1 ? 'Just now' : "$minutes minutes ago";
} elseif ($diff < 86400) {
$hours = floor($diff / 3600);
return $hours <= 1 ? '1 hour ago' : "$hours hours ago";
} elseif ($diff < 604800) {
$days = floor($diff / 86400);
return $days <= 1 ? '1 day ago' : "$days days ago";
} else {
return date('M j, Y', $date);
}
}
}

View File

@@ -0,0 +1,364 @@
<?php
namespace App\Controllers\Api;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Models\Game;
use App\Controllers\Api\BaseApiController;
class GameController extends BaseApiController
{
/**
* Get all games grouped by completion status
*/
public function index(Request $request, Response $response, array $args): Response
{
$queryParams = $request->getQueryParams();
$search = $queryParams['search'] ?? '';
$sort = $queryParams['sort'] ?? 'title_asc';
try {
// Get games from database
$games = $this->getAllGamesWithCategories($search, $sort);
// Group games by completion status
$groupedGames = $this->groupGamesByCategory($games);
return $this->json($response, [
'success' => true,
'data' => $groupedGames
]);
} catch (\Exception $e) {
return $this->json($response, [
'success' => false,
'message' => 'Failed to fetch games: ' . $e->getMessage()
], 500);
}
}
/**
* Get game by ID
*/
public function show(Request $request, Response $response, array $args): Response
{
$gameId = $args['id'] ?? null;
if (!$gameId) {
return $this->json($response, [
'success' => false,
'message' => 'Game ID is required'
], 400);
}
try {
$game = $this->getGameDetails($gameId);
if (!$game) {
return $this->json($response, [
'success' => false,
'message' => 'Game not found'
], 404);
}
return $this->json($response, [
'success' => true,
'data' => $game
]);
} catch (\Exception $e) {
return $this->json($response, [
'success' => false,
'message' => 'Failed to fetch game: ' . $e->getMessage()
], 500);
}
}
/**
* Get games by category (BEATEN, PLAYING, etc.)
*/
public function getByCategory(Request $request, Response $response, array $args): Response
{
$category = strtoupper($args['category'] ?? '');
$queryParams = $request->getQueryParams();
$search = $queryParams['search'] ?? '';
$sort = $queryParams['sort'] ?? 'title_asc';
if (!in_array($category, ['BEATEN', 'PLAYING', 'COMPLETED', 'UNPLAYED'])) {
return $this->json($response, [
'success' => false,
'message' => 'Invalid category. Must be one of: BEATEN, PLAYING, COMPLETED, UNPLAYED'
], 400);
}
try {
$games = $this->getGamesByCategory($category, $search, $sort);
return $this->json($response, [
'success' => true,
'data' => [
'category' => $category,
'games' => $games,
'count' => count($games)
]
]);
} catch (\Exception $e) {
return $this->json($response, [
'success' => false,
'message' => 'Failed to fetch games by category: ' . $e->getMessage()
], 500);
}
}
/**
* Get all games from database
*/
private function getAllGamesWithCategories(string $search = '', string $sort = 'title_asc'): array
{
$pdo = $this->getPdo();
$sql = "
SELECT
g.id,
g.title,
g.poster_url,
g.backdrop_url,
g.rating,
g.release_date,
g.platform,
g.developer,
g.genres,
g.playtime_hours,
g.completion_status,
g.last_played,
g.community_score,
g.critic_score,
g.source_name
FROM games g
WHERE 1=1
";
$params = [];
// Add search filter
if (!empty($search)) {
$sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)";
$params[':search'] = '%' . $search . '%';
}
// Add sorting
switch ($sort) {
case 'title_desc':
$sql .= " ORDER BY g.title DESC";
break;
case 'year_asc':
$sql .= " ORDER BY g.release_date ASC";
break;
case 'year_desc':
$sql .= " ORDER BY g.release_date DESC";
break;
case 'playtime_desc':
$sql .= " ORDER BY g.playtime_hours DESC";
break;
case 'rating_desc':
$sql .= " ORDER BY g.rating DESC";
break;
case 'last_played_desc':
$sql .= " ORDER BY g.last_played DESC";
break;
default:
$sql .= " ORDER BY g.title ASC";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$games = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
// Parse genres if stored as JSON
if (!empty($row['genres'])) {
$genres = json_decode($row['genres'], true);
$row['genres'] = is_array($genres) ? $genres : [];
} else {
$row['genres'] = [];
}
// Set default completion status
if (empty($row['completion_status'])) {
$row['completion_status'] = 'UNPLAYED';
}
$games[] = $row;
}
return $games;
}
/**
* Group games by completion status
*/
private function groupGamesByCategory(array $games): array
{
$categories = [
'BEATEN' => ['name' => 'BEATEN', 'count' => 0, 'games' => []],
'PLAYING' => ['name' => 'PLAYING', 'count' => 0, 'games' => []],
'COMPLETED' => ['name' => 'COMPLETED', 'count' => 0, 'games' => []],
'UNPLAYED' => ['name' => 'UNPLAYED', 'count' => 0, 'games' => []]
];
foreach ($games as $game) {
$status = $game['completion_status'] ?? 'UNPLAYED';
if (isset($categories[$status])) {
$categories[$status]['games'][] = $game;
$categories[$status]['count']++;
}
}
return array_values($categories);
}
/**
* Get games by specific category
*/
private function getGamesByCategory(string $category, string $search = '', string $sort = 'title_asc'): array
{
$pdo = $this->getPdo();
$sql = "
SELECT
g.id,
g.title,
g.poster_url,
g.backdrop_url,
g.rating,
g.release_date,
g.platform,
g.developer,
g.genres,
g.playtime_hours,
g.completion_status,
g.last_played,
g.community_score,
g.critic_score,
g.source_name
FROM games g
WHERE g.completion_status = :category
";
$params = [':category' => $category];
// Add search filter
if (!empty($search)) {
$sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)";
$params[':search'] = '%' . $search . '%';
}
// Add sorting
switch ($sort) {
case 'title_desc':
$sql .= " ORDER BY g.title DESC";
break;
case 'year_asc':
$sql .= " ORDER BY g.release_date ASC";
break;
case 'year_desc':
$sql .= " ORDER BY g.release_date DESC";
break;
case 'playtime_desc':
$sql .= " ORDER BY g.playtime_hours DESC";
break;
case 'rating_desc':
$sql .= " ORDER BY g.rating DESC";
break;
case 'last_played_desc':
$sql .= " ORDER BY g.last_played DESC";
break;
default:
$sql .= " ORDER BY g.title ASC";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$games = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
// Parse genres if stored as JSON
if (!empty($row['genres'])) {
$genres = json_decode($row['genres'], true);
$row['genres'] = is_array($genres) ? $genres : [];
} else {
$row['genres'] = [];
}
$games[] = $row;
}
return $games;
}
/**
* Get detailed game information
*/
private function getGameDetails(int $gameId): ?array
{
$pdo = $this->getPdo();
$sql = "
SELECT
g.id,
g.title,
g.poster_url,
g.backdrop_url,
g.rating,
g.release_date,
g.platform,
g.developer,
g.publisher,
g.genres,
g.playtime_hours,
g.completion_status,
g.last_played,
g.community_score,
g.critic_score,
g.source_name,
g.description,
g.gameplay,
g.synopsis,
g.age_ratings,
g.version,
g.time_to_beat,
g.controls,
g.pacing,
g.perspective,
g.series
FROM games g
WHERE g.id = :id
";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $gameId]);
$game = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$game) {
return null;
}
// Parse JSON fields
$jsonFields = ['genres', 'age_ratings'];
foreach ($jsonFields as $field) {
if (!empty($game[$field])) {
$decoded = json_decode($game[$field], true);
$game[$field] = is_array($decoded) ? $decoded : [];
} else {
$game[$field] = [];
}
}
// Set default completion status
if (empty($game['completion_status'])) {
$game['completion_status'] = 'UNPLAYED';
}
return $game;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,139 @@ class Actor extends Model
'metadata' => 'array' 'metadata' => 'array'
]; ];
/**
* Get all actors with filtering and pagination
*/
public function findAll(array $filters = [], int $limit = null, int $offset = 0): array
{
$sql = "
SELECT a.*,
COUNT(DISTINCT am.movie_id) as movie_count,
COUNT(DISTINCT te.tv_show_id) as tv_show_count,
COUNT(DISTINCT aav.adult_video_id) as adult_video_count
FROM actors a
LEFT JOIN actor_movie am ON a.id = am.actor_id
LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id
LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "a.name LIKE :search";
$params['search'] = "%{$filters['search']}%";
}
// Gender filter
if (!empty($filters['gender'])) {
$whereClauses[] = "JSON_UNQUOTE(JSON_EXTRACT(a.metadata, '$.gender')) = :gender";
$params['gender'] = strtoupper($filters['gender']);
}
// Adult filter
if (isset($filters['adult'])) {
if ($filters['adult']) {
$whereClauses[] = "aav.adult_video_id IS NOT NULL";
} else {
$whereClauses[] = "aav.adult_video_id IS NULL";
}
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$sql .= " GROUP BY a.id";
// Add sorting
$sortBy = $filters['sort'] ?? 'name';
$sortOrder = $filters['order'] ?? 'asc';
switch ($sortBy) {
case 'name':
$sql .= " ORDER BY a.name {$sortOrder}";
break;
case 'age':
$sql .= " ORDER BY JSON_EXTRACT(a.metadata, '$.age') {$sortOrder}";
break;
case 'media_count':
$sql .= " ORDER BY (COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT te.tv_show_id) + COUNT(DISTINCT aav.adult_video_id)) {$sortOrder}";
break;
case 'movie_count':
$sql .= " ORDER BY COUNT(DISTINCT am.movie_id) {$sortOrder}";
break;
case 'tv_show_count':
$sql .= " ORDER BY COUNT(DISTINCT te.tv_show_id) {$sortOrder}";
break;
case 'adult_count':
$sql .= " ORDER BY COUNT(DISTINCT aav.adult_video_id) {$sortOrder}";
break;
default:
$sql .= " ORDER BY a.name ASC";
}
if ($limit) {
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Count actors with filters
*/
public function count(array $filters = []): int
{
$sql = "
SELECT COUNT(DISTINCT a.id) as total
FROM actors a
LEFT JOIN actor_movie am ON a.id = am.actor_id
LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id
LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "a.name LIKE :search";
$params['search'] = "%{$filters['search']}%";
}
// Gender filter
if (!empty($filters['gender'])) {
$whereClauses[] = "JSON_UNQUOTE(JSON_EXTRACT(a.metadata, '$.gender')) = :gender";
$params['gender'] = strtoupper($filters['gender']);
}
// Adult filter
if (isset($filters['adult'])) {
if ($filters['adult']) {
$whereClauses[] = "aav.adult_video_id IS NOT NULL";
} else {
$whereClauses[] = "aav.adult_video_id IS NULL";
}
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return (int)$result['total'];
}
/** /**
* Get all movies this actor is associated with * Get all movies this actor is associated with
*/ */
@@ -29,24 +162,83 @@ class Actor extends Model
ORDER BY m.release_date DESC, m.title ASC ORDER BY m.release_date DESC, m.title ASC
"); ");
$stmt->execute(['actor_id' => $this->id]); $stmt->execute(['actor_id' => $this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC); $movies = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Process poster URLs for each movie
foreach ($movies as &$movie) {
// Use poster_url field directly if available
if (!empty($movie['poster_url'])) {
// Keep the existing poster_url as-is since it's already in the correct format
}
// Also process metadata for additional fields
if (!empty($movie['metadata'])) {
$metadata = json_decode($movie['metadata'], true);
// Extract poster aspect ratio from metadata if available
if (!empty($metadata['poster_aspect_ratio'])) {
$movie['poster_aspect_ratio'] = $metadata['poster_aspect_ratio'];
}
// If no poster_url in main field, try to get it from metadata
if (empty($movie['poster_url'])) {
if (!empty($metadata['local_cover_path'])) {
$movie['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$movie['poster_url'] = $metadata['cover_url'];
}
}
}
}
return $movies;
} }
/** /**
* Get all TV shows this actor is associated with * Get all TV shows this actor is associated with (via episodes)
*/ */
public function tvShows() public function tvShows()
{ {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
SELECT ts.*, s.display_name as source_name SELECT DISTINCT ts.*, s.display_name as source_name
FROM tv_shows ts FROM tv_shows ts
JOIN sources s ON ts.source_id = s.id JOIN sources s ON ts.source_id = s.id
JOIN actor_tv_show ats ON ts.id = ats.tv_show_id JOIN tv_episodes te ON ts.id = te.tv_show_id
WHERE ats.actor_id = :actor_id JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id
WHERE ate.actor_id = :actor_id
ORDER BY ts.first_air_date DESC, ts.title ASC ORDER BY ts.first_air_date DESC, ts.title ASC
"); ");
$stmt->execute(['actor_id' => $this->id]); $stmt->execute(['actor_id' => $this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC); $tvShows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Process poster URLs for each TV show
foreach ($tvShows as &$tvShow) {
// Use poster_url field directly if available
if (!empty($tvShow['poster_url'])) {
// Keep the existing poster_url as-is since it's already in the correct format
}
// Also process metadata for additional fields
if (!empty($tvShow['metadata'])) {
$metadata = json_decode($tvShow['metadata'], true);
// Extract poster aspect ratio from metadata if available
if (!empty($metadata['poster_aspect_ratio'])) {
$tvShow['poster_aspect_ratio'] = $metadata['poster_aspect_ratio'];
}
// If no poster_url in main field, try to get it from metadata
if (empty($tvShow['poster_url'])) {
if (!empty($metadata['local_cover_path'])) {
$tvShow['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$tvShow['poster_url'] = $metadata['cover_url'];
}
}
}
}
return $tvShows;
} }
/** /**
@@ -63,7 +255,41 @@ class Actor extends Model
ORDER BY av.release_date DESC, av.title ASC ORDER BY av.release_date DESC, av.title ASC
"); ");
$stmt->execute(['actor_id' => $this->id]); $stmt->execute(['actor_id' => $this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC); $adultVideos = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Process poster URLs for each adult video
foreach ($adultVideos as &$adultVideo) {
// Use poster_url field directly if available
if (!empty($adultVideo['poster_url'])) {
// Keep the existing poster_url as-is since it's already in the correct format
}
// Also process metadata for additional fields
if (!empty($adultVideo['metadata'])) {
$metadata = json_decode($adultVideo['metadata'], true);
// Extract poster aspect ratio from metadata if available
if (!empty($metadata['poster_aspect_ratio'])) {
$adultVideo['poster_aspect_ratio'] = $metadata['poster_aspect_ratio'];
}
// If no poster_url in main field, try to get it from metadata
if (empty($adultVideo['poster_url'])) {
if (!empty($metadata['local_cover_path'])) {
$adultVideo['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$adultVideo['poster_url'] = $metadata['cover_url'];
}
}
// Add screenshot URL if available
if (!empty($metadata['screenshot_url'])) {
$adultVideo['screenshot_url'] = $metadata['screenshot_url'];
}
}
}
return $adultVideos;
} }
/** /**
@@ -74,12 +300,13 @@ class Actor extends Model
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
SELECT SELECT
COUNT(DISTINCT am.movie_id) as movie_count, COUNT(DISTINCT am.movie_id) as movie_count,
COUNT(DISTINCT ats.tv_show_id) as tv_show_count, COUNT(DISTINCT te.tv_show_id) as tv_show_count,
COUNT(DISTINCT aav.adult_video_id) as adult_video_count, COUNT(DISTINCT aav.adult_video_id) as adult_video_count,
COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id) + COUNT(DISTINCT aav.adult_video_id) as total_media_count COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT te.tv_show_id) + COUNT(DISTINCT aav.adult_video_id) as total_media_count
FROM actors a FROM actors a
LEFT JOIN actor_movie am ON a.id = am.actor_id LEFT JOIN actor_movie am ON a.id = am.actor_id
LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id
LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
WHERE a.id = :actor_id WHERE a.id = :actor_id
"); ");
@@ -196,8 +423,8 @@ class Actor extends Model
// Build ORDER BY clause // Build ORDER BY clause
$orderBy = match ($sort) { $orderBy = match ($sort) {
'name_desc' => 'name DESC', 'name_desc' => 'name DESC',
'media_desc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) DESC', 'media_desc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) DESC',
'media_asc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) ASC', 'media_asc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) ASC',
default => 'name ASC' default => 'name ASC'
}; };
@@ -206,7 +433,7 @@ class Actor extends Model
SELECT SELECT
a.*, a.*,
(SELECT COUNT(*) FROM actor_movie WHERE actor_id = a.id) as movie_count, (SELECT COUNT(*) FROM actor_movie WHERE actor_id = a.id) as movie_count,
(SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = a.id) as tv_show_count, (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = a.id) as tv_show_count,
(SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = a.id) as adult_video_count (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = a.id) as adult_video_count
FROM actors a FROM actors a
{$whereClause} {$whereClause}
@@ -279,10 +506,11 @@ class Actor extends Model
private function getActorTvShows(int $actorId): array private function getActorTvShows(int $actorId): array
{ {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
SELECT ts.id, ts.title SELECT DISTINCT ts.id, ts.title
FROM tv_shows ts FROM tv_shows ts
JOIN actor_tv_show ats ON ts.id = ats.tv_show_id JOIN tv_episodes te ON ts.id = te.tv_show_id
WHERE ats.actor_id = ? JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id
WHERE ate.actor_id = ?
ORDER BY ts.title ORDER BY ts.title
"); ");
$stmt->execute([$actorId]); $stmt->execute([$actorId]);

View File

@@ -230,7 +230,7 @@ class AdultVideo extends Model
*/ */
public function updateCastField(): bool public function updateCastField(): bool
{ {
$actors = $this->actors(); $actors = $this->actors($this->id);
$actorNames = array_column($actors, 'name'); $actorNames = array_column($actors, 'name');
$castString = implode(', ', $actorNames); $castString = implode(', ', $actorNames);
@@ -239,6 +239,194 @@ class AdultVideo extends Model
]); ]);
} }
/**
* Find single adult video by ID with metadata processing
*/
public function find(int $id): ?array
{
$stmt = $this->pdo->prepare("
SELECT av.*, s.display_name as source_name
FROM adult_videos av
JOIN sources s ON av.source_id = s.id
WHERE av.id = :id
");
$stmt->execute(['id' => $id]);
$video = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$video) {
return null;
}
// Process metadata to extract additional fields
if (!empty($video['metadata'])) {
$metadata = json_decode($video['metadata'], true);
// Extract poster aspect ratio from metadata if available
if (!empty($metadata['poster_aspect_ratio'])) {
$video['poster_aspect_ratio'] = $metadata['poster_aspect_ratio'];
}
// Use local cover path if available, otherwise fall back to original URL
if (!empty($metadata['local_cover_path'])) {
$video['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$video['poster_url'] = $metadata['cover_url'];
}
// Add screenshot URL if available
if (!empty($metadata['screenshot_url'])) {
$video['screenshot_url'] = $metadata['screenshot_url'];
}
// Add actors data if available
if (!empty($metadata['actors'])) {
$video['actors'] = $metadata['actors'];
}
}
return $video;
}
/**
* Get all adult videos with filtering and pagination
*/
public function findAll(array $filters = [], int $limit = null, int $offset = 0): array
{
$sql = "
SELECT av.*, s.display_name as source_name
FROM adult_videos av
JOIN sources s ON av.source_id = s.id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "(av.title LIKE :search OR av.overview LIKE :search)";
$params['search'] = "%{$filters['search']}%";
}
// Genre filter
if (!empty($filters['genre'])) {
$whereClauses[] = "av.genre = :genre";
$params['genre'] = $filters['genre'];
}
// Year filter
if (!empty($filters['year'])) {
$whereClauses[] = "YEAR(av.release_date) = :year";
$params['year'] = $filters['year'];
}
// Source filter
if (!empty($filters['sources'])) {
$sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']];
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")";
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
// Add ordering and pagination
$sql .= " ORDER BY av.release_date DESC, av.title ASC";
if ($limit) {
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Process metadata to extract additional fields
foreach ($results as &$video) {
if (!empty($video['metadata'])) {
$metadata = json_decode($video['metadata'], true);
// Extract poster aspect ratio from metadata if available
if (!empty($metadata['poster_aspect_ratio'])) {
$video['poster_aspect_ratio'] = $metadata['poster_aspect_ratio'];
}
// Use local cover path if available, otherwise fall back to original URL
if (!empty($metadata['local_cover_path'])) {
$video['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$video['poster_url'] = $metadata['cover_url'];
}
// Add actors data if available
if (!empty($metadata['actors'])) {
$video['actors'] = $metadata['actors'];
}
}
}
return $results;
}
/**
* Count adult videos with filters
*/
public function count(array $filters = []): int
{
$sql = "
SELECT COUNT(*) as total
FROM adult_videos av
JOIN sources s ON av.source_id = s.id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "(av.title LIKE :search OR av.overview LIKE :search)";
$params['search'] = "%{$filters['search']}%";
}
// Genre filter
if (!empty($filters['genre'])) {
$whereClauses[] = "av.genre = :genre";
$params['genre'] = $filters['genre'];
}
// Year filter
if (!empty($filters['year'])) {
$whereClauses[] = "YEAR(av.release_date) = :year";
$params['year'] = $filters['year'];
}
// Source filter
if (!empty($filters['sources'])) {
$sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']];
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")";
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return (int)$result['total'];
}
/** /**
* Get available genres for filtering * Get available genres for filtering
*/ */

View File

@@ -356,6 +356,7 @@ class TvShow extends Model
public function getSeasonsWithEpisodes(): array public function getSeasonsWithEpisodes(): array
{ {
try {
// Get all episodes for this TV show, grouped by season // Get all episodes for this TV show, grouped by season
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
SELECT season_number, SELECT season_number,
@@ -390,6 +391,7 @@ class TvShow extends Model
// Add actors for each episode // Add actors for each episode
foreach ($episodes as &$episode) { foreach ($episodes as &$episode) {
try {
$episodeStmt = $this->pdo->prepare(" $episodeStmt = $this->pdo->prepare("
SELECT a.* SELECT a.*
FROM actors a FROM actors a
@@ -399,6 +401,9 @@ class TvShow extends Model
"); ");
$episodeStmt->execute(['tv_episode_id' => $episode['id']]); $episodeStmt->execute(['tv_episode_id' => $episode['id']]);
$episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC); $episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Exception $e) {
$episode['actors'] = [];
}
} }
// Create a season object (simulating the old seasons table structure) // Create a season object (simulating the old seasons table structure)
@@ -412,6 +417,32 @@ class TvShow extends Model
} }
return $seasons; return $seasons;
} catch (\Exception $e) {
// Return empty array if tables don't exist or query fails
return [];
}
}
/**
* Get all actors for this TV show (from all episodes)
*/
public function getActors(): array
{
try {
$stmt = $this->pdo->prepare("
SELECT DISTINCT a.*
FROM actors a
JOIN actor_tv_episode ate ON a.id = ate.actor_id
JOIN tv_episodes te ON ate.tv_episode_id = te.id
WHERE te.tv_show_id = :tv_show_id
ORDER BY a.name ASC
");
$stmt->execute(['tv_show_id' => $this->id]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Exception $e) {
// Return empty array if tables don't exist or query fails
return [];
}
} }
/** /**
@@ -471,6 +502,147 @@ public static function getAvailableGenres(\PDO $pdo): array
return $stmt->fetchAll(\PDO::FETCH_COLUMN); return $stmt->fetchAll(\PDO::FETCH_COLUMN);
} }
/**
* Get all available sources from TV shows
*/
public static function getAvailableSources(\PDO $pdo): array
{
$stmt = $pdo->query("
SELECT DISTINCT s.display_name
FROM tv_shows t
JOIN sources s ON t.source_id = s.id
WHERE t.source_id IS NOT NULL
ORDER BY s.display_name
");
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get all TV shows with filtering and pagination
*/
public function findAll(array $filters = [], int $limit = null, int $offset = 0): array
{
$sql = "
SELECT t.*, s.display_name as source_name
FROM tv_shows t
LEFT JOIN sources s ON t.source_id = s.id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "(t.title LIKE :search OR t.overview LIKE :search)";
$params['search'] = "%{$filters['search']}%";
}
// Genre filter
if (!empty($filters['genre'])) {
$whereClauses[] = "t.genre = :genre";
$params['genre'] = $filters['genre'];
}
// Year filter
if (!empty($filters['year'])) {
$whereClauses[] = "YEAR(t.first_air_date) = :year";
$params['year'] = $filters['year'];
}
// Source filter
if (!empty($filters['sources'])) {
$sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']];
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")";
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
// Add ordering and pagination
$sql .= " ORDER BY t.first_air_date DESC, t.title ASC";
if ($limit) {
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Process metadata to extract additional fields
foreach ($results as &$tvShow) {
if (!empty($tvShow['metadata'])) {
$metadata = json_decode($tvShow['metadata'], true);
// Extract additional fields from metadata if available
// This can be expanded based on your metadata structure
}
}
return $results;
}
/**
* Count TV shows with filters
*/
public function count(array $filters = []): int
{
$sql = "
SELECT COUNT(*) as total
FROM tv_shows t
LEFT JOIN sources s ON t.source_id = s.id
";
$params = [];
$whereClauses = [];
// Search filter
if (!empty($filters['search'])) {
$whereClauses[] = "(t.title LIKE :search OR t.overview LIKE :search)";
$params['search'] = "%{$filters['search']}%";
}
// Genre filter
if (!empty($filters['genre'])) {
$whereClauses[] = "t.genre = :genre";
$params['genre'] = $filters['genre'];
}
// Year filter
if (!empty($filters['year'])) {
$whereClauses[] = "YEAR(t.first_air_date) = :year";
$params['year'] = $filters['year'];
}
// Source filter
if (!empty($filters['sources'])) {
$sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']];
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")";
}
// Combine WHERE clauses
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return (int)$result['total'];
}
/** /**
* Get all available years from TV shows' first_air_date * Get all available years from TV shows' first_air_date
*/ */

40
check_adult_structure.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
} catch (Exception $e) {
die('❌ Database connection failed: ' . $e->getMessage());
}
// Check adult videos structure
try {
$stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM adult_videos LIMIT 1");
$adultVideo = $stmt->fetch(PDO::FETCH_ASSOC);
if ($adultVideo) {
echo "🔞 Sample adult video structure:\n";
echo " - ID: {$adultVideo['id']}\n";
echo " - Title: {$adultVideo['title']}\n";
echo " - poster_url: " . ($adultVideo['poster_url'] ?? 'NULL') . "\n";
echo " - metadata: " . substr($adultVideo['metadata'] ?? 'NULL', 0, 200) . "...\n";
} else {
echo "🔞 No adult videos found\n";
}
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
echo "\n✨ Check completed!\n";

0
check_adult_videos.php Normal file
View File

23
check_games_columns.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
require_once 'vendor/autoload.php';
$container = require 'bootstrap/app.php';
$pdo = $container->get('pdo');
// Get column names from games table
$stmt = $pdo->query('DESCRIBE games');
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo 'Games table columns:' . PHP_EOL;
foreach ($columns as $column) {
echo '- ' . $column['Field'] . ' (' . $column['Type'] . ')' . PHP_EOL;
}
// Also check a sample row to see the actual data
echo PHP_EOL . 'Sample game data:' . PHP_EOL;
$stmt = $pdo->query('SELECT * FROM games LIMIT 1');
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
foreach ($row as $key => $value) {
echo '- ' . $key . ': ' . (is_null($value) ? 'NULL' : substr($value, 0, 50)) . PHP_EOL;
}
}

80
check_gender_data.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
} catch (Exception $e) {
die('❌ Database connection failed: ' . $e->getMessage());
}
// Check gender data in actors table
try {
echo "\n🔍 Checking Gender Data:\n";
// Check if metadata column exists and has gender data
$stmt = $pdo->query("
SELECT name, metadata
FROM actors
WHERE metadata IS NOT NULL
AND metadata != ''
AND JSON_EXTRACT(metadata, '$.gender') IS NOT NULL
LIMIT 10
");
$actors = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Found " . count($actors) . " actors with gender metadata:\n";
foreach ($actors as $actor) {
$metadata = json_decode($actor['metadata'], true);
$gender = $metadata['gender'] ?? 'null';
echo " - {$actor['name']}: {$gender}\n";
}
// Count actors by gender
echo "\n📊 Gender Statistics:\n";
$genders = ['male', 'female', 'non-binary'];
foreach ($genders as $gender) {
$stmt = $pdo->prepare("
SELECT COUNT(*) as count
FROM actors
WHERE JSON_EXTRACT(metadata, '$.gender') = :gender
");
$stmt->execute(['gender' => $gender]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo " - " . ucfirst($gender) . ": " . $result['count'] . "\n";
}
// Check actors without gender
$stmt = $pdo->query("
SELECT COUNT(*) as count
FROM actors
WHERE metadata IS NULL
OR metadata = ''
OR JSON_EXTRACT(metadata, '$.gender') IS NULL
");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo " - No gender data: " . $result['count'] . "\n";
// Total actors
$stmt = $pdo->query("SELECT COUNT(*) as count FROM actors");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo " - Total actors: " . $result['count'] . "\n";
} catch (Exception $e) {
echo "❌ Error checking gender data: " . $e->getMessage() . "\n";
echo "Stack trace: " . $e->getTraceAsString() . "\n";
}
echo "\n✨ Check completed!\n";

View File

@@ -0,0 +1,68 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
} catch (Exception $e) {
die('❌ Database connection failed: ' . $e->getMessage());
}
// Check sample movie and TV show structure
try {
// Check movies
$stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM movies LIMIT 1");
$movie = $stmt->fetch(PDO::FETCH_ASSOC);
if ($movie) {
echo "🎬 Sample movie structure:\n";
echo " - ID: {$movie['id']}\n";
echo " - Title: {$movie['title']}\n";
echo " - poster_url: " . ($movie['poster_url'] ?? 'NULL') . "\n";
echo " - metadata: " . substr($movie['metadata'] ?? 'NULL', 0, 200) . "...\n";
}
// Check TV shows
$stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM tv_shows LIMIT 1");
$tvshow = $stmt->fetch(PDO::FETCH_ASSOC);
if ($tvshow) {
echo "\n📺 Sample TV show structure:\n";
echo " - ID: {$tvshow['id']}\n";
echo " - Title: {$tvshow['title']}\n";
echo " - poster_url: " . ($tvshow['poster_url'] ?? 'NULL') . "\n";
echo " - metadata: " . substr($tvshow['metadata'] ?? 'NULL', 0, 200) . "...\n";
}
// Test actor movies method
echo "\n🎭 Testing actor movies method:\n";
$stmt = $pdo->query("SELECT id FROM actors LIMIT 1");
$actor = $stmt->fetch(PDO::FETCH_ASSOC);
if ($actor) {
$actorModel = new \App\Models\Actor($pdo);
$actorModel->id = $actor['id'];
$movies = $actorModel->movies();
echo "Found " . count($movies) . " movies for actor {$actor['id']}\n";
if (!empty($movies)) {
$firstMovie = $movies[0];
echo "First movie poster_url: " . ($firstMovie['poster_url'] ?? 'NULL') . "\n";
}
}
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
echo "\n✨ Check completed!\n";

89
check_tvshow_actors.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
} catch (Exception $e) {
die('❌ Database connection failed: ' . $e->getMessage());
}
// Check TV shows count
try {
$stmt = $pdo->query("SELECT COUNT(*) as count FROM tv_shows");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo "📺 TV shows in database: {$result['count']}\n";
} catch (Exception $e) {
echo "❌ Error checking TV shows: " . $e->getMessage() . "\n";
}
// Check actor_tv_episode table exists and has data
try {
$stmt = $pdo->query("SELECT COUNT(*) as count FROM actor_tv_episode");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo "🔗 Actor-TV episode relationships: {$result['count']}\n";
} catch (Exception $e) {
echo "❌ Error checking actor_tv_episode table: " . $e->getMessage() . "\n";
}
// Check actors count
try {
$stmt = $pdo->query("SELECT COUNT(*) as count FROM actors");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo "👥 Actors in database: {$result['count']}\n";
} catch (Exception $e) {
echo "❌ Error checking actors: " . $e->getMessage() . "\n";
}
// Sample TV show with cast
try {
$stmt = $pdo->query("SELECT id, title, cast FROM tv_shows WHERE cast IS NOT NULL AND cast != '' LIMIT 3");
$tvshows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($tvshows)) {
echo "⚠️ No TV shows with cast data found\n";
} else {
echo "📋 Sample TV shows with cast:\n";
foreach ($tvshows as $tvshow) {
echo " - ID: {$tvshow['id']}, Title: {$tvshow['title']}, Cast: " . substr($tvshow['cast'], 0, 100) . "...\n";
}
}
} catch (Exception $e) {
echo "❌ Error checking TV show cast data: " . $e->getMessage() . "\n";
}
// Sample actor relationships
try {
$stmt = $pdo->query("
SELECT ts.title, a.name
FROM tv_shows ts
JOIN tv_episodes te ON ts.id = te.tv_show_id
JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id
JOIN actors a ON ate.actor_id = a.id
LIMIT 5
");
$relationships = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($relationships)) {
echo "⚠️ No actor-TV episode relationships found\n";
} else {
echo "🔗 Sample actor-TV show relationships (via episodes):\n";
foreach ($relationships as $rel) {
echo " - {$rel['name']} in {$rel['title']}\n";
}
}
} catch (Exception $e) {
echo "❌ Error checking actor relationships: " . $e->getMessage() . "\n";
}
echo "\n✨ Check completed!\n";

View File

@@ -1,90 +0,0 @@
<?php
// Debug script to check Jellyfin sync issues
require_once __DIR__ . '/vendor/autoload.php';
try {
echo "=== Jellyfin Sync Debug ===\n";
// Check if TvEpisode model exists and works
echo "Checking TvEpisode model...\n";
if (class_exists('App\Models\TvEpisode')) {
echo "✓ TvEpisode model exists\n";
} else {
echo "✗ TvEpisode model missing\n";
}
// Check if database tables exist
echo "\nChecking database tables...\n";
$config = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($config);
try {
$pdo = \App\Database\Database::getInstance();
$tables = ['tv_episodes', 'tv_shows', 'actors', 'actor_tv_episode', 'actor_tv_show', 'actor_movie'];
foreach ($tables as $table) {
try {
$stmt = $pdo->query("SHOW TABLES LIKE '$table'");
if ($stmt->rowCount() > 0) {
echo "✓ Table '$table' exists\n";
} else {
echo "✗ Table '$table' missing\n";
}
} catch (Exception $e) {
echo "✗ Error checking table '$table': " . $e->getMessage() . "\n";
}
}
} catch (Exception $e) {
echo "Database connection error: " . $e->getMessage() . "\n";
}
// Test episode data structure
echo "\nTesting episode data structure...\n";
$testEpisodeData = [
'Id' => 'test-episode-123',
'Name' => 'Test Episode 1',
'ParentIndexNumber' => 1,
'IndexNumber' => 1,
'PremiereDate' => '2023-01-01T00:00:00Z',
'RunTimeTicks' => 18000000000, // 30 minutes
'CommunityRating' => 8.5,
'Overview' => 'Test episode overview',
'ProviderIds' => [
'Imdb' => 'tt1234567',
'Tmdb' => '123456'
],
'People' => [
[
'Name' => 'Test Actor',
'Type' => 'Actor'
]
]
];
echo "✓ Episode has required fields:\n";
echo " - ID: " . $testEpisodeData['Id'] . "\n";
echo " - Name: " . $testEpisodeData['Name'] . "\n";
echo " - Season: " . $testEpisodeData['ParentIndexNumber'] . "\n";
echo " - Episode: " . $testEpisodeData['IndexNumber'] . "\n";
echo " - People: " . (isset($testEpisodeData['People']) ? 'Yes' : 'No') . "\n";
echo " - ProviderIds: " . (isset($testEpisodeData['ProviderIds']) ? 'Yes' : 'No') . "\n";
if (isset($testEpisodeData['People']) && is_array($testEpisodeData['People'])) {
foreach ($testEpisodeData['People'] as $person) {
if (isset($person['Type']) && $person['Type'] === 'Actor') {
echo " - Actor found: " . $person['Name'] . "\n";
}
}
}
echo "\n=== Debug Complete ===\n";
echo "The sync should work if:\n";
echo "1. All models exist ✓\n";
echo "2. Database tables exist ✓\n";
echo "3. Jellyfin API returns 'People' field ✓\n";
echo "4. syncTvShows() is called in executeSync() ✓\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}

1607
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{
"name": "media-collector",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"sass": "^1.93.2",
"vite": "^4.3.9"
},
"dependencies": {
"alpinejs": "^3.12.0",
"axios": "^1.4.0"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -49,6 +49,8 @@ $app->group('/api', function (RouteCollectorProxy $group) use ($container) {
// Games // Games
$group->get('/games', [$mediaController, 'listGames']); $group->get('/games', [$mediaController, 'listGames']);
$group->get('/games/{id:[0-9]+}', [$mediaController, 'getGame']); $group->get('/games/{id:[0-9]+}', [$mediaController, 'getGame']);
$group->get('/games/grouped', [$mediaController, 'getGamesGroupedByPlatform']);
$group->get('/games/categories/{category}', [$mediaController, 'getGamesByCategory']);
// Movies // Movies
$group->get('/movies', [$mediaController, 'listMovies']); $group->get('/movies', [$mediaController, 'listMovies']);
@@ -58,10 +60,22 @@ $app->group('/api', function (RouteCollectorProxy $group) use ($container) {
$group->get('/tvshows', [$mediaController, 'listTvShows']); $group->get('/tvshows', [$mediaController, 'listTvShows']);
$group->get('/tvshows/{id:[0-9]+}', [$mediaController, 'getTvShow']); $group->get('/tvshows/{id:[0-9]+}', [$mediaController, 'getTvShow']);
// Actors
$group->get('/actors', [$mediaController, 'listActors']);
$group->get('/actors/{id:[0-9]+}', [$mediaController, 'getActor']);
// Adult Content
$group->get('/adult', [$mediaController, 'listAdult']);
$group->get('/adult/{id:[0-9]+}', [$mediaController, 'getAdult']);
// Search // Search
$group->get('/search', [$mediaController, 'search']); $group->get('/search', [$mediaController, 'search']);
})->add(new ApiAuthMiddleware($container->get(AuthService::class))); // Dashboard
$group->get('/dashboard/stats', [$container->get(\App\Controllers\Api\DashboardController::class), 'getStats']);
$group->get('/dashboard/activity', [$container->get(\App\Controllers\Api\DashboardController::class), 'getRecentActivity']);
});
// Admin routes (require admin role) // Admin routes (require admin role)
$group->group('/admin', function (RouteCollectorProxy $group) use ($container) { $group->group('/admin', function (RouteCollectorProxy $group) use ($container) {

View File

@@ -18,11 +18,11 @@ $app->post('/login', AuthController::class . ':login')->setName('auth.login.post
$app->post('/logout', AuthController::class . ':logout')->setName('auth.logout'); $app->post('/logout', AuthController::class . ':logout')->setName('auth.logout');
$app->get('/logout', AuthController::class . ':logout')->setName('auth.logout'); $app->get('/logout', AuthController::class . ':logout')->setName('auth.logout');
// Public image serving (no auth required)
$app->get('/images/{path:.+}', 'App\Controllers\ImageController:serve')->setName('images.serve');
// Protected routes (require authentication) // Protected routes (require authentication)
$app->group('', function (RouteCollectorProxy $group) { $app->group('', function (RouteCollectorProxy $group) {
// Image serving (no auth required for public images)
$group->get('/images/{path:.+}', 'App\Controllers\ImageController:serve')->setName('images.serve');
// Global Search // Global Search
$group->get('/search', 'App\Controllers\SearchController:index')->setName('search.index'); $group->get('/search', 'App\Controllers\SearchController:index')->setName('search.index');
$group->get('/', 'App\Controllers\DashboardController:index')->setName('dashboard.index'); $group->get('/', 'App\Controllers\DashboardController:index')->setName('dashboard.index');

View File

@@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./resources/views/**/*.twig",
"./public/**/*.js",
"./resources/**/*.js",
"./resources/**/*.css",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -1,70 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
} catch (Exception $e) {
die('Database connection failed: ' . $e->getMessage());
}
// Start session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Test authentication service
$authService = new \App\Services\AuthService($pdo);
echo "Testing login functionality...\n";
// Test 1: Check if admin user exists
$user = \App\Models\User::findByUsername($pdo, 'admin');
if (!$user) {
echo "ERROR: Admin user not found in database!\n";
exit(1);
}
echo "✓ Admin user exists in database\n";
// Test 2: Test password verification
$userModel = new \App\Models\User($pdo);
$userModel->password = $user['password'];
$passwordValid = $userModel->verifyPassword('admin123');
if (!$passwordValid) {
echo "ERROR: Password verification failed!\n";
exit(1);
}
echo "✓ Password verification works\n";
// Test 3: Test login method
$loginSuccess = $authService->login('admin', 'admin123', '127.0.0.1');
if (!$loginSuccess) {
echo "ERROR: Login method failed!\n";
exit(1);
}
echo "✓ Login method works\n";
// Test 4: Check if user is logged in
if (!$authService->isLoggedIn()) {
echo "ERROR: User is not logged in after successful login!\n";
exit(1);
}
echo "✓ User is logged in\n";
// Test 5: Check if user is admin
if (!$authService->isAdmin()) {
echo "ERROR: User is not recognized as admin!\n";
exit(1);
}
echo "✓ User is recognized as admin\n";
echo "\n🎉 All authentication tests passed!\n";

View File

@@ -1,42 +0,0 @@
<?php
// Test script to verify Jellyfin episode syncing
require_once __DIR__ . '/vendor/autoload.php';
try {
echo "Testing Jellyfin episode sync...\n";
// Mock some test data to verify the logic works
$testEpisodeData = [
'Id' => 'test-episode-123',
'Name' => 'Test Episode',
'ParentIndexNumber' => 1,
'IndexNumber' => 1,
'PremiereDate' => '2023-01-01T00:00:00Z',
'RunTimeTicks' => 18000000000, // 30 minutes in ticks
'CommunityRating' => 8.5,
'Overview' => 'Test episode overview',
'ProviderIds' => [
'Imdb' => 'tt1234567',
'Tmdb' => '123456'
],
'People' => [
[
'Name' => 'Test Actor',
'Type' => 'Actor'
]
]
];
echo "Test episode data structure looks correct\n";
echo "Episode ID: " . $testEpisodeData['Id'] . "\n";
echo "Episode Name: " . $testEpisodeData['Name'] . "\n";
echo "Season: " . $testEpisodeData['ParentIndexNumber'] . "\n";
echo "Episode Number: " . $testEpisodeData['IndexNumber'] . "\n";
echo "Has Actor: " . (isset($testEpisodeData['People'][0]['Name']) ? 'Yes' : 'No') . "\n";
echo "\nEpisode sync logic should work correctly!\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}

View File

@@ -1,115 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
} catch (Exception $e) {
die('❌ Database connection failed: ' . $e->getMessage());
}
// Check if tables exist
try {
$tables = ['sources', 'movies', 'sync_logs', 'games'];
foreach ($tables as $table) {
$stmt = $pdo->query("SHOW TABLES LIKE '{$table}'");
if ($stmt->rowCount() > 0) {
echo "✅ Table '{$table}' exists\n";
} else {
echo "❌ Table '{$table}' does not exist\n";
}
}
} catch (Exception $e) {
echo "❌ Error checking tables: " . $e->getMessage() . "\n";
}
// Test Jellyfin connectivity (if configured)
echo "\n🔍 Testing Jellyfin connectivity...\n";
try {
$sourceModel = new \App\Models\Source($pdo);
$sources = $sourceModel->findAll(['name' => 'jellyfin']);
if (empty($sources)) {
echo "❌ No Jellyfin source configured\n";
exit;
}
$jellyfinSource = $sources[0];
echo "✅ Found Jellyfin source: {$jellyfinSource['display_name']}\n";
if (empty($jellyfinSource['api_key']) || empty($jellyfinSource['api_url'])) {
echo "❌ Jellyfin API key or URL not configured\n";
exit;
}
// Test HTTP connection to Jellyfin
$client = new GuzzleHttp\Client(['timeout' => 10]);
$url = rtrim($jellyfinSource['api_url'], '/') . '/Users';
echo "🔗 Testing connection to: {$url}\n";
$response = $client->get($url, [
'headers' => [
'X-MediaBrowser-Token' => $jellyfinSource['api_key'],
'User-Agent' => 'MediaCollector-Test/1.0'
]
]);
$httpCode = $response->getStatusCode();
echo "✅ HTTP Response: {$httpCode}\n";
if ($httpCode === 200) {
$data = json_decode($response->getBody(), true);
$userCount = count($data);
echo "✅ Found {$userCount} users in Jellyfin\n";
if ($userCount > 0) {
$userId = $data[0]['Id'];
echo "✅ Using user ID: {$userId}\n";
// Test getting movies
$moviesUrl = rtrim($jellyfinSource['api_url'], '/') . "/Users/{$userId}/Items";
echo "🔗 Testing movies endpoint: {$moviesUrl}\n";
$moviesResponse = $client->get($moviesUrl, [
'headers' => [
'X-MediaBrowser-Token' => $jellyfinSource['api_key'],
'User-Agent' => 'MediaCollector-Test/1.0'
],
'query' => [
'IncludeItemTypes' => 'Movie',
'Recursive' => 'true',
'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating'
]
]);
$moviesHttpCode = $moviesResponse->getStatusCode();
echo "✅ Movies HTTP Response: {$moviesHttpCode}\n";
if ($moviesHttpCode === 200) {
$moviesData = json_decode($moviesResponse->getBody(), true);
$movieCount = count($moviesData['Items'] ?? []);
echo "✅ Found {$movieCount} movies in Jellyfin library\n";
} else {
echo "❌ Failed to fetch movies from Jellyfin\n";
}
}
} else {
echo "❌ Jellyfin API returned HTTP {$httpCode}\n";
}
} catch (Exception $e) {
echo "❌ Jellyfin connectivity test failed: " . $e->getMessage() . "\n";
}
echo "\n✨ Test completed!\n";

View File

@@ -1,45 +0,0 @@
<?php
// Test Jellyfin sync execution
require_once __DIR__ . '/vendor/autoload.php';
try {
echo "=== Testing Jellyfin Sync Execution ===\n";
// Load database config
$config = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($config);
// Create a mock source for testing
$testSource = [
'id' => 1,
'name' => 'jellyfin',
'api_url' => 'http://192.168.1.102:8096', // Adjust this to your Jellyfin URL
'api_key' => '1db2d28854e541dd90c32ea6aab5e603' // Adjust this to your API key
];
echo "Testing with source: " . $testSource['name'] . "\n";
echo "API URL: " . $testSource['api_url'] . "\n";
// Create Jellyfin service instance
$pdo = \App\Database\Database::getInstance();
$jellyfinService = new \App\Services\JellyfinSyncService($pdo, $testSource);
// Test basic sync execution
echo "\nTesting basic sync execution...\n";
try {
// This will trigger the executeSync method which calls all the private methods
echo "Attempting to run sync (this may fail if Jellyfin server is not accessible)...\n";
$jellyfinService->startSync('full');
echo "✓ Sync completed successfully\n";
} catch (Exception $e) {
echo "✗ Sync failed (expected if Jellyfin server not accessible): " . $e->getMessage() . "\n";
echo "This is normal if your Jellyfin server is not running or credentials are incorrect.\n";
}
echo "\n=== Test Complete ===\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
}

View File

@@ -1,136 +0,0 @@
<?php
/**
* Stash Connectivity Test
*
* Tests if the Stash server is reachable and responding properly.
*/
// Check if we're in the right directory
if (!file_exists('app/Services/StashSyncService.php')) {
echo "Error: Please run this script from the project root directory.\n";
exit(1);
}
require_once 'vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
\App\Database\Database::setConfig($dbConfig);
// Initialize database
try {
$pdo = \App\Database\Database::getInstance();
echo "✅ Database connection successful\n";
// Get Stash source
$stmt = $pdo->prepare('SELECT * FROM sources WHERE name = ?');
$stmt->execute(['stash']);
$stashSource = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$stashSource) {
echo "❌ No Stash source found in database\n";
exit(1);
}
echo "🔍 Testing Stash server connectivity...\n";
echo " URL: {$stashSource['api_url']}\n";
echo " API Key: " . (empty($stashSource['api_key']) ? 'NOT SET' : 'SET') . "\n\n";
// Test basic connectivity
$client = new GuzzleHttp\Client([
'timeout' => 10,
'verify' => false // Disable SSL verification for testing
]);
try {
$response = $client->get($stashSource['api_url'], [
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'ApiKey' => $stashSource['api_key'] ?? ''
]
]);
echo "✅ Stash server is reachable\n";
echo " Status: {$response->getStatusCode()}\n";
echo " Response received successfully\n";
// Test GraphQL endpoint
echo "\n🔍 Testing GraphQL endpoint...\n";
$query = '
query FindScenes($filter: FindFilterType) {
findScenes(filter: $filter) {
count
scenes {
id
title
}
}
}
';
$variables = [
'filter' => [
'per_page' => 1,
'page' => 1,
'sort' => 'created_at',
'direction' => 'DESC'
]
];
$response = $client->post("{$stashSource['api_url']}/graphql", [
'json' => [
'query' => $query,
'variables' => $variables
],
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'ApiKey' => $stashSource['api_key'] ?? '',
'Content-Type' => 'application/json'
],
'timeout' => 15
]);
$data = json_decode($response->getBody(), true);
if (isset($data['data']['findScenes'])) {
$count = $data['data']['findScenes']['count'];
echo "✅ GraphQL endpoint working\n";
echo " Total scenes in Stash: {$count}\n";
if ($count > 0) {
$firstScene = $data['data']['findScenes']['scenes'][0] ?? null;
if ($firstScene) {
echo " First scene: {$firstScene['title']} (ID: {$firstScene['id']})\n";
}
}
} else {
echo "❌ GraphQL response format unexpected\n";
echo " Response: " . json_encode($data) . "\n";
}
} catch (GuzzleHttp\Exception\RequestException $e) {
echo "❌ Failed to connect to Stash server\n";
echo " Error: " . $e->getMessage() . "\n";
if ($e->hasResponse()) {
$response = $e->getResponse();
echo " Status: {$response->getStatusCode()}\n";
echo " Response: " . $response->getBody() . "\n";
}
echo "\n💡 Troubleshooting tips:\n";
echo " 1. Check if Stash server is running\n";
echo " 2. Verify the API URL is correct\n";
echo " 3. Check if API key is required and correct\n";
echo " 4. Ensure the server is accessible from this machine\n";
}
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}

View File

@@ -1,107 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load helper functions
require_once __DIR__ . '/app/helpers.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
// Set up database connection
try {
\App\Database\Database::setConfig($dbConfig);
$pdo = \App\Database\Database::getInstance();
echo "Database connection established successfully.\n";
} catch (Exception $e) {
die('Database connection failed: ' . $e->getMessage() . "\n");
}
// Test XBVR connectivity
echo "\nTesting XBVR source connectivity...\n";
// Find XBVR source
$stmt = $pdo->prepare("SELECT * FROM sources WHERE name = 'xbvr' AND is_active = 1 LIMIT 1");
$stmt->execute();
$xbvrSource = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$xbvrSource) {
echo "No active XBVR source found. Please create an XBVR source first.\n";
exit(1);
}
echo "Found XBVR source: {$xbvrSource['display_name']} ({$xbvrSource['api_url']})\n";
// Test basic connectivity
$baseUrl = rtrim($xbvrSource['api_url'], '/');
try {
$httpClient = new GuzzleHttp\Client([
'timeout' => 10,
'headers' => [
'User-Agent' => 'MediaCollector/1.0'
],
'verify' => false
]);
// Try different XBVR API endpoints
$endpoints = [
"{$baseUrl}/deovr"
];
foreach ($endpoints as $endpoint) {
echo "\nTrying endpoint: {$endpoint}\n";
try {
$response = $httpClient->get($endpoint, [
'timeout' => 10,
'connect_timeout' => 5
]);
echo "✓ Status: {$response->getStatusCode()}\n";
echo "Content-Type: " . $response->getHeaderLine('content-type') . "\n";
$data = json_decode($response->getBody(), true);
echo "Response keys: " . implode(', ', array_keys($data)) . "\n";
// XBVR DeoVR API response structure
$scenes = null;
// Try different DeoVR response structures
if (isset($data['scenes'])) {
$scenes = $data['scenes'];
} elseif (isset($data['content'])) {
$scenes = $data['content'];
} elseif (isset($data['videos'])) {
$scenes = $data['videos'];
} elseif (isset($data[0]) && is_array($data[0])) {
// Array of scenes directly
$scenes = $data;
}
if ($scenes !== null) {
echo "✓ Found " . count($scenes) . " scenes in XBVR DeoVR response\n";
if (count($scenes) > 0) {
echo "Sample scene keys: " . implode(', ', array_keys($scenes[0])) . "\n";
}
break;
} else {
echo "✗ No scenes array found in response. Response keys: " . implode(', ', array_keys($data)) . "\n";
echo "Sample response structure: " . json_encode(array_slice($data, 0, 2), JSON_PRETTY_PRINT) . "\n";
}
} catch (Exception $e) {
echo "✗ Error: " . $e->getMessage() . "\n";
}
}
} catch (Exception $e) {
echo "Error testing XBVR connectivity: " . $e->getMessage() . "\n";
exit(1);
}
echo "\nXBVR connectivity test complete.\n";

View File

@@ -1,64 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Load helper functions
require_once __DIR__ . '/app/helpers.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Load database configuration
$dbConfig = require __DIR__ . '/config/database.php';
// Set up database connection
try {
\App\Database\Database::setConfig($dbConfig);
$pdo = \App\Database\Database::getInstance();
echo "Database connection established successfully.\n";
} catch (Exception $e) {
die('Database connection failed: ' . $e->getMessage() . "\n");
}
// Find XBVR source
$stmt = $pdo->prepare("SELECT * FROM sources WHERE name = 'xbvr' AND is_active = 1 LIMIT 1");
$stmt->execute();
$xbvrSource = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$xbvrSource) {
echo "No active XBVR source found. Please create an XBVR source first.\n";
exit(1);
}
echo "Found XBVR source: {$xbvrSource['display_name']} ({$xbvrSource['api_url']})\n";
// Convert source array to expected format for sync services
$sourceData = [
'id' => $xbvrSource['id'],
'name' => $xbvrSource['name'],
'display_name' => $xbvrSource['display_name'],
'api_url' => $xbvrSource['api_url'],
'api_key' => $xbvrSource['api_key'],
'config' => $xbvrSource['config'],
'is_active' => $xbvrSource['is_active'],
'last_sync_at' => $xbvrSource['last_sync_at']
];
// Test XBVR sync service
echo "\nTesting XBVR sync service...\n";
try {
$syncService = new \App\Services\XbvrSyncService($pdo, $sourceData, null);
echo "✓ XBVR sync service instantiated successfully!\n";
echo "✓ No PHP errors during instantiation - the PDO fix worked!\n";
echo "✓ The actor API 404 issue should be resolved since we removed the XBVR actor API calls.\n";
} catch (Exception $e) {
echo "✗ Error testing XBVR sync service: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
exit(1);
}
echo "\nXBVR sync service test complete.\n";

View File

@@ -1,31 +0,0 @@
import { defineConfig } from 'vite';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
root: 'public',
base: '/build/',
publicDir: 'public',
build: {
outDir: '../public/build',
emptyOutDir: true,
manifest: true,
rollupOptions: {
input: 'resources/js/app.js',
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './resources/js'),
'~': path.resolve(__dirname, './node_modules'),
},
},
server: {
host: '0.0.0.0',
port: 3000,
strictPort: true,
hmr: {
host: 'localhost',
},
},
});