Compare commits

...

9 Commits

23 changed files with 180 additions and 209 deletions

View File

@@ -1,4 +1,4 @@
FROM dunglas/frankenphp
FROM dunglas/frankenphp:php8.4-alpine
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
@@ -11,6 +11,8 @@ RUN install-php-extensions \
zip \
opcache
RUN apk add --no-cache wget
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile

View File

@@ -35,7 +35,7 @@ services:
volumes:
- $PWD:/app
tty: true
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 limit=10
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=10
scheduler:

View File

@@ -11,6 +11,8 @@ RUN install-php-extensions \
zip \
opcache
RUN apk add --no-cache wget
COPY . /app
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]

View File

@@ -3,8 +3,6 @@
frankenphp {
{$FRANKENPHP_CONFIG}
}
}

View File

@@ -33,11 +33,12 @@ final class SearchController extends AbstractController
]);
}
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
#[Route('/result/{mediaType}/{imdbId}', name: 'app_search_result')]
public function result(
GetMediaInfoInput $input,
): Response {
$result = $this->getMediaInfoHandler->handle($input->toCommand());
$this->warmDownloadOptionCache($result->media);
return $this->render('search/result.html.twig', [

View File

@@ -3,8 +3,11 @@
namespace App\Download\Framework\Entity;
use App\Download\Framework\Repository\DownloadRepository;
use App\Torrentio\Result\ResultFactory;
use App\Torrentio\Result\TorrentioResult;
use App\User\Framework\Entity\User;
use Doctrine\ORM\Mapping as ORM;
use Nihilarr\PTN;
use Symfony\UX\Turbo\Attribute\Broadcast;
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
@@ -162,4 +165,16 @@ class Download
return $this;
}
public function getPtn(): object
{
$ptn = (object) (new PTN())->parse($this->filename);
if ($this->mediaType === "tvshows") {
$ptn->season = str_pad($ptn->season, 2, "0", STR_PAD_LEFT);
$ptn->episode = str_pad($ptn->episode, 2, "0", STR_PAD_LEFT);
}
return $ptn;
}
}

View File

@@ -8,7 +8,7 @@ use OneToMany\RichBundle\Contract\CommandInterface;
class GetMediaInfoCommand implements CommandInterface
{
public function __construct(
public string $tmdbId,
public string $imdbId,
public string $mediaType,
) {}
}

View File

@@ -18,7 +18,7 @@ class GetMediaInfoHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface
{
$media = $this->tmdb->mediaDetails($command->tmdbId, $command->mediaType);
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
return new GetMediaInfoResult($media);
}

View File

@@ -12,8 +12,8 @@ use OneToMany\RichBundle\Contract\InputInterface;
class GetMediaInfoInput implements InputInterface
{
public function __construct(
#[SourceRoute('tmdbId')]
public string $tmdbId,
#[SourceRoute('imdbId')]
public string $imdbId,
#[SourceRoute('mediaType')]
public string $mediaType,
@@ -21,6 +21,6 @@ class GetMediaInfoInput implements InputInterface
public function toCommand(): CommandInterface
{
return new GetMediaInfoCommand($this->tmdbId, $this->mediaType);
return new GetMediaInfoCommand($this->imdbId, $this->mediaType);
}
}

View File

@@ -133,9 +133,12 @@ class Tmdb
if (!$result instanceof Movie && !$result instanceof Tv) {
continue;
}
$results[] = $this->parseResult($result);
}
$results = array_filter($results, fn ($result) => null !== $result->imdbId);
return $results;
}
@@ -143,7 +146,16 @@ class Tmdb
{
$finder = new Find($this->client);
$result = $finder->findBy($id, ['external_source' => 'imdb_id']);
return $this->parseResult($result);
if (count($result['movie_results']) > 0) {
return $result['movie_results'][0]['id'];
} elseif (count($result['tv_results']) > 0) {
return $result['tv_results'][0]['id'];
} elseif (count($result['tv_episode_results']) > 0) {
return $result['tv_episode_results'][0]['show_id'];
}
throw new \Exception("No results found for $id");
}
public function movieDetails(string $id)
@@ -183,6 +195,8 @@ class Tmdb
public function mediaDetails(string $id, string $type)
{
$id = $this->find($id);
if ($type === "movies") {
return $this->movieDetails($id);
} else {

View File

@@ -19,7 +19,7 @@ class GetMovieOptionsHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface
{
return new GetMovieOptionsResult(
media: $this->tmdb->mediaDetails($command->tmdbId, 'movies'),
media: $this->tmdb->mediaDetails($command->imdbId, 'movies'),
results: $this->torrentio->search($command->imdbId, 'movies'),
);
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Twig\Components;
use App\Download\Framework\Entity\Download;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class DownloadListRow
{
public Download $download;
public bool $isWidget = true;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Twig\Components;
use App\Monitor\Framework\Entity\Monitor;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class MonitorListRow
{
public Monitor $monitor;
}

View File

@@ -7,7 +7,7 @@ module.exports = {
safelist: [
"bg-blue-300",
"bg-orange-300",
"bg-rose-300",
"bg-fuchsia-300",
"bg-green-400",
"bg-purple-400",
"bg-orange-400",

View File

@@ -5,38 +5,19 @@
<turbo-stream action="append" target="active_downloads">
<template>
<tr data-download-list-target="download" id="ad_download_{{ entity.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
{{ entity.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
{% if entity.progress < 100 %}
<div class="w-[3.25ch] h-[3.25ch] bg-purple-600 rounded-full block text-center table-cell align-middle text-xs text-gray-50">
{{ entity.progress }}
</div>
{% else %}
<twig:StatusBadge color="green" status="Complete" />
{% endif %}
</td>
<td></td>
</tr>
<twig:DownloadListRow download="{{ entity }}" />
</template>
</turbo-stream>
{% endblock %}
{% block update %}
{% if entity.status != "Complete" %}
<turbo-stream action="update" target="ad_download_{{ id }}">
<turbo-stream action="update" target="download_progress_{{ id }}">
<template>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
{{ entity.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<div class="border-2 border-green-700 rounded-md w-full h-6 align-middle overflow-hidden">
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5" style="width:{{ entity.progress }}%">{{ entity.progress }}%</div>
</div>
</td>
<td></td>
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5"
style="width:{{ entity.progress }}%">
<span>{{ entity.progress }}%</span>
</div>
</template>
</turbo-stream>
{% else %}
@@ -54,23 +35,7 @@
<turbo-stream action="prepend" target="complete_downloads">
<template>
<tr id="ad_download_{{ entity.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
{{ entity.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
<twig:StatusBadge color="green" status="Complete" />
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ download.title }} from download list. This will not delete the file."
/>
</button>
</td>
</tr>
<twig:DownloadListRow download="{{ entity }}" />
</template>
</turbo-stream>
{% endif %}

View File

@@ -3,81 +3,19 @@
<turbo-stream action="remove" target="active_monitors_no_monitors">
</turbo-stream>
<turbo-stream action="append" target="monitors">
<turbo-stream action="prepend" target="monitors">
<template>
<tr id="monitor_{{ entity.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 min-w-[50ch] max-w-[50ch] truncate">
{{ entity.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.searchCount }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if entity.status == "New" %}
<twig:StatusBadge color="orange" status="{{ entity.status }}" />
{% elseif entity.status == "In Progress" or entity.status == "Active" %}
<twig:StatusBadge color="purple" status="{{ entity.status }}" />
{% else %}
<twig:StatusBadge color="green" status="{{ entity.status }}" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: entity.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ entity.title }} from your Monitor list."
/>
</button>
</td>
</tr>
<twig:MonitorListRow monitor="{{ entity }}" />
</template>
</turbo-stream>
{% endblock %}
{% block update %}
<turbo-stream action="prepend" target="monitors">
<template>
<tr id="monitor_{{ entity.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 min-w-[50ch] max-w-[50ch] truncate">
{{ entity.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.searchCount }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ entity.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if entity.status == "New" %}
<twig:StatusBadge color="orange" status="{{ entity.status }}" />
{% elseif entity.status == "In Progress" or entity.status == "Active" %}
<twig:StatusBadge color="purple" status="{{ entity.status }}" />
{% else %}
<twig:StatusBadge color="green" status="{{ entity.status }}" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: entity.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ entity.title }} from your Monitor list."
/>
</button>
</td>
</tr>
</template>
</turbo-stream>
<turbo-stream action="replace" target="monitor_{{ id }}">
<template>
<twig:MonitorListRow monitor="{{ entity }}" />
</template>
</turbo-stream>
{% endblock %}
{% block remove %}

View File

@@ -31,42 +31,7 @@
<tbody id="{{ table_body_id }}" class="divide-y divide-gray-200 dark:divide-gray-50">
{% if this.downloads.items|length > 0 %}
{% for download in this.downloads.items %}
<tr id="ad_download_{{ download.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate">
{{ download.title }}
</td>
{% if this.isWidget == false %}
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
{{ download.filename }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
{{ download.mediaType }}
</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
{% if download.progress < 100 %}
<div class="border-2 border-green-700 rounded-md w-full h-6 align-middle overflow-hidden">
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5"
style="width:{{ download.progress }}%">
{{ download.progress }}%
</div>
</div>
{% else %}
<twig:StatusBadge color="green" status="Complete" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ download.title }} from your Download list. This will not delete the file."
/>
</button>
</td>
</tr>
<twig:DownloadListRow download="{{ download }}" isWidget="{{ this.isWidget }}" />
{% endfor %}
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
<tr id="download_view_all">

View File

@@ -0,0 +1,44 @@
<tr{{ attributes }} id="ad_download_{{ download.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
class="hover:underline mr-1"
>
{{ download.title }}
</a>
{% if download.mediaType == "tvshows" %}
&mdash; <span class="ml-1">(S{{ download.ptn.season }}E{{ download.ptn.episode }})</span>
{% endif %}
</td>
{% if isWidget == false %}
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] truncate">
{{ download.filename }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
{{ download.mediaType }}
</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
{% if download.progress < 100 %}
<div id="download_progress_{{ download.id }}" class="border-2 border-green-600 rounded-md text-center w-full h-6 align-middle overflow-hidden">
<div class="text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
style="width:{{ download.progress }}%">
</div>
<div class="absolute text-black text-center" style="z-index: 400;margin-top: -1.25rem; margin-left: 1.2rem">{{ download.progress }}%</div>
</div>
{% else %}
<twig:StatusBadge color="green" status="Complete" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ download.title }} from your Download list. This will not delete the file."
/>
</button>
</td>
</tr>

View File

@@ -36,50 +36,7 @@
<tbody id="monitors" class="divide-y divide-gray-50">
{% if this.monitors.items|length > 0 %}
{% for monitor in this.monitors.items %}
<tr id="monitor_{{ monitor.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
{{ monitor.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor|monitor_media_id }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.searchCount }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %}
<twig:StatusBadge color="orange" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% else %}
<twig:StatusBadge color="rose" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if monitor.status == "New" %}
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
<twig:StatusBadge color="purple" status="{{ monitor.status }}" />
{% else %}
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: monitor.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ monitor.title }} from your Monitor list."
/>
</button>
</td>
</tr>
<twig:MonitorListRow :monitor="monitor" />
{% endfor %}
{% if this.isWidget and this.monitors.items|length > 5 %}
<tr id="monitor_view_all">

View File

@@ -0,0 +1,44 @@
<tr{{ attributes }} id="monitor_{{ monitor.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
{{ monitor.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor|monitor_media_id }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.searchCount }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %}
<twig:StatusBadge color="orange" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% else %}
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if monitor.status == "New" %}
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
<twig:StatusBadge color="purple" status="{{ monitor.status }}" />
{% else %}
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
{% endif %}
</td>
<td class="px-6 py-4 flex flex-row align-middle justify-center">
<button {{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: monitor.id}) }}>
<twig:ux:icon
name="ic:twotone-cancel" width="18px"
class="rounded-full align-middle text-red-600"
title="Remove {{ monitor.title }} from your Monitor list."
/>
</button>
</td>
</tr>

View File

@@ -1,13 +1,13 @@
<div{{ attributes }}>
<a href="{{ path('app_search_result', {
mediaType: mediaType,
tmdbId: tmdbId
imdbId: imdbId
}) }}">
<img src="{{ image }}" class="w-40 rounded-md" />
</a>
<a href="{{ path('app_search_result', {
mediaType: mediaType,
tmdbId: tmdbId
imdbId: imdbId
}) }}">
<h3 class="text-center text-gray-50 max-w-[16ch] text-extrabold">{{ title }}</h3>
</a>

View File

@@ -17,7 +17,7 @@
</p>
</div>
<a class="h-9 rounded-md py-1 px-2 bg-green-600 text-gray-50"
href="{{ path('app_search_result', {mediaType: mediaType, tmdbId: tmdbId}) }}"
href="{{ path('app_search_result', {mediaType: mediaType, imdbId: imdbId}) }}"
>choose</a>
</div>
</div>

View File

@@ -22,7 +22,7 @@
<div class="flex flex-col gap-4">
<twig:Card title="Popular Movies" contentClass="flex flex-row justify-between w-full">
{% for movie in popular_movies %}
<twig:Poster imdbId=""
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"
title="{{ movie.title }}"
description="{{ movie.description }}"
@@ -34,7 +34,7 @@
</twig:Card>
<twig:Card title="Popular TV Shows" contentClass="flex flex-row justify-between w-full">
{% for movie in popular_tvshows %}
<twig:Poster imdbId=""
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"
title="{{ movie.title }}"
description="{{ movie.description }}"