Compare commits
19 Commits
dev-notifi
...
dev-torren
| Author | SHA1 | Date | |
|---|---|---|---|
| f9ddd70668 | |||
| 33bfe40b11 | |||
| f3a2f35571 | |||
| 9eef567974 | |||
| 070723581a | |||
| f3a5c2012e | |||
| 5581a82554 | |||
| 3703272f59 | |||
| b587302b30 | |||
| e5bab8e6fd | |||
| 502b85dda4 | |||
| 9c430290e9 | |||
| 583591bf4f | |||
| 182708b8f0 | |||
| d6ba4d7d2a | |||
| e5c5ec93a8 | |||
| 942911d8ef | |||
| 2f7d406d12 | |||
| 4e1adc576c |
2
.env
2
.env
@@ -57,4 +57,4 @@ OIDC_BYPASS_FORM_LOGIN=false
|
||||
###< symfony/ntfy-notifier ###
|
||||
|
||||
NOTIFICATION_TRANSPORT=
|
||||
NTFY_DNS=
|
||||
NTFY_DSN=
|
||||
|
||||
@@ -18,11 +18,3 @@ var observer = new MutationObserver(function(mutations) {
|
||||
});
|
||||
|
||||
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
||||
|
||||
const ptr = PullToRefresh.init({
|
||||
mainElement: 'body',
|
||||
onRefresh() {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
57
assets/controllers/upcoming_episodes_controller.js
Normal file
57
assets/controllers/upcoming_episodes_controller.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Calendar } from '@fullcalendar/core';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
calendar = null;
|
||||
|
||||
initialize() {
|
||||
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.calendar = new Calendar(this.element, {
|
||||
plugins: [ dayGridPlugin, timeGridPlugin ],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
},
|
||||
editable: true, // Allow events to be dragged and resized
|
||||
events: '/api/events', // Symfony route to fetch events
|
||||
eventDrop: function(info) {
|
||||
// Handle event drop (e.g., update event in database via AJAX)
|
||||
},
|
||||
eventResize: function(info) {
|
||||
// Handle event resize (e.g., update event in database via AJAX)
|
||||
}
|
||||
});
|
||||
this.calendar.render();
|
||||
// this.calendar = new Calendar(this.element, {
|
||||
// plugins: [ dayGridPlugin, timeGridPlugin, listPlugin ],
|
||||
// initialView: 'dayGridMonth',
|
||||
// headerToolbar: {
|
||||
// left: 'prev,next today',
|
||||
// center: 'title',
|
||||
// right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||
// }
|
||||
// });
|
||||
// this.calendar.render();
|
||||
// calendar.render();
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 529 B |
1
assets/icons/solar/calendar-linear.svg
Normal file
1
assets/icons/solar/calendar-linear.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="currentColor" stroke-width="1.5" d="M2 12c0-3.771 0-5.657 1.172-6.828S6.229 4 10 4h4c3.771 0 5.657 0 6.828 1.172S22 8.229 22 12v2c0 3.771 0 5.657-1.172 6.828S17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.172S2 17.771 2 14z"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M7 4V2.5M17 4V2.5M2.5 9h19"/><path fill="currentColor" d="M18 17a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 653 B |
@@ -27,6 +27,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--fc-border-color: #a65b27;
|
||||
--fc-page-bg-color: #a65b27;
|
||||
}
|
||||
|
||||
/* Prevent scrolling while dialog is open */
|
||||
body:has(dialog[data-dialog-target="dialog"][open]) {
|
||||
overflow: hidden;
|
||||
@@ -163,6 +168,7 @@ dialog[data-dialog-target="dialog"][closing] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
form[name="torrentio_preferences_form"],
|
||||
#filter {
|
||||
.ts-wrapper {
|
||||
box-shadow: none !important;
|
||||
@@ -193,3 +199,7 @@ dialog[data-dialog-target="dialog"][closing] {
|
||||
.filter-label {
|
||||
@apply flex flex-col gap-1 justify-between;
|
||||
}
|
||||
|
||||
.fc-col-header-cell {
|
||||
@apply bg-orange-500/60 text-white;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"php-tmdb/api": "^4.1",
|
||||
"predis/predis": "^2.4",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"spatie/icalendar-generator": "^3.0",
|
||||
"spomky-labs/pwa-bundle": "^1.2",
|
||||
"stof/doctrine-extensions-bundle": "^1.14",
|
||||
"symfony/asset": "7.3.*",
|
||||
|
||||
61
composer.lock
generated
61
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6f57ba35ae317ec6370836bda0012db8",
|
||||
"content-hash": "9ffd10f98137e8975de2c04ac2412ed5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "1tomany/rich-bundle",
|
||||
@@ -4757,6 +4757,65 @@
|
||||
],
|
||||
"time": "2023-12-12T12:06:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/icalendar-generator",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/icalendar-generator.git",
|
||||
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"larapack/dd": "^1.1",
|
||||
"nesbot/carbon": "^3.5",
|
||||
"pestphp/pest": "^2.34",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"spatie/pest-plugin-snapshots": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\IcalendarGenerator\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ruben Van Assche",
|
||||
"email": "ruben@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Build calendars in the iCalendar format",
|
||||
"homepage": "https://github.com/spatie/icalendar-generator",
|
||||
"keywords": [
|
||||
"calendar",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/icalendar-generator/issues",
|
||||
"source": "https://github.com/spatie/icalendar-generator/tree/3.0.0"
|
||||
},
|
||||
"time": "2025-04-17T14:50:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/pki-framework",
|
||||
"version": "1.3.0",
|
||||
|
||||
@@ -45,6 +45,7 @@ security:
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
FROM dunglas/frankenphp:php8.4-alpine
|
||||
ARG APP_VERSION
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
|
||||
ARG APP_VERSION="0.dev"
|
||||
ENV APP_VERSION="${APP_VERSION}"
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
COPY . /app
|
||||
FROM code.caldwell.digital/torsearch/torsearch-app:${APP_VERSION}
|
||||
|
||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||
|
||||
|
||||
35
migrations/Version20250823173128.php
Normal file
35
migrations/Version20250823173128.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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 Version20250823173128 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE monitor ADD air_date DATETIME 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 air_date
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
47
migrations/Version20250831013403.php
Normal file
47
migrations/Version20250831013403.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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 Version20250831013403 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE sessions
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value 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'
|
||||
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -4,7 +4,6 @@ namespace App\Base\Framework\Command;
|
||||
|
||||
use App\User\Framework\Entity\Preference;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
@@ -135,6 +134,20 @@ class SeedDatabaseCommand extends Command
|
||||
'enabled' => true,
|
||||
'type' => 'download'
|
||||
],
|
||||
[
|
||||
'id' => 'enable_ical_up_ep',
|
||||
'name' => 'Enable a publicly available iCal calendar?',
|
||||
'description' => 'Enable a publicly accessible iCal URL for your upcoming episodes.',
|
||||
'enabled' => true,
|
||||
'type' => 'calendar'
|
||||
],
|
||||
[
|
||||
'id' => 'torrentio_url',
|
||||
'name' => 'A custom Torrentio URL',
|
||||
'description' => 'If you want to use a custom Torrentio URL, enter it here. Otherwise, leave it blank to use the one provided as an environment variable.',
|
||||
'enabled' => true,
|
||||
'type' => 'torrentio'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +53,6 @@ final class IndexController extends AbstractController
|
||||
#[Route('/test')]
|
||||
public function monitorTvShow(): Response
|
||||
{
|
||||
$this->monitorTvShowHandler->handle(new MonitorTvShowCommand(96));
|
||||
return $this->json([
|
||||
'Success' => 'Monitor added'
|
||||
]);
|
||||
return $this->render('index/test.html.twig', []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Monitor\Action\Handler;
|
||||
use App\Base\Util\EpisodeId;
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\DownloadOptionEvaluator;
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||
@@ -44,6 +43,11 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
||||
|
||||
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||
|
||||
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
||||
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
||||
}
|
||||
|
||||
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
|
||||
return new MonitorTvEpisodeResult(
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Tmdb\Tmdb;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Base\Util\PTN;
|
||||
@@ -96,6 +97,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($episode['season_number'])
|
||||
->setEpisode($episode['episode_number'])
|
||||
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?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,
|
||||
) {}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Base\Service\Broadcaster;
|
||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
||||
use App\Monitor\Action\Input\AddMonitorInput;
|
||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -65,4 +67,45 @@ class ApiController extends AbstractController
|
||||
'message' => 'Manually dispatched MonitorDispatcher'
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/monitor/upcoming-episodes', name: 'api.monitor.upcoming-episodes', methods: ['GET'])]
|
||||
public function upcomingEpisodes(MonitorRepository $repository): Response
|
||||
{
|
||||
$colors = [
|
||||
'blue' => '#007bff',
|
||||
'indigo' => '#6610f2',
|
||||
'purple' => '#6f42c1',
|
||||
'pink' => '#e83e8c',
|
||||
'red' => '#dc3545',
|
||||
'orange' => '#fd7e14',
|
||||
'yellow' => '#ffc107',
|
||||
'green' => '#28a745',
|
||||
'teal' => '#20c997',
|
||||
'cyan' => '#17a2b8',
|
||||
];
|
||||
|
||||
$eventColors = [];
|
||||
$monitors = $repository->whereAirDateNotNull();
|
||||
$monitors = Map::from($monitors)->map(function ($monitor) use (&$eventColors, $colors) {
|
||||
if (!array_key_exists($monitor->getImdbId(), $eventColors)) {
|
||||
$eventColors[$monitor->getImdbId()] = $colors[array_rand($colors)];
|
||||
}
|
||||
return [
|
||||
'id' => $monitor->getId(),
|
||||
'title' => $monitor->getTitle() . ' (S' . str_pad($monitor->getSeason(), 2, '0', STR_PAD_LEFT) . 'E' . str_pad($monitor->getEpisode(), 2, '0', STR_PAD_LEFT) . ')',
|
||||
'start' => $monitor->getAirDate()->format('Y-m-d H:i:s'),
|
||||
'groupId' => $monitor->getImdbId(),
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
||||
'borderColor' => $eventColors[$monitor->getImdbId()],
|
||||
];
|
||||
});
|
||||
|
||||
return $this->json([
|
||||
'status' => 200,
|
||||
'data' => [
|
||||
'episodes' => $monitors->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
41
src/Monitor/Framework/Controller/CalendarController.php
Normal file
41
src/Monitor/Framework/Controller/CalendarController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||
use Spatie\IcalendarGenerator\Components\Event;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
#[Route('/monitors/ical/{email:user}/upcoming-episodes.ics', name: 'app.monitors.ical')]
|
||||
public function icalAction(MonitorRepository $monitorRepository, User $user)
|
||||
{
|
||||
if (false === $user->hasICalEnabled()) {
|
||||
return new Response('Calendar not found.', 404);
|
||||
}
|
||||
|
||||
$calendar = Calendar::create()
|
||||
->name('Upcoming Episodes')
|
||||
->refreshInterval(10);
|
||||
|
||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||
$calendar->event(Map::from($monitors)->map(function ($monitor) {
|
||||
return new Event($monitor->getTitle())
|
||||
->startsAt($monitor->getAirDate())
|
||||
->fullDay();
|
||||
})->toArray());
|
||||
|
||||
return new Response($calendar->get(), 200, [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Content-Disposition' => 'inline; filename="upcoming-episodes.ics"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,10 @@ class WebController extends AbstractController
|
||||
{
|
||||
return $this->render('monitor/index.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/monitors/upcoming-episodes', name: 'app.monitor.upcoming-episodes', methods: ['GET'])]
|
||||
public function upcomingEpisodes()
|
||||
{
|
||||
return $this->render('monitor/upcoming-episodes.html.twig');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ class Monitor
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||
private ?\DateTimeInterface $lastSearch = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||
private ?\DateTime $airDate = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
@@ -257,6 +260,17 @@ class Monitor
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAirDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->airDate;
|
||||
}
|
||||
|
||||
public function setAirDate(?\DateTimeInterface $airDate): static
|
||||
{
|
||||
$this->airDate = $airDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(self $child): static
|
||||
{
|
||||
if ($this->children->removeElement($child)) {
|
||||
|
||||
@@ -33,4 +33,12 @@ class MonitorRepository extends ServiceEntityRepository
|
||||
|
||||
return $this->paginator->paginate($query, $page, $perPage);
|
||||
}
|
||||
|
||||
public function whereAirDateNotNull()
|
||||
{
|
||||
$query = $this->createQueryBuilder('m')
|
||||
->andWhere('m.airDate IS NOT NULL')
|
||||
->getQuery();
|
||||
return $query->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ class GetMediaInfoHandler implements HandlerInterface
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
$relatedMedia = $this->tmdb->relatedMedia($media->tmdbId, $command->mediaType);
|
||||
|
||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
||||
return new GetMediaInfoResult($media, $relatedMedia, $command->season, $command->episode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class GetMediaInfoResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public array $relatedMedia,
|
||||
public ?int $season,
|
||||
public ?int $episode,
|
||||
) {}
|
||||
|
||||
@@ -7,9 +7,6 @@ use App\Search\Action\Handler\SearchHandler;
|
||||
use App\Search\Action\Input\GetMediaInfoInput;
|
||||
use App\Search\Action\Input\SearchInput;
|
||||
use App\Search\Action\Result\RedirectToMediaResult;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
@@ -247,6 +247,21 @@ class Tmdb
|
||||
return $series;
|
||||
}
|
||||
|
||||
public function relatedMedia(string $tmdbId, string $mediaType, int $maxResults = 6)
|
||||
{
|
||||
$repos = [
|
||||
'movies' => $this->movieRepository,
|
||||
'tvshows' => $this->tvRepository,
|
||||
];
|
||||
|
||||
$results = $repos[$mediaType]->getRecommendations($tmdbId);
|
||||
return Map::from(array_values($results->toArray()))
|
||||
->slice(0, 6)
|
||||
->map(function ($result) use ($mediaType) {
|
||||
return $this->parseResult($result, $mediaType);
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function mediaDetails(string $id, string $type)
|
||||
{
|
||||
$id = $this->find($id);
|
||||
@@ -286,13 +301,18 @@ class Tmdb
|
||||
|
||||
private function parseTvShow(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
if (!in_array($data['first_air_date'], ['', null,])) {
|
||||
$airDate = (new \DateTime($data['first_air_date']))->format('Y-m-d');
|
||||
} else {
|
||||
$airDate = null;
|
||||
}
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['first_air_date']))->format('Y'),
|
||||
year: $airDate,
|
||||
mediaType: "tvshows",
|
||||
episodes: $data['episodes'],
|
||||
);
|
||||
@@ -300,6 +320,11 @@ class Tmdb
|
||||
|
||||
private function parseEpisode(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
if (!in_array($data['air_date'], ['', null,])) {
|
||||
$airDate = (new \DateTime($data['air_date']))->format('Y-m-d');
|
||||
} else {
|
||||
$airDate = null;
|
||||
}
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
@@ -309,12 +334,17 @@ class Tmdb
|
||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: null,
|
||||
episodeAirDate: (new \DateTime($data['air_date']))->format('m/d/Y'),
|
||||
episodeAirDate: $airDate,
|
||||
);
|
||||
}
|
||||
|
||||
private function parseMovie(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
if (!in_array($data['release_date'], ['', null,])) {
|
||||
$airDate = (new \DateTime($data['release_date']))->format('Y-m-d');
|
||||
} else {
|
||||
$airDate = null;
|
||||
}
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
@@ -323,7 +353,7 @@ class Tmdb
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||
mediaType: "movies",
|
||||
episodeAirDate: (new \DateTime($data['release_date']))->format('m/d/Y'),
|
||||
episodeAirDate: $airDate,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@ use Carbon\Carbon;
|
||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||
use GuzzleHttp\Client;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
class Torrentio
|
||||
{
|
||||
private string $baseUrl = 'https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={realDebridKey}/stream/movie';
|
||||
private string $baseUrl = 'https://torrentio.strem.fun/realdebrid={realDebridKey}/stream/movie';
|
||||
|
||||
private string $searchUrl;
|
||||
|
||||
@@ -25,8 +26,15 @@ class Torrentio
|
||||
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
|
||||
private TagAwareCacheInterface $cache,
|
||||
private LoggerInterface $logger,
|
||||
private Security $security,
|
||||
) {
|
||||
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
|
||||
// $this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
|
||||
$user = $this->security->getUser();
|
||||
$this->searchUrl = $user->getUserPreference('torrentio_url')->getPreferenceValue() . '/stream/movie';
|
||||
|
||||
// dd($this->searchUrl);
|
||||
|
||||
|
||||
$this->client = new Client([
|
||||
'base_uri' => $this->searchUrl,
|
||||
]);
|
||||
@@ -36,25 +44,26 @@ class Torrentio
|
||||
{
|
||||
$cacheKey = "torrentio.{$imdbCode}";
|
||||
|
||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$item->tag(['torrentio', $type, $imdbCode]);
|
||||
// $results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
|
||||
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
// $item->tag(['torrentio', $type, $imdbCode]);
|
||||
try {
|
||||
$response = $this->client->get("$this->searchUrl/$imdbCode.json");
|
||||
return json_decode(
|
||||
$results = json_decode(
|
||||
$response->getBody()->getContents(),
|
||||
true
|
||||
);
|
||||
// dd($results);
|
||||
} catch (\Throwable $exception) {
|
||||
if ($exception->getCode() === 429) {
|
||||
$this->logger->warning("> [TorrentioClient] Rate limit exceeded");
|
||||
return null;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents());
|
||||
return [];
|
||||
});
|
||||
// return [];
|
||||
// });
|
||||
|
||||
if (true === $parseResults) {
|
||||
return $this->parse($results);
|
||||
@@ -65,26 +74,26 @@ class Torrentio
|
||||
|
||||
public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): array
|
||||
{
|
||||
$cacheKey = "torrentio.$imdbId.$season.$episode";
|
||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
|
||||
// $cacheKey = "torrentio.$imdbId.$season.$episode";
|
||||
// $results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
|
||||
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
// $item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
|
||||
try {
|
||||
$response = $this->client->get("$this->searchUrl/$imdbId:$season:$episode.json");
|
||||
return json_decode(
|
||||
$results = json_decode(
|
||||
$response->getBody()->getContents(),
|
||||
true
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
if ($exception->getCode() === 429) {
|
||||
$this->logger->warning("> [TorrentioClient] Rate limit exceeded");
|
||||
return null;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents());
|
||||
return [];
|
||||
});
|
||||
// return [];
|
||||
// });
|
||||
|
||||
if (null === $results) {
|
||||
throw new TorrentioRateLimitException();
|
||||
|
||||
50
src/Torrentio/Client/TorrentioUrl.php
Normal file
50
src/Torrentio/Client/TorrentioUrl.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Client;
|
||||
|
||||
use App\Values\TorrentioDebridProviderChoices;
|
||||
|
||||
class TorrentioUrl
|
||||
{
|
||||
const BASE_URL = "https://torrentio.strem.fun/";
|
||||
|
||||
public ?array $providers = null;
|
||||
public ?array $language = null;
|
||||
public ?array $qualityfilter = null;
|
||||
public ?string $sorting = null;
|
||||
public ?string $debridProvider = null;
|
||||
public ?string $limit = null;
|
||||
public ?string $sizefilter = null;
|
||||
public ?string $debridToken = null;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$result = "";
|
||||
if (null !== $this->debridProvider && null !== $this->debridToken) {
|
||||
$result .= "$this->debridProvider=$this->debridToken";
|
||||
}
|
||||
|
||||
return static::BASE_URL . $result;
|
||||
}
|
||||
|
||||
public static function fromString(string $url): self
|
||||
{
|
||||
$arrayData = ['providers', 'language', 'qualityfilter'];
|
||||
$data = explode('|', str_replace('/', '', urldecode(urldecode(parse_url($url)['path']))));
|
||||
|
||||
$result = new self();
|
||||
foreach ($data as $item) {$item = explode('=', $item);
|
||||
if (count($item) !== 2) continue;
|
||||
if (in_array($item[0], array_keys(TorrentioDebridProviderChoices::$providers))) {
|
||||
$result->debridProvider = $item[0];
|
||||
$result->debridToken = $item[1];
|
||||
} elseif (in_array($item[0], $arrayData)) {
|
||||
$result->{$item[0]} = explode(',', $item[1]);
|
||||
} else {
|
||||
$result->{$item[0]} = $item[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
99
src/Torrentio/Framework/Form/TorrentioPreferencesForm.php
Normal file
99
src/Torrentio/Framework/Form/TorrentioPreferencesForm.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Framework\Form;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\User\Database\ProviderList;
|
||||
use App\Values\TorrentioDebridProviderChoices;
|
||||
use App\Values\TorrentioExcludeQualityChoices;
|
||||
use App\Values\TorrentioLanguageChoices;
|
||||
use App\Values\TorrentioProviderChoices;
|
||||
use App\Values\TorrentioSortChoices;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class TorrentioPreferencesForm extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addChoiceField($builder, 'providers', TorrentioProviderChoices::asSelectOptions());
|
||||
$this->addChoiceField($builder, 'sorting', TorrentioSortChoices::asSelectOptions(), 1);
|
||||
$this->addChoiceField($builder, 'language', TorrentioLanguageChoices::asSelectOptions());
|
||||
$this->addChoiceField($builder, 'qualityfilter', TorrentioExcludeQualityChoices::asSelectOptions());
|
||||
$this->addTextField($builder, 'limit');
|
||||
$this->addTextField($builder, 'sizefilter');
|
||||
$this->addChoiceField($builder, 'debridProvider', TorrentioDebridProviderChoices::asSelectOptions(), 1);
|
||||
$this->addTextField($builder, 'debridToken');
|
||||
}
|
||||
|
||||
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices, ?int $maxItems = null): void
|
||||
{
|
||||
$question = [
|
||||
'attr' => [
|
||||
'class' => 'min-w-24 text-input mb-4',
|
||||
],
|
||||
'row_attr' => [
|
||||
'class' => 'filter-label text-white'
|
||||
],
|
||||
'label_attr' => ['class' => 'block font-semibold mb-2'],
|
||||
'choices' => $choices,
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
if (null === $maxItems) {
|
||||
$question['multiple'] = true;
|
||||
$question['attr'] += [
|
||||
'data-result-filter-target' => $fieldName,
|
||||
'data-controller' => 'symfony--ux-autocomplete--autocomplete',
|
||||
'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value' => json_encode([
|
||||
'highlight' => false,
|
||||
'maxItems' => $maxItems,
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
$question += [
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$builder->add($fieldName, ChoiceType::class, $question);
|
||||
}
|
||||
|
||||
public function addTextField(FormBuilderInterface $builder, string $fieldName, ?array $options = null): void
|
||||
{
|
||||
$optinos = $options ?? [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'method' => 'post',
|
||||
'action' => $this->urlGenerator->generate('app.torrentio-preferences.save'),
|
||||
'class' => 'min-w-24 text-input mb-4 block',
|
||||
]
|
||||
];
|
||||
|
||||
$builder->add(
|
||||
$fieldName,
|
||||
TextType::class,
|
||||
$optinos,
|
||||
);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'id' => 'torrentio-preferences-form',
|
||||
// 'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'),
|
||||
'attr' => [
|
||||
'class' => 'filter-items w-full p-4 text-md dark:text-gray-50 rounded-lg',
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Dto\UpcomingEpisode;
|
||||
use App\Monitor\Factory\UpcomingEpisodeDto;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Tmdb\Tmdb;
|
||||
use Carbon\CarbonImmutable;
|
||||
@@ -70,7 +70,7 @@ final class UpcomingEpisodes extends AbstractController
|
||||
}
|
||||
|
||||
return $episodes->map(function (array $episode) use ($monitor) {
|
||||
return new UpcomingEpisode(
|
||||
return new UpcomingEpisodeDto(
|
||||
$monitor->getTitle(),
|
||||
$episode['air_date'],
|
||||
$episode['name'],
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserCalendarPreferencesCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $enable_ical_up_ep,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Command;
|
||||
|
||||
use App\Torrentio\Client\TorrentioUrl;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserTorrentioPreferencesCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TorrentioUrl $torrentioUrl,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Handler;
|
||||
|
||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||
use App\User\Action\Result\SaveUserDownloadPreferencesResult;
|
||||
use App\User\Action\Result\SaveUserMediaPreferencesResult;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/** @implements HandlerInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserCalendarPreferencesHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly PreferencesRepository $preferenceRepository,
|
||||
private readonly Security $token,
|
||||
) {}
|
||||
|
||||
public function handle(C $command): R
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->token->getUser();
|
||||
|
||||
foreach ($command as $preference => $value) {
|
||||
if ($user->hasUserPreference($preference)) {
|
||||
$user->updateUserPreference($preference, $value);
|
||||
$this->entityManager->flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
$preference = $this->preferenceRepository->find($preference);
|
||||
|
||||
$user->addUserPreference(
|
||||
(new UserPreference())
|
||||
->setUser($user)
|
||||
->setPreference($preference)
|
||||
->setPreferenceValue($value)
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new SaveUserDownloadPreferencesResult($user->getDownloadPreferences());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Handler;
|
||||
|
||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||
use App\User\Action\Result\SaveUserDownloadPreferencesResult;
|
||||
use App\User\Action\Result\SaveUserMediaPreferencesResult;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/** @implements HandlerInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserTorrentioPreferencesHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly PreferencesRepository $preferenceRepository,
|
||||
private readonly Security $token,
|
||||
) {}
|
||||
|
||||
public function handle(C $command): R
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->token->getUser();
|
||||
|
||||
if ($user->hasUserPreference('torrentio_url')) {
|
||||
$user->updateUserPreference('torrentio_url', (string) $command->torrentioUrl);
|
||||
$this->entityManager->flush();
|
||||
} else {
|
||||
$preference = $this->preferenceRepository->find('torrentio_url');
|
||||
$user->addUserPreference(
|
||||
(new UserPreference())
|
||||
->setUser($user)
|
||||
->setPreference($preference)
|
||||
->setPreferenceValue((string) $command->torrentioUrl)
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new SaveUserDownloadPreferencesResult($user->getDownloadPreferences());
|
||||
}
|
||||
}
|
||||
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Input;
|
||||
|
||||
use App\User\Action\Command\SaveUserCalendarPreferencesCommand;
|
||||
use App\User\Action\Command\SaveUserDownloadPreferencesCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/** @implements InputInterface<SaveUserDownloadPreferencesInput, SaveUserDownloadPreferencesCommand> */
|
||||
class SaveUserCalendarPreferencesInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceSecurity]
|
||||
public mixed $userId,
|
||||
|
||||
#[SourceRequest('enable_ical_up_ep', nullify: true)]
|
||||
public bool $enableIcalUpcomingEpisodes,
|
||||
) {}
|
||||
|
||||
public function toCommand(): C
|
||||
{
|
||||
return new SaveUserCalendarPreferencesCommand(
|
||||
$this->enableIcalUpcomingEpisodes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,31 @@ namespace App\User\Database;
|
||||
class ProviderList
|
||||
{
|
||||
public static $providers = [
|
||||
'1337x',
|
||||
'Comando',
|
||||
'YTS',
|
||||
'EZTV',
|
||||
'ilCorSaRoNeRo',
|
||||
'MagnetDL',
|
||||
'MejorTorrent',
|
||||
'RARBG',
|
||||
'Rutor',
|
||||
'Rutracker',
|
||||
'1337x',
|
||||
'ThePirateBay',
|
||||
'Torrent9',
|
||||
'KickassTorrents',
|
||||
'TorrentGalaxy',
|
||||
'MagnetDL',
|
||||
'HorribleSubs',
|
||||
'NyaaSi',
|
||||
'TokyoTosho',
|
||||
'AniDex',
|
||||
'🇷🇺 Rutor',
|
||||
'🇷🇺 Rutracker',
|
||||
'🇵🇹 Comando',
|
||||
'🇵🇹 BluDV',
|
||||
'🇫🇷 Torrent9',
|
||||
'🇮🇹 ilCorSaRoNeRo',
|
||||
'🇪🇸 MejorTorrent',
|
||||
'🇪🇸 Wolfmax4k',
|
||||
'🇲🇽 Cinecalidad',
|
||||
'🇵🇱 BestTorrents'
|
||||
];
|
||||
|
||||
|
||||
public static function getProviders()
|
||||
{
|
||||
return self::$providers;
|
||||
|
||||
@@ -5,9 +5,15 @@ declare(strict_types=1);
|
||||
namespace App\User\Framework\Controller\Web;
|
||||
|
||||
use App\Base\Service\Broadcaster;
|
||||
use App\Torrentio\Client\TorrentioUrl;
|
||||
use App\Torrentio\Framework\Form\TorrentioPreferencesForm;
|
||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||
use App\User\Action\Command\SaveUserTorrentioPreferencesCommand;
|
||||
use App\User\Action\Handler\SaveUserCalendarPreferencesHandler;
|
||||
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
||||
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||
use App\User\Action\Handler\SaveUserTorrentioPreferencesHandler;
|
||||
use App\User\Action\Input\SaveUserCalendarPreferencesInput;
|
||||
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
||||
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
||||
use App\User\Database\CountryLanguages;
|
||||
@@ -33,16 +39,20 @@ class PreferencesController extends AbstractController
|
||||
public function mediaPreferences(): Response
|
||||
{
|
||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||
|
||||
// dd($form);
|
||||
$torrentioForm = $this->createForm(TorrentioPreferencesForm::class, TorrentioUrl::fromString(
|
||||
$this->getUser()->getUserPreference('torrentio_url')->getPreferenceValue()
|
||||
));
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'downloadPreferences' => $downloadPreferences,
|
||||
'calendarPreferences' => $calendarPreferences,
|
||||
'preferences_form' => $form,
|
||||
'torrentio_form' => $torrentioForm,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -54,8 +64,8 @@ class PreferencesController extends AbstractController
|
||||
): Response
|
||||
{
|
||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||
$form = $this->createForm(UserMediaPreferencesForm::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -69,6 +79,7 @@ class PreferencesController extends AbstractController
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'downloadPreferences' => $downloadPreferences,
|
||||
'calendarPreferences' => $calendarPreferences,
|
||||
'preferences_form' => $form,
|
||||
]
|
||||
);
|
||||
@@ -81,6 +92,7 @@ class PreferencesController extends AbstractController
|
||||
): Response
|
||||
{
|
||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||
|
||||
@@ -95,6 +107,56 @@ class PreferencesController extends AbstractController
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'downloadPreferences' => $downloadPreferences,
|
||||
'calendarPreferences' => $calendarPreferences,
|
||||
'preferences_form' => $form,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/user/preferences/calendar', 'app.save.calendar-preferences', methods: ['POST'])]
|
||||
public function saveCalendarPreferences(
|
||||
SaveUserCalendarPreferencesInput $input,
|
||||
SaveUserCalendarPreferencesHandler $handler,
|
||||
): Response
|
||||
{
|
||||
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||
|
||||
$handler->handle($input->toCommand());
|
||||
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: 'Your calendar preferences have been saved.'
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'downloadPreferences' => $this->getUser()->getDownloadPreferences(),
|
||||
'calendarPreferences' => $calendarPreferences,
|
||||
'preferences_form' => $form,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/user/preferences/torrentio', 'app.torrentio-preferences.save', methods: ['POST'])]
|
||||
public function saveTorrentioPreferences(Request $request, SaveUserTorrentioPreferencesHandler $handler)
|
||||
{
|
||||
$form = $this->createForm(TorrentioPreferencesForm::class, new TorrentioUrl());
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$command = new SaveUserTorrentioPreferencesCommand(
|
||||
$form->getData(),
|
||||
);
|
||||
$handler->handle($command);
|
||||
$this->broadcaster->alert('Success', 'Your Torrentio preferences have been saved.');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'preferences_form' => $form,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\User\Framework\Entity;
|
||||
use Aimeos\Map;
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Torrentio\Client\TorrentioUrl;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -327,4 +328,26 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCalendarPreferences(): array
|
||||
{
|
||||
return Map::from($this->userPreferences)
|
||||
->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId())
|
||||
->filter(fn(UserPreference $userPreference) => $userPreference->getPreference()->getType() === 'calendar')
|
||||
->toArray()
|
||||
;
|
||||
}
|
||||
|
||||
public function hasICalEnabled(): bool
|
||||
{
|
||||
return $this->hasUserPreference('enable_ical_up_ep') &&
|
||||
(bool) $this->getUserPreference('enable_ical_up_ep')->getPreferenceValue() === true;
|
||||
}
|
||||
|
||||
public function getTorrentioUrl()
|
||||
{
|
||||
return $this->hasUserPreference('torrentio_url')
|
||||
? TorrentioUrl::fromString($this->getUserPreference('torrentio_url')->getPreferenceValue())
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class UserPreference
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Preference $preference = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(length: 1024, nullable: true)]
|
||||
private ?string $preference_value = null;
|
||||
|
||||
public function getId(): ?int
|
||||
|
||||
27
src/Values/TorrentioDebridProviderChoices.php
Normal file
27
src/Values/TorrentioDebridProviderChoices.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
class TorrentioDebridProviderChoices
|
||||
{
|
||||
public static array $providers = [
|
||||
'realdebrid' => 'RealDebrid',
|
||||
'premiumize' => 'Premiumize',
|
||||
'alldebrid' => 'AllDebrid',
|
||||
'debridlink' => 'DebridLink',
|
||||
'easydebrid' => 'EasyDebrid',
|
||||
'offcloud' => 'Offcloud',
|
||||
'torbox' => 'TorBox',
|
||||
'putio' => 'Put.io',
|
||||
];
|
||||
|
||||
public static function getProviders(): array
|
||||
{
|
||||
return self::$providers;
|
||||
}
|
||||
|
||||
public static function asSelectOptions(): array
|
||||
{
|
||||
return array_flip(self::$providers);
|
||||
}
|
||||
}
|
||||
34
src/Values/TorrentioExcludeQualityChoices.php
Normal file
34
src/Values/TorrentioExcludeQualityChoices.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
class TorrentioExcludeQualityChoices
|
||||
{
|
||||
public static array $choices = [
|
||||
'brremux' => 'BluRay REMUX',
|
||||
'hdrall' => 'HDR/HDR10+/Dolby Vision',
|
||||
'dolbyvision' => 'Dolby Vision',
|
||||
'dolbyvisionwithhdr' => 'Dolby Vision + HDR',
|
||||
'threed' => '3D',
|
||||
'nonthreed' => 'Non 3D (DO NOT SELECT IF NOT SURE)',
|
||||
'4k' => '4k',
|
||||
'1080p' => '1080p',
|
||||
'720p' => '720p',
|
||||
'480p' => '480p',
|
||||
'other' => 'Other (DVDRip/HDRip/BDRip...)',
|
||||
'scr' => 'Screener',
|
||||
'cam' => 'Cam',
|
||||
'unknown' => 'Unknown'
|
||||
];
|
||||
|
||||
|
||||
public static function getChoices(): array
|
||||
{
|
||||
return self::$choices;
|
||||
}
|
||||
|
||||
public static function asSelectOptions(): array
|
||||
{
|
||||
return array_flip(self::$choices);
|
||||
}
|
||||
}
|
||||
60
src/Values/TorrentioLanguageChoices.php
Normal file
60
src/Values/TorrentioLanguageChoices.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
class TorrentioLanguageChoices
|
||||
{
|
||||
public static array $languages = [
|
||||
'japanese' => '🇯🇵 Japanese',
|
||||
'russian' => '🇷🇺 Russian',
|
||||
'italian' => '🇮🇹 Italian',
|
||||
'portuguese' => '🇵🇹 Portuguese',
|
||||
'spanish' => '🇪🇸 Spanish',
|
||||
'latino' => '🇲🇽 Latino',
|
||||
'korean' => '🇰🇷 Korean',
|
||||
'chinese' => '🇨🇳 Chinese',
|
||||
'taiwanese' => '🇹🇼 Taiwanese',
|
||||
'french' => '🇫🇷 French',
|
||||
'german' => '🇩🇪 German',
|
||||
'dutch' => '🇳🇱 Dutch',
|
||||
'hindi' => '🇮🇳 Hindi',
|
||||
'telugu' => '🇮🇳 Telugu',
|
||||
'tamil' => '🇮🇳 Tamil',
|
||||
'polish' => '🇵🇱 Polish',
|
||||
'lithuanian' => '🇱🇹 Lithuanian',
|
||||
'latvian' => '🇱🇻 Latvian',
|
||||
'estonian' => '🇪🇪 Estonian',
|
||||
'czech' => '🇨🇿 Czech',
|
||||
'slovakian' => '🇸🇰 Slovakian',
|
||||
'slovenian' => '🇸🇮 Slovenian',
|
||||
'hungarian' => '🇭🇺 Hungarian',
|
||||
'romanian' => '🇷🇴 Romanian',
|
||||
'bulgarian' => '🇧🇬 Bulgarian',
|
||||
'serbian' => '🇷🇸 Serbian',
|
||||
'croatian' => '🇭🇷 Croatian',
|
||||
'ukrainian' => '🇺🇦 Ukrainian',
|
||||
'greek' => '🇬🇷 Greek',
|
||||
'danish' => '🇩🇰 Danish',
|
||||
'finnish' => '🇫🇮 Finnish',
|
||||
'swedish' => '🇸🇪 Swedish',
|
||||
'norwegian' => '🇳🇴 Norwegian',
|
||||
'turkish' => '🇹🇷 Turkish',
|
||||
'arabic' => '🇸🇦 Arabic',
|
||||
'persian' => '🇮🇷 Persian',
|
||||
'hebrew' => '🇮🇱 Hebrew',
|
||||
'vietnamese' => '🇻🇳 Vietnamese',
|
||||
'indonesian' => '🇮🇩 Indonesian',
|
||||
'malay' => '🇲🇾 Malay',
|
||||
'thai' => '🇹🇭 Thai'
|
||||
];
|
||||
|
||||
public static function getLanguages(): array
|
||||
{
|
||||
return self::$languages;
|
||||
}
|
||||
|
||||
public static function asSelectOptions(): array
|
||||
{
|
||||
return array_flip(self::$languages);
|
||||
}
|
||||
}
|
||||
41
src/Values/TorrentioProviderChoices.php
Normal file
41
src/Values/TorrentioProviderChoices.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
class TorrentioProviderChoices
|
||||
{
|
||||
public static array $choices = [
|
||||
'yts' => 'YTS',
|
||||
'eztv' => 'EZTV',
|
||||
'rarbg' => 'RARBG',
|
||||
'1337x' => '1337x',
|
||||
'thepiratebay' => 'ThePirateBay',
|
||||
'kickasstorrents' => 'KickassTorrents',
|
||||
'torrentgalaxy' => 'TorrentGalaxy',
|
||||
'magnetdl' => 'MagnetDL',
|
||||
'horriblesubs' => 'HorribleSubs',
|
||||
'nyaasi' => 'NyaaSi',
|
||||
'tokyotosho' => 'TokyoTosho',
|
||||
'anidex' => 'AniDex',
|
||||
'rutor' => '🇷🇺 Rutor',
|
||||
'rutracker' => '🇷🇺 Rutracker',
|
||||
'comando' => '🇵🇹 Comando',
|
||||
'bludv' => '🇵🇹 BluDV',
|
||||
'torrent9' => '🇫🇷 Torrent9',
|
||||
'ilcorsaronero' => '🇮🇹 ilCorSaRoNeRo',
|
||||
'mejortorrent' => '🇪🇸 MejorTorrent',
|
||||
'wolfmax4k' => '🇪🇸 Wolfmax4k',
|
||||
'cinecalidad' => '🇲🇽 Cinecalidad',
|
||||
'besttorrents' => '🇵🇱 BestTorrents'
|
||||
];
|
||||
|
||||
public static function getChoices(): array
|
||||
{
|
||||
return self::$choices;
|
||||
}
|
||||
|
||||
public static function asSelectOptions(): array
|
||||
{
|
||||
return array_flip(self::$choices);
|
||||
}
|
||||
}
|
||||
23
src/Values/TorrentioSortChoices.php
Normal file
23
src/Values/TorrentioSortChoices.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
class TorrentioSortChoices
|
||||
{
|
||||
public static array $choices = [
|
||||
'quality' => 'By quality then seeders',
|
||||
'qualitysize' => 'By quality then size',
|
||||
'seeders' => 'By seeders',
|
||||
'size' => 'By size',
|
||||
];
|
||||
|
||||
public static function getChoices(): array
|
||||
{
|
||||
return self::$choices;
|
||||
}
|
||||
|
||||
public static function asSelectOptions(): array
|
||||
{
|
||||
return array_flip(self::$choices);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="flex flex-col bg-stone-700">
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
<twig:SearchBar />
|
||||
<div class="md:flex md:items-center md:gap-12">
|
||||
<nav aria-label="Global" class="md:block">
|
||||
<ul class="flex items-center gap-6 text-sm">
|
||||
<ul class="ml-4 flex items-end md:items-center md:gap-6 text-sm">
|
||||
<li>
|
||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
||||
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="hidden">
|
||||
<twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/>
|
||||
</li>
|
||||
<li class="hidden md:block">
|
||||
<a href="{{ path('app_logout') }}">
|
||||
<a href="{{ path('app_logout') }}" title="Log out of Torsearch.">
|
||||
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{% endfor %}
|
||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||
<tr id="monitor_view_all">
|
||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-gray-400 dark:bg-gray-700 uppercase text-xs font-medium text-center text-black dark:text-white min-w-[50ch] max-w-[50ch] truncate">
|
||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<button class="submit-button flex flex-row gap-2 items-center">
|
||||
<button class="submit-button {{ class|default('flex flex-row gap-2 items-center') }}">
|
||||
{% if show_icon|default %}
|
||||
<twig:ux:icon name="zondicons:checkmark" width=".8rem" class="text-green-500" />
|
||||
{% endif %}
|
||||
|
||||
9
templates/index/test.html.twig
Normal file
9
templates/index/test.html.twig
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block h2 %}Test Test{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div>
|
||||
<!-- Well what are you doing here? -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,10 @@
|
||||
{% block h2 %}Monitors{% endblock %}
|
||||
|
||||
{% block action_buttons %}
|
||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false"
|
||||
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||
Upcoming Episodes
|
||||
</a>
|
||||
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
50
templates/monitor/upcoming-episodes.html.twig
Normal file
50
templates/monitor/upcoming-episodes.html.twig
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block h2 %}Upcoming Episodes{% endblock %}
|
||||
|
||||
{% block action_buttons %}
|
||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false"
|
||||
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||
Upcoming Episodes
|
||||
</a>
|
||||
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{{ turbo_page_requires_reload() }}
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||
|
||||
<div class="p-4">
|
||||
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
||||
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||
</a>
|
||||
<div id="calendar" class="text-white">
|
||||
</div>
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getView() {
|
||||
if (window.innerWidth < 768) {
|
||||
return 'listWeek';
|
||||
} else {
|
||||
return 'dayGridMonth';
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
let data = await fetch('/api/monitor/upcoming-episodes');
|
||||
data = (await data.json())['data'];
|
||||
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: getView(),
|
||||
events: data['episodes'],
|
||||
windowResize: function(arg) {
|
||||
this.changeView(getView());
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
||||
|
||||
{% block h2 %}Media Results{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4 flex flex-col grow gap-4">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
||||
<div class="flex flex-row w-full gap-2">
|
||||
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||
@@ -19,7 +20,7 @@
|
||||
<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 }})
|
||||
{{ results.media.title }} ({{ results.media.year|date('Y') }})
|
||||
</h3>
|
||||
|
||||
{% if results.media.mediaType == "tvshows" %}
|
||||
@@ -95,12 +96,35 @@
|
||||
{% elseif "tvshows" == results.media.mediaType %}
|
||||
<twig:TvEpisodeList
|
||||
results="results"
|
||||
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
||||
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer" :episodeNumber="results.episode"
|
||||
loading="defer"
|
||||
:imdbId="results.media.imdbId"
|
||||
:season="results.season"
|
||||
:perPage="20"
|
||||
:pageNumber="1"
|
||||
:tmdbId="results.media.tmdbId"
|
||||
:title="results.media.title"
|
||||
:episodeNumber="results.episode"
|
||||
/>
|
||||
{% endif %}
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
||||
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
||||
{% for media in results.relatedMedia %}
|
||||
<twig:Poster imdbId="{{ media.imdbId }}"
|
||||
tmdbId="{{ media.tmdbId }}"
|
||||
title="{{ media.title }}"
|
||||
description="{{ media.description }}"
|
||||
image="{{ media.poster }}"
|
||||
year="{{ media.year }}"
|
||||
mediaType="{{ media.mediaType }}"
|
||||
/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</twig:Card>
|
||||
</div>
|
||||
<style>
|
||||
html,
|
||||
|
||||
@@ -36,4 +36,51 @@
|
||||
</form>
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||
<twig:Card title="Calendar Preferences" class="w-full">
|
||||
<p class="text-gray-50 mb-4">Manage your Upcoming Episodes calendar.</p>
|
||||
<form id="calendar_preferences" class="flex flex-col" name="calendar_preferences" method="post" action="{{ path('app.save.calendar-preferences') }}">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2 mb-1">
|
||||
<input type="hidden" name="enable_ical_up_ep" id="enable_ical_up_ep_hidden" value="0" />
|
||||
<input type="checkbox" name="enable_ical_up_ep" id="enable_ical_up_ep" value="1" {{ calendarPreferences['enable_ical_up_ep'].getPreferenceValue() == true ? 'checked' }} />
|
||||
<label class="text-gray-50" for="enable_ical_up_ep">Enable a publicly available iCal calendar?</label>
|
||||
</div>
|
||||
<small class="text-gray-50 mb-4">Enabling the iCal calendar will allow you to subscribe from iCal
|
||||
supporting clients. This endpoint will be publicly available with no authentication required.
|
||||
Disabling this option will disable the calendar and public endpoint for your user.
|
||||
This will not affect the calendar within the app.
|
||||
</small>
|
||||
</div>
|
||||
<button class="px-1.5 py-1 max-w-20 rounded-md bg-green-600 text-white" type="submit">Submit</button>
|
||||
</form>
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||
<twig:Card title="Torrentio Preferences" class="w-full">
|
||||
<p class="text-gray-50 mb-4">Configure your Torrentio client.</p>
|
||||
{{ form_start(torrentio_form, {
|
||||
action: path('app.torrentio-preferences.save')
|
||||
}) }}
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<div class="self-end mb-4">
|
||||
{{ form_row(torrentio_form.providers) }}
|
||||
{{ form_row(torrentio_form.sorting) }}
|
||||
{{ form_row(torrentio_form.language) }}
|
||||
{{ form_row(torrentio_form.qualityfilter) }}
|
||||
{{ form_row(torrentio_form.limit) }}
|
||||
{{ form_row(torrentio_form.sizefilter) }}
|
||||
{{ form_row(torrentio_form.debridProvider) }}
|
||||
{{ form_row(torrentio_form.debridToken) }}
|
||||
{{ form_widget(torrentio_form._token) }}
|
||||
<div class="w-[5rem]">
|
||||
<twig:SubmitButton show_icon text="Save"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(preferences_form) }}
|
||||
</twig:Card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user