Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21bc43bb84 | ||
|
|
4ae70115b5 | ||
|
|
f4982af991 | ||
|
|
f253b33910 | ||
|
|
ec0d2a198c | ||
|
|
1f1c6f775f | ||
|
|
cd14a197aa |
1
assets/bootstrap.js
vendored
1
assets/bootstrap.js
vendored
@@ -10,6 +10,7 @@ import { startStimulusApp } from '@symfony/stimulus-bundle';
|
|||||||
import Popover from '@stimulus-components/popover';
|
import Popover from '@stimulus-components/popover';
|
||||||
import Dialog from '@stimulus-components/dialog';
|
import Dialog from '@stimulus-components/dialog';
|
||||||
import Dropdown from '@stimulus-components/dropdown';
|
import Dropdown from '@stimulus-components/dropdown';
|
||||||
|
|
||||||
import 'animate.css';
|
import 'animate.css';
|
||||||
|
|
||||||
const app = startStimulusApp();
|
const app = startStimulusApp();
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
|||||||
<th class="px-4 py-2">
|
<th class="px-4 py-2">
|
||||||
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
|
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
|
||||||
</th>
|
</th>
|
||||||
|
<th class="px-4 py-2">
|
||||||
|
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Air Date</div>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -107,6 +110,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
|||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
|
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('air-date') ?? "-"}</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export default class PreviewContentDialog extends HTMLDialogElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display({ heading, content }) {
|
display({ heading, content }) {
|
||||||
|
if (this.hasAttribute('mdWidth')) {
|
||||||
|
this.style.width = this.getAttribute('mdWidth');
|
||||||
|
}
|
||||||
this.setHeading(heading);
|
this.setHeading(heading);
|
||||||
this.setContent(content);
|
this.setContent(content);
|
||||||
this.showModal();
|
this.showModal();
|
||||||
|
|||||||
40
assets/controllers/dashboard_widgets_controller.js
Normal file
40
assets/controllers/dashboard_widgets_controller.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
import {GridStack} from "../vendor/gridstack/gridstack.index.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||||
|
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
grid;
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.grid = GridStack.init({
|
||||||
|
column: 2,
|
||||||
|
alwaysShowResizeHandle: true,
|
||||||
|
margin: "2rem",
|
||||||
|
resizable: {
|
||||||
|
handles: 'e,se,s,sw,w'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.grid.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom controller actions here
|
||||||
|
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Called anytime its element is disconnected from the DOM
|
||||||
|
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||||
|
|
||||||
|
// Here you should remove all event listeners added in "connect()"
|
||||||
|
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,4 +70,7 @@ return [
|
|||||||
'@ungap/custom-elements' => [
|
'@ungap/custom-elements' => [
|
||||||
'version' => '1.3.0',
|
'version' => '1.3.0',
|
||||||
],
|
],
|
||||||
|
'gridstack' => [
|
||||||
|
'version' => '12.3.3',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
34
migrations/Version20251106045808.php
Normal file
34
migrations/Version20251106045808.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251106045808 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor ADD poster VARCHAR(1024) DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP poster
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,6 +168,37 @@ class MediaFiles
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $tvshowTitle
|
||||||
|
* @return array<SplFileInfo>|false
|
||||||
|
*/
|
||||||
|
public function tvshowExists(string $tvshowTitle): Map|false
|
||||||
|
{
|
||||||
|
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||||
|
|
||||||
|
if ($existingEpisodes->isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodes = new Map;
|
||||||
|
/** @var SplFileInfo $episode */
|
||||||
|
foreach ($existingEpisodes as $episode) {
|
||||||
|
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
||||||
|
|
||||||
|
if (!property_exists($ptn, 'season') || !property_exists($ptn, 'episode')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodes->push($episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($episodes->count() > 0) {
|
||||||
|
return $episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function movieExists(string $title)
|
public function movieExists(string $title)
|
||||||
{
|
{
|
||||||
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
||||||
|
|||||||
20
src/Library/Action/Command/GetMediaFromLibraryCommand.php
Normal file
20
src/Library/Action/Command/GetMediaFromLibraryCommand.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements CommandInterface<GetMediaFromLibraryCommand>
|
||||||
|
*/
|
||||||
|
class GetMediaFromLibraryCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $userId = null,
|
||||||
|
public ?string $mediaType = null,
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
public ?string $title = null,
|
||||||
|
public ?string $season = null,
|
||||||
|
public ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
169
src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php
Normal file
169
src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Handler;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
|
use App\Base\Util\PTN;
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use App\Library\Action\Result\GetMediaFromLibraryResult;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements HandlerInterface<GetMediaFromLibraryCommand,GetMediaFromLibraryResult>
|
||||||
|
*/
|
||||||
|
class GetMediaInfoFromLibraryHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TmdbClient $tmdb,
|
||||||
|
private readonly MediaFiles $mediaFiles,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly MonitorRepository $monitorRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(C $command): R
|
||||||
|
{
|
||||||
|
$result = new GetMediaFromLibraryResult();
|
||||||
|
$tmdbResult = $this->fetchTmdbData($command->imdbId, $command->mediaType);
|
||||||
|
|
||||||
|
if (null === $tmdbResult) {
|
||||||
|
$this->logger->warning('[GetMediaInfoFromLibraryHandler] TMDb result was not found, this may lead to issues in the rest of the library search', (array) $command);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setResultExists($tmdbResult->mediaType, $tmdbResult->title, $result);
|
||||||
|
if ($result->notExists()) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->parseFromTmdbResult($tmdbResult, $result);
|
||||||
|
|
||||||
|
if ($command->mediaType === MediaType::TvShow->value) {
|
||||||
|
$this->setEpisodes($tmdbResult, $result);
|
||||||
|
$this->setSeasons($tmdbResult, $result);
|
||||||
|
$this->setMonitors($command->userId, $command->imdbId, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchTmdbData(string $imdbId, string $mediaType): ?TmdbResult
|
||||||
|
{
|
||||||
|
return match($mediaType) {
|
||||||
|
MediaType::Movie->value => $this->tmdb->movieDetails($imdbId),
|
||||||
|
MediaType::TvShow->value => $this->tmdb->tvShowDetails($imdbId),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setResultExists(string $mediaType, string $title, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$fsResult = match($mediaType) {
|
||||||
|
MediaType::Movie->value => $this->mediaFiles->movieExists($title),
|
||||||
|
MediaType::TvShow->value => $this->mediaFiles->tvShowExists($title),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
if (false === $fsResult) {
|
||||||
|
$result->setExists(false);
|
||||||
|
} else {
|
||||||
|
$result->setExists(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseFromTmdbResult(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$result->setTitle($tmdbResult->title);
|
||||||
|
$result->setMediaType($tmdbResult->mediaType);
|
||||||
|
$result->setImdbId($tmdbResult->imdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEpisodes(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title);
|
||||||
|
|
||||||
|
$existingEpisodeMap = [];
|
||||||
|
foreach ($existingEpisodeFiles as $file) {
|
||||||
|
/** @var \SplFileInfo $file */
|
||||||
|
$ptn = (object) new PTN()->parse($file->getBasename());
|
||||||
|
|
||||||
|
if (!array_key_exists($ptn->season, $existingEpisodeMap)) {
|
||||||
|
$existingEpisodeMap[$ptn->season] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($ptn->episode, $existingEpisodeMap[$ptn->season])) {
|
||||||
|
$existingEpisodeMap[$ptn->season][] = $ptn->episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingEpisodes = [];
|
||||||
|
$missingEpisodes = [];
|
||||||
|
foreach ($tmdbResult->episodes as $season => $episodes) {
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
if (array_key_exists($season, $existingEpisodeMap)) {
|
||||||
|
if (in_array($episode->episodeNumber, $existingEpisodeMap[$season])) {
|
||||||
|
$existingEpisodes[] = $episode;
|
||||||
|
} else {
|
||||||
|
$missingEpisodes[] = $episode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$missingEpisodes[] = $episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result->setEpisodes($existingEpisodes);
|
||||||
|
$result->setMissingEpisodes($missingEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSeasons(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title);
|
||||||
|
|
||||||
|
$existingEpisodeMap = [];
|
||||||
|
foreach ($existingEpisodeFiles as $file) {
|
||||||
|
/** @var \SplFileInfo $file */
|
||||||
|
$ptn = (object) new PTN()->parse($file->getBasename());
|
||||||
|
$existingEpisodeMap[$ptn->season][] = $ptn->episode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingFullSeasons = [];
|
||||||
|
$existingPartialSeasons = [];
|
||||||
|
$missingSeasons = [];
|
||||||
|
foreach ($existingEpisodeMap as $season => $episodes) {
|
||||||
|
if (count($tmdbResult->episodes[$season]) === count($episodes)) {
|
||||||
|
$existingFullSeasons[] = $season;
|
||||||
|
} elseif (count($episodes) > 0) {
|
||||||
|
$existingPartialSeasons[] = $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$seasons = array_keys($tmdbResult->episodes);
|
||||||
|
foreach ($seasons as $season) {
|
||||||
|
if (!in_array($season, $existingFullSeasons) && !in_array($season, $existingPartialSeasons)) {
|
||||||
|
$missingSeasons[] = $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->setSeasons($existingFullSeasons);
|
||||||
|
$result->setPartialSeasons($existingPartialSeasons);
|
||||||
|
$result->setMissingSeasons($missingSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMonitors(int $userId, string $imdbId, GetMediaFromLibraryResult $result)
|
||||||
|
{
|
||||||
|
$result->setMonitorCount(
|
||||||
|
$this->monitorRepository->countUserChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
$result->setActiveMonitorCount(
|
||||||
|
$this->monitorRepository->countUserActiveChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
$result->setCompleteMonitorCount(
|
||||||
|
$this->monitorRepository->countUserCompleteChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Library/Action/Input/GetMediaInfoFromLibraryInput.php
Normal file
39
src/Library/Action/Input/GetMediaInfoFromLibraryInput.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Input;
|
||||||
|
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements InputInterface<GetMediaInfoFromLibraryInput, GetMediaFromLibraryCommand>
|
||||||
|
*/
|
||||||
|
class GetMediaInfoFromLibraryInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceRequest('imdbId', nullify: true)]
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
#[SourceRequest('title', nullify: true)]
|
||||||
|
public ?string $title = null,
|
||||||
|
#[SourceRequest('season', nullify: true)]
|
||||||
|
public ?string $season = null,
|
||||||
|
#[SourceRequest('episode', nullify: true)]
|
||||||
|
public ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): C
|
||||||
|
{
|
||||||
|
if (null === $this->imdbId && null === $this->title) {
|
||||||
|
throw new \InvalidArgumentException('Either imdbId or title must be set', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetMediaFromLibraryCommand(
|
||||||
|
imdbId: $this->imdbId,
|
||||||
|
title: $this->title,
|
||||||
|
season: $this->season,
|
||||||
|
episode: $this->episode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Library/Action/Result/GetMediaFromLibraryResult.php
Normal file
171
src/Library/Action/Result/GetMediaFromLibraryResult.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class GetMediaFromLibraryResult implements ResultInterface
|
||||||
|
{
|
||||||
|
private bool $exists;
|
||||||
|
private ?string $title = null;
|
||||||
|
private ?string $imdbId = null;
|
||||||
|
private ?string $mediaType = null;
|
||||||
|
private ?array $episodes = null;
|
||||||
|
private ?array $missingEpisodes = null;
|
||||||
|
private ?array $seasons = null;
|
||||||
|
private ?array $partialSeasons = null;
|
||||||
|
private ?array $missingSeasons = null;
|
||||||
|
private ?int $monitorCount = null; // Monitor Repo
|
||||||
|
private ?int $activeMonitorCount = null; // Monitor Repo
|
||||||
|
private ?int $completeMonitorCount = null; // Monitor Repo
|
||||||
|
|
||||||
|
public function exists(): bool
|
||||||
|
{
|
||||||
|
return $this->exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notExists(): bool
|
||||||
|
{
|
||||||
|
return !$this->exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExists(bool $exists): void
|
||||||
|
{
|
||||||
|
$this->exists = $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(?string $title): void
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImdbId(): ?string
|
||||||
|
{
|
||||||
|
return $this->imdbId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImdbId(?string $imdbId): void
|
||||||
|
{
|
||||||
|
$this->imdbId = $imdbId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMediaType(): ?string
|
||||||
|
{
|
||||||
|
return $this->mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMediaType(?string $mediaType): void
|
||||||
|
{
|
||||||
|
$this->mediaType = $mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodes(): ?array
|
||||||
|
{
|
||||||
|
return $this->episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEpisodes(?array $episodes): void
|
||||||
|
{
|
||||||
|
$this->episodes = $episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodeCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->episodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingEpisodes(): ?array
|
||||||
|
{
|
||||||
|
return $this->missingEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMissingEpisodes(?array $missingEpisodes): void
|
||||||
|
{
|
||||||
|
$this->missingEpisodes = $missingEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingEpisodeCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->missingEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSeasons(?array $seasons): void
|
||||||
|
{
|
||||||
|
$this->seasons = $seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->seasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPartialSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->partialSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPartialSeasons(?array $partialSeasons): void
|
||||||
|
{
|
||||||
|
$this->partialSeasons = $partialSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPartialSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->partialSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->missingSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMissingSeasons(?array $missingSeasons): void
|
||||||
|
{
|
||||||
|
$this->missingSeasons = $missingSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->missingSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->monitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMonitorCount(?int $monitorCount): void
|
||||||
|
{
|
||||||
|
$this->monitorCount = $monitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->activeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActiveMonitorCount(?int $activeMonitorCount): void
|
||||||
|
{
|
||||||
|
$this->activeMonitorCount = $activeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCompleteMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->completeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCompleteMonitorCount(?int $completeMonitorCount): void
|
||||||
|
{
|
||||||
|
$this->completeMonitorCount = $completeMonitorCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Monitor\Action\Result\AddMonitorResult;
|
|||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\MonitorEvents;
|
use App\Monitor\MonitorEvents;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
@@ -22,13 +23,17 @@ readonly class AddMonitorHandler implements HandlerInterface
|
|||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private MonitorRepository $movieMonitorRepository,
|
private MonitorRepository $movieMonitorRepository,
|
||||||
private UserRepository $userRepository,
|
private UserRepository $userRepository,
|
||||||
|
private TmdbClient $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$user = $this->userRepository->find($command->userId);
|
$user = $this->userRepository->find($command->userId);
|
||||||
|
$poster = $this->getPoster($command->imdbId);
|
||||||
|
|
||||||
$monitor = (new Monitor())
|
$monitor = (new Monitor())
|
||||||
->setUser($user)
|
->setUser($user)
|
||||||
|
->setPoster($poster)
|
||||||
->setTmdbId($command->tmdbId)
|
->setTmdbId($command->tmdbId)
|
||||||
->setImdbId($command->imdbId)
|
->setImdbId($command->imdbId)
|
||||||
->setTitle($command->title)
|
->setTitle($command->title)
|
||||||
@@ -56,4 +61,10 @@ readonly class AddMonitorHandler implements HandlerInterface
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getPoster(string $imdbId): ?string
|
||||||
|
{
|
||||||
|
$data = $this->tmdb->tvShowDetails($imdbId);
|
||||||
|
return $data->poster;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Download\Framework\Repository\DownloadRepository;
|
|||||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\MonitorEvents;
|
use App\Monitor\MonitorEvents;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
@@ -43,6 +44,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
try {
|
try {
|
||||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
||||||
|
$this->refreshData($monitor);
|
||||||
|
|
||||||
$this->bus->dispatch(new AddEventLogCommand(
|
$this->bus->dispatch(new AddEventLogCommand(
|
||||||
$monitor->getUser(),
|
$monitor->getUser(),
|
||||||
@@ -151,4 +153,15 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function refreshData(Monitor $monitor)
|
||||||
|
{
|
||||||
|
if (null === $monitor->getPoster()) {
|
||||||
|
$this->logger->info('> [MonitorTvEpisodeHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
||||||
|
$poster = $monitor->getParent()->getPoster();
|
||||||
|
if (null !== $poster && "" !== $poster) {
|
||||||
|
$monitor->setPoster($poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
||||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||||
|
$this->refreshData($monitor);
|
||||||
|
|
||||||
// Check current episodes
|
// Check current episodes
|
||||||
$downloadedEpisodes = $this->mediaFiles
|
$downloadedEpisodes = $this->mediaFiles
|
||||||
@@ -157,4 +158,15 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
'status' => ['New', 'Active', 'In Progress']
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
]) !== null;
|
]) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function refreshData(Monitor $monitor)
|
||||||
|
{
|
||||||
|
if (null === $monitor->getPoster()) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
||||||
|
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster;
|
||||||
|
if (null !== $poster && "" !== $poster) {
|
||||||
|
$monitor->setPoster($poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class ApiController extends AbstractController
|
|||||||
'allDay' => true,
|
'allDay' => true,
|
||||||
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
||||||
'borderColor' => $eventColors[$monitor->getImdbId()],
|
'borderColor' => $eventColors[$monitor->getImdbId()],
|
||||||
|
'attachment' => $monitor->getPoster(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Monitor\Framework\Controller;
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use Spatie\IcalendarGenerator\Components\Calendar;
|
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||||
@@ -27,9 +28,10 @@ class CalendarController extends AbstractController
|
|||||||
->refreshInterval(10);
|
->refreshInterval(10);
|
||||||
|
|
||||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
$calendar->event(Map::from($monitors)->map(function ($monitor) {
|
$calendar->event(Map::from($monitors)->map(function (Monitor $monitor) {
|
||||||
return new Event($monitor->getTitle())
|
return new Event($monitor->getTitle())
|
||||||
->startsAt($monitor->getAirDate())
|
->startsAt($monitor->getAirDate())
|
||||||
|
->attachment($monitor->getPoster())
|
||||||
->fullDay();
|
->fullDay();
|
||||||
})->toArray());
|
})->toArray());
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,17 @@
|
|||||||
namespace App\Monitor\Framework\Controller;
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
use App\Download\Action\Input\DeleteDownloadInput;
|
use App\Download\Action\Input\DeleteDownloadInput;
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use App\Library\Action\Handler\GetMediaInfoFromLibraryHandler;
|
||||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||||
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
||||||
use App\Monitor\Action\Input\AddMonitorInput;
|
use App\Monitor\Action\Input\AddMonitorInput;
|
||||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
|
use App\Search\Action\Handler\GetMediaInfoHandler;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
@@ -37,10 +42,27 @@ class WebController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/monitors/{id}', name: 'app.monitor.view', methods: ['GET'])]
|
#[Route('/monitors/{id}', name: 'app.monitor.view', methods: ['GET'])]
|
||||||
public function viewMonitor(Monitor $monitor)
|
public function viewMonitor(Monitor $monitor, GetMediaInfoHandler $getMediaInfoHandler, GetMediaInfoFromLibraryHandler $handler)
|
||||||
{
|
{
|
||||||
|
$media = $getMediaInfoHandler->handle(
|
||||||
|
new GetMediaInfoCommand(
|
||||||
|
imdbId: $monitor->getImdbId(),
|
||||||
|
mediaType: 'tvshows',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$libraryResult = $handler->handle(
|
||||||
|
new GetMediaFromLibraryCommand(
|
||||||
|
$this->getUser()->getId(),
|
||||||
|
$media->media->mediaType,
|
||||||
|
$media->media->imdbId,
|
||||||
|
$media->media->title,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return $this->render('monitor/view.html.twig', [
|
return $this->render('monitor/view.html.twig', [
|
||||||
'monitor' => $monitor,
|
'monitor' => $monitor,
|
||||||
|
'results' => $media,
|
||||||
|
'library' => $libraryResult
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class Monitor
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?int $searchCount = null;
|
private ?int $searchCount = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?string $poster = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private bool $onlyFuture = true;
|
private bool $onlyFuture = true;
|
||||||
|
|
||||||
@@ -230,6 +233,17 @@ class Monitor
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPoster(): ?string
|
||||||
|
{
|
||||||
|
return $this->poster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPoster(?string $poster): ?self
|
||||||
|
{
|
||||||
|
$this->poster = $poster;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getParent(): ?self
|
public function getParent(): ?self
|
||||||
{
|
{
|
||||||
return $this->parent;
|
return $this->parent;
|
||||||
|
|||||||
@@ -41,4 +41,83 @@ class MonitorRepository extends ServiceEntityRepository
|
|||||||
->getQuery();
|
->getQuery();
|
||||||
return $query->getResult();
|
return $query->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActiveUserMonitors()
|
||||||
|
{
|
||||||
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
|
->andWhere('m.parent IS NULL')
|
||||||
|
->setParameter('statuses', ['New', 'In Progress', 'Active'])
|
||||||
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChildMonitorsByParentId(int $parentId)
|
||||||
|
{
|
||||||
|
return $this->asPaginator(
|
||||||
|
$this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere("m.parent = :parentId")
|
||||||
|
->setParameter('parentId', $parentId)
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCompleteUserMonitors()
|
||||||
|
{
|
||||||
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere('m.status = :status')
|
||||||
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
|
->setParameter('status', 'Complete')
|
||||||
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserActiveChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('statuses', ['Active', 'New', 'In Progress'])
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserCompleteChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('statuses', ['Complete'])
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class MonitorDispatcher
|
|||||||
'tvshows' => MonitorTvShowCommand::class,
|
'tvshows' => MonitorTvShowCommand::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
$monitors = $this->monitorRepository->findBy([
|
||||||
|
'status' => ['New', 'Active'],
|
||||||
|
]);
|
||||||
|
|
||||||
foreach ($monitors as $monitor) {
|
foreach ($monitors as $monitor) {
|
||||||
$monitor->setStatus('In Progress');
|
$monitor->setStatus('In Progress');
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block pre_js %}{% endblock %}
|
||||||
|
|
||||||
|
{% block importmap %}
|
||||||
|
{{ importmap('app') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block post_js %}{% endblock %}
|
||||||
|
|
||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<tr{{ attributes }} is="monitor-list-row" id="monitor_{{ monitor.id }}" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900"
|
<tr{{ attributes }} is="monitor-list-row" id="monitor_{{ monitor.id }}" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer"
|
||||||
monitor-id="{{ monitor.id }}"
|
monitor-id="{{ monitor.id }}"
|
||||||
parent-id="{{ monitor.parent.id ?? null }}"
|
parent-id="{{ monitor.parent.id ?? null }}"
|
||||||
imdb-id="{{ monitor.imdbId }}"
|
imdb-id="{{ monitor.imdbId }}"
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
created-at="{{ monitor.createdAt|date('m/d/Y g:i a') }}"
|
created-at="{{ monitor.createdAt|date('m/d/Y g:i a') }}"
|
||||||
last-search="{{ monitor.lastSearch|date('m/d/Y g:i a') }}"
|
last-search="{{ monitor.lastSearch|date('m/d/Y g:i a') }}"
|
||||||
downloaded-at="{{null != monitor.downloadedAt ? monitor.downloadedAt|date('m/d/Y g:i a') : '-' }}"
|
downloaded-at="{{null != monitor.downloadedAt ? monitor.downloadedAt|date('m/d/Y g:i a') : '-' }}"
|
||||||
|
air-date="{{ null != monitor.airDate ? monitor.airDate|date('m/d/Y g:i a') : '-' }}"
|
||||||
>
|
>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
||||||
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
||||||
@@ -27,11 +28,13 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
{{ monitor|monitor_media_id }}
|
{{ monitor|monitor_media_id }}
|
||||||
</td>
|
</td>
|
||||||
|
{# Monitor is a CHILD monitor #}
|
||||||
{% if null != monitor.parent %}
|
{% if null != monitor.parent %}
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
||||||
{{ monitor.searchCount }}
|
{{ monitor.searchCount }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{# Monitor is a PARENT monitor #}
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
||||||
{{ monitor.children|length }}
|
{{ monitor.children|length }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<dialog{{ attributes }} is="preview-content-dialog" class="py-3 px-4 w-full md:w-[50rem] rounded-md dark:bg-gray-950/80 dark:border-2 dark:border-orange-500 dark:text-white backdrop-filter backdrop-blur-3xl">
|
<dialog{{ attributes }} is="preview-content-dialog" class="py-3 px-4 w-full md:w-[{{ mdWidth|default('50rem') }}] rounded-md dark:bg-gray-950/80 dark:border-2 dark:border-orange-500 dark:text-white backdrop-filter backdrop-blur-3xl">
|
||||||
<div class="flex flex-row justify-end">
|
<div class="flex flex-row justify-end">
|
||||||
<twig:ux:icon name="ic:twotone-cancel" width="16.75px" height="16.75px" class="modal-close rounded-full align-middle text-red-600 hover:text-red-700" />
|
<twig:ux:icon name="ic:twotone-cancel" width="16.75px" height="16.75px" class="modal-close rounded-full align-middle text-red-600 hover:text-red-700" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,20 +4,31 @@
|
|||||||
{% block h2 %}Dashboard{% endblock %}
|
{% block h2 %}Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col grow gap-4 z-10">
|
<div class="p-4 z-10">
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="grid-stack gs-2">
|
||||||
<twig:Card title="Active Downloads" class="w-full">
|
<div class="grid-stack-item" gs-x="1">
|
||||||
<twig:DownloadList :type="'active'" />
|
<div class="grid-stack-item-content">
|
||||||
</twig:Card>
|
<twig:Card title="Active Downloads">
|
||||||
|
<twig:DownloadList :type="'active'" />
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<twig:Card title="Recent Downloads" class="w-full">
|
<div class="grid-stack-item" gs-x="2">
|
||||||
<twig:DownloadList :type="'complete'" />
|
<div class="grid-stack-item-content">
|
||||||
</twig:Card>
|
<twig:Card title="Complete Downloads" >
|
||||||
</div>
|
<twig:DownloadList :type="'complete'" />
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
</twig:Card>
|
||||||
<twig:Card title="Monitors" class="w-full">
|
</div>
|
||||||
<twig:MonitorList :type="'active'" :isWidget="true" />
|
</div>
|
||||||
</twig:Card>
|
|
||||||
|
<div class="grid-stack-item" gs-x="3">
|
||||||
|
<div class="grid-stack-item-content">
|
||||||
|
<twig:Card title="Active Monitors">
|
||||||
|
<twig:MonitorList :type="'active'" />
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<twig:Card title="Popular Movies" contentClass="grid grid-cols-2 gap-4 md:flex md:flex-row md:justify-between w-full">
|
<twig:Card title="Popular Movies" contentClass="grid grid-cols-2 gap-4 md:flex md:flex-row md:justify-between w-full">
|
||||||
@@ -45,5 +56,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-stack" data-controller="dashboard-widgets"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const modal = document.getElementById('previewModal');
|
const modal = document.getElementById('previewModal');
|
||||||
|
modal.setAttribute('mdWidth', '25rem');
|
||||||
let data = await fetch('/api/monitor/upcoming-episodes');
|
let data = await fetch('/api/monitor/upcoming-episodes');
|
||||||
data = (await data.json())['data'];
|
data = (await data.json())['data'];
|
||||||
|
|
||||||
@@ -47,7 +48,12 @@
|
|||||||
eventClick: function (data) {
|
eventClick: function (data) {
|
||||||
modal.display({
|
modal.display({
|
||||||
heading: data.event.title,
|
heading: data.event.title,
|
||||||
content: `<p>${data.event.startStr}</p>`
|
content: `
|
||||||
|
<div class="flex flex-col gap-4 justify-center items-center">
|
||||||
|
<img src="${data.event.extendedProps.attachment}" class="w-[90%] rounded-md" />
|
||||||
|
<p>${data.event.startStr}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,141 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="px-4 py-2">
|
<div class="px-4 py-2">
|
||||||
<twig:Card title="Viewing your monitors for {{ monitor.title }}">
|
<twig:Card title="Viewing your monitors for {{ monitor.title }}">
|
||||||
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
|
{% if results.media.poster != null %}
|
||||||
|
<img class="w-full md:w-[12.5rem] rounded-lg" src="{{ results.media.poster }}" />
|
||||||
|
{% else %}
|
||||||
|
<div class="w-full md:w-[12.5rem] h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
|
<twig:ux:icon width="24" name="hugeicons:loading-01" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col">
|
||||||
|
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||||
|
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
|
{{ results.media.title }} ({{ results.media.year }})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-50 mb-4">
|
||||||
|
{{ results.media.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-gray-50 mb-2">
|
||||||
|
<div id="people" class="mb-1">
|
||||||
|
{% if results.media.stars != null %}
|
||||||
|
<strong>Starring</strong>: {{ results.media.stars|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.directors != null %}
|
||||||
|
<strong>Directors</strong>: {{ results.media.directors|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.producers != null %}
|
||||||
|
<strong>Producers</strong>: {{ results.media.producers|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.creators != null %}
|
||||||
|
<strong>Creators</strong>: {{ results.media.creators|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dates" class="mb-1">
|
||||||
|
{% if results.media.premiereDate %}
|
||||||
|
<strong>Premiered</strong>: {{ results.media.premiereDate|date('n/j/Y', 'UTC') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results.media.genres != null %}
|
||||||
|
<div id="genres" class="text-gray-50 my-4">
|
||||||
|
{# <strong>Genres</strong>: <br />#}
|
||||||
|
{% for genre in results.media.genres %}
|
||||||
|
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col grow text-white">
|
||||||
|
<strong class="mb-1">In Your Library</strong>
|
||||||
|
<div class="flex flex-col md:flex-row border-t-orange-500 text-xs gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Seasons</span>
|
||||||
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.seasonCount }}</span> full
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.partialSeasonCount }}</span> partial
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.missingSeasonCount }}</span> missing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Episodes</span>
|
||||||
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.episodeCount }}</span> existing
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.missingEpisodeCount }}</span> missing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Monitors</span>
|
||||||
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.monitorCount }}</span> total
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.activeMonitorCount }}</span> active
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.completeMonitorCount }}</span> complete
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "movies" == results.media.mediaType %}
|
||||||
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
|
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
|
<span class="results-count-number" id="movie_results_count">-</span> results
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||||
|
title: results.media.title,
|
||||||
|
block: 'media_exists_badge',
|
||||||
|
target: "meb_" ~ results.media.imdbId
|
||||||
|
}) }}">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
||||||
|
missing
|
||||||
|
</span>
|
||||||
|
</twig:Turbo:Frame>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||||
|
{{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white" title="This movie has a runtime of {{ results.media.runtime }} minutes.">
|
||||||
|
{{ results.media.runtime }} minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<twig:MonitorList :parentMonitorId="monitor.id" :isWidget="false" :perPage="10"></twig:MonitorList>
|
<twig:MonitorList :parentMonitorId="monitor.id" :isWidget="false" :perPage="10"></twig:MonitorList>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user