Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63850e48fd | |||
| f9a284cb67 | |||
| 228d320edc |
3
assets/bootstrap.js
vendored
3
assets/bootstrap.js
vendored
@@ -1,5 +1,6 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
import Popover from '@stimulus-components/popover'
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
app.register('popover', Popover);
|
||||
|
||||
1
assets/icons/line-md/circle-twotone.svg
Normal file
1
assets/icons/line-md/circle-twotone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0" stroke="currentColor" stroke-dasharray="64" stroke-dashoffset="64" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12c0 -4.97 4.03 -9 9 -9c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.15s" values="0;0.3"/><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0"/></path></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -18,6 +18,7 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
- $PWD/var/download:/var/download
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
tty: true
|
||||
@@ -34,6 +35,7 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
- $PWD/var/download:/var/download
|
||||
tty: true
|
||||
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=10
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"ext-iconv": "*",
|
||||
"1tomany/rich-bundle": "^1.8",
|
||||
"aimeos/map": "^3.12",
|
||||
"chrisullyott/php-filesize": "^4.2",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.14",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.1",
|
||||
|
||||
51
composer.lock
generated
51
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": "63610a631352051ae8327669536efcef",
|
||||
"content-hash": "c519733202d45f8fb3a4f5b8e7dfb95b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "1tomany/rich-bundle",
|
||||
@@ -186,6 +186,55 @@
|
||||
],
|
||||
"time": "2023-12-11T17:09:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "chrisullyott/php-filesize",
|
||||
"version": "v4.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chrisullyott/php-filesize.git",
|
||||
"reference": "967ea3365c00974b50b608ffc045a267ab92ef43"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/chrisullyott/php-filesize/zipball/967ea3365c00974b50b608ffc045a267ab92ef43",
|
||||
"reference": "967ea3365c00974b50b608ffc045a267ab92ef43",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ChrisUllyott\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Ullyott",
|
||||
"email": "contact@chrisullyott.com",
|
||||
"homepage": "http://www.chrisullyott.com"
|
||||
}
|
||||
],
|
||||
"description": "Easily calculate file sizes and convert between units.",
|
||||
"homepage": "https://github.com/chrisullyott/php-filesize",
|
||||
"keywords": [
|
||||
"php",
|
||||
"size-calculation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/chrisullyott/php-filesize/issues",
|
||||
"source": "https://github.com/chrisullyott/php-filesize"
|
||||
},
|
||||
"time": "2021-10-17T22:52:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.3",
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
# Media
|
||||
media.base_path: '/var/download'
|
||||
media.default_movies_dir: movies
|
||||
media.default_tvshows_dir: tvshows
|
||||
media.movies_path: '%env(default:media.default_movies_dir:MOVIES_PATH)%'
|
||||
media.movies_path: '/var/download/%env(default:media.default_movies_dir:MOVIES_PATH)%'
|
||||
media.tvshows_path: '/var/download/%env(default:media.default_tvshows_dir:TVSHOWS_PATH)%'
|
||||
|
||||
# Mercure
|
||||
|
||||
@@ -28,4 +28,7 @@ return [
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
'@stimulus-components/popover' => [
|
||||
'version' => '7.0.0',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
namespace App\Monitor\Service;
|
||||
|
||||
use Aimeos\Map;
|
||||
use Nihilarr\PTN;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class MediaFiles
|
||||
{
|
||||
private Finder $finder;
|
||||
|
||||
private string $basePath;
|
||||
|
||||
private string $moviesPath;
|
||||
|
||||
private string $tvShowsPath;
|
||||
@@ -18,6 +22,9 @@ class MediaFiles
|
||||
private Filesystem $filesystem;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(param: 'media.base_path')]
|
||||
string $basePath,
|
||||
|
||||
#[Autowire(param: 'media.movies_path')]
|
||||
string $moviesPath,
|
||||
|
||||
@@ -27,6 +34,7 @@ class MediaFiles
|
||||
Filesystem $filesystem,
|
||||
) {
|
||||
$this->finder = new Finder();
|
||||
$this->basePath = $basePath;
|
||||
$this->moviesPath = $moviesPath;
|
||||
$this->tvShowsPath = $tvShowsPath;
|
||||
$this->filesystem = $filesystem;
|
||||
@@ -43,6 +51,11 @@ class MediaFiles
|
||||
throw new \Exception(sprintf('A path for media type %s does not exist.', $mediaType));
|
||||
}
|
||||
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
public function getMoviesPath(): string
|
||||
{
|
||||
return $this->moviesPath;
|
||||
@@ -125,4 +138,49 @@ class MediaFiles
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber)
|
||||
{
|
||||
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||
|
||||
if ($existingEpisodes->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var SplFileInfo $episode */
|
||||
foreach ($existingEpisodes as $episode) {
|
||||
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
||||
|
||||
if ($ptn->season === $seasonNumber && $ptn->episode === $episodeNumber) {
|
||||
return $episode;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function movieExists(string $title)
|
||||
{
|
||||
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
||||
$directoryExists = $this->filesystem->exists($filepath);
|
||||
|
||||
if (false === $directoryExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === $this->finder->in($filepath)->files()->hasResults()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$files = Map::from($this->finder->in($filepath)->files())->filter(function (SplFileInfo $file) {
|
||||
$validExtensions = ['mkv', 'mp4', 'mpeg'];
|
||||
return in_array($file->getExtension(), $validExtensions);
|
||||
})->values();
|
||||
|
||||
if (false === $files->isEmpty()) {
|
||||
return $files[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Torrentio\Action\Handler;
|
||||
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
||||
use App\Torrentio\Client\Torrentio;
|
||||
@@ -14,12 +15,15 @@ class GetMovieOptionsHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly Torrentio $torrentio,
|
||||
private readonly MediaFiles $mediaFiles
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, 'movies');
|
||||
return new GetMovieOptionsResult(
|
||||
media: $this->tmdb->mediaDetails($command->imdbId, 'movies'),
|
||||
media: $media,
|
||||
file: $this->mediaFiles->movieExists($media->title),
|
||||
results: $this->torrentio->search($command->imdbId, 'movies'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Torrentio\Action\Handler;
|
||||
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||
@@ -16,12 +17,18 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly Torrentio $torrentio,
|
||||
private readonly MediaFiles $mediaFiles,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode);
|
||||
$parentShow = $this->tmdb->mediaDetails($command->imdbId, 'tvshows');
|
||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||
|
||||
return new GetTvShowOptionsResult(
|
||||
media: $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode),
|
||||
media: $media,
|
||||
file: $file,
|
||||
season: $command->season,
|
||||
episode: $command->episode,
|
||||
results: $this->torrentio->fetchEpisodeResults(
|
||||
|
||||
@@ -9,6 +9,7 @@ class GetMovieOptionsResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public bool|\SplFileInfo $file,
|
||||
public array $results
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ namespace App\Torrentio\Action\Result;
|
||||
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class GetTvShowOptionsResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public bool|SplFileInfo $file,
|
||||
public string $season,
|
||||
public string $episode,
|
||||
public array $results
|
||||
|
||||
32
src/Twig/Extensions/UtilExtension.php
Normal file
32
src/Twig/Extensions/UtilExtension.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extensions;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use ChrisUllyott\FileSize;
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
|
||||
class UtilExtension
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly MediaFiles $mediaFiles,
|
||||
) {}
|
||||
|
||||
#[AsTwigFilter('filesize')]
|
||||
public function type(string|int $size)
|
||||
{
|
||||
return (new FileSize($size))->asAuto();
|
||||
}
|
||||
|
||||
#[AsTwigFilter('strip_media_path')]
|
||||
public function stripMediaPath(string $path)
|
||||
{
|
||||
return str_replace(
|
||||
$this->mediaFiles->getBasePath() . DIRECTORY_SEPARATOR,
|
||||
'',
|
||||
$path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,26 @@ module.exports = {
|
||||
"bg-green-400",
|
||||
"bg-purple-400",
|
||||
"bg-orange-400",
|
||||
"bg-blue-600",
|
||||
"bg-rose-600",
|
||||
"transition-opacity",
|
||||
"ease-in",
|
||||
"duration-700",
|
||||
"opacity-100"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
animation: {
|
||||
fade: 'fadeIn .3s ease-in-out',
|
||||
},
|
||||
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
from: { opacity: 0 },
|
||||
to: { opacity: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<div class="p-4 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
{% if results.file != false %}
|
||||
<div class="p-3 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this movie:</p>
|
||||
<ul class="list-disc ml-3">
|
||||
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
||||
</div>
|
||||
|
||||
@@ -8,23 +8,59 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-col gap-4 grow">
|
||||
<h4 class="text-md font-bold">{{ results.episode }}. {{ results.media.title }}</h4>
|
||||
<h4 class="text-md font-bold">
|
||||
{{ results.episode }}. {{ results.media.title }}
|
||||
</h4>
|
||||
<p>{{ results.media.description }}</p>
|
||||
<div>
|
||||
<small class="py-1 px-1.5 grow-0 font-bold bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"
|
||||
{{ stimulus_action('tv-results', 'toggleList', 'click') }}
|
||||
><span {{ stimulus_target('tv-results', 'count') }}>{{ results.results|length }}</span> results</small>
|
||||
<small class="py-1 px-1.5 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ results.media.episodeAirDate }}"
|
||||
>{{ results.media.episodeAirDate }}</small>
|
||||
>
|
||||
<span {{ stimulus_target('tv-results', 'count') }}>{{ results.results|length }}</span> results
|
||||
</small>
|
||||
|
||||
{% if results.file != false %}
|
||||
<span data-controller="popover">
|
||||
<template data-popover-target="content">
|
||||
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||
<ul class="list-disc ml-3">
|
||||
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<small
|
||||
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||
>
|
||||
exists
|
||||
</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if results.file == false %}
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||
missing
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ results.media.episodeAirDate }}">
|
||||
{{ results.media.episodeAirDate }}
|
||||
</small>
|
||||
{# <small class="py-1 px-1.5 grow-0 font-bold bg-red-600 hover:bg-red-700 rounded-lg font-normal text-white cursor-pointer" title="Clear cache for {{ results.media.title }}"#}
|
||||
{# {{ stimulus_action('tv-results', 'clearCache', 'click') }}#}
|
||||
{# >Clear Cache</small>#}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 justify-between">
|
||||
<input type="checkbox"
|
||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||
/>
|
||||
<div class="flex flex-col items-center">
|
||||
<input type="checkbox"
|
||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||
/>
|
||||
<span title="You have this downloaded!">
|
||||
<twig:ux:icon width="20" class="mt-2 text-green-600" name="line-md:circle-twotone" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-end hover:cursor-pointer"
|
||||
{{ stimulus_action('tv-results', 'toggleList', 'click') }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 32 32">
|
||||
|
||||
Reference in New Issue
Block a user