Instagram repost job
Some checks failed
Push image to registry / build-image (push) Failing after 6m44s
Some checks failed
Push image to registry / build-image (push) Failing after 6m44s
This commit is contained in:
54
app/Browser/Jobs/InstagramRepost/IInstagramVideo.php
Normal file
54
app/Browser/Jobs/InstagramRepost/IInstagramVideo.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\InstagramRepost;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Interface for Instagram video handling.
|
||||
* Holds the data of a video from Instagram.
|
||||
*/
|
||||
interface IInstagramVideo
|
||||
{
|
||||
/**
|
||||
* Get the URL of the video.
|
||||
*
|
||||
* @return string The URL of the video.
|
||||
*/
|
||||
public function getUrl(): string;
|
||||
|
||||
/**
|
||||
* Get the title of the video.
|
||||
*
|
||||
* @return string The title of the video.
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Get the caption of the video.
|
||||
*
|
||||
* @return string The caption of the video.
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Get the date when the video was posted.
|
||||
*
|
||||
* @return ?DateTimeImmutable The date of the post.
|
||||
*/
|
||||
public function getPostDate(): ?DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Get the filename of the video.
|
||||
*
|
||||
* @return string The filename of the video.
|
||||
*/
|
||||
public function getFilename(): string;
|
||||
|
||||
/**
|
||||
* Set the filename of the video.
|
||||
*
|
||||
* @param string $filename The filename to set.
|
||||
*/
|
||||
public function setFilename(string $filename): void;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\InstagramRepost;
|
||||
|
||||
use App\Models\JobRun;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface IInstagramVideoDownloader
|
||||
{
|
||||
/**
|
||||
* Download the video from the given Instagram post URL.
|
||||
*
|
||||
* @param int $jobId The ID of the job for which the video is being downloaded.
|
||||
* @param JobRun $jobRun The job run instance for logging and tracking.
|
||||
* @param string $postUrl The URL of the Instagram post.
|
||||
* @param Collection $jobInfos The job information collection.
|
||||
* @param string $downloadFolder The folder where the video should be downloaded.
|
||||
* @param string $accountEmail The email or username of the Instagram account.
|
||||
* @param string $accountPassword The password for the Instagram account.
|
||||
* @return IInstagramVideo|null The path to the downloaded video file, or null if the download failed.
|
||||
*/
|
||||
public function downloadVideo(int $jobId, JobRun $jobRun, string $postUrl, string $downloadFolder, string $accountEmail, string $accountPassword): ?IInstagramVideo;
|
||||
}
|
406
app/Browser/Jobs/InstagramRepost/InstagramRepostJob.php
Normal file
406
app/Browser/Jobs/InstagramRepost/InstagramRepostJob.php
Normal file
@ -0,0 +1,406 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\InstagramRepost;
|
||||
|
||||
use App\Browser\BrowserJob;
|
||||
use App\Browser\JobDebugScreenshot;
|
||||
use App\Models\InstagramRepost;
|
||||
use App\Models\InstagramAccount;
|
||||
use App\Models\Job;
|
||||
use App\Models\JobRun;
|
||||
use App\Notification\Notifications\JobDebugNotification;
|
||||
use App\Notification\Providers\AllNotification;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// === CONFIGURATION ===
|
||||
// TODO : put that in laravel config file
|
||||
/**
|
||||
* Maximum number of posts to repost per account
|
||||
* @var int
|
||||
*/
|
||||
private const MAX_REPOSTS_PER_ACCOUNT = 2;
|
||||
|
||||
/**
|
||||
* Max number of reposts per job
|
||||
* This is the maximum number of posts that will be reposted in a single job run.
|
||||
* @var int
|
||||
*/
|
||||
private const MAX_REPOSTS_PER_JOB = 3;
|
||||
|
||||
/**
|
||||
* Maximum number of tries to repost a reel
|
||||
* If a reel fails to be reposted, it will be retried up to this number of times.
|
||||
* @var int
|
||||
*/
|
||||
private const MAX_REPOST_TRIES = 3;
|
||||
|
||||
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
||||
|
||||
private Collection $jobInfos;
|
||||
protected JobRun $jobRun;
|
||||
|
||||
protected IInstagramVideoDownloader $videoDownloader;
|
||||
|
||||
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
|
||||
|
||||
public function __construct($jobId = 4)
|
||||
{
|
||||
parent::__construct($jobId);
|
||||
|
||||
$this->downloadFolder = base_path($this->downloadFolder);
|
||||
$this->videoDownloader = new YTDLPDownloader();
|
||||
}
|
||||
|
||||
public function run(Browser $browser): ?JobRun
|
||||
{
|
||||
Log::info("Running InstagramRepostJob");
|
||||
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
|
||||
$this->jobRun = new JobRun([
|
||||
"job_id" => $this->jobId,
|
||||
"success" => false,
|
||||
]);
|
||||
$this->jobRun->save();
|
||||
|
||||
$browser->visit('https://instagram.com');
|
||||
sleep(5);
|
||||
$this->removePopups($browser);
|
||||
sleep(2);
|
||||
$this->signin($browser);
|
||||
sleep(2);
|
||||
$this->repostLatestPosts($browser);
|
||||
sleep(5);
|
||||
|
||||
$this->jobRun->success = true;
|
||||
$this->jobRun->save();
|
||||
|
||||
Log::info("InstagramRepostJob run ended");
|
||||
|
||||
return $this->jobRun;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function runTest(Browser $browser): ?JobRun
|
||||
{
|
||||
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
|
||||
try {
|
||||
$browser->visit('https://instagram.com');
|
||||
sleep(2);
|
||||
$this->removePopups($browser);
|
||||
sleep(2);
|
||||
$this->signin($browser);
|
||||
sleep(3);
|
||||
return $this->makeSimpleJobRun(
|
||||
true,
|
||||
"Connexion réussie",
|
||||
"Datboi a réussi à se connecter sur Instagram"
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return $this->makeSimpleJobRun(
|
||||
false,
|
||||
"Connexion échouée",
|
||||
"Datboi n'a pas réussi à se connecter sur Instagram :\n" . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function repostLatestPosts(Browser $browser) {
|
||||
|
||||
try {
|
||||
// Download the latest reels from the accounts specified in the job infos
|
||||
$toDownloadReels = []; // Array to store to download reels to post later
|
||||
|
||||
$accounts = explode(",", $this->jobInfos->get("instagram_repost_accounts"));
|
||||
foreach ($accounts as $account) {
|
||||
$account = trim($account);
|
||||
|
||||
$toDownloadReels = array_merge($toDownloadReels, $this->getLatestReelsFromAccount($browser, $account));
|
||||
}
|
||||
|
||||
// Add unreposted reels to the job run if not enough reels were downloaded
|
||||
if (count($toDownloadReels) < self::MAX_REPOSTS_PER_JOB) {
|
||||
$unrepostedReels = InstagramRepost::where("reposted", false)
|
||||
->where("repost_tries", "<", self::MAX_REPOST_TRIES) // Limit to 3 tries
|
||||
->whereIn("account_id", InstagramAccount::whereIn("username", $accounts)->pluck("id"))
|
||||
->take(self::MAX_REPOSTS_PER_JOB - count($toDownloadReels))
|
||||
->get();
|
||||
|
||||
foreach ($unrepostedReels as $reel) {
|
||||
$toDownloadReels[] = $reel;
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffling and keeping only the x first posts
|
||||
shuffle($toDownloadReels);
|
||||
$toDownloadReels = array_slice($toDownloadReels, 0, self::MAX_REPOSTS_PER_JOB);
|
||||
|
||||
// Download the reels
|
||||
$downloadedReels = [];
|
||||
foreach ($toDownloadReels as $repost) {
|
||||
$downloadedReels[] = [
|
||||
$repost,
|
||||
$this->downloadReel(
|
||||
$browser,
|
||||
$repost
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
// Now repost all downloaded reels
|
||||
foreach ($downloadedReels as $infos) {
|
||||
$reel = $infos[0];
|
||||
$videoInfo = $infos[1];
|
||||
|
||||
try {
|
||||
// TODO : Avoid getting the reel from the db again, store it in the downloadedReels array
|
||||
$this->repostReel($browser, InstagramRepost::where('reel_id', $reel->reel_id)->first(), $videoInfo);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to repost reel: {$videoInfo->getTitle()} - " . $e->getMessage());
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to repost reel: {$videoInfo->getTitle()} - " . $e->getMessage()));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
dump($e->getMessage());
|
||||
} finally {
|
||||
// Removes all videos in the download folder
|
||||
$files = glob($this->downloadFolder . '*'); // Get all files in the download folder
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file); // Delete the file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getLatestReelsFromAccount(Browser $browser, string $account): array
|
||||
{
|
||||
$accountReels = []; // Indexed array to store new reels from the account
|
||||
|
||||
$browser->visit("https://instagram.com/{$account}/reels");
|
||||
sleep(3);
|
||||
$browser->waitForText("followers", 10, true);
|
||||
|
||||
// If we are here, the account exists
|
||||
$accountModel = InstagramAccount::where("username", $account)->first();
|
||||
if ($accountModel == null) { // Does not exist in the database yet
|
||||
$accountModel = new InstagramAccount([
|
||||
"username" => $account,
|
||||
]);
|
||||
$accountModel->save();
|
||||
Log::info("New Instagram account added: {$account}");
|
||||
} else {
|
||||
Log::debug("Instagram account already exists: {$account}");
|
||||
}
|
||||
|
||||
$repostedPosts = $accountModel->reposts()->where("reposted", true)->pluck("reel_id");
|
||||
|
||||
// Posts must be sorted by latest post date first
|
||||
// TODO : Scroll when not enough post are shown
|
||||
$posts = $browser->driver->findElements(WebDriverBy::xpath('//a[contains(@href, "'.$account.'/reel/")][not(.//*[local-name() = "svg"][@aria-label="Pinned post icon"])]'));
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$postUrl = $post->getAttribute('href');
|
||||
$postId = explode("/", $postUrl)[3] ?? null;
|
||||
|
||||
if ($postId === null) {
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Can't get Instagram post ID from url"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break the loop if the post has already been reposted
|
||||
if ($repostedPosts->contains($postId)) {
|
||||
Log::debug("Post already reposted: {$postUrl}");
|
||||
break;
|
||||
}
|
||||
|
||||
$reelModel = InstagramRepost::firstOrCreate(
|
||||
["reel_id" => $postId, "account_id" => $accountModel->id],
|
||||
["reposted" => false, "repost_tries" => 0]
|
||||
);
|
||||
|
||||
if (count($accountReels) < self::MAX_REPOSTS_PER_ACCOUNT) {
|
||||
$accountReels[] = $reelModel; // Add it to the to be downloaded reels array
|
||||
}
|
||||
}
|
||||
|
||||
return $accountReels;
|
||||
}
|
||||
|
||||
private function downloadReel(Browser $browser, InstagramRepost $reel): ?IInstagramVideo
|
||||
{
|
||||
$videoInfo = $this->videoDownloader->downloadVideo(
|
||||
$this->jobId,
|
||||
$this->jobRun,
|
||||
$reel->getUrl(),
|
||||
$this->downloadFolder,
|
||||
$this->jobInfos->get("instagram_repost_account_email"),
|
||||
$this->jobInfos->get("instagram_repost_account_password")
|
||||
);
|
||||
|
||||
if ($videoInfo === null) {
|
||||
Log::error("Failed to download video for post: {$reel->reel_id}");
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
// Set the filename to the post ID
|
||||
$newFilename = $this->downloadFolder . $reel->reel_id . ".mp4";
|
||||
rename($videoInfo->getFilename(), $newFilename);
|
||||
$videoInfo->setFilename($newFilename);
|
||||
}
|
||||
|
||||
Log::info("Downloaded video: {$videoInfo->getTitle()} : {$videoInfo->getDescription()}");
|
||||
|
||||
return $videoInfo;
|
||||
}
|
||||
|
||||
protected function repostReel(Browser $browser, InstagramRepost $reel, IInstagramVideo $videoInfo)
|
||||
{
|
||||
Log::info("Reposting reel: {$reel->reel_id} - {$videoInfo->getTitle()}");
|
||||
|
||||
// Increment the repost tries
|
||||
$reel->repost_tries++;
|
||||
$reel->save();
|
||||
|
||||
// TODO Reset if a problem occurs and try again with a limit of 3 attempts
|
||||
$browser->visit('https://instagram.com');
|
||||
sleep(2);
|
||||
|
||||
// Navigate to the reel upload page
|
||||
$createButton = $browser->driver->findElement(WebDriverBy::xpath('//a[./div//span[contains(text(), "Create")]]'));
|
||||
$createButton->click();
|
||||
sleep(2);
|
||||
$newPostButton = $browser->driver->findElement(WebDriverBy::xpath('//a[./div//span[contains(text(), "Post")]][@href="#"]'));
|
||||
$newPostButton->click();
|
||||
sleep(3);
|
||||
|
||||
// Upload the video file
|
||||
$selectFileButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(text(), "Select from computer")]'));
|
||||
$selectFileButton->click();
|
||||
sleep(2);
|
||||
$browser->attach('input[type="file"]._ac69', $this->downloadFolder . $reel->reel_id . ".mp4");
|
||||
|
||||
sleep(5); // TODO : Wait for the file to be uploaded
|
||||
|
||||
$this->removePopups($browser);
|
||||
sleep(2);
|
||||
|
||||
// Put original resolution
|
||||
$this->putOriginalResolution($browser);
|
||||
|
||||
$this->clickNext($browser);
|
||||
$this->clickNext($browser); // Skip cover photo and trim
|
||||
|
||||
// Add a caption
|
||||
$captionInput = $browser->driver->findElement(WebDriverBy::xpath('//div[@contenteditable]'));
|
||||
$captionInput->sendKeys($videoInfo->getDescription());
|
||||
|
||||
sleep(2); // Wait for the caption to be added
|
||||
|
||||
$this->clickNext($browser); // Share the post
|
||||
|
||||
sleep(5); // Wait for the post to be completed
|
||||
|
||||
// Check if the post was successful
|
||||
try {
|
||||
$browser->waitForText("Your reel has been shared.", 60, true);
|
||||
Log::info("Reel reposted successfully: {$reel->reel_id}");
|
||||
|
||||
// Mark the reel as reposted in the database
|
||||
$reel->reposted = true;
|
||||
$reel->save();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
try {
|
||||
$browser->waitForText("Your post was shared", 60, true);
|
||||
$closeButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/*[local-name() = "svg"][@aria-label="Close"]]'));
|
||||
$closeButton->click();
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
}
|
||||
Log::error("Failed to repost reel: {$reel->reel_id} - " . $e->getMessage());
|
||||
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to repost reel: {$reel->reel_id} - " . $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private function clickNext(Browser $browser) {
|
||||
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
|
||||
$nextButton->click();
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
private function putOriginalResolution(Browser $browser)
|
||||
{
|
||||
try {
|
||||
$chooseResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//button[./div/*[local-name() = "svg"][@aria-label="Select crop"]]'));
|
||||
$chooseResolutionButton->click();
|
||||
sleep(2);
|
||||
// Choos "original" resolution
|
||||
$originalResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/div/span[contains(text(), "Original")]]'));
|
||||
$originalResolutionButton->click();
|
||||
sleep(2);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to set original resolution: " . $e->getMessage());
|
||||
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to set \"original\" resolution: " . $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
protected function signin(Browser $browser)
|
||||
{
|
||||
try {
|
||||
$browser->waitForText("Log in", 10, true);
|
||||
sleep(3);
|
||||
$emailButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "email")]'));
|
||||
$emailButton->click();
|
||||
$emailButton->sendKeys($this->jobInfos->get("instagram_repost_account_email"));
|
||||
sleep(3);
|
||||
$passwordButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "Password")]'));
|
||||
$passwordButton->click();
|
||||
$passwordButton->sendKeys($this->jobInfos->get("instagram_repost_account_password") . "\n");
|
||||
} catch (\Exception $e) {
|
||||
// Probably no need to signin
|
||||
}
|
||||
}
|
||||
|
||||
protected function removePopups(Browser $browser)
|
||||
{
|
||||
try {
|
||||
$cookiesAllowButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(text(), "Allow all cookies")]'));
|
||||
$cookiesAllowButton->click();
|
||||
sleep(2);
|
||||
} catch (\Exception $e) {
|
||||
// No cookie popup found, continue
|
||||
}
|
||||
|
||||
try {
|
||||
$notNowButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(text(), "Not Now")]'));
|
||||
$notNowButton->click();
|
||||
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Popup Not Now clicked"));
|
||||
sleep(2);
|
||||
} catch (\Exception $e) {
|
||||
// No "Not Now" popup found, continue
|
||||
}
|
||||
|
||||
try {
|
||||
$okButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(text(), "OK")]'));
|
||||
$okButton->click();
|
||||
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||
AllNotification::send(new JobDebugNotification($this->jobId, "Popup Ok clicked"));
|
||||
sleep(2);
|
||||
} catch (\Exception $e) {
|
||||
// No "Ok" popup found, continue
|
||||
}
|
||||
|
||||
// $browser->script('document.querySelector("div.app-modal")[0].remove();');
|
||||
// $browser->driver->executeScript('document.querySelector("div.app-modal")[0].remove();');
|
||||
}
|
||||
}
|
55
app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php
Normal file
55
app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\InstagramRepost;
|
||||
|
||||
use App\Models\JobArtifact;
|
||||
use App\Models\JobRun;
|
||||
use App\Notification\Notifications\JobDebugNotification;
|
||||
use App\Notification\Providers\AllNotification;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use YoutubeDl\Options;
|
||||
use YoutubeDl\YoutubeDl;
|
||||
|
||||
class YTDLPDownloader implements IInstagramVideoDownloader
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function downloadVideo(int $jobId, JobRun $jobRun, string $postUrl, string $downloadFolder, string $accountEmail, string $accountPassword): ?YTDLPVideo
|
||||
{
|
||||
$dl = new YoutubeDl();
|
||||
$options = Options::create()
|
||||
->downloadPath($downloadFolder)
|
||||
->apLogin($accountEmail, $accountPassword)
|
||||
->checkAllFormats(true)
|
||||
->format('bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]')
|
||||
->url($postUrl);
|
||||
|
||||
try {
|
||||
$videosCollection = $dl->download($options);
|
||||
foreach ($videosCollection->getVideos() as $video) {
|
||||
if ($video->getError() !== null) {
|
||||
$jobRun->addArtifact(new JobArtifact([
|
||||
"name" => "Erreur lors du téléchargement de la vidéo \"{$video->getTitle()}\"",
|
||||
"content" => $video->getError(),
|
||||
]));
|
||||
Log::error("Error downloading video: " . $video->getError());
|
||||
return null; // Return null if there was an error downloading the video
|
||||
} else {
|
||||
$IVideo = new YTDLPVideo(
|
||||
$video->getWebpageUrl(),
|
||||
$video->getTitle(),
|
||||
$video->getDescription() ?? "",
|
||||
$video->getUploadDate(),
|
||||
$video->getFilename(),
|
||||
);
|
||||
|
||||
return $IVideo; // Return the video object if download was successful
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
AllNotification::send(new JobDebugNotification($jobId, "Error while downloading video: " . $e->getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
71
app/Browser/Jobs/InstagramRepost/YTDLPVideo.php
Normal file
71
app/Browser/Jobs/InstagramRepost/YTDLPVideo.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\InstagramRepost;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class YTDLPVideo implements IInstagramVideo
|
||||
{
|
||||
private string $url;
|
||||
private string $title;
|
||||
private string $description;
|
||||
private ?DateTimeImmutable $postDate;
|
||||
private string $fileName;
|
||||
|
||||
public function __construct(string $url, string $title, string $description, ?DateTimeImmutable $postDate, string $filename)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
$this->postDate = $postDate;
|
||||
$this->fileName = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getPostDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->postDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setFilename(string $filename): void
|
||||
{
|
||||
$this->fileName = $filename;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user