mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
i dont know
This commit is contained in:
@@ -78,6 +78,28 @@ class ActorController extends Controller
|
||||
$stmt->execute(['actor_id' => $actorId]);
|
||||
$tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
|
||||
|
||||
foreach ($scenes as &$scene) {
|
||||
if (!empty($scene['metadata'])) {
|
||||
$metadata = json_decode($scene['metadata'], true);
|
||||
|
||||
// Use local cover path if available, otherwise fall back to original URL
|
||||
if (!empty($metadata['local_cover_path'])) {
|
||||
$scene['poster_url'] = $metadata['local_cover_path'];
|
||||
} elseif (!empty($metadata['cover_url'])) {
|
||||
$scene['poster_url'] = $metadata['cover_url'];
|
||||
}
|
||||
|
||||
// Add actors data if available
|
||||
if (!empty($metadata['actors'])) {
|
||||
$scene['actors'] = $metadata['actors'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $this->view->render($response, 'actor/show.twig', [
|
||||
'title' => $actor['name'],
|
||||
'actor' => $actor,
|
||||
@@ -86,7 +108,6 @@ class ActorController extends Controller
|
||||
'tv_shows' => $tvShows
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request, Response $response, $args)
|
||||
{
|
||||
// Get all actors with their media counts from all types
|
||||
|
||||
70
app/Controllers/AdminBaseController.php
Normal file
70
app/Controllers/AdminBaseController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class AdminBaseController
|
||||
{
|
||||
protected Twig $view;
|
||||
protected \PDO $pdo;
|
||||
|
||||
public function __construct(\PDO $pdo, Twig $view)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template
|
||||
*/
|
||||
protected function render(Response $response, string $template, array $data = []): Response
|
||||
{
|
||||
// Add common admin data
|
||||
$data['auth'] = [
|
||||
'check' => isset($_SESSION['user_id']),
|
||||
'user' => [
|
||||
'username' => $_SESSION['username'] ?? 'Admin',
|
||||
'is_admin' => $_SESSION['is_admin'] ?? false
|
||||
]
|
||||
];
|
||||
|
||||
// Add current route for active menu highlighting
|
||||
$route = $this->getCurrentRoute();
|
||||
if ($route) {
|
||||
$data['current_route'] = $route;
|
||||
}
|
||||
|
||||
return $this->view->render($response, $template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current route name
|
||||
*/
|
||||
protected function getCurrentRoute(): ?string
|
||||
{
|
||||
$route = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$basePath = '/admin/';
|
||||
|
||||
if (strpos($route, $basePath) === 0) {
|
||||
$route = substr($route, strlen($basePath));
|
||||
$parts = explode('/', $route);
|
||||
return $parts[0] ?: 'index';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return JSON response
|
||||
*/
|
||||
protected function json(Response $response, $data, int $status = 200): Response
|
||||
{
|
||||
$response->getBody()->write(json_encode($data));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($status);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ use App\Services\ExophaseSyncService;
|
||||
use PDO;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class AdminController extends Controller
|
||||
class AdminController extends AdminBaseController
|
||||
{
|
||||
private PDO $pdo;
|
||||
protected PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo, Twig $view)
|
||||
{
|
||||
parent::__construct($view);
|
||||
parent::__construct($pdo, $view);
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class AdminController extends Controller
|
||||
$syncLogModel = new SyncLog($this->pdo);
|
||||
$recentSyncs = SyncLog::getRecent($this->pdo, 10);
|
||||
|
||||
return $this->view->render($response, 'admin/index.twig', [
|
||||
return $this->render($response, 'admin/index.twig', [
|
||||
'title' => 'Admin Dashboard',
|
||||
'sources' => $sources,
|
||||
'recent_syncs' => $recentSyncs
|
||||
@@ -119,14 +119,23 @@ class AdminController extends Controller
|
||||
return min(100, round(($processed / $total) * 100, 2));
|
||||
}
|
||||
|
||||
public function settings(Request $request, Response $response, $args)
|
||||
{
|
||||
return $this->render($response, 'admin/settings.twig', [
|
||||
'title' => 'Admin Settings',
|
||||
'current_route' => 'settings'
|
||||
]);
|
||||
}
|
||||
|
||||
public function sources(Request $request, Response $response, $args)
|
||||
{
|
||||
$sourceModel = new Source($this->pdo);
|
||||
$sources = $sourceModel->findAll();
|
||||
|
||||
return $this->view->render($response, 'admin/sources.twig', [
|
||||
return $this->render($response, 'admin/sources.twig', [
|
||||
'title' => 'Source Management',
|
||||
'sources' => $sources
|
||||
'sources' => $sources,
|
||||
'current_route' => 'sources'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class AdultController extends Controller
|
||||
|
||||
// Use local cover path if available, otherwise fall back to original URL
|
||||
if (!empty($metadata['local_cover_path'])) {
|
||||
$video['poster_url'] = '/public/images/'.$metadata['local_cover_path'];
|
||||
$video['poster_url'] = $metadata['local_cover_path'];
|
||||
} elseif (!empty($metadata['cover_url'])) {
|
||||
$video['poster_url'] = $metadata['cover_url'];
|
||||
}
|
||||
@@ -103,13 +103,13 @@ class AdultController extends Controller
|
||||
|
||||
// Add local image paths and other metadata to the video data for template compatibility
|
||||
if (!empty($metadata['local_cover_path'])) {
|
||||
$adultVideo['poster_url'] = '/public/images/'.$metadata['local_cover_path'];
|
||||
$adultVideo['poster_url'] = '/images/'.$metadata['local_cover_path'];
|
||||
} elseif (!empty($metadata['cover_url'])) {
|
||||
$adultVideo['poster_url'] = $metadata['cover_url'];
|
||||
}
|
||||
|
||||
if (!empty($metadata['local_screenshot_path'])) {
|
||||
$adultVideo['screenshot_url'] = '/public/images/'.$metadata['local_screenshot_path'];
|
||||
$adultVideo['screenshot_url'] = '/images/'.$metadata['local_screenshot_path'];
|
||||
}
|
||||
|
||||
// Add actors data if available
|
||||
|
||||
@@ -7,22 +7,22 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\Game;
|
||||
use App\Models\Movie;
|
||||
use App\Models\TvShow;
|
||||
use App\Models\MusicArtist;
|
||||
use App\Models\AdultVideo;
|
||||
use App\Models\SyncLog;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __construct(Twig $view)
|
||||
private \PDO $pdo;
|
||||
public function __construct(\PDO $pdo, Twig $view)
|
||||
{
|
||||
parent::__construct($view);
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function index(Request $request, Response $response, $args)
|
||||
{
|
||||
$pdo = $this->view->getEnvironment()->getGlobals()['pdo'] ?? null;
|
||||
|
||||
if (!$pdo) {
|
||||
if (!$this->pdo) {
|
||||
return $this->view->render($response, 'dashboard/index.twig', [
|
||||
'title' => 'Dashboard',
|
||||
'stats' => [
|
||||
@@ -38,22 +38,24 @@ class DashboardController extends Controller
|
||||
}
|
||||
|
||||
// Get statistics from models
|
||||
$gameStats = Game::getStats($pdo);
|
||||
$movieStats = Movie::getStats($pdo);
|
||||
$tvShowStats = TvShow::getStats($pdo);
|
||||
$musicStats = MusicArtist::getStats($pdo);
|
||||
$syncStats = SyncLog::getStats($pdo);
|
||||
$gameStats = Game::getStats($this->pdo);
|
||||
$movieStats = Movie::getStats($this->pdo);
|
||||
$tvShowStats = TvShow::getStats($this->pdo);
|
||||
$adultStats = AdultVideo::getStats($this->pdo);
|
||||
// $musicStats = MusicArtist::getStats($this->pdo);
|
||||
//$syncStats = SyncLog::getStats($this->pdo);
|
||||
|
||||
// Get recent activity
|
||||
$recentGames = Game::getRecent($pdo, 5);
|
||||
$recentMovies = Movie::getRecent($pdo, 5);
|
||||
$recentSyncs = SyncLog::getRecent($pdo, 5);
|
||||
$recentGames = Game::getRecent($this->pdo, 5);
|
||||
$recentMovies = Movie::getRecent($this->pdo, 5);
|
||||
$recentSyncs = SyncLog::getRecent($this->pdo, 5);
|
||||
|
||||
// Calculate total media count
|
||||
$totalMedia = ($gameStats['total_games'] ?? 0) +
|
||||
($movieStats['total_movies'] ?? 0) +
|
||||
($tvShowStats['total_shows'] ?? 0) +
|
||||
($musicStats['total_artists'] ?? 0);
|
||||
($musicStats['total_artists'] ?? 0)+
|
||||
($adultStats['total_adult_videos'] ?? 0);
|
||||
|
||||
$stats = [
|
||||
'total_media' => $totalMedia,
|
||||
@@ -63,11 +65,13 @@ class DashboardController extends Controller
|
||||
'total_episodes' => $tvShowStats['total_episodes'] ?? 0,
|
||||
'total_music' => $musicStats['total_artists'] ?? 0,
|
||||
'total_playtime' => $gameStats['total_playtime'] ?? 0,
|
||||
'total_adult_videos' => $adultStats['total_adult_videos'] ?? 0,
|
||||
'watched_movies' => $movieStats['watched_movies'] ?? 0,
|
||||
'favorite_games' => $gameStats['favorite_games'] ?? 0,
|
||||
'favorite_movies' => $movieStats['favorite_movies'] ?? 0,
|
||||
'favorite_shows' => $tvShowStats['favorite_shows'] ?? 0,
|
||||
'favorite_music' => $musicStats['favorite_artists'] ?? 0,
|
||||
'favorite_adult_videos' => $adultStats['favorite_adult_videos'] ?? 0,
|
||||
];
|
||||
|
||||
return $this->view->render($response, 'dashboard/index.twig', [
|
||||
|
||||
53
app/Controllers/ImageController.php
Normal file
53
app/Controllers/ImageController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Serve an image from internal storage
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param array $args
|
||||
* @return Response
|
||||
*/
|
||||
public function serve(Request $request, Response $response, $args): Response
|
||||
{
|
||||
$imagePath = $args['path'] ?? '';
|
||||
|
||||
// Security: Prevent directory traversal
|
||||
$imagePath = str_replace(['../', '..\\'], '', $imagePath);
|
||||
|
||||
$fullPath = __DIR__ . '/../../storage/images/' . $imagePath;
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($fullPath)) {
|
||||
return $response->withStatus(404, 'Image not found');
|
||||
}
|
||||
|
||||
// Get file extension and set appropriate content type
|
||||
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
|
||||
$contentTypes = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
$contentType = $contentTypes[$extension] ?? 'application/octet-stream';
|
||||
|
||||
// Read and serve the file
|
||||
$fileContent = file_get_contents($fullPath);
|
||||
|
||||
$response = $response->withHeader('Content-Type', $contentType);
|
||||
$response = $response->withHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
|
||||
$response->getBody()->write($fileContent);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
219
app/Controllers/MediaSourceController.php
Normal file
219
app/Controllers/MediaSourceController.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\Source;
|
||||
use App\Models\SyncLog;
|
||||
|
||||
class MediaSourceController extends AdminBaseController
|
||||
{
|
||||
private $source;
|
||||
private $syncLog;
|
||||
|
||||
public function __construct(\PDO $pdo, \Slim\Views\Twig $view)
|
||||
{
|
||||
parent::__construct($pdo, $view);
|
||||
$this->source = new Source($pdo);
|
||||
$this->syncLog = new SyncLog($pdo);
|
||||
}
|
||||
|
||||
// List all sources
|
||||
public function index(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$sources = $this->source->all();
|
||||
|
||||
return $this->render($response, 'admin/sources.twig', [
|
||||
'sources' => $sources,
|
||||
'current_route' => 'sources'
|
||||
]);
|
||||
}
|
||||
|
||||
// Show create form
|
||||
public function create(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
return $this->render($response, 'admin/sources/create.twig', [
|
||||
'current_route' => 'sources'
|
||||
]);
|
||||
}
|
||||
|
||||
// Store new source
|
||||
public function store(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
// Basic validation
|
||||
if (empty($data['name']) || empty($data['type']) || empty($data['path'])) {
|
||||
$this->flash->addMessage('error', 'Name, type, and path are required');
|
||||
return $response->withHeader('Location', '/admin/sources/create')->withStatus(302);
|
||||
}
|
||||
|
||||
try {
|
||||
$sourceData = [
|
||||
'name' => $data['name'],
|
||||
'type' => $data['type'],
|
||||
'path' => $data['path'],
|
||||
'username' => $data['username'] ?? null,
|
||||
'password' => !empty($data['password']) ? password_hash($data['password'], PASSWORD_DEFAULT) : null,
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$this->source->create($sourceData);
|
||||
$this->flash->addMessage('success', 'Source created successfully');
|
||||
return $response->withHeader('Location', '/admin/sources')->withStatus(302);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->flash->addMessage('error', 'Error creating source: ' . $e->getMessage());
|
||||
return $response->withHeader('Location', '/admin/sources/create')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
// Show edit form
|
||||
public function edit(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$id = $args['id'];
|
||||
$source = $this->source->find($id);
|
||||
|
||||
if (!$source) {
|
||||
$this->flash->addMessage('error', 'Source not found');
|
||||
return $response->withHeader('Location', '/admin/sources')->withStatus(302);
|
||||
}
|
||||
|
||||
return $this->render($response, 'admin/sources/edit.twig', [
|
||||
'source' => $source,
|
||||
'current_route' => 'sources'
|
||||
]);
|
||||
}
|
||||
|
||||
// Update source
|
||||
public function update(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$id = $args['id'];
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
try {
|
||||
$sourceData = [
|
||||
'name' => $data['name'],
|
||||
'type' => $data['type'],
|
||||
'path' => $data['path'],
|
||||
'username' => $data['username'] ?? null,
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Only update password if provided
|
||||
if (!empty($data['password'])) {
|
||||
$sourceData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$this->source->update($id, $sourceData);
|
||||
$this->flash->addMessage('success', 'Source updated successfully');
|
||||
return $response->withHeader('Location', '/admin/sources')->withStatus(302);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->flash->addMessage('error', 'Error updating source: ' . $e->getMessage());
|
||||
return $response->withHeader('Location', '/admin/sources/' . $id . '/edit')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete source
|
||||
public function destroy(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$id = $args['id'];
|
||||
|
||||
try {
|
||||
$this->source->delete($id);
|
||||
$this->flash->addMessage('success', 'Source deleted successfully');
|
||||
} catch (\Exception $e) {
|
||||
$this->flash->addMessage('error', 'Error deleting source: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $response->withHeader('Location', '/admin/sources')->withStatus(302);
|
||||
}
|
||||
|
||||
// Start sync for a source
|
||||
public function startSync(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$sourceId = $args['id'];
|
||||
$source = $this->source->find($sourceId);
|
||||
|
||||
if (!$source) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Source not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a sync log entry
|
||||
$logId = $this->syncLog->create([
|
||||
'source_id' => $sourceId,
|
||||
'type' => 'full',
|
||||
'status' => 'pending',
|
||||
'started_at' => date('Y-m-d H:i:s'),
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
// Start sync in background (you'll need to implement this)
|
||||
$this->startBackgroundSync($sourceId, $logId);
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'message' => 'Sync started',
|
||||
'log_id' => $logId
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Error starting sync: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Get sync status
|
||||
public function syncStatus(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$logId = $args['log_id'] ?? null;
|
||||
|
||||
if (!$logId) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Log ID is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$log = $this->syncLog->find($logId);
|
||||
|
||||
if (!$log) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Sync log not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'status' => $log['status'],
|
||||
'progress' => $log['progress'] ?? 0,
|
||||
'message' => $log['message'] ?? ''
|
||||
]);
|
||||
}
|
||||
|
||||
// Start background sync process
|
||||
private function startBackgroundSync($sourceId, $logId)
|
||||
{
|
||||
// This is a simplified example - you'll need to implement this based on your needs
|
||||
$command = sprintf(
|
||||
'php %s/console.php sync:source %d --log=%d > /dev/null 2>&1 &',
|
||||
dirname(__DIR__, 3),
|
||||
$sourceId,
|
||||
$logId
|
||||
);
|
||||
|
||||
exec($command);
|
||||
}
|
||||
}
|
||||
158
app/Controllers/SettingsController.php
Normal file
158
app/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use PDO;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo, Twig $view)
|
||||
{
|
||||
parent::__construct($view);
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function index(Request $request, Response $response, $args)
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
$sources = $this->getSources();
|
||||
|
||||
return $this->view->render($response, 'admin/settings.twig', [
|
||||
'title' => 'Admin Settings',
|
||||
'settings' => $settings,
|
||||
'sources' => $sources
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(Request $request, Response $response, $args)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
// Save general settings
|
||||
$this->saveGeneralSettings($data);
|
||||
|
||||
// Save source-specific settings
|
||||
if (isset($data['sources']) && is_array($data['sources'])) {
|
||||
$this->saveSourceSettings($data['sources']);
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'admin/settings.twig', [
|
||||
'title' => 'Admin Settings',
|
||||
'settings' => $this->getSettings(),
|
||||
'sources' => $this->getSources(),
|
||||
'success' => 'Settings saved successfully!'
|
||||
]);
|
||||
}
|
||||
|
||||
private function getSettings(): array
|
||||
{
|
||||
// Get general application settings
|
||||
$settings = [];
|
||||
|
||||
// Get media type visibility settings
|
||||
$stmt = $this->pdo->prepare("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'media_visibility_%'");
|
||||
$stmt->execute();
|
||||
$mediaVisibilitySettings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
$settings['media_visibility'] = [
|
||||
'games' => $mediaVisibilitySettings['media_visibility_games'] ?? 'authenticated', // Default: authenticated users only
|
||||
'movies' => $mediaVisibilitySettings['media_visibility_movies'] ?? 'authenticated',
|
||||
'tvshows' => $mediaVisibilitySettings['media_visibility_tvshows'] ?? 'authenticated',
|
||||
'music' => $mediaVisibilitySettings['media_visibility_music'] ?? 'authenticated',
|
||||
'adult' => $mediaVisibilitySettings['media_visibility_adult'] ?? 'authenticated', // Adult content requires auth by default
|
||||
'actors' => $mediaVisibilitySettings['media_visibility_actors'] ?? 'authenticated'
|
||||
];
|
||||
|
||||
// You can extend this to include more settings like:
|
||||
// - Sync intervals
|
||||
// - Default sync types
|
||||
// - Notification preferences
|
||||
// - Theme settings
|
||||
// - etc.
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function getSources(): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM sources ORDER BY display_name");
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
private function saveGeneralSettings(array $data): void
|
||||
{
|
||||
// Save general settings to a settings table or config file
|
||||
// For now, we'll store them in a simple settings table
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (strpos($key, 'setting_') === 0) {
|
||||
$settingKey = substr($key, 8); // Remove 'setting_' prefix
|
||||
|
||||
// Check if setting exists
|
||||
$stmt = $this->pdo->prepare("SELECT id FROM settings WHERE setting_key = :key LIMIT 1");
|
||||
$stmt->execute(['key' => $settingKey]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing) {
|
||||
// Update existing setting
|
||||
$stmt = $this->pdo->prepare("UPDATE settings SET setting_value = :value WHERE setting_key = :key");
|
||||
$stmt->execute(['key' => $settingKey, 'value' => $value]);
|
||||
} else {
|
||||
// Insert new setting
|
||||
$stmt = $this->pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)");
|
||||
$stmt->execute(['key' => $settingKey, 'value' => $value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save media visibility settings
|
||||
if (isset($data['media_visibility']) && is_array($data['media_visibility'])) {
|
||||
foreach ($data['media_visibility'] as $mediaType => $visibility) {
|
||||
$settingKey = "media_visibility_{$mediaType}";
|
||||
|
||||
// Check if setting exists
|
||||
$stmt = $this->pdo->prepare("SELECT id FROM settings WHERE setting_key = :key LIMIT 1");
|
||||
$stmt->execute(['key' => $settingKey]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing) {
|
||||
// Update existing setting
|
||||
$stmt = $this->pdo->prepare("UPDATE settings SET setting_value = :value WHERE setting_key = :key");
|
||||
$stmt->execute(['key' => $settingKey, 'value' => $visibility]);
|
||||
} else {
|
||||
// Insert new setting
|
||||
$stmt = $this->pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)");
|
||||
$stmt->execute(['key' => $settingKey, 'value' => $visibility]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function saveSourceSettings(array $sources): void
|
||||
{
|
||||
foreach ($sources as $sourceId => $sourceData) {
|
||||
// Update source configuration
|
||||
$config = isset($sourceData['config']) ? json_encode($sourceData['config']) : '{}';
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE sources
|
||||
SET api_url = :api_url, api_key = :api_key, config = :config, is_active = :is_active
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
'id' => $sourceId,
|
||||
'api_url' => $sourceData['api_url'] ?? '',
|
||||
'api_key' => $sourceData['api_key'] ?? '',
|
||||
'config' => $config,
|
||||
'is_active' => isset($sourceData['is_active']) ? 1 : 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
294
app/Controllers/SyncController.php
Normal file
294
app/Controllers/SyncController.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\Source;
|
||||
use App\Models\SyncLog;
|
||||
use App\Services\JellyfinSyncService;
|
||||
use App\Services\LocalSyncService;
|
||||
use App\Services\SambaSyncService;
|
||||
use App\Services\NfsSyncService;
|
||||
|
||||
class SyncController extends AdminBaseController
|
||||
{
|
||||
private $source;
|
||||
private $syncLog;
|
||||
private $syncServices = [];
|
||||
|
||||
public function __construct(\PDO $pdo, \Slim\Views\Twig $view)
|
||||
{
|
||||
parent::__construct($pdo, $view);
|
||||
$this->source = new Source($pdo);
|
||||
$this->syncLog = new SyncLog($pdo);
|
||||
|
||||
// Initialize sync services
|
||||
$this->syncServices = [
|
||||
'jellyfin' => new JellyfinSyncService($pdo),
|
||||
'local' => new LocalSyncService($pdo),
|
||||
'samba' => new SambaSyncService($pdo),
|
||||
'nfs' => new NfsSyncService($pdo)
|
||||
];
|
||||
}
|
||||
|
||||
// Show sync dashboard
|
||||
public function index(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
// Get recent sync logs
|
||||
$recentLogs = $this->syncLog->orderBy('created_at', 'desc')->limit(10)->get();
|
||||
|
||||
// Get sync statistics
|
||||
$stats = [
|
||||
'total_syncs' => $this->syncLog->count(),
|
||||
'successful_syncs' => $this->syncLog->where('status', 'completed')->count(),
|
||||
'failed_syncs' => $this->syncLog->where('status', 'failed')->count(),
|
||||
'pending_syncs' => $this->syncLog->where('status', 'pending')->count(),
|
||||
];
|
||||
|
||||
return $this->render($response, 'admin/sync/index.twig', [
|
||||
'recent_logs' => $recentLogs,
|
||||
'stats' => $stats,
|
||||
'current_route' => 'sync'
|
||||
]);
|
||||
}
|
||||
|
||||
// Start a new sync
|
||||
public function start(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$type = $data['type'] ?? 'full'; // full, scan, update
|
||||
$sourceId = $data['source_id'] ?? null;
|
||||
|
||||
try {
|
||||
// Create a new sync log
|
||||
$logData = [
|
||||
'source_id' => $sourceId,
|
||||
'sync_type' => $type,
|
||||
'status' => 'pending',
|
||||
'started_at' => date('Y-m-d H:i:s'),
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$logId = $this->syncLog->create($logData);
|
||||
|
||||
// Start sync in background
|
||||
$this->startBackgroundSync($sourceId, $logId, $type);
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'message' => 'Sync started',
|
||||
'log_id' => $logId
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Error starting sync: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Get sync status
|
||||
public function status(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$logId = $args['log_id'] ?? null;
|
||||
|
||||
if (!$logId) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Log ID is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$log = $this->syncLog->find($logId);
|
||||
|
||||
if (!$log) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Sync log not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Get detailed status from the sync service if available
|
||||
$status = [
|
||||
'status' => $log['status'],
|
||||
'progress' => (int)($log['progress'] ?? 0),
|
||||
'message' => $log['message'] ?? '',
|
||||
'started_at' => $log['started_at'],
|
||||
'completed_at' => $log['completed_at'] ?? null,
|
||||
'total_items' => (int)($log['total_items'] ?? 0),
|
||||
'processed_items' => (int)($log['processed_items'] ?? 0),
|
||||
'new_items' => (int)($log['new_items'] ?? 0),
|
||||
'updated_items' => (int)($log['updated_items'] ?? 0),
|
||||
'deleted_items' => (int)($log['deleted_items'] ?? 0)
|
||||
];
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'log' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
// Cancel a running sync
|
||||
public function cancel(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$logId = $args['log_id'] ?? null;
|
||||
|
||||
if (!$logId) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Log ID is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// Update the log status to cancelled
|
||||
$this->syncLog->update($logId, [
|
||||
'status' => 'cancelled',
|
||||
'completed_at' => date('Y-m-d H:i:s'),
|
||||
'message' => 'Sync cancelled by user'
|
||||
]);
|
||||
|
||||
// TODO: Send a signal to the running process to cancel
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'message' => 'Sync cancelled'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Error cancelling sync: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear sync logs
|
||||
public function clearLogs(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$type = $request->getParsedBody()['type'] ?? 'completed'; // completed, all
|
||||
|
||||
try {
|
||||
if ($type === 'all') {
|
||||
$this->syncLog->query('TRUNCATE TABLE sync_logs');
|
||||
} else {
|
||||
$this->syncLog->where('status', 'completed')->delete();
|
||||
$this->syncLog->where('status', 'failed')->delete();
|
||||
$this->syncLog->where('status', 'cancelled')->delete();
|
||||
}
|
||||
|
||||
return $this->json($response, [
|
||||
'success' => true,
|
||||
'message' => 'Logs cleared successfully'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($response, [
|
||||
'success' => false,
|
||||
'message' => 'Error clearing logs: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Start background sync process
|
||||
private function startBackgroundSync($sourceId, $logId, $type = 'full')
|
||||
{
|
||||
// This is a simplified example - you'll need to implement this based on your needs
|
||||
$command = sprintf(
|
||||
'php %s/console.php sync:start --log=%d --type=%s %s > /dev/null 2>&1 &',
|
||||
dirname(__DIR__, 3), // Path to your project root
|
||||
$logId,
|
||||
escapeshellarg($type),
|
||||
$sourceId ? '--source=' . $sourceId : ''
|
||||
);
|
||||
|
||||
exec($command);
|
||||
}
|
||||
|
||||
// Process sync (called from CLI)
|
||||
public function processSync($sourceId, $logId, $type = 'full')
|
||||
{
|
||||
try {
|
||||
$log = $this->syncLog->find($logId);
|
||||
|
||||
if (!$log) {
|
||||
throw new \Exception('Sync log not found');
|
||||
}
|
||||
|
||||
// Update log status to started
|
||||
$this->syncLog->update($logId, [
|
||||
'status' => 'in_progress',
|
||||
'started_at' => date('Y-m-d H:i:s'),
|
||||
'message' => 'Sync started'
|
||||
]);
|
||||
|
||||
$source = null;
|
||||
if ($sourceId) {
|
||||
$source = $this->source->find($sourceId);
|
||||
if (!$source) {
|
||||
throw new \Exception('Source not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Get the appropriate sync service
|
||||
$service = $this->getSyncService($source ? $source['type'] : 'local');
|
||||
|
||||
// Start sync
|
||||
$result = $service->sync($source, $type, function($progress, $message) use ($logId) {
|
||||
// Update progress callback
|
||||
$this->updateSyncProgress($logId, $progress, $message);
|
||||
});
|
||||
|
||||
// Update log with final status
|
||||
$this->syncLog->update($logId, [
|
||||
'status' => $result['success'] ? 'completed' : 'failed',
|
||||
'completed_at' => date('Y-m-d H:i:s'),
|
||||
'message' => $result['message'] ?? 'Sync completed',
|
||||
'total_items' => $result['total_items'] ?? 0,
|
||||
'processed_items' => $result['processed_items'] ?? 0,
|
||||
'new_items' => $result['new_items'] ?? 0,
|
||||
'updated_items' => $result['updated_items'] ?? 0,
|
||||
'deleted_items' => $result['deleted_items'] ?? 0,
|
||||
'errors' => !empty($result['errors']) ? json_encode($result['errors']) : null
|
||||
]);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Update log with error
|
||||
if (isset($logId) && $this->syncLog) {
|
||||
$this->syncLog->update($logId, [
|
||||
'status' => 'failed',
|
||||
'completed_at' => date('Y-m-d H:i:s'),
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Update sync progress
|
||||
private function updateSyncProgress($logId, $progress, $message = '')
|
||||
{
|
||||
$this->syncLog->update($logId, [
|
||||
'progress' => $progress,
|
||||
'message' => $message,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
// Get the appropriate sync service
|
||||
private function getSyncService($type)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
|
||||
if (!isset($this->syncServices[$type])) {
|
||||
throw new \Exception("Unsupported source type: $type");
|
||||
}
|
||||
|
||||
return $this->syncServices[$type];
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,11 @@ class TvShowController extends Controller
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
$hasNextPage = $page < $totalPages;
|
||||
$hasPrevPage = $page > 1;
|
||||
|
||||
/*
|
||||
echo '<pre>';
|
||||
print_r($tvshows);
|
||||
die();
|
||||
*/
|
||||
return $this->view->render($response, 'tvshows/index.twig', [
|
||||
'title' => 'TV Shows',
|
||||
'tvshows' => $tvshows,
|
||||
@@ -107,4 +111,67 @@ class TvShowController extends Controller
|
||||
'seasons' => $seasons
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Request $request, Response $response, $args)
|
||||
{
|
||||
$tvShowId = (int) $args['id'];
|
||||
|
||||
// Get TV show details to access metadata
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT t.*
|
||||
FROM tv_shows t
|
||||
WHERE t.id = :id
|
||||
");
|
||||
$stmt->execute(['id' => $tvShowId]);
|
||||
$tvShow = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$tvShow) {
|
||||
return $response->withStatus(404);
|
||||
}
|
||||
|
||||
// Decode metadata to find image paths
|
||||
$metadata = json_decode($tvShow['metadata'], true);
|
||||
|
||||
// Delete associated images
|
||||
$imagesDeleted = [];
|
||||
if (!empty($metadata['local_poster_path'])) {
|
||||
$posterPath = __DIR__ . '/../storage/images/' . $metadata['local_poster_path'];
|
||||
if (file_exists($posterPath)) {
|
||||
unlink($posterPath);
|
||||
$imagesDeleted[] = $metadata['local_poster_path'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($metadata['local_backdrop_path'])) {
|
||||
$backdropPath = __DIR__ . '/../storage/images/' . $metadata['local_backdrop_path'];
|
||||
if (file_exists($backdropPath)) {
|
||||
unlink($backdropPath);
|
||||
$imagesDeleted[] = $metadata['local_backdrop_path'];
|
||||
}
|
||||
}
|
||||
|
||||
// Delete actor relationships
|
||||
$stmt = $this->pdo->prepare("
|
||||
DELETE FROM actor_tv_show
|
||||
WHERE tv_show_id = :tv_show_id
|
||||
");
|
||||
$stmt->execute(['tv_show_id' => $tvShowId]);
|
||||
|
||||
// Delete the TV show record
|
||||
$stmt = $this->pdo->prepare("DELETE FROM tv_shows WHERE id = :id");
|
||||
$result = $stmt->execute(['id' => $tvShowId]);
|
||||
|
||||
if ($result) {
|
||||
return $response->withJson([
|
||||
'success' => true,
|
||||
'message' => 'TV show deleted successfully',
|
||||
'images_deleted' => $imagesDeleted
|
||||
]);
|
||||
} else {
|
||||
return $response->withJson([
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete TV show'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Http/Middleware/MediaVisibilityMiddleware.php
Normal file
68
app/Http/Middleware/MediaVisibilityMiddleware.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class MediaVisibilityMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
{
|
||||
$path = $request->getUri()->getPath();
|
||||
|
||||
// Map routes to media types
|
||||
$mediaRoutes = [
|
||||
'/media/games' => 'games',
|
||||
'/media/movies' => 'movies',
|
||||
'/media/tv-shows' => 'tvshows',
|
||||
'/media/music' => 'music',
|
||||
'/media/adult' => 'adult',
|
||||
'/media/actors' => 'actors'
|
||||
];
|
||||
|
||||
foreach ($mediaRoutes as $route => $mediaType) {
|
||||
if (strpos($path, $route) === 0) {
|
||||
// Check if this media type is visible to the current user
|
||||
if (!$this->isMediaTypeVisible($mediaType)) {
|
||||
// Redirect to login or show 404 based on configuration
|
||||
if (!is_logged_in()) {
|
||||
return $handler->handle($request)->withStatus(401)->withHeader('Location', '/login');
|
||||
} else {
|
||||
return $handler->handle($request)->withStatus(404);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
private function isMediaTypeVisible(string $mediaType): bool
|
||||
{
|
||||
// Get database connection
|
||||
$pdo = \App\Database\Database::getInstance();
|
||||
|
||||
// Get media visibility setting
|
||||
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = :key LIMIT 1");
|
||||
$stmt->execute(['key' => "media_visibility_{$mediaType}"]);
|
||||
$visibility = $stmt->fetchColumn() ?: 'authenticated'; // Default to authenticated only
|
||||
|
||||
// Check user authentication status
|
||||
$isLoggedIn = is_logged_in();
|
||||
|
||||
switch ($visibility) {
|
||||
case 'public':
|
||||
return true; // Visible to everyone
|
||||
case 'authenticated':
|
||||
return $isLoggedIn; // Visible only to authenticated users
|
||||
case 'hidden':
|
||||
return false; // Hidden from all users
|
||||
default:
|
||||
return $isLoggedIn; // Default to authenticated only
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,4 +166,19 @@ class AdultVideo extends Model
|
||||
'cast' => $castString
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TV show statistics
|
||||
*/
|
||||
public static function getStats(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
COUNT(*) as total_adult_videos,
|
||||
COUNT(CASE WHEN is_favorite = 1 THEN 1 END) as favorite_adult_videos,
|
||||
AVG(rating) as avg_rating
|
||||
FROM adult_videos
|
||||
");
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,7 +790,7 @@ class JellyfinSyncService extends BaseSyncService
|
||||
|
||||
try {
|
||||
// Create images directory structure if it doesn't exist
|
||||
$imagesDir = "public/images/{$type}";
|
||||
$imagesDir = __DIR__ . "/../../storage/images/{$type}";
|
||||
if (!is_dir($imagesDir)) {
|
||||
if (!mkdir($imagesDir, 0755, true)) {
|
||||
$this->logProgress("Warning: Could not create images directory: {$imagesDir}");
|
||||
|
||||
313
app/Services/LocalSyncService.php
Normal file
313
app/Services/LocalSyncService.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\MediaItem;
|
||||
use App\Models\SyncLog;
|
||||
use Exception;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class LocalSyncService extends BaseSyncService implements SyncServiceInterface
|
||||
{
|
||||
protected string $sourceType = 'local';
|
||||
|
||||
/**
|
||||
* @var array Supported file extensions for each media type
|
||||
*/
|
||||
protected array $supportedExtensions = [
|
||||
'movies' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'm4v', 'mpg', 'mpeg'],
|
||||
'tv_shows' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'm4v', 'mpg', 'mpeg'],
|
||||
'music' => ['mp3', 'flac', 'wav', 'aac', 'ogg', 'm4a'],
|
||||
'games' => ['iso', 'rom', 'nsp', 'xci', 'rvz', 'ciso', 'gcm', 'wbfs'],
|
||||
'books' => ['epub', 'mobi', 'pdf', 'azw', 'azw3', 'djvu'],
|
||||
'pictures' => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'raw']
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sync($source, string $type = 'full', callable $progressCallback = null): array
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->sourceId = $source['id'] ?? null;
|
||||
|
||||
if (!$this->sourceId) {
|
||||
throw new Exception('Source ID is required for sync');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logProgress("Starting {$type} sync for local source: " . ($source['name'] ?? 'Unknown'));
|
||||
|
||||
$path = $source['path'] ?? null;
|
||||
if (empty($path) || !is_dir($path)) {
|
||||
throw new Exception("Invalid or inaccessible source path: {$path}");
|
||||
}
|
||||
|
||||
// Determine the media type from the source configuration
|
||||
$mediaType = $this->determineMediaType($source);
|
||||
|
||||
// Get all files from the source directory recursively
|
||||
$files = $this->scanDirectory($path, $mediaType);
|
||||
$this->logProgress(sprintf('Found %d files to process', count($files)));
|
||||
|
||||
// Get existing media items from the database
|
||||
$existingItems = $this->getExistingMediaItems($mediaType);
|
||||
$this->logProgress(sprintf('Found %d existing items in database', count($existingItems)));
|
||||
|
||||
$result = [
|
||||
'total_items' => count($files),
|
||||
'processed_items' => 0,
|
||||
'new_items' => 0,
|
||||
'updated_items' => 0,
|
||||
'deleted_items' => 0,
|
||||
'errors' => []
|
||||
];
|
||||
|
||||
// Process each file
|
||||
foreach ($files as $filePath => $fileInfo) {
|
||||
try {
|
||||
$relativePath = $this->getRelativePath($path, $filePath);
|
||||
$fileKey = $this->generateFileKey($relativePath, $fileInfo);
|
||||
|
||||
if (isset($existingItems[$fileKey])) {
|
||||
// Update existing item if needed
|
||||
$item = $existingItems[$fileKey];
|
||||
$updated = $this->updateMediaItem($item, $filePath, $fileInfo, $mediaType);
|
||||
|
||||
if ($updated) {
|
||||
$result['updated_items']++;
|
||||
$this->logProgress("Updated: {$relativePath}");
|
||||
}
|
||||
|
||||
// Remove from existing items to track deletions
|
||||
unset($existingItems[$fileKey]);
|
||||
} else {
|
||||
// Add new item
|
||||
$this->createMediaItem($filePath, $fileInfo, $mediaType, $relativePath);
|
||||
$result['new_items']++;
|
||||
$this->logProgress("Added: {$relativePath}");
|
||||
}
|
||||
|
||||
$result['processed_items']++;
|
||||
|
||||
// Update progress
|
||||
if ($progressCallback) {
|
||||
$progress = (int)(($result['processed_items'] / $result['total_items']) * 100);
|
||||
$progressCallback($progress, "Processing: {$relativePath}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = "Error processing {$filePath}: " . $e->getMessage();
|
||||
$this->logProgress($errorMsg, 'ERROR');
|
||||
$result['errors'][] = $errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deleted files
|
||||
if ($type === 'full' && !empty($existingItems)) {
|
||||
foreach ($existingItems as $item) {
|
||||
try {
|
||||
$this->deleteMediaItem($item, $mediaType);
|
||||
$result['deleted_items']++;
|
||||
$this->logProgress("Deleted: {$item['file_path']}");
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = "Error deleting {$item['file_path']}: " . $e->getMessage();
|
||||
$this->logProgress($errorMsg, 'ERROR');
|
||||
$result['errors'][] = $errorMsg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("Sync completed successfully");
|
||||
|
||||
return array_merge($result, [
|
||||
'success' => true,
|
||||
'message' => 'Sync completed successfully',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = 'Sync failed: ' . $e->getMessage();
|
||||
$this->logProgress($errorMsg, 'ERROR');
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $errorMsg,
|
||||
'errors' => [$errorMsg]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSupportedTypes(): array
|
||||
{
|
||||
return ['local', 'file'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the media type from the source configuration
|
||||
*/
|
||||
protected function determineMediaType(array $source): string
|
||||
{
|
||||
// First check if media_type is explicitly set in the source config
|
||||
if (!empty($source['media_type'])) {
|
||||
return strtolower($source['media_type']);
|
||||
}
|
||||
|
||||
// Otherwise, try to guess from the path or name
|
||||
$path = strtolower($source['path'] ?? '');
|
||||
$name = strtolower($source['name'] ?? '');
|
||||
|
||||
foreach (array_keys($this->supportedExtensions) as $type) {
|
||||
if (strpos($path, $type) !== false || strpos($name, $type) !== false) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 'other' if we can't determine the type
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory recursively for media files
|
||||
*/
|
||||
protected function scanDirectory(string $path, string $mediaType): array
|
||||
{
|
||||
$files = [];
|
||||
$extensions = $this->supportedExtensions[$mediaType] ?? [];
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$ext = strtolower($file->getExtension());
|
||||
|
||||
// If we have specific extensions for this media type, filter by them
|
||||
if (!empty($extensions) && !in_array($ext, $extensions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[$file->getPathname()] = [
|
||||
'size' => $file->getSize(),
|
||||
'modified' => $file->getMTime(),
|
||||
'extension' => $ext,
|
||||
'path' => $file->getPathname(),
|
||||
'filename' => $file->getFilename()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing media items from the database
|
||||
*/
|
||||
protected function getExistingMediaItems(string $mediaType): array
|
||||
{
|
||||
$mediaItem = new MediaItem($this->pdo);
|
||||
$items = $mediaItem->where('source_id', $this->sourceId)
|
||||
->where('media_type', $mediaType)
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
foreach ($items as $item) {
|
||||
$result[$item['file_key']] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for a file
|
||||
*/
|
||||
protected function generateFileKey(string $relativePath, array $fileInfo): string
|
||||
{
|
||||
return md5($relativePath . $fileInfo['size'] . $fileInfo['modified']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative path from the base path
|
||||
*/
|
||||
protected function getRelativePath(string $basePath, string $filePath): string
|
||||
{
|
||||
return ltrim(str_replace('\\', '/', substr($filePath, strlen($basePath))), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new media item in the database
|
||||
*/
|
||||
protected function createMediaItem(string $filePath, array $fileInfo, string $mediaType, string $relativePath): void
|
||||
{
|
||||
$mediaItem = new MediaItem($this->pdo);
|
||||
|
||||
$data = [
|
||||
'source_id' => $this->sourceId,
|
||||
'media_type' => $mediaType,
|
||||
'file_path' => $relativePath,
|
||||
'file_name' => $fileInfo['filename'],
|
||||
'file_size' => $fileInfo['size'],
|
||||
'file_modified' => date('Y-m-d H:i:s', $fileInfo['modified']),
|
||||
'file_extension' => $fileInfo['extension'],
|
||||
'file_key' => $this->generateFileKey($relativePath, $fileInfo),
|
||||
'metadata' => json_encode([
|
||||
'original_path' => $filePath,
|
||||
'imported_at' => date('Y-m-d H:i:s')
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$mediaItem->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing media item if needed
|
||||
*/
|
||||
protected function updateMediaItem(array $item, string $filePath, array $fileInfo, string $mediaType): bool
|
||||
{
|
||||
$needsUpdate = false;
|
||||
$updates = [];
|
||||
|
||||
// Check if file has been modified
|
||||
if ($item['file_size'] != $fileInfo['size'] ||
|
||||
$item['file_modified'] != date('Y-m-d H:i:s', $fileInfo['modified'])) {
|
||||
|
||||
$updates = [
|
||||
'file_size' => $fileInfo['size'],
|
||||
'file_modified' => date('Y-m-d H:i:s', $fileInfo['modified']),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
$needsUpdate = true;
|
||||
}
|
||||
|
||||
// Update metadata if needed
|
||||
$metadata = json_decode($item['metadata'] ?? '{}', true);
|
||||
$metadata['last_checked'] = date('Y-m-d H:i:s');
|
||||
$updates['metadata'] = json_encode($metadata);
|
||||
|
||||
if ($needsUpdate) {
|
||||
$mediaItem = new MediaItem($this->pdo);
|
||||
$mediaItem->update($item['id'], $updates);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a media item from the database
|
||||
*/
|
||||
protected function deleteMediaItem(array $item, string $mediaType): void
|
||||
{
|
||||
$mediaItem = new MediaItem($this->pdo);
|
||||
$mediaItem->delete($item['id']);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class StashSyncService extends BaseSyncService
|
||||
'verify' => false // Disable SSL verification for problematic servers
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
|
||||
$this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images', $this->apiKey);
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
|
||||
23
app/Services/SyncServiceInterface.php
Normal file
23
app/Services/SyncServiceInterface.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
interface SyncServiceInterface
|
||||
{
|
||||
/**
|
||||
* Synchronize media from the source
|
||||
*
|
||||
* @param array $source The source configuration
|
||||
* @param string $type Sync type (full, scan, update)
|
||||
* @param callable $progressCallback Callback for progress updates
|
||||
* @return array Result of the sync operation
|
||||
*/
|
||||
public function sync($source, string $type = 'full', callable $progressCallback = null): array;
|
||||
|
||||
/**
|
||||
* Get the supported source types
|
||||
*
|
||||
* @return array Array of supported source types
|
||||
*/
|
||||
public function getSupportedTypes(): array;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class XbvrSyncService extends BaseSyncService
|
||||
]
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader('public/images');
|
||||
$this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images');
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
|
||||
@@ -10,7 +10,7 @@ class ImageDownloader
|
||||
private Client $httpClient;
|
||||
private string $basePath;
|
||||
|
||||
public function __construct(string $basePath = 'public/images', ?string $apiKey = null)
|
||||
public function __construct(string $basePath = 'storage/images', ?string $apiKey = null)
|
||||
{
|
||||
$headers = [
|
||||
'User-Agent' => 'MediaCollector/1.0'
|
||||
@@ -25,7 +25,13 @@ class ImageDownloader
|
||||
'headers' => $headers,
|
||||
'verify' => false // Disable SSL verification for problematic servers
|
||||
]);
|
||||
$this->basePath = rtrim($basePath, '/');
|
||||
|
||||
// Convert relative path to absolute path
|
||||
if (strpos($basePath, '/') !== 0) {
|
||||
$this->basePath = __DIR__ . '/../' . $basePath;
|
||||
} else {
|
||||
$this->basePath = $basePath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +121,8 @@ class ImageDownloader
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Not a valid image type
|
||||
}
|
||||
|
||||
public function saveImage(string $imageData, string $filename, string $subfolder = ''): ?string
|
||||
@@ -167,9 +175,9 @@ class ImageDownloader
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the public/ prefix to get the web-accessible path
|
||||
// Remove the absolute basePath prefix to get the relative path
|
||||
$relativePath = str_replace($this->basePath . '/', '', $localPath);
|
||||
|
||||
return '/' . $relativePath;
|
||||
return '/images/' . $relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +164,29 @@ function array_to_object(array $array): object
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object to array recursively
|
||||
* Check if a media type is visible to the current user
|
||||
*/
|
||||
function object_to_array(object $object): array
|
||||
function is_media_type_visible(string $mediaType): bool
|
||||
{
|
||||
return json_decode(json_encode($object), true);
|
||||
// Get database connection
|
||||
$pdo = \App\Database\Database::getInstance();
|
||||
|
||||
// Get media visibility setting
|
||||
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = :key LIMIT 1");
|
||||
$stmt->execute(['key' => "media_visibility_{$mediaType}"]);
|
||||
$visibility = $stmt->fetchColumn() ?: 'authenticated'; // Default to authenticated only
|
||||
|
||||
// Check user authentication status
|
||||
$isLoggedIn = is_logged_in();
|
||||
|
||||
switch ($visibility) {
|
||||
case 'public':
|
||||
return true; // Visible to everyone
|
||||
case 'authenticated':
|
||||
return $isLoggedIn; // Visible only to authenticated users
|
||||
case 'hidden':
|
||||
return false; // Hidden from all users
|
||||
default:
|
||||
return $isLoggedIn; // Default to authenticated only
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user