Compare commits

...

16 Commits

Author SHA1 Message Date
c664e9fbca feat: presents popular tv shows on landing page 2025-04-27 21:36:45 -05:00
5d257e4404 fix: removes duplicate Dockerfile instruction 2025-04-27 21:15:01 -05:00
a6dc4f0b03 fix: removes default apache vhost 2025-04-27 16:33:21 -05:00
3c6e41af94 wip-feat: adds mercure to deployment 2025-04-27 16:32:34 -05:00
3e081df01c fix: adds download record at time of download 2025-04-27 16:30:08 -05:00
3384720c09 wip-feat: mercure download progress 2025-04-27 11:04:40 -05:00
c32ff2e464 wip-feat: mercure alerts 2025-04-27 09:55:55 -05:00
6138c94d7a fix: missing app secret 2025-04-27 00:24:55 -05:00
a1a38cb74c fix-feat: download selected button 2025-04-26 22:30:44 -05:00
e9ccb5ad2b fix: select all button 2025-04-26 21:38:17 -05:00
9d350a572d wip-feat: pre-selects option for each episode 2025-04-25 22:01:13 -05:00
cd271b568b feat: converts active download list to live component with polling 2025-04-25 21:50:51 -05:00
6b88483635 fix: links popular movies to their download options 2025-04-25 16:50:33 -05:00
0120ddcedd feat: shows popular movies 2025-04-25 16:42:57 -05:00
7270fa2936 chore: adds doc blocks to Search module 2025-04-25 15:23:04 -05:00
6a2567bf98 wip-feat: lists active & recent downloads on landing page 2025-04-25 13:30:15 -05:00
48 changed files with 1239 additions and 188 deletions

View File

@@ -1,3 +1,4 @@
APP_SECRET="%%app_secret%%"
DATABASE_URL="%%db_url%%"
DOWNLOAD_DIR=%%download_dir%%
REAL_DEBRID_KEY="%%rd_key%%"

View File

@@ -1 +1,4 @@
FROM registry.caldwell.digital/library/php:8.4-apache
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
RUN rm /etc/apache2/sites-enabled/000-default.conf

View File

@@ -2,5 +2,4 @@ FROM registry.caldwell.digital/library/php:8.4-apache
COPY --chown=www-data:www-data . /var/www
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
RUN rm /etc/apache2/sites-enabled/000-default.conf

View File

@@ -8,3 +8,13 @@ import './bootstrap.js';
import './styles/app.css';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
let alert = document.querySelector('.alert');
var observer = new MutationObserver(function(mutations) {
if (document.contains(alert)) {
observer.disconnect();
}
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});

View File

@@ -1,4 +1,24 @@
{
"controllers": [],
"controllers": {
"@symfony/ux-live-component": {
"live": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"@symfony/ux-live-component/dist/live.min.css": true
}
}
},
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View File

@@ -0,0 +1,18 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
let timer = setTimeout(() => {
this.element.remove();
},
"3000"
);
this.element.addEventListener('mouseout', () => timer = setTimeout(() => {
this.element.remove();
},
"3000"
));
this.element.addEventListener('mouseover', () => clearTimeout(timer));
}
}

View File

@@ -21,7 +21,7 @@ export default class extends Controller {
}
static outlets = ['movie-results', 'tv-results']
static targets = ['resolution', 'codec', 'language', 'provider', 'season']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'selectAll', 'downloadSelected']
static values = {
'media-type': String,
'episodes': Array,
@@ -29,7 +29,8 @@ export default class extends Controller {
connect() {
if (this.mediaTypeValue === "tvshows") {
this.activeFilter['season'] = 1;}
this.activeFilter['season'] = 1;
}
}
async movieResultsOutletConnected(outlet) {
@@ -148,7 +149,7 @@ export default class extends Controller {
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
// option.selectInput.checked = true;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
@@ -158,8 +159,30 @@ export default class extends Controller {
resultList.countTarget.innerText = count;
}
});
}
await results.forEach((list) => filterOperation(list, currentSeason));
}
uncheckSelectAllBtn() {
this.selectAllTarget.checked = false;
}
selectAllEpisodes() {
this.tvResultsOutlets.forEach((episode) => {
if (episode.isActive()) {
episode.selectEpisodeForDownload()
}
});
}
downloadSelectedEpisodes() {
this.tvResultsOutlets.forEach(episode => {
if (episode.isActive() && episode.isSelected()) {
episode.download();
}
});
this.selectAllTarget.checked = false;
}
}

View File

@@ -15,7 +15,7 @@ export default class extends Controller {
active: Boolean,
};
static targets = ['list', 'count']
static targets = ['list', 'count', 'episodeSelector']
static outlets = ['loading-icon']
options = []
@@ -32,7 +32,12 @@ export default class extends Controller {
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
if (this.options.length > 0) {
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.options[0].querySelector('input[type="checkbox"]').checked = true;
} else {
this.episodeSelectorTarget.disabled = true;
}
this.optionsLoaded = true;
this.loadingIconOutlet.increaseCount();
});
@@ -49,6 +54,7 @@ export default class extends Controller {
setInActive() {
this.activeValue = false;
this.episodeSelectorTarget.checked = false;
this.element.classList.add('hidden');
}
@@ -56,7 +62,30 @@ export default class extends Controller {
return this.activeValue;
}
isSelected() {
return this.episodeSelectorTarget.checked;
}
selectEpisodeForDownload() {
if (true === this.isActive() && this.episodeSelectorTarget.disabled === false) {
this.episodeSelectorTarget.checked = !this.episodeSelectorTarget.checked;
}
}
toggleList() {
this.listTarget.classList.toggle('hidden');
}
download() {
this.options.forEach(option => {
const optionSelector = option.querySelector('input[type="checkbox"]');
if (true === optionSelector.checked) {
const downloadBtn = option.querySelector('button.download-btn');
const downloadBtnController = this.application.getControllerForElementAndIdentifier(downloadBtn, 'download-button');
downloadBtnController.download();
optionSelector.checked = false;
this.episodeSelectorTarget.checked = false;
}
})
}
}

View File

@@ -4,6 +4,11 @@
DocumentRoot /var/www/public
DirectoryIndex /index.php
<LocationMatch "/hub/">
ProxyPass http://mercure:80/
ProxyPassReverse http://mercure:80/
</LocationMatch>
<Directory /var/www/public>
AllowOverride None
Order Allow,Deny

View File

@@ -24,6 +24,7 @@
<copy file="${project.basedir}/.env.dist" tofile="${project.basedir}/.env.local" overwrite="true">
<filterchain>
<replacetokens begintoken="%%" endtoken="%%">
<token key="app_secret" value="${APP_SECRET}" />
<token key="db_url" value="${DATABASE_URL}" />
<token key="download_dir" value="${DOWNLOAD_DIR}" />
<token key="rd_key" value="${REAL_DEBRID_KEY}" />

View File

@@ -24,6 +24,24 @@ services:
- ./var/download:/var/download
command: php ./bin/console messenger:consume async -v --time-limit=3600 --limit=10
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3000:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database:
image: mariadb:10.11.2
ports:
@@ -43,3 +61,5 @@ services:
volumes:
mysql:
mercure_data:
mercure_config:

View File

@@ -23,12 +23,15 @@
"symfony/flex": "^2",
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "7.2.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/stimulus-bundle": "^2.24",
"symfony/twig-bundle": "7.2.*",
"symfony/ux-icons": "^2.24",
"symfony/ux-live-component": "^2.24",
"symfony/ux-turbo": "^2.24",
"symfony/ux-twig-component": "^2.24",
"symfony/yaml": "7.2.*",
"symfonycasts/tailwind-bundle": "^0.10.0",

573
composer.lock generated
View File

@@ -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": "0448ecb537f5d169d81a56ba2b3c2cc6",
"content-hash": "740c22e45c004ece09230b2bb113e949",
"packages": [
{
"name": "1tomany/data-uri",
@@ -1421,6 +1421,79 @@
},
"time": "2025-01-24T11:45:48+00:00"
},
{
"name": "lcobucci/jwt",
"version": "5.5.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "a835af59b030d3f2967725697cf88300f579088e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e",
"reference": "a835af59b030d3f2967725697cf88300f579088e",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-sodium": "*",
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"psr/clock": "^1.0"
},
"require-dev": {
"infection/infection": "^0.29",
"lcobucci/clock": "^3.2",
"lcobucci/coding-standard": "^11.0",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.10.7",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.10",
"phpstan/phpstan-strict-rules": "^1.5.0",
"phpunit/phpunit": "^11.1"
},
"suggest": {
"lcobucci/clock": ">= 3.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/5.5.0"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2025-01-26T21:29:45+00:00"
},
{
"name": "nihilarr/parse-torrent-name",
"version": "v0.0.1",
@@ -2548,6 +2621,62 @@
},
"time": "2023-04-04T09:50:52+00:00"
},
{
"name": "psr/link",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/link.git",
"reference": "84b159194ecfd7eaa472280213976e96415433f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7",
"reference": "84b159194ecfd7eaa472280213976e96415433f7",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"suggest": {
"fig/link-util": "Provides some useful PSR-13 utilities"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Link\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for HTTP links",
"homepage": "https://github.com/php-fig/link",
"keywords": [
"http",
"http-link",
"link",
"psr",
"psr-13",
"rest"
],
"support": {
"source": "https://github.com/php-fig/link/tree/2.0.1"
},
"time": "2021-03-11T23:00:27+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
@@ -4656,6 +4785,173 @@
],
"time": "2025-03-28T13:32:50+00:00"
},
{
"name": "symfony/mercure",
"version": "v0.6.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure.git",
"reference": "304cf84609ef645d63adc65fc6250292909a461b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure/zipball/304cf84609ef645d63adc65fc6250292909a461b",
"reference": "304cf84609ef645d63adc65fc6250292909a461b",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/deprecation-contracts": "^2.0|^3.0|^4.0",
"symfony/http-client": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0",
"symfony/polyfill-php80": "^1.22",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0|^7.0",
"twig/twig": "^2.0|^3.0|^4.0"
},
"suggest": {
"symfony/stopwatch": "Integration with the profiler performances"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/dunglas/mercure",
"name": "dunglas/mercure"
},
"branch-alias": {
"dev-main": "0.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Mercure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Mercure Component",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure/issues",
"source": "https://github.com/symfony/mercure/tree/v0.6.5"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure",
"type": "tidelift"
}
],
"time": "2024-04-08T12:51:34+00:00"
},
{
"name": "symfony/mercure-bundle",
"version": "v0.3.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure-bundle.git",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/77435d740b228e9f5f3f065b6db564f85f2cdb64",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64",
"shasum": ""
},
"require": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"php": ">=7.1.3",
"symfony/config": "^4.4|^5.0|^6.0|^7.0",
"symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/mercure": "^0.6.1",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/stopwatch": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/ux-turbo": "*",
"symfony/var-dumper": "^4.3.7|^5.0|^6.0|^7.0"
},
"suggest": {
"symfony/messenger": "To use the Messenger integration"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-main": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MercureBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MercureBundle",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure-bundle/issues",
"source": "https://github.com/symfony/mercure-bundle/tree/v0.3.9"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle",
"type": "tidelift"
}
],
"time": "2024-05-31T09:07:18+00:00"
},
{
"name": "symfony/messenger",
"version": "v7.2.5",
@@ -6976,6 +7272,198 @@
],
"time": "2025-04-04T17:32:18+00:00"
},
{
"name": "symfony/ux-live-component",
"version": "v2.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-live-component.git",
"reference": "ee1a8e5d01f5b3f2f8e6856941fa8c944677e41c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-live-component/zipball/ee1a8e5d01f5b3f2f8e6856941fa8c944677e41c",
"reference": "ee1a8e5d01f5b3f2f8e6856941fa8c944677e41c",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/property-access": "^5.4.5|^6.0|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/stimulus-bundle": "^2.9",
"symfony/ux-twig-component": "^2.8",
"twig/twig": "^3.8.0"
},
"conflict": {
"symfony/config": "<5.4.0"
},
"require-dev": {
"doctrine/annotations": "^1.0",
"doctrine/collections": "^1.6.8|^2.0",
"doctrine/doctrine-bundle": "^2.4.3",
"doctrine/orm": "^2.9.4",
"doctrine/persistence": "^2.5.2|^3.0",
"phpdocumentor/reflection-docblock": "5.x-dev",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/form": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/options-resolver": "^5.4|^6.0|^7.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symfony/security-bundle": "^5.4|^6.0|^7.0",
"symfony/serializer": "^5.4|^6.0|^7.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
"symfony/validator": "^5.4|^6.0|^7.0",
"zenstruck/browser": "^1.2.0",
"zenstruck/foundry": "^2.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\LiveComponent\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Live components for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"components",
"symfony-ux",
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-live-component/tree/v2.24.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-03-12T08:41:47+00:00"
},
{
"name": "symfony/ux-turbo",
"version": "v2.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-turbo.git",
"reference": "22954300bd0b01ca46f17c7890ea15138d9cf67f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/22954300bd0b01ca46f17c7890ea15138d9cf67f",
"reference": "22954300bd0b01ca46f17c7890ea15138d9cf67f",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/stimulus-bundle": "^2.9.1"
},
"conflict": {
"symfony/flex": "<1.13"
},
"require-dev": {
"dbrekelmans/bdi": "dev-main",
"doctrine/doctrine-bundle": "^2.4.3",
"doctrine/orm": "^2.8 | 3.0",
"phpstan/phpstan": "^1.10",
"symfony/asset-mapper": "^6.4|^7.0",
"symfony/debug-bundle": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/form": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/mercure-bundle": "^0.3.7",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/panther": "^2.1",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|6.3.*|^7.0",
"symfony/property-access": "^5.4|^6.0|^7.0",
"symfony/security-core": "^5.4|^6.0|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/ux-twig-component": "^2.21",
"symfony/web-profiler-bundle": "^5.4|^6.0|^7.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Turbo\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "kevin@dunglas.fr"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Hotwire Turbo integration for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"hotwire",
"javascript",
"mercure",
"symfony-ux",
"turbo",
"turbo-stream"
],
"support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.24.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-04T17:29:20+00:00"
},
{
"name": "symfony/ux-twig-component",
"version": "v2.24.0",
@@ -7315,6 +7803,89 @@
],
"time": "2025-03-13T12:21:46+00:00"
},
{
"name": "symfony/web-link",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/web-link.git",
"reference": "f537556a885e14a1d28f6c759d41e57e93d0a532"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/web-link/zipball/f537556a885e14a1d28f6c759d41e57e93d0a532",
"reference": "f537556a885e14a1d28f6c759d41e57e93d0a532",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/link": "^1.1|^2.0"
},
"conflict": {
"symfony/http-kernel": "<6.4"
},
"provide": {
"psr/link-implementation": "1.0|2.0"
},
"require-dev": {
"symfony/http-kernel": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\WebLink\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Manages links between resources",
"homepage": "https://symfony.com",
"keywords": [
"dns-prefetch",
"http",
"http2",
"link",
"performance",
"prefetch",
"preload",
"prerender",
"psr13",
"push"
],
"support": {
"source": "https://github.com/symfony/web-link/tree/v7.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.2.5",

View File

@@ -13,4 +13,7 @@ return [
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,8 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

View File

@@ -0,0 +1,5 @@
live_component:
resource: '@LiveComponentBundle/config/routes.php'
prefix: '/_components'
# adjust prefix to add localization to your components
#prefix: '/{_locale}/_components'

View File

@@ -12,4 +12,25 @@ services:
- /mnt/media/downloads:/var/download
command: php ./bin/console messenger:consume async -v --time-limit=3600 --limit=10
deploy:
replicas: 2
replicas: 2
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3000:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
volumes:
mercure_config:
mercure_data:

View File

@@ -22,4 +22,10 @@ return [
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@symfony/ux-live-component' => [
'path' => './vendor/symfony/ux-live-component/assets/dist/live_controller.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Routing\Attribute\Route;
use Twig\Environment;
final class AlertController extends AbstractController
{
public function __construct(
#[Autowire(service: 'twig')] private readonly Environment $renderer,
private readonly HubInterface $hub,
) {}
#[Route('/alert', name: 'app_alert')]
public function index(): Response
{
$update = new Update(
'alerts',
$this->renderer->render('broadcast/Alert.html.twig', [
'alert_id' => 1,
'title' => 'Added to queue',
'message' => 'This is a testy test!',
])
);
$this->hub->publish($update);
return $this->json([
'Success' => 'Published'
]);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Controller;
use App\Download\Action\Input\DownloadMediaInput;
use App\Download\Framework\Repository\DownloadRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -11,6 +12,7 @@ use Symfony\Component\Routing\Attribute\Route;
class DownloadController extends AbstractController
{
public function __construct(
private DownloadRepository $downloadRepository,
private MessageBusInterface $bus,
) {}
@@ -18,6 +20,15 @@ class DownloadController extends AbstractController
public function download(
DownloadMediaInput $input,
): Response {
$download = $this->downloadRepository->insert(
$input->url,
$input->title,
$input->filename,
$input->imdbId,
$input->mediaType,
"",
);
$input->downloadId = $download->getId();
try {
$this->bus->dispatch($input->toCommand());
} catch (\Throwable $exception) {

View File

@@ -2,17 +2,27 @@
namespace App\Controller;
use App\Download\Framework\Repository\DownloadRepository;
use App\Tmdb\Tmdb;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class IndexController extends AbstractController
{
public function __construct(
private readonly DownloadRepository $downloadRepository,
private readonly Tmdb $tmdb,
) {}
#[Route('/', name: 'app_index')]
public function index(): Response
{
return $this->render('index/index.html.twig', [
'controller_name' => 'IndexController',
'active_downloads' => $this->downloadRepository->getActivePaginated(),
'recent_downloads' => $this->downloadRepository->latest(5),
'popular_movies' => $this->tmdb->popularMovies(1, 6),
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
]);
}
}

View File

@@ -15,5 +15,6 @@ class DownloadMediaCommand implements CommandInterface
public string $filename,
public string $mediaType,
public string $imdbId,
public ?int $downloadId = null,
) {}
}

View File

@@ -21,14 +21,19 @@ readonly class DownloadMediaHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface
{
$download = $this->downloadRepository->insert(
$command->url,
$command->title,
$command->filename,
$command->imdbId,
$command->mediaType,
""
);
if (null === $command->downloadId) {
$download = $this->downloadRepository->insert(
$command->url,
$command->title,
$command->filename,
$command->imdbId,
$command->mediaType,
""
);
} else {
$download = $this->downloadRepository->find($command->downloadId);
}
try {
$this->downloadRepository->updateStatus($download->getId(), 'In Progress');

View File

@@ -25,6 +25,8 @@ class DownloadMediaInput implements InputInterface
#[SourceRequest('imdbId')]
public string $imdbId,
public ?int $downloadId = null,
) {}
public function toCommand(): CommandInterface
@@ -35,6 +37,7 @@ class DownloadMediaInput implements InputInterface
$this->filename,
$this->mediaType,
$this->imdbId,
$this->downloadId,
);
}
}

View File

@@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\UX\Turbo\Attribute\Broadcast;
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
#[Broadcast]
#[Broadcast(template: 'broadcast/Download.stream.html.twig')]
class Download
{
#[ORM\Id]

View File

@@ -36,11 +36,12 @@ class DownloadRepository extends ServiceEntityRepository
return new \Doctrine\ORM\Tools\Pagination\Paginator($query);
}
public function getActivePaginated(int $pageNumber = 1, int $perPage = 10)
public function getActivePaginated(int $pageNumber = 1, int $perPage = 5)
{
$firstResult = ($pageNumber - 1) * $perPage;
$query = $this->createQueryBuilder('d')
->andWhere('d.status IN (:statuses)')
->orderBy('d.id', 'DESC')
->setParameter('statuses', ['New', 'In Progress'])
->setFirstResult($firstResult)
->setMaxResults($perPage)
@@ -115,4 +116,15 @@ class DownloadRepository extends ServiceEntityRepository
return $query->getResult();
}
public function latest(int $limit = 1)
{
return $this->createQueryBuilder('d')
->andWhere('d.status IN (:statuses)')
->setParameter('statuses', ['Complete'])
->setMaxResults($limit)
->orderBy('d.id', 'DESC')
->getQuery()
->getResult();
}
}

View File

@@ -4,9 +4,9 @@ namespace App\Search\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
/** @implements CommandInterface<GetMediaInfoCommand> */
class GetMediaInfoCommand implements CommandInterface
{
/** @implements CommandInterface<GetMediaInfoCommand> */
public function __construct(
public string $tmdbId,
public string $mediaType,

View File

@@ -4,12 +4,9 @@ namespace App\Search\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
/** @implements CommandInterface<SearchCommand> */
class SearchCommand implements CommandInterface
{
/**
* @param string $term
* @implements CommandInterface<SearchCommand>
*/
public function __construct(
public string $term
) {}

View File

@@ -2,12 +2,14 @@
namespace App\Search\Action\Handler;
use App\Search\Action\Command\GetMediaInfoCommand;
use App\Search\Action\Result\GetMediaInfoResult;
use App\Tmdb\Tmdb;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements HandlerInterface<GetMediaInfoCommand, GetMediaInfoResult> */
class GetMediaInfoHandler implements HandlerInterface
{
public function __construct(

View File

@@ -8,13 +8,13 @@ use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/*** @implements HandlerInterface<SearchResult> */
class SearchHandler implements HandlerInterface
{
public function __construct(
private Tmdb $tmdb,
) {}
/*** @implements HandlerInterface<SearchResult> */
public function handle(CommandInterface $command): ResultInterface
{
return new SearchResult(

View File

@@ -8,6 +8,7 @@ use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/** @implements InputInterface<GetMediaInfoInput> */
class GetMediaInfoInput implements InputInterface
{
public function __construct(

View File

@@ -8,9 +8,7 @@ use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/**
* @implements InputInterface<SearchCommand>
*/
/** @implements InputInterface<SearchCommand> */
class SearchInput implements InputInterface
{
public function __construct(

View File

@@ -5,9 +5,9 @@ namespace App\Search\Action\Result;
use App\Tmdb\TmdbResult;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements ResultInterface<GetMediaInfoResult> */
class GetMediaInfoResult implements ResultInterface
{
/** @implements ResultInterface<GetMediaInfoResult> */
public function __construct(
public TmdbResult $media,
) {}

View File

@@ -4,6 +4,7 @@ namespace App\Search\Action\Result;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements ResultInterface<SearchResult> */
class SearchResult implements ResultInterface
{
public function __construct(

View File

@@ -94,12 +94,35 @@ class Tmdb
$this->tvRepository = new TvRepository($this->client);
}
public function popularMovies(int $page = 1)
public function popularMovies(int $page = 1, ?int $limit = null)
{
$movies = $this->movieRepository->getPopular(['page' => $page]);
foreach ($movies as $movie) {
$this->parseResult($movie);
$movies = $movies->map(function ($movie) use ($movies) {
return $this->parseResult($movies[$movie], "movie");
});
$movies = array_values($movies->toArray());
if (null !== $limit) {
$movies = array_slice($movies, 0, $limit);
}
return $movies;
}
public function popularTvShows(int $page = 1, ?int $limit = null)
{
$movies = $this->tvRepository->getPopular(['page' => $page]);
$movies = $movies->map(function ($movie) use ($movies) {
return $this->parseResult($movies[$movie], "movie");
});
$movies = array_values($movies->toArray());
if (null !== $limit) {
$movies = array_slice($movies, 0, $limit);
}
return $movies;

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Twig\Components;
use App\Download\Framework\Repository\DownloadRepository;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
final class ActiveDownloadList
{
use DefaultActionTrait;
public function __construct(
private DownloadRepository $downloadRepository,
) {}
#[LiveAction]
public function getDownloads()
{
return $this->downloadRepository->getActivePaginated();
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
final class Alert
{
use DefaultActionTrait;
}

View File

@@ -121,6 +121,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/mercure-bundle": {
"version": "0.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.3",
"ref": "528285147494380298f8f991ee8c47abebaf79db"
},
"files": [
"config/packages/mercure.yaml"
]
},
"symfony/messenger": {
"version": "7.2",
"recipe": {
@@ -199,6 +211,27 @@
"assets/icons/symfony.svg"
]
},
"symfony/ux-live-component": {
"version": "2.24",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.6",
"ref": "73e69baf18f47740d6f58688c5464b10cdacae06"
},
"files": [
"config/routes/ux_live_component.yaml"
]
},
"symfony/ux-turbo": {
"version": "2.24",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.20",
"ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9"
}
},
"symfony/ux-twig-component": {
"version": "2.24",
"recipe": {

View File

@@ -0,0 +1,5 @@
<turbo-stream action="prepend" target="alert_list">
<template>
<twig:Alert :title="title|default('')" :message="message" :alert_id="alert_id" data-controller="alert" />
</template>
</turbo-stream>

View File

@@ -0,0 +1,69 @@
{# Learn how to use Turbo Streams: https://github.com/symfony/ux-turbo#broadcast-doctrine-entities-update #}
{% block create %}
<turbo-stream action="append" target="active_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-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
</span>
</td>
</tr>
</template>
</turbo-stream>
<turbo-stream action="prepend" target="alert_list">
<template>
<twig:Alert title="Success" message="{{ entity.title }} has been added to the Download queue" alert_id="{{ entity.id }}" data-controller="alert" />
</template>
</turbo-stream>
{% endblock %}
{% block update %}
{% if entity.status != "Complete" %}
<turbo-stream action="update" target="ad_download_{{ 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">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
</span>
</td>
</template>
</turbo-stream>
{% else %}
<turbo-stream action="remove" target="ad_download_{{ id }}">
</turbo-stream>
<turbo-stream action="prepend" target="alert_list">
<template>
<twig:Alert title="Finished downloading" message="{{ entity.title }}" alert_id="{{ entity.id }}" data-controller="alert" />
</template>
</turbo-stream>
<turbo-stream action="prepend" target="recent_downloads">
<template>
<tr id="recent_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-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
</span>
</td>
</tr>
</template>
</turbo-stream>
{% endif %}
{% endblock %}
{% block remove %}
<turbo-stream action="remove" target="ad_download_{{ id }}"></turbo-stream>
{# <turbo-stream action="remove" target="cd_download_{{ id }}"></turbo-stream>#}
{% endblock %}

View File

@@ -0,0 +1,39 @@
<div{{ attributes }} class="min-w-48">
<table id="active_downloads" class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-gray-50 table-fixed" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
<thead>
<tr class="dark:bg-gray-50">
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 min-w-[55ch] max-w-[55ch] truncate">
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
Progress
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-50">
{% if this.getDownloads()|length > 0 %}
{% for download in this.getDownloads() %}
<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 min-w-[45ch] max-w-[45ch] truncate">
{{ download.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="w-4 inline-block text-center text-gray-50">{{ download.progress }}</span>
</span>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-gray-800 dark:text-stone-800" colspan="2">
No active downloads
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,23 @@
<li {{ attributes }} id="alert_{{ alert_id }}" class="alert p-4 text-green-800 border border-green-300 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400 dark:border-green-800" role="alert">
<div class="flex items-center">
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<h3 class="text-lg font-medium">{{ title|default('') }}</h3>
</div>
<div class="mt-2 text-sm">
{{ message }}
</div>
{# <div class="flex">#}
{# <button type="button" class="text-white bg-green-800 hover:bg-green-900 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-xs px-3 py-1.5 me-2 text-center inline-flex items-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800">#}
{# <svg class="me-2 h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 14">#}
{# <path d="M10 0C4.612 0 0 5.336 0 7c0 1.742 3.546 7 10 7 6.454 0 10-5.258 10-7 0-1.664-4.612-7-10-7Zm0 10a3 3 0 1 1 0-6 3 3 0 0 1 0 6Z"/>#}
{# </svg>#}
{# View more#}
{# </button>#}
{# <button type="button" class="text-green-800 bg-transparent border border-green-800 hover:bg-green-900 hover:text-white focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-xs px-3 py-1.5 text-center dark:hover:bg-green-600 dark:border-green-600 dark:text-green-400 dark:hover:text-white dark:focus:ring-green-800" data-dismiss-target="#alert-additional-content-3" aria-label="Close">#}
{# Dismiss#}
{# </button>#}
{# </div>#}
</li>

View File

@@ -12,4 +12,10 @@
</div>
</div>
</div>
<div {{ turbo_stream_listen('alerts') }} class="absolute top-10 right-10 size-96">
<div >
<ul id="alert_list">
</ul>
</div>
</div>
</header>

View File

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

View File

@@ -1,64 +1,17 @@
{% extends 'base.html.twig' %}
{% block title %}Dashboard &mdash - Torsearch{% endblock %}
{% block title %}Dashboard &mdash; Torsearch{% endblock %}
{% block body %}
<div class="p-4 flex flex-col grow gap-4">
<h2 class="mb-2 text-3xl font-bold text-gray-50">Dashboard</h2>
<div class="flex flex-row gap-4">
<twig:Card title="Active Downloads" class="w-full">
<table class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-gray-50">
<thead>
<tr class="dark:bg-gray-50">
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800">
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
Progress
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-50">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
The Wolf of Wallstreet
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="text-gray-50">11</span>
</span>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
Inception
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="text-gray-50">36</span>
</span>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
Hop
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1.5 bg-purple-600 rounded-full">
<span class="text-gray-50">0</span>
</span>
</td>
</tr>
</tbody>
</table>
<twig:ActiveDownloadList />
</twig:Card>
<twig:Card title="Recent Downloads" class="w-full">
<table class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-gray-50">
<table id="recent_downloads" class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-gray-50 table-fixed">
<thead>
<tr class="dark:bg-gray-50">
<th scope="col"
@@ -66,56 +19,65 @@
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-end text-gray-500 uppercase dark:text-stone-800">
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
Status
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-50">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
The Family Plan
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1 bg-green-600 rounded-lg">
<span class="text-gray-50">Complete</span>
</span>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
It
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1 bg-green-600 rounded-lg">
<span class="text-gray-50">Complete</span>
</span>
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
Silicon Cowboys
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1 bg-green-600 rounded-lg">
<span class="text-gray-50">Complete</span>
</span>
</td>
</tr>
{% if recent_downloads|length > 0 %}
{% for download in recent_downloads %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800">
{{ download.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
<span class="p-1 bg-green-600 rounded-lg">
<span class="text-gray-50">Complete</span>
</span>
</td>
</tr>
{% endfor %}
<tr class="bg-blue-400">
<td class="px-6 py-3 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-rose-400 dark:text-stone-800" colspan="2">
<a href="#">View all downloads</a>
</td>
</tr>
{% else %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-gray-800 dark:text-stone-800" colspan="2">
No recent downloads
</td>
</tr>
{% endif %}
</tbody>
</table>
</twig:Card>
</div>
<div class="">
<div class="flex flex-col gap-4">
<twig:Card title="Popular Movies" contentClass="flex flex-row justify-between w-full">
<twig:Poster imdbId="" title="A Working Man" description="" image="https://image.tmdb.org/t/p/w500/xUkUZ8eOnrOnnJAfusZUqKYZiDu.jpg" year="" />
<twig:Poster imdbId="" title="In the Lost Lands" description="" image="https://image.tmdb.org/t/p/w500/iHf6bXPghWB6gT8kFkL1zo00x6X.jpg" year="" />
<twig:Poster imdbId="" title="A Minecraft Movie" description="" image="https://image.tmdb.org/t/p/w500/yFHHfHcUgGAxziP1C3lLt0q2T4s.jpg" year="" />
<twig:Poster imdbId="" title="G20" description="" image="https://image.tmdb.org/t/p/w500/wv6oWAleCJZUk5htrGg413t3GCy.jpg" year="" />
<twig:Poster imdbId="" title="Novocaine" description="" image="https://image.tmdb.org/t/p/w500/xmMHGz9dVRaMY6rRAlEX4W0Wdhm.jpg" year="" />
<twig:Poster imdbId="" title="Gunslingers" description="" image="https://image.tmdb.org/t/p/w500/O7REXWPANWXvX2jhQydHjAq2DV.jpg" year="" />
{% for movie in popular_movies %}
<twig:Poster imdbId=""
tmdbId="{{ movie.tmdbId }}"
title="{{ movie.title }}"
description="{{ movie.description }}"
image="{{ movie.poster }}"
year="{{ movie.year }}"
mediaType="movies"
/>
{% endfor %}
</twig:Card>
<twig:Card title="Popular TV Shows" contentClass="flex flex-row justify-between w-full">
{% for movie in popular_tvshows %}
<twig:Poster imdbId=""
tmdbId="{{ movie.tmdbId }}"
title="{{ movie.title }}"
description="{{ movie.description }}"
image="{{ movie.poster }}"
year="{{ movie.year }}"
mediaType="tvshows"
/>
{% endfor %}
</twig:Card>
</div>
</div>

View File

@@ -1,63 +1,84 @@
<div id="filter" class="w-full p-4 flex flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg"
<div id="filter" class="flex flex-col gap-4"
{{ stimulus_controller('result_filter') }}
{{ stimulus_action('result_filter', 'filter', 'change') }}
data-result-filter-media-type-value="{{ results.media.mediaType }}"
data-result-filter-movie-results-outlet=".results"
data-result-filter-tv-results-outlet=".results"
>
<label for="resolution">
Resolution
<select id="resolution" data-result-filter-target="resolution" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">
<option {{ filter.resolution == "n/a" ? "selected" }}
value="">n/a</option>
<option {{ filter.resolution == "720p" ? "selected" }}
value="720p">720p</option>
<option {{ filter.resolution == "1080p" ? "selected" }}
value="1080p">1080p</option>
<option {{ filter.resolution == "2160p" ? "selected" }}
value="2160p">2160p</option>
</select>
</label>
<label for="codec">
Codec
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-sm">
<option {{ filter.codec == "n/a" ? "selected" }}
value="">n/a</option>
<option {{ filter.codec == "-" ? "selected" }}
value="-">-</option>
<option {{ filter.codec == "h264" ? "selected" }}
value="h264">h264</option>
<option {{ filter.codec == "h265" ? "selected" }}
value="h265">h265/HEVC</option>
</select>
</label>
<label for="language">
Language
<select id="language" data-result-filter-target="language" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">
<option selected value="{{ filter.language }}">{{ filter.language }}</option>
</select>
</label>
<label for="provider">
Provider
<select id="provider" data-result-filter-target="provider" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">
<option selected value="">n/a</option>
</select>
</label>
{% if results.media.mediaType == "tvshows" %}
<label for="season">
Season
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">
<option selected value="1">1</option>
{% for season in range(2, results.media.episodes|length) %}
<option value="{{ season }}">{{ season }}</option>
{% endfor %}
<div class="w-full p-4 flex flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
<label for="resolution">
Resolution
<select id="resolution" data-result-filter-target="resolution" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md">
<option {{ filter.resolution == "n/a" ? "selected" }}
value="">n/a</option>
<option {{ filter.resolution == "720p" ? "selected" }}
value="720p">720p</option>
<option {{ filter.resolution == "1080p" ? "selected" }}
value="1080p">1080p</option>
<option {{ filter.resolution == "2160p" ? "selected" }}
value="2160p">2160p</option>
</select>
</label>
{# <label for="episodeNumber">#}
{# Episode#}
{# <select id="episodeNumber" name="episodeNumber" data-result-filter-target="episode" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">#}
{# <option selected value="">n/a</option>#}
{# </select>#}
{# </label>#}
<label for="codec">
Codec
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
<option {{ filter.codec == "n/a" ? "selected" }}
value="">n/a</option>
<option {{ filter.codec == "-" ? "selected" }}
value="-">-</option>
<option {{ filter.codec == "h264" ? "selected" }}
value="h264">h264</option>
<option {{ filter.codec == "h265" ? "selected" }}
value="h265">h265/HEVC</option>
</select>
</label>
<label for="language">
Language
<select id="language" data-result-filter-target="language" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md">
<option selected value="{{ filter.language }}">{{ filter.language }}</option>
</select>
</label>
<label for="provider">
Provider
<select id="provider" data-result-filter-target="provider" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md">
<option selected value="">n/a</option>
</select>
</label>
{% if results.media.mediaType == "tvshows" %}
<label for="season">
Season
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}>
<option selected value="1">1</option>
{% for season in range(2, results.media.episodes|length) %}
<option value="{{ season }}">{{ season }}</option>
{% endfor %}
</select>
</label>
{# <label for="episodeNumber">#}
{# Episode#}
{# <select id="episodeNumber" name="episodeNumber" data-result-filter-target="episode" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">#}
{# <option selected value="">n/a</option>#}
{# </select>#}
{# </label>#}
{% endif %}
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
class="loading-icon"
>
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
</span>
</div>
{% if results.media.mediaType == "tvshows" %}
<div class="flex flex-row gap-2 justify-end px-8">
<button class="px-1.5 py-1 bg-green-600 rounded-md text-sm"
{{ stimulus_target('result_filter', 'downloadSelected') }}
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
>Download Selected</button>
<input type="checkbox" name="selectAll" id="selectAll"
{{ stimulus_target('result_filter', 'selectAll') }}
{{ stimulus_action('result_filter', 'selectAllEpisodes', 'change') }}
/>
</div>
{% endif %}
</div>

View File

@@ -17,11 +17,6 @@
{{ results.media.description }}
</p>
</div>
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
class="loading-icon"
>
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" />
</span>
</div>
{{ include('search/partial/filter.html.twig') }}

View File

@@ -10,11 +10,16 @@
><span {{ stimulus_target('tv-results', 'count') }}>{{ results.results|length }}</span> results</small>
</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">
<path fill="currentColor" d="m16 10l10 10l-1.4 1.4l-8.6-8.6l-8.6 8.6L6 20z"/>
</svg>
<div class="flex flex-col gap-4 justify-between">
<input type="checkbox"
{{ stimulus_target('tv-results', 'episodeSelector') }}
/>
<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">
<path fill="currentColor" d="m16 10l10 10l-1.4 1.4l-8.6-8.6l-8.6 8.6L6 20z"/>
</svg>
</div>
</div>
</div>
<div class="inline-block overflow-hidden rounded-lg">