Compare commits
10 Commits
e5d5fe4343
...
a196fac6c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a196fac6c6 | ||
|
|
1ebf8b99b3 | ||
|
|
f0af17024e | ||
|
|
b764116552 | ||
|
|
5e715a40c6 | ||
|
|
cb57824861 | ||
|
|
dcc47fcb65 | ||
|
|
be171b45b4 | ||
|
|
b38ef63395 | ||
|
|
5c35aff23b |
13
.env
13
.env
@@ -26,16 +26,6 @@ APP_SHARE_DIR=var/share
|
||||
DEFAULT_URI=http://localhost
|
||||
###< symfony/routing ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
@@ -46,6 +36,3 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
TMDB_API_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkZmE5NDZmMTZmMTcyYmNlMzk0MzZiZmVhZDc2ZTk3NCIsIm5iZiI6MTY0OTE4MjMyNS43NTAwMDAyLCJzdWIiOiI2MjRjODY3NWFmNThjYjAwNTE1NzZiYmEiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.KE68nNxPGYWr5WHVaUuILMOH3sPhiAc9CucPVTgRPpM
|
||||
TMDB_HOST=https://api.themoviedb.org/3
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,3 +18,5 @@
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
/.idea/
|
||||
|
||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="app@localhost" uuid="587aeb55-4ca6-47d6-8bc5-b430816b19dd">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/app</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/ltbxd-actorle.iml
generated
1
.idea/ltbxd-actorle.iml
generated
@@ -129,6 +129,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/var/cache" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
#actors td {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-pwd}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
|
||||
@@ -17,6 +17,8 @@ services:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
ports:
|
||||
- "0.0.0.0:5432:5432"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: pdo_pgsql
|
||||
host: '%postgres_host%'
|
||||
port: '%postgres_port%'
|
||||
dbname: '%postgres_db%'
|
||||
user: '%postgres_user%'
|
||||
password: '%postgres_password%'
|
||||
server_version: '%postgres_version%'
|
||||
charset: utf8
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
@@ -22,12 +29,9 @@ doctrine:
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
dql:
|
||||
numeric_functions:
|
||||
Random: App\Doctrine\Extension\Random
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
|
||||
11
config/parameters.yml
Normal file
11
config/parameters.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
parameters:
|
||||
postgres_version: "16"
|
||||
postgres_host: "127.0.0.1"
|
||||
postgres_port: "5432"
|
||||
postgres_db: "app"
|
||||
postgres_user: "app"
|
||||
postgres_password: "pwd"
|
||||
|
||||
tmdb_host: "https://api.themoviedb.org/3"
|
||||
|
||||
ltbxd_watched_file: "%kernel.project_dir%/public/files/ltbxd/watched.csv"
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
imports:
|
||||
- { resource: parameters.yml }
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
||||
34
migrations/Version20260113232805.php
Normal file
34
migrations/Version20260113232805.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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 Version20260113232805 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('CREATE TABLE movie (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, tmdb_id INT NOT NULL, ltbxd_ref VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE movie');
|
||||
$this->addSql('DROP TABLE messenger_messages');
|
||||
}
|
||||
}
|
||||
39
migrations/Version20260115201004.php
Normal file
39
migrations/Version20260115201004.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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 Version20260115201004 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('CREATE TABLE actor (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, popularity DOUBLE PRECISION DEFAULT NULL, tmdb_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE TABLE movie_role (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, character VARCHAR(255) NOT NULL, actor_id INT DEFAULT NULL, movie_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE INDEX IDX_A40FAB6710DAF24A ON movie_role (actor_id)');
|
||||
$this->addSql('CREATE INDEX IDX_A40FAB678F93B6FC ON movie_role (movie_id)');
|
||||
$this->addSql('ALTER TABLE movie_role ADD CONSTRAINT FK_A40FAB6710DAF24A FOREIGN KEY (actor_id) REFERENCES actor (id)');
|
||||
$this->addSql('ALTER TABLE movie_role ADD CONSTRAINT FK_A40FAB678F93B6FC FOREIGN KEY (movie_id) REFERENCES movie (id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE movie_role DROP CONSTRAINT FK_A40FAB6710DAF24A');
|
||||
$this->addSql('ALTER TABLE movie_role DROP CONSTRAINT FK_A40FAB678F93B6FC');
|
||||
$this->addSql('DROP TABLE actor');
|
||||
$this->addSql('DROP TABLE movie_role');
|
||||
}
|
||||
}
|
||||
65
src/Command/SyncActorsCommand.php
Normal file
65
src/Command/SyncActorsCommand.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Movie;
|
||||
use App\Entity\MovieRole;
|
||||
use App\Exception\GatewayException;
|
||||
use App\Gateway\TMDBGateway;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand('app:sync-actors')]
|
||||
readonly class SyncActorsCommand
|
||||
{
|
||||
public function __construct (
|
||||
private TMDBGateway $TMDBGateway,
|
||||
private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function __invoke(OutputInterface $output): int
|
||||
{
|
||||
foreach ($this->em->getRepository(Movie::class)->findAll() as $film) {
|
||||
try {
|
||||
$creditsContext = $this->TMDBGateway->getMovieCredits($film->getTmdbId());
|
||||
} catch (GatewayException $e) {
|
||||
$output->writeln('/!\ '.$e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($creditsContext->cast)) {
|
||||
$output->writeln('Syncing cast for '.$film->getTitle());
|
||||
}
|
||||
foreach ($creditsContext->cast as $actorModel) {
|
||||
// Get existing or create new
|
||||
$actor = $this->em->getRepository(Actor::class)->findOneBy(['tmdbId' => $actorModel->id]);
|
||||
if (!$actor instanceof Actor) {
|
||||
$output->writeln('* New actor found: '.$actorModel->name);
|
||||
|
||||
$actor = new Actor()
|
||||
->setPopularity($actorModel->popularity)
|
||||
->setName($actorModel->name)
|
||||
->setTmdbId($actorModel->id)
|
||||
;
|
||||
|
||||
$this->em->persist($actor);
|
||||
}
|
||||
|
||||
// Get or create the role
|
||||
if (0 < $this->em->getRepository(MovieRole::class)->count(['actor' => $actor, 'movie' => $film])) {
|
||||
$actor->addMovieRole(new MovieRole()
|
||||
->setMovie($film)
|
||||
->setCharacter($actorModel->character)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
72
src/Command/SyncFilmsCommands.php
Normal file
72
src/Command/SyncFilmsCommands.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Movie;
|
||||
use App\Exception\GatewayException;
|
||||
use App\Gateway\LtbxdGateway;
|
||||
use App\Gateway\TMDBGateway;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand('app:sync-films')]
|
||||
readonly class SyncFilmsCommands
|
||||
{
|
||||
public function __construct(
|
||||
private LtbxdGateway $ltbxdGateway,
|
||||
private TMDBGateway $TMDBGateway,
|
||||
private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function __invoke(OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$ltbxdMovies = $this->ltbxdGateway->parseFile();
|
||||
} catch (GatewayException $e) {
|
||||
$output->writeln('/!\ '.$e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
foreach ($ltbxdMovies as $ltbxdMovie) {
|
||||
// If the movie already exists, skip
|
||||
if (0 < $this->em->getRepository(Movie::class)->count(['ltbxdRef' => $ltbxdMovie->getLtbxdRef()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Search movie on TMDB
|
||||
try {
|
||||
$film = $this->TMDBGateway->searchMovie($ltbxdMovie->getName());
|
||||
} catch (GatewayException $e) {
|
||||
$output->writeln('/!\ '.$e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if ($film) {
|
||||
$output->writeln('* Found '.$ltbxdMovie->getName());
|
||||
|
||||
$filmEntity = new Movie()
|
||||
->setLtbxdRef($ltbxdMovie->getLtbxdRef())
|
||||
->setTitle($ltbxdMovie->getName())
|
||||
->setTmdbId($film->getId())
|
||||
;
|
||||
$this->em->persist($filmEntity);
|
||||
}
|
||||
|
||||
++$i;
|
||||
if (0 === $i % 50) {
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$output->writeln('Films synced');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
13
src/Context/TMDB/MovieCreditsContext.php
Normal file
13
src/Context/TMDB/MovieCreditsContext.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Context\TMDB;
|
||||
|
||||
use App\Model\TMDB\TMDBMovieCredit;
|
||||
|
||||
class MovieCreditsContext
|
||||
{
|
||||
public function __construct(
|
||||
/** @var TMDBMovieCredit[] */
|
||||
public array $cast { get => $this->cast; },
|
||||
) {}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Gateway\TMDBGateway;
|
||||
use App\Model\Ltbxd\LtbxdMovie;
|
||||
use App\Repository\ActorRepository;
|
||||
use App\Repository\MovieRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
@@ -15,23 +15,60 @@ use Symfony\Component\Serializer\SerializerInterface;
|
||||
class HomepageController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TMDBGateway $TMDBGateway, private readonly SerializerInterface $serializer,
|
||||
private readonly ActorRepository $actorRepository
|
||||
) {}
|
||||
|
||||
#[Route('/')]
|
||||
public function index(SerializerInterface $serializer): Response
|
||||
{
|
||||
$file = file_get_contents('files/watched.csv');
|
||||
$ltbxdMovies = $this->serializer->deserialize($file, LtbxdMovie::class.'[]', 'csv');
|
||||
/** @var LtbxdMovie $ltbxdMovie */
|
||||
$films = [];
|
||||
foreach ($ltbxdMovies as $ltbxdMovie) {
|
||||
// Search movie on TMDB
|
||||
$searchResult = $this->TMDBGateway->searchMovie($ltbxdMovie->getName());
|
||||
$films[] = $searchResult->getResults()[0];
|
||||
}
|
||||
dd($films);
|
||||
// Final actor to be guessed
|
||||
$mainActor = $this->actorRepository->findOneRandom(4);
|
||||
|
||||
return $this->render('homepage/index.html.twig');
|
||||
// Actors for the grid
|
||||
$actors = [];
|
||||
$leftSize = 0;
|
||||
$rightSize = 0;
|
||||
foreach (str_split(strtolower($mainActor->getName())) as $char) {
|
||||
if (!preg_match('/[a-z]/', $char)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tryFindActor = 0;
|
||||
do {
|
||||
$actor = $this->actorRepository->findOneRandom(4, $char);
|
||||
++$tryFindActor;
|
||||
} while (
|
||||
$actor === $mainActor
|
||||
|| in_array($actor, array_map(fn ($actorMap) => $actorMap['actor'], $actors))
|
||||
|| $tryFindActor < 5
|
||||
);
|
||||
|
||||
$actorData = [
|
||||
'actor' => $actor,
|
||||
'pos' => strpos($actor->getName(), $char),
|
||||
];
|
||||
|
||||
if ($leftSize < $actorData['pos']) {
|
||||
$leftSize = $actorData['pos'];
|
||||
}
|
||||
|
||||
$rightSizeActor = strlen($actor->getName()) - $actorData['pos'] - 1;
|
||||
if ($rightSize < $rightSizeActor) {
|
||||
$rightSize = $rightSizeActor;
|
||||
}
|
||||
|
||||
$actors[] = $actorData;
|
||||
}
|
||||
|
||||
// Predict grid size
|
||||
$width = $rightSize + $leftSize + 1;
|
||||
$middle = $leftSize;
|
||||
|
||||
return $this->render('homepage/index.html.twig', [
|
||||
'mainActor' => $mainActor,
|
||||
'actors' => $actors,
|
||||
'width' => $width,
|
||||
'middle' => $middle,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
27
src/Doctrine/Extension/Random.php
Normal file
27
src/Doctrine/Extension/Random.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Doctrine\Extension;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
class Random extends FunctionNode
|
||||
{
|
||||
/**
|
||||
* @throws QueryException
|
||||
*/
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
return 'RANDOM()';
|
||||
}
|
||||
}
|
||||
108
src/Entity/Actor.php
Normal file
108
src/Entity/Actor.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ActorRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ActorRepository::class)]
|
||||
class Actor
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?float $popularity = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MovieRole>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: MovieRole::class, mappedBy: 'actor')]
|
||||
private Collection $movieRoles;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $tmdbId = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->movieRoles = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPopularity(): ?float
|
||||
{
|
||||
return $this->popularity;
|
||||
}
|
||||
|
||||
public function setPopularity(?float $popularity): static
|
||||
{
|
||||
$this->popularity = $popularity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MovieRole>
|
||||
*/
|
||||
public function getMovieRoles(): Collection
|
||||
{
|
||||
return $this->movieRoles;
|
||||
}
|
||||
|
||||
public function addMovieRole(MovieRole $movieRole): static
|
||||
{
|
||||
if (!$this->movieRoles->contains($movieRole)) {
|
||||
$this->movieRoles->add($movieRole);
|
||||
$movieRole->setActor($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMovieRole(MovieRole $movieRole): static
|
||||
{
|
||||
if ($this->movieRoles->removeElement($movieRole)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($movieRole->getActor() === $this) {
|
||||
$movieRole->setActor(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTmdbId(): ?int
|
||||
{
|
||||
return $this->tmdbId;
|
||||
}
|
||||
|
||||
public function setTmdbId(?int $tmdbId): static
|
||||
{
|
||||
$this->tmdbId = $tmdbId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
108
src/Entity/Movie.php
Normal file
108
src/Entity/Movie.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\MovieRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MovieRepository::class)]
|
||||
class Movie
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $tmdbId = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $ltbxdRef = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MovieRole>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: MovieRole::class, mappedBy: 'movie')]
|
||||
private Collection $actors;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->actors = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTmdbId(): ?int
|
||||
{
|
||||
return $this->tmdbId;
|
||||
}
|
||||
|
||||
public function setTmdbId(int $tmdbId): static
|
||||
{
|
||||
$this->tmdbId = $tmdbId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLtbxdRef(): ?string
|
||||
{
|
||||
return $this->ltbxdRef;
|
||||
}
|
||||
|
||||
public function setLtbxdRef(string $ltbxdRef): static
|
||||
{
|
||||
$this->ltbxdRef = $ltbxdRef;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MovieRole>
|
||||
*/
|
||||
public function getActors(): Collection
|
||||
{
|
||||
return $this->actors;
|
||||
}
|
||||
|
||||
public function addActor(MovieRole $actor): static
|
||||
{
|
||||
if (!$this->actors->contains($actor)) {
|
||||
$this->actors->add($actor);
|
||||
$actor->setMovie($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeActor(MovieRole $actor): static
|
||||
{
|
||||
if ($this->actors->removeElement($actor)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($actor->getMovie() === $this) {
|
||||
$actor->setMovie(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
65
src/Entity/MovieRole.php
Normal file
65
src/Entity/MovieRole.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\MovieRoleRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MovieRoleRepository::class)]
|
||||
class MovieRole
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $character = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'movieRoles')]
|
||||
private ?Actor $actor = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'actors')]
|
||||
private ?Movie $movie = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCharacter(): ?string
|
||||
{
|
||||
return $this->character;
|
||||
}
|
||||
|
||||
public function setCharacter(string $character): static
|
||||
{
|
||||
$this->character = $character;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActor(): ?Actor
|
||||
{
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
public function setActor(?Actor $actor): static
|
||||
{
|
||||
$this->actor = $actor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMovie(): ?Movie
|
||||
{
|
||||
return $this->movie;
|
||||
}
|
||||
|
||||
public function setMovie(?Movie $movie): static
|
||||
{
|
||||
$this->movie = $movie;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
15
src/Exception/GatewayException.php
Normal file
15
src/Exception/GatewayException.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class GatewayException extends \Exception
|
||||
{
|
||||
public function __construct(
|
||||
public string $gateway { get => $this->gateway; },
|
||||
string $message = '',
|
||||
?\Throwable $previous = null,
|
||||
)
|
||||
{
|
||||
parent::__construct($message, previous: $previous);
|
||||
}
|
||||
}
|
||||
37
src/Gateway/LtbxdGateway.php
Normal file
37
src/Gateway/LtbxdGateway.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\Exception\GatewayException;
|
||||
use App\Model\Ltbxd\LtbxdMovie;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
readonly class LtbxdGateway
|
||||
{
|
||||
public function __construct(
|
||||
private SerializerInterface $serializer,
|
||||
#[Autowire('%ltbxd_watched_file%')]
|
||||
private string $fileDir,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return LtbxdMovie[]
|
||||
* @throws GatewayException
|
||||
*/
|
||||
public function parseFile(): array
|
||||
{
|
||||
if (!file_exists($this->fileDir)) {
|
||||
throw new GatewayException(sprintf('Could not find file %s', $this->fileDir));
|
||||
}
|
||||
|
||||
$fileContent = file_get_contents($this->fileDir);
|
||||
|
||||
try {
|
||||
return $this->serializer->deserialize($fileContent, LtbxdMovie::class.'[]', 'csv');
|
||||
} catch (ExceptionInterface $e) {
|
||||
throw new GatewayException('Error while deserializing Letterboxd data', previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,47 +2,75 @@
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\Context\TMDB\ActorCreditsContext;
|
||||
use App\Context\TMDB\MovieCreditsContext;
|
||||
use App\Context\TMDB\MovieSearchContext;
|
||||
use App\Exception\GatewayException;
|
||||
use App\Model\TMDB\TMDBMovie;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use function Symfony\Component\String\u;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class TMDBGateway
|
||||
readonly class TMDBGateway
|
||||
{
|
||||
private const string SEARCH_URI = '/search/movie';
|
||||
private const string MOVIE_CREDITS_URI = '/movie/{id}/credits';
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly CacheInterface $cache,
|
||||
private HttpClientInterface $client,
|
||||
private SerializerInterface $serializer,
|
||||
#[Autowire('%env(TMDB_API_TOKEN)%')]
|
||||
private readonly string $apiToken,
|
||||
#[Autowire('%env(TMDB_HOST)%')]
|
||||
private readonly string $host,
|
||||
private string $apiToken,
|
||||
#[Autowire('%tmdb_host%')]
|
||||
private string $host,
|
||||
) {
|
||||
}
|
||||
|
||||
public function searchMovie(string $movieName): ?MovieSearchContext
|
||||
/**
|
||||
* @throws GatewayException
|
||||
*/
|
||||
public function searchMovie(string $movieName): ?TMDBMovie
|
||||
{
|
||||
$cacheKey = 'tmdb_movie.'.u($movieName)->snake();
|
||||
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($movieName) {
|
||||
$url = $this->host.self::SEARCH_URI.'?'.http_build_query(['query' => $movieName]);
|
||||
try {
|
||||
$response = $this->client->request('GET', $url, ['headers' => $this->getHeaders()]);
|
||||
$result = $response->getContent();
|
||||
return $this->serializer->deserialize($result, MovieSearchContext::class, 'json');
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
$url = $this->host.self::SEARCH_URI.'?'.http_build_query(['query' => $movieName]);
|
||||
$searchContext = $this->fetchSerialized('GET', $url, MovieSearchContext::class);
|
||||
if (empty($searchResult = $searchContext->getResults())) {
|
||||
return null;
|
||||
}
|
||||
return reset($searchResult);
|
||||
}
|
||||
|
||||
private function getHeaders(): array
|
||||
/**
|
||||
* @throws GatewayException
|
||||
*/
|
||||
public function getMovieCredits(int $movieId): ?MovieCreditsContext
|
||||
{
|
||||
return ['Authorization' => 'Bearer '.$this->apiToken, 'accept' => 'application/json'];
|
||||
$url = $this->host.str_replace('{id}', $movieId, self::MOVIE_CREDITS_URI);
|
||||
return $this->fetchSerialized('GET', $url, MovieCreditsContext::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GatewayException
|
||||
*/
|
||||
private function fetch(string $method, string $url): ResponseInterface
|
||||
{
|
||||
try {
|
||||
return $this->client->request($method, $url, ['headers' => ['Authorization' => 'Bearer '.$this->apiToken, 'accept' => 'application/json']]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new GatewayException(self::class, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GatewayException
|
||||
*/
|
||||
private function fetchSerialized(string $method, string $url, string $class, string $type = 'json'): mixed
|
||||
{
|
||||
$result = $this->fetch($method, $url);
|
||||
try {
|
||||
return $this->serializer->deserialize($result->getContent(), $class, $type);
|
||||
} catch (\Throwable $e) {
|
||||
throw new GatewayException(self::class, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,9 @@ class LtbxdMovie
|
||||
{
|
||||
return $this->ltbxdUri;
|
||||
}
|
||||
|
||||
public function getLtbxdRef(): string
|
||||
{
|
||||
return basename($this->ltbxdUri);
|
||||
}
|
||||
}
|
||||
|
||||
13
src/Model/TMDB/TMDBMovieCredit.php
Normal file
13
src/Model/TMDB/TMDBMovieCredit.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model\TMDB;
|
||||
|
||||
class TMDBMovieCredit
|
||||
{
|
||||
public function __construct(
|
||||
public int $id { get => $this->id; },
|
||||
public string $name { get => $this->name; },
|
||||
public float $popularity { get => $this->popularity; },
|
||||
public string $character { get => $this->character; },
|
||||
) {}
|
||||
}
|
||||
41
src/Repository/ActorRepository.php
Normal file
41
src/Repository/ActorRepository.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Actor;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Actor>
|
||||
*/
|
||||
class ActorRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Actor::class);
|
||||
}
|
||||
|
||||
public function findOneRandom(?float $popularity = null, ?string $char = null): Actor
|
||||
{
|
||||
$qb = $this->createQueryBuilder('o');
|
||||
$expr = $qb->expr();
|
||||
|
||||
if (!empty($popularity)) {
|
||||
$qb->andWhere($expr->gte('o.popularity', ':popularity'))
|
||||
->setParameter('popularity', $popularity);
|
||||
}
|
||||
|
||||
if (!empty($char)) {
|
||||
$qb->andWhere($expr->like('o.name', ':name'))
|
||||
->setParameter('name', '%'.$char.'%');
|
||||
}
|
||||
|
||||
return $qb
|
||||
->orderBy('RANDOM()')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
43
src/Repository/MovieRepository.php
Normal file
43
src/Repository/MovieRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Movie;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Movie>
|
||||
*/
|
||||
class MovieRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Movie::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Movie[] Returns an array of Movie objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('m.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Movie
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
43
src/Repository/MovieRoleRepository.php
Normal file
43
src/Repository/MovieRoleRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\MovieRole;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<MovieRole>
|
||||
*/
|
||||
class MovieRoleRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, MovieRole::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return MovieRole[] Returns an array of MovieRole objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('m.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?MovieRole
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
21
src/Twig/AppExtension.php
Normal file
21
src/Twig/AppExtension.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class AppExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('match', [$this, 'match']),
|
||||
];
|
||||
}
|
||||
|
||||
public function match(string $string, string $pattern): bool
|
||||
{
|
||||
return preg_match($pattern, $string);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,26 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<table id="actors">
|
||||
{% set iActor = 0 %}
|
||||
{% for mainChar in mainActor.name|split('') %}
|
||||
{% if not mainChar|match('/[a-zA-Z]/') %}
|
||||
<tr><td></td></tr>
|
||||
{% else %}
|
||||
{% set actor = actors[iActor] %}
|
||||
<tr>
|
||||
{% set i = 0 %}
|
||||
{% set start = middle - actor.pos %}
|
||||
{% for c in range(0, width) %}
|
||||
{% if c >= start and c - start < actor.actor.name|length %}
|
||||
<td {% if c - start == actor.pos %}style="color:red;"{% endif %}>{{ actor.actor.name|slice(c - start, 1)|upper }}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% set iActor = iActor + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user