feat: upcoming episodes component

This commit is contained in:
2025-06-12 23:39:06 -05:00
parent 937313fe59
commit 0f16423f66
7 changed files with 210 additions and 82 deletions

View File

@@ -4,7 +4,7 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"1tomany/rich-bundle": "^1.8",
@@ -57,6 +57,9 @@
"symfony/flex": true,
"symfony/runtime": true
},
"platform": {
"php": "8.4"
},
"bump-after-update": true,
"sort-packages": true
},

148
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "55c76ae7fe5ad6e5c7edbb0150987fc7",
"content-hash": "e8b5e39f9d73a6ace2b9e39240186b4f",
"packages": [
{
"name": "1tomany/rich-bundle",
"version": "v1.9.5",
"version": "v1.10.4",
"source": {
"type": "git",
"url": "https://github.com/1tomany/rich-bundle.git",
"reference": "434c0fa70aefa11a23342006a10221360beb0f71"
"reference": "63a728e22632082d6db07e158bf1f5a4e5854d01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/1tomany/rich-bundle/zipball/434c0fa70aefa11a23342006a10221360beb0f71",
"reference": "434c0fa70aefa11a23342006a10221360beb0f71",
"url": "https://api.github.com/repos/1tomany/rich-bundle/zipball/63a728e22632082d6db07e158bf1f5a4e5854d01",
"reference": "63a728e22632082d6db07e158bf1f5a4e5854d01",
"shasum": ""
},
"require": {
@@ -60,9 +60,9 @@
"description": "Symfony bundle that provides easy scaffolding to build RICH applications",
"support": {
"issues": "https://github.com/1tomany/rich-bundle/issues",
"source": "https://github.com/1tomany/rich-bundle/tree/v1.9.5"
"source": "https://github.com/1tomany/rich-bundle/tree/v1.10.4"
},
"time": "2025-05-01T16:54:44+00:00"
"time": "2025-06-05T00:20:20+00:00"
},
{
"name": "aimeos/map",
@@ -997,16 +997,16 @@
},
{
"name": "doctrine/doctrine-migrations-bundle",
"version": "3.4.1",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineMigrationsBundle.git",
"reference": "e858ce0f5c12b266dce7dce24834448355155da7"
"reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7",
"reference": "e858ce0f5c12b266dce7dce24834448355155da7",
"url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9",
"reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9",
"shasum": ""
},
"require": {
@@ -1020,7 +1020,6 @@
"composer/semver": "^3.0",
"doctrine/coding-standard": "^12",
"doctrine/orm": "^2.6 || ^3",
"doctrine/persistence": "^2.0 || ^3",
"phpstan/phpstan": "^1.4 || ^2",
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2",
@@ -1063,7 +1062,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues",
"source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1"
"source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.2"
},
"funding": [
{
@@ -1079,7 +1078,7 @@
"type": "tidelift"
}
],
"time": "2025-01-27T22:48:22+00:00"
"time": "2025-03-11T17:36:26+00:00"
},
{
"name": "doctrine/event-manager",
@@ -2079,16 +2078,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.9.1",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "ced71f79398ece168e24f7f7710462f462310d4d"
"reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d",
"reference": "ced71f79398ece168e24f7f7710462f462310d4d",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
"reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
"shasum": ""
},
"require": {
@@ -2096,9 +2095,9 @@
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
"symfony/clock": "^6.3 || ^7.0",
"symfony/clock": "^6.3.12 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0"
"symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -2106,14 +2105,13 @@
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.57.2",
"friendsofphp/php-cs-fixer": "^3.75.0",
"kylekatarnls/multi-tester": "^2.5.3",
"ondrejmirtes/better-reflection": "^6.25.0.4",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.11.2",
"phpunit/phpunit": "^10.5.20",
"squizlabs/php_codesniffer": "^3.9.0"
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.17",
"phpunit/phpunit": "^10.5.46",
"squizlabs/php_codesniffer": "^3.13.0"
},
"bin": [
"bin/carbon"
@@ -2181,7 +2179,7 @@
"type": "tidelift"
}
],
"time": "2025-05-01T19:51:51+00:00"
"time": "2025-06-12T10:24:28+00:00"
},
{
"name": "nihilarr/parse-torrent-name",
@@ -7672,16 +7670,16 @@
},
{
"name": "symfony/stimulus-bundle",
"version": "v2.25.2",
"version": "v2.26.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/stimulus-bundle.git",
"reference": "5a6aef0646119530da862d5afa1386ade3b9ed43"
"reference": "82c174ebe564e6ecc1412974b6380b86d450675f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/5a6aef0646119530da862d5afa1386ade3b9ed43",
"reference": "5a6aef0646119530da862d5afa1386ade3b9ed43",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/82c174ebe564e6ecc1412974b6380b86d450675f",
"reference": "82c174ebe564e6ecc1412974b6380b86d450675f",
"shasum": ""
},
"require": {
@@ -7721,7 +7719,7 @@
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.25.2"
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.26.1"
},
"funding": [
{
@@ -7737,7 +7735,7 @@
"type": "tidelift"
}
],
"time": "2025-05-19T11:54:27+00:00"
"time": "2025-06-05T17:25:17+00:00"
},
{
"name": "symfony/stopwatch",
@@ -8338,16 +8336,16 @@
},
{
"name": "symfony/ux-icons",
"version": "v2.25.0",
"version": "v2.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-icons.git",
"reference": "430b2753aa55a46baa001055bf7976b62bc96942"
"reference": "e5c1e5b5093ae26dba45d0f3390a1e21f305c47a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-icons/zipball/430b2753aa55a46baa001055bf7976b62bc96942",
"reference": "430b2753aa55a46baa001055bf7976b62bc96942",
"url": "https://api.github.com/repos/symfony/ux-icons/zipball/e5c1e5b5093ae26dba45d0f3390a1e21f305c47a",
"reference": "e5c1e5b5093ae26dba45d0f3390a1e21f305c47a",
"shasum": ""
},
"require": {
@@ -8407,7 +8405,7 @@
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-icons/tree/v2.25.0"
"source": "https://github.com/symfony/ux-icons/tree/v2.26.0"
},
"funding": [
{
@@ -8423,20 +8421,20 @@
"type": "tidelift"
}
],
"time": "2025-04-07T13:54:07+00:00"
"time": "2025-05-30T02:07:34+00:00"
},
{
"name": "symfony/ux-live-component",
"version": "v2.25.2",
"version": "v2.26.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-live-component.git",
"reference": "79e8cc179eb21119547c492ae21e4bf529ac1a15"
"reference": "92b300bb90d87f14aeae47b0f5c9e058b15f5c2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-live-component/zipball/79e8cc179eb21119547c492ae21e4bf529ac1a15",
"reference": "79e8cc179eb21119547c492ae21e4bf529ac1a15",
"url": "https://api.github.com/repos/symfony/ux-live-component/zipball/92b300bb90d87f14aeae47b0f5c9e058b15f5c2f",
"reference": "92b300bb90d87f14aeae47b0f5c9e058b15f5c2f",
"shasum": ""
},
"require": {
@@ -8449,7 +8447,9 @@
"twig/twig": "^3.10.3"
},
"conflict": {
"symfony/config": "<5.4.0"
"symfony/config": "<5.4.0",
"symfony/property-info": "~7.0.0",
"symfony/type-info": "<7.2"
},
"require-dev": {
"doctrine/annotations": "^1.0",
@@ -8502,7 +8502,7 @@
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-live-component/tree/v2.25.2"
"source": "https://github.com/symfony/ux-live-component/tree/v2.26.1"
},
"funding": [
{
@@ -8518,20 +8518,20 @@
"type": "tidelift"
}
],
"time": "2025-05-19T11:54:27+00:00"
"time": "2025-06-06T19:57:53+00:00"
},
{
"name": "symfony/ux-turbo",
"version": "v2.25.2",
"version": "v2.26.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-turbo.git",
"reference": "11ebca138005c7e25678c3f98a07ddf718a0480c"
"reference": "3754ac2b41220127e58c62f7599eaf7834b69a55"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/11ebca138005c7e25678c3f98a07ddf718a0480c",
"reference": "11ebca138005c7e25678c3f98a07ddf718a0480c",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/3754ac2b41220127e58c62f7599eaf7834b69a55",
"reference": "3754ac2b41220127e58c62f7599eaf7834b69a55",
"shasum": ""
},
"require": {
@@ -8545,7 +8545,8 @@
"dbrekelmans/bdi": "dev-main",
"doctrine/doctrine-bundle": "^2.4.3",
"doctrine/orm": "^2.8 | 3.0",
"phpstan/phpstan": "^1.10",
"php-webdriver/webdriver": "^1.15",
"phpstan/phpstan": "^2.1.17",
"symfony/asset-mapper": "^6.4|^7.0",
"symfony/debug-bundle": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
@@ -8553,7 +8554,7 @@
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/mercure-bundle": "^0.3.7",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/panther": "^2.1",
"symfony/panther": "^2.2",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|6.3.*|^7.0",
"symfony/property-access": "^5.4|^6.0|^7.0",
@@ -8600,7 +8601,7 @@
"turbo-stream"
],
"support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.25.2"
"source": "https://github.com/symfony/ux-turbo/tree/v2.26.1"
},
"funding": [
{
@@ -8616,20 +8617,20 @@
"type": "tidelift"
}
],
"time": "2025-05-19T11:54:27+00:00"
"time": "2025-06-05T17:25:17+00:00"
},
{
"name": "symfony/ux-twig-component",
"version": "v2.25.2",
"version": "v2.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-twig-component.git",
"reference": "d20da25517fc09d147897d02819a046f0a0f6735"
"reference": "825e653b34fb48ed2198913c603d80f7632fe9c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/d20da25517fc09d147897d02819a046f0a0f6735",
"reference": "d20da25517fc09d147897d02819a046f0a0f6735",
"url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/825e653b34fb48ed2198913c603d80f7632fe9c1",
"reference": "825e653b34fb48ed2198913c603d80f7632fe9c1",
"shasum": ""
},
"require": {
@@ -8683,7 +8684,7 @@
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-twig-component/tree/v2.25.2"
"source": "https://github.com/symfony/ux-twig-component/tree/v2.26.0"
},
"funding": [
{
@@ -8699,7 +8700,7 @@
"type": "tidelift"
}
],
"time": "2025-05-20T13:06:01+00:00"
"time": "2025-05-26T06:21:54+00:00"
},
{
"name": "symfony/validator",
@@ -9386,16 +9387,16 @@
"packages-dev": [
{
"name": "nikic/php-parser",
"version": "v5.4.0",
"version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"shasum": ""
},
"require": {
@@ -9438,22 +9439,22 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
},
"time": "2024-12-30T11:07:19+00:00"
"time": "2025-05-31T08:24:38+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.14",
"version": "2.1.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2"
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": ""
},
"require": {
@@ -9498,7 +9499,7 @@
"type": "github"
}
],
"time": "2025-05-02T15:32:28+00:00"
"time": "2025-05-21T20:55:28+00:00"
},
{
"name": "symfony/maker-bundle",
@@ -9684,10 +9685,13 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=8.2",
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*"
},
"platform-dev": [],
"platform-overrides": {
"php": "8.4"
},
"plugin-api-version": "2.3.0"
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Monitor\Dto;
use Carbon\Carbon;
class UpcomingEpisode
{
public function __construct(
public string $title,
public string $airDate {
get => Carbon::parse($this->airDate)->format('m/d/Y');
},
public string $episodeTitle,
public int $episodeNumber,
) {}
}

View File

@@ -259,4 +259,9 @@ class Monitor
return $this;
}
public function isActive(): bool
{
return in_array($this->status, ['New', 'Active', 'In Progress']);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Twig\Components;
use Aimeos\Map;
use App\Monitor\Dto\UpcomingEpisode;
use App\Monitor\Framework\Entity\Monitor;
use App\Tmdb\Tmdb;
use Carbon\CarbonImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Tmdb\Model\Tv\Episode;
#[AsTwigComponent]
final class UpcomingEpisodes extends AbstractController
{
// Get active monitors
// Search TMDB for upcoming episodes
public function __construct(
private readonly Tmdb $tmdb,
) {}
public function getUpcomingEpisodes(int $limit = 5): array
{
$upcomingEpisodes = new Map();
$monitors = $this->getMonitors();
foreach ($monitors as $monitor) {
$upcomingEpisodes->merge($this->getNextEpisodes($monitor));
}
return $upcomingEpisodes->slice(0, $limit)->toArray();
}
private function getMonitors()
{
$user = $this->getUser();
return $user->getMonitors()->filter(
fn (Monitor $monitor) => null === $monitor->getParent() && $monitor->isActive()
) ?? [];
}
private function getNextEpisodes(Monitor $monitor): Map
{
$today = CarbonImmutable::now();
$seriesInfo = $this->tmdb->tvDetails($monitor->getTmdbId());
switch ($monitor->getMonitorType()) {
case "tvseason":
$episodes = Map::from($seriesInfo->episodes[$monitor->getSeason()])
->filter(function (array $episode) use ($today) {
$airDate = CarbonImmutable::parse($episode['air_date']);
return $airDate->lte($today);
})
;
break;
case "tvshows":
$episodes = [];
foreach ($seriesInfo->episodes as $season => $episodeList) {
$episodes = array_merge($episodes, $episodeList);
}
$episodes = Map::from($episodes)
->filter(function (array $episode) use ($today) {
$airDate = CarbonImmutable::parse($episode['air_date']);
return $airDate->gte($today);
})
;
break;
}
return $episodes->map(function (array $episode) use ($monitor) {
return new UpcomingEpisode(
$monitor->getTitle(),
$episode['air_date'],
$episode['name'],
$episode['episode_number'],
);
});
}
}

View File

@@ -0,0 +1,11 @@
<div{{ attributes }}>
<ul class="text-white flex flex-col gap-2">
{% for episode in this.upcomingEpisodes %}
<li class="flex flex-col">
<span class="bg-[#f98e44] bg-filter bg-blur-lg bg-opacity-100 text-gray-950 w-full p-[.1rem] pl-[.3rem] rounded-ms">{{ episode.title }}</span>
<span>{{ episode.episodeTitle }}</span>
<span>{{ episode.airDate }}</span>
</li>
{% endfor %}
</ul>
</div>

View File

@@ -4,15 +4,22 @@
{% block h2 %}Monitors{% endblock %}
{% block body %}
<div class="p-4">
<twig:Card title="Active Monitors">
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
<div class="flex flex-row">
<div class="p-2 flex flex-col gap-4">
<twig:Card title="Active Monitors">
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
<twig:Card title="Complete Monitors">
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
<div class="p-2">
<twig:Card title="Upcoming Episodes" >
<twig:UpcomingEpisodes />
</twig:Card>
</div>
<div class="p-4">
<twig:Card title="Complete Monitors">
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
{% endblock %}