From 24c56345df1f1b35b44630b2bc2a0494533773ab Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Sun, 3 Mar 2024 16:21:52 -0600 Subject: [PATCH] Set up sync task for podcast playlists. --- config/events.php | 1 + src/Entity/PodcastEpisode.php | 3 + .../Repository/PodcastEpisodeRepository.php | 7 + src/Sync/Task/CheckPodcastPlaylistsTask.php | 164 ++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 src/Sync/Task/CheckPodcastPlaylistsTask.php diff --git a/config/events.php b/config/events.php index 9f7d2b970..b55b9d8aa 100644 --- a/config/events.php +++ b/config/events.php @@ -114,6 +114,7 @@ return static function (CallableEventDispatcherInterface $dispatcher) { $e->addTasks([ App\Sync\Task\CheckFolderPlaylistsTask::class, App\Sync\Task\CheckMediaTask::class, + App\Sync\Task\CheckPodcastPlaylistsTask::class, App\Sync\Task\CheckRequestsTask::class, App\Sync\Task\CheckUpdatesTask::class, App\Sync\Task\CleanupHistoryTask::class, diff --git a/src/Entity/PodcastEpisode.php b/src/Entity/PodcastEpisode.php index d24e990d9..20a1c92b5 100644 --- a/src/Entity/PodcastEpisode.php +++ b/src/Entity/PodcastEpisode.php @@ -29,6 +29,9 @@ class PodcastEpisode implements IdentifiableEntityInterface #[ORM\JoinColumn(name: 'playlist_media_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] protected ?StationMedia $playlist_media = null; + #[ORM\Column(nullable: true, insertable: false, updatable: false)] + protected ?int $playlist_media_id = null; + #[ORM\OneToOne(mappedBy: 'episode')] protected ?PodcastMedia $media = null; diff --git a/src/Entity/Repository/PodcastEpisodeRepository.php b/src/Entity/Repository/PodcastEpisodeRepository.php index c1872db22..93b03313e 100644 --- a/src/Entity/Repository/PodcastEpisodeRepository.php +++ b/src/Entity/Repository/PodcastEpisodeRepository.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Entity\Repository; use App\Doctrine\Repository; +use App\Entity\Enums\PodcastSources; use App\Entity\Podcast; use App\Entity\PodcastEpisode; use App\Entity\PodcastMedia; @@ -17,6 +18,7 @@ use App\Media\MetadataManager; use InvalidArgumentException; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToRetrieveMetadata; +use LogicException; /** * @extends Repository @@ -124,6 +126,11 @@ final class PodcastEpisodeRepository extends Repository ?ExtendedFilesystemInterface $fs = null ): void { $podcast = $episode->getPodcast(); + + if ($podcast->getSource() !== PodcastSources::Manual) { + throw new LogicException('Cannot upload media to this podcast type.'); + } + $storageLocation = $podcast->getStorageLocation(); $fs ??= $this->storageLocationRepo->getAdapter($storageLocation) diff --git a/src/Sync/Task/CheckPodcastPlaylistsTask.php b/src/Sync/Task/CheckPodcastPlaylistsTask.php new file mode 100644 index 000000000..588a46426 --- /dev/null +++ b/src/Sync/Task/CheckPodcastPlaylistsTask.php @@ -0,0 +1,164 @@ +iterateStations() as $station) { + $this->syncPodcastPlaylists($station); + } + } + + public function syncPodcastPlaylists(Station $station): void + { + $this->logger->info( + 'Processing playlist-based podcasts for station...', + [ + 'station' => $station->getName(), + ] + ); + + $fsMedia = $this->stationFilesystems->getMediaFilesystem($station); + $fsPodcasts = $this->stationFilesystems->getPodcastsFilesystem($station); + + $podcasts = $this->em->createQuery( + <<<'DQL' + SELECT p, sp + FROM App\Entity\Podcast p + JOIN p.playlist sp + WHERE p.source = :source + DQL + )->setParameter('source', PodcastSources::Playlist->value) + ->execute(); + + $mediaInPlaylistQuery = $this->em->createQuery( + <<<'DQL' + SELECT spm.media_id + FROM App\Entity\StationPlaylistMedia spm + WHERE spm.playlist = :playlist + DQL + ); + + $mediaInPodcastQuery = $this->em->createQuery( + <<<'DQL' + SELECT pe.id, pe.playlist_media_id + FROM App\Entity\PodcastEpisode pe + WHERE pe.podcast = :podcast + DQL + ); + + $stats = [ + 'added' => 0, + 'removed' => 0, + 'unchanged' => 0, + ]; + + /** @var Podcast $podcast */ + foreach ($podcasts as $podcast) { + $playlist = $podcast->getPlaylist(); + + $mediaInPlaylist = array_column( + $mediaInPlaylistQuery->setParameter('playlist', $playlist)->getArrayResult(), + 'media_id', + 'media_id' + ); + + $mediaInPodcast = array_column( + $mediaInPodcastQuery->setParameter('podcast', $podcast)->getArrayResult(), + 'id', + 'playlist_media_id' + ); + + $mediaToAdd = []; + foreach ($mediaInPlaylist as $mediaId) { + if (isset($mediaInPodcast[$mediaId])) { + $stats['unchanged']++; + unset($mediaInPodcast[$mediaId]); + } else { + $mediaToAdd[] = $mediaId; + } + } + + foreach ($mediaToAdd as $mediaId) { + $media = $this->em->find(StationMedia::class, $mediaId); + + if ($media instanceof StationMedia) { + // Create new podcast episode. + $podcastEpisode = new PodcastEpisode($podcast); + $podcastEpisode->setPlaylistMedia($media); + + $podcastEpisode->setDescription( + implode("\n", array_filter([ + $media->getArtist(), + $media->getAlbum(), + $media->getLyrics(), + ])) + ); + $podcastEpisode->setTitle($media->getTitle() ?? 'Untitled Episode'); + + if (!$podcast->playlistAutoPublish()) { + // Set a date in the future to unpublish the episode. + $podcastEpisode->setPublishAt( + strtotime('+10 years') + ); + } else { + $podcastEpisode->setPublishAt($media->getMtime()); + } + + $this->em->persist($podcastEpisode); + $this->em->flush(); + + $artPath = StationMedia::getArtPath($media->getUniqueId()); + if ($fsMedia->fileExists($artPath)) { + $art = $fsMedia->read($artPath); + $this->podcastEpisodeRepo->writeEpisodeArt($podcastEpisode, $art); + } + + $stats['added']++; + } + } + + // Remove remaining media that doesn't match. + foreach ($mediaInPodcast as $episodeId) { + $episode = $this->em->find(PodcastEpisode::class, $episodeId); + + if ($episode instanceof PodcastEpisode) { + $this->podcastEpisodeRepo->delete($episode, $fsPodcasts); + } + + $stats['removed']++; + } + } + + $this->logger->debug( + 'Playlist-based podcasts for station processed.', + [ + 'station' => $station->getName(), + 'stats' => $stats, + ] + ); + } +}