diff --git a/.idea/ltbxd-actorle.iml b/.idea/ltbxd-actorle.iml
index 0a60166..681bda2 100644
--- a/.idea/ltbxd-actorle.iml
+++ b/.idea/ltbxd-actorle.iml
@@ -129,6 +129,7 @@
+
diff --git a/migrations/Version20260115201004.php b/migrations/Version20260115201004.php
new file mode 100644
index 0000000..86860ad
--- /dev/null
+++ b/migrations/Version20260115201004.php
@@ -0,0 +1,39 @@
+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');
+ }
+}
diff --git a/src/Command/SyncDataCommand.php b/src/Command/SyncDataCommand.php
new file mode 100644
index 0000000..41a257b
--- /dev/null
+++ b/src/Command/SyncDataCommand.php
@@ -0,0 +1,135 @@
+syncMovies($output);
+ } catch (\Exception $e) {
+ $output->writeln('/!\ '.$e->getMessage());
+ $output->writeln('/!\ '.$e->getPrevious()->getMessage());
+
+ return Command::FAILURE;
+ }
+
+ if (!$skipFilmsSync) {
+ $this->syncActors($output, $films);
+ }
+
+ // awards, quotes, complete roles
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * @return Movie[]
+ * @throws \Exception
+ */
+ private function syncMovies(OutputInterface $output): array
+ {
+ $file = file_get_contents('public/files/watched.csv');
+ try {
+ $ltbxdMovies = $this->serializer->deserialize($file, LtbxdMovie::class.'[]', 'csv');
+ } catch (ExceptionInterface $e) {
+ throw new \Exception('Error while deserializing Letterboxd data', previous: $e);
+ }
+
+ $films = [];
+ /** @var LtbxdMovie $ltbxdMovie */
+ foreach ($ltbxdMovies as $ltbxdMovie) {
+ // If the movie already exists, skip
+ if (($film = $this->em->getRepository(Movie::class)->findOneBy(['ltbxdRef' => $ltbxdMovie->getLtbxdRef()])) instanceof Movie) {
+ $films[] = $film;
+ continue;
+ }
+
+ // Search movie on TMDB
+ $film = $this->TMDBGateway->searchMovie($ltbxdMovie->getName());
+ if ($film) {
+ $output->writeln('* Found '.$ltbxdMovie->getName());
+
+ $filmEntity = new Movie()
+ ->setLtbxdRef($ltbxdMovie->getLtbxdRef())
+ ->setTitle($ltbxdMovie->getName())
+ ->setTmdbId($film->getId())
+ ;
+ $this->em->persist($filmEntity);
+ $films[] = $film;
+ }
+
+ if (0 === \count($films) % 50) {
+ $this->em->flush();
+ }
+ }
+
+ $this->em->flush();
+
+ return $films;
+ }
+
+ private function syncActors(OutputInterface $output, array $films): void
+ {
+ foreach ($films as $film) {
+ try {
+ $creditsContext = $this->TMDBGateway->getMovieCredits($film->getTmdbId());
+ } catch (GatewayException $e) {
+ $output->writeln('/!\ '.$e->getMessage());
+ continue;
+ }
+
+ 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();
+ }
+ }
+}
diff --git a/src/Command/SyncMoviesCommand.php b/src/Command/SyncMoviesCommand.php
deleted file mode 100644
index ec7accf..0000000
--- a/src/Command/SyncMoviesCommand.php
+++ /dev/null
@@ -1,63 +0,0 @@
-serializer->deserialize($file, LtbxdMovie::class.'[]', 'csv');
- } catch (ExceptionInterface $e) {
- $output->writeln($e->getMessage());
- return Command::FAILURE;
- }
-
- $i = 0;
- /** @var LtbxdMovie $ltbxdMovie */
- 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
- $film = $this->TMDBGateway->searchMovie($ltbxdMovie->getName());
- 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();
- }
- }
-
- return Command::SUCCESS;
- }
-}
diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php
index e2feca5..4a3f72c 100644
--- a/src/Entity/Actor.php
+++ b/src/Entity/Actor.php
@@ -27,6 +27,9 @@ class Actor
#[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();
@@ -90,4 +93,16 @@ class Actor
return $this;
}
+
+ public function getTmdbId(): ?int
+ {
+ return $this->tmdbId;
+ }
+
+ public function setTmdbId(?int $tmdbId): static
+ {
+ $this->tmdbId = $tmdbId;
+
+ return $this;
+ }
}