diff --git a/Dockerfile b/Dockerfile index 2c6163b..ee077cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,7 @@ RUN apk update && apk add --no-cache \ openssl \ linux-headers \ supervisor \ + yt-dlp \ && rm -rf /tmp/* /var/cache/apk/* RUN docker-php-ext-configure zip && docker-php-ext-install zip diff --git a/app/Browser/Jobs/InstagramRepost/IInstagramVideo.php b/app/Browser/Jobs/InstagramRepost/IInstagramVideo.php new file mode 100644 index 0000000..e8ca0c4 --- /dev/null +++ b/app/Browser/Jobs/InstagramRepost/IInstagramVideo.php @@ -0,0 +1,54 @@ +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();'); + } +} diff --git a/app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php b/app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php new file mode 100644 index 0000000..b0e7402 --- /dev/null +++ b/app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php @@ -0,0 +1,55 @@ +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; + } + } +} diff --git a/app/Browser/Jobs/InstagramRepost/YTDLPVideo.php b/app/Browser/Jobs/InstagramRepost/YTDLPVideo.php new file mode 100644 index 0000000..4bfe31e --- /dev/null +++ b/app/Browser/Jobs/InstagramRepost/YTDLPVideo.php @@ -0,0 +1,71 @@ +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; + } +} diff --git a/app/Models/InstagramAccount.php b/app/Models/InstagramAccount.php new file mode 100644 index 0000000..a947436 --- /dev/null +++ b/app/Models/InstagramAccount.php @@ -0,0 +1,19 @@ +hasMany(InstagramRepost::class, 'account_id'); + } +} diff --git a/app/Models/InstagramRepost.php b/app/Models/InstagramRepost.php new file mode 100644 index 0000000..cca9bd3 --- /dev/null +++ b/app/Models/InstagramRepost.php @@ -0,0 +1,26 @@ +belongsTo(InstagramAccount::class, 'account_id'); + } + + public function getUrl(): string + { + return "https://www.instagram.com/reel/{$this->reel_id}/"; + } +} diff --git a/composer.json b/composer.json index 72d3138..cffebf7 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ ], "license": "MIT", "require": { - "php": "^8.2", + "php": "^8.3", "erusev/parsedown": "^1.7", "inertiajs/inertia-laravel": "^2.0", "laravel/dusk": "^8.2", @@ -18,6 +18,7 @@ "laravel/sanctum": "^4.0", "laravel/telescope": "^5.5", "laravel/tinker": "^2.9", + "norkunas/youtube-dl-php": "dev-master", "tightenco/ziggy": "^2.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 93583df..8b27c8c 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "6008577001548e6e63c074be98000d97", + "content-hash": "9a964008040d9ce219547515fe65dd86", "packages": [ { "name": "brick/math", @@ -3011,6 +3011,69 @@ }, "time": "2024-12-30T11:07:19+00:00" }, + { + "name": "norkunas/youtube-dl-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/norkunas/youtube-dl-php.git", + "reference": "4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/norkunas/youtube-dl-php/zipball/4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a", + "reference": "4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4.0", + "symfony/filesystem": "^5.1|^6.0|^7.0", + "symfony/polyfill-php80": "^1.28", + "symfony/process": "^5.1|^6.0|^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6.11", + "php-cs-fixer/shim": "^3.60", + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "symfony/phpunit-bridge": "^6.4.10" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "YoutubeDl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tomas Norkūnas", + "email": "norkunas.tom@gmail.com" + } + ], + "description": "youtube-dl / yt-dlp wrapper for php", + "keywords": [ + "youtube", + "youtube-dl", + "yt-dlp" + ], + "support": { + "issues": "https://github.com/norkunas/youtube-dl-php/issues", + "source": "https://github.com/norkunas/youtube-dl-php/tree/v2.10.0" + }, + "time": "2025-02-20T17:32:37+00:00" + }, { "name": "nunomaduro/termwind", "version": "v2.3.0", @@ -5211,6 +5274,72 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.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-10-25T15:15:23+00:00" + }, { "name": "symfony/finder", "version": "v7.2.2", @@ -9591,11 +9720,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "norkunas/youtube-dl-php": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.3" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/database/migrations/2025_05_29_145435_add_instagram_repost_job.php b/database/migrations/2025_05_29_145435_add_instagram_repost_job.php new file mode 100644 index 0000000..043de32 --- /dev/null +++ b/database/migrations/2025_05_29_145435_add_instagram_repost_job.php @@ -0,0 +1,114 @@ + $newJobId, + "name" => "Instagram Repost", + "description" => "Reposte les publications Instagram des comptes donnés.", + ]); + + JobInfo::forceCreate([ + "key" => "instagram_repost_accounts", + "name" => "Comptes Instagram à reposter", + "description" => "Liste des noms des comptes Instagram à partir desquels les publications seront repostées.\nSéparez les comptes par des virgules.", + "placeholder" => "is.it.ninluc, freddiedredd", + "is_required" => true, + "job_info_type_id" => 1, + "job_id" => $newJobId, + ]); + + JobInfo::forceCreate([ + "key" => "instagram_repost_account_email", + "name" => "Identifiant", + "description" => "L'adresse e-mail/nom d'utilisateur/N° de téléphone utilisée pour le compte Instagram de repost.", + "is_required" => true, + "job_info_type_id" => 1, + "job_id" => $newJobId, + ]); + + JobInfo::forceCreate([ + "key" => "instagram_repost_account_password", + "name" => "Mot de passe", + "description" => "Le mot de passe utilisée pour le compte Instagram de repost.", + "is_required" => true, + "job_info_type_id" => 3, + "job_id" => $newJobId, + ]); + + Schema::create('instagram_repost_accounts', function (Blueprint $table) { + $table->id(); + + $table->string("username")->unique(); + + $table->timestamps(); + }); + + Schema::create('instagram_reposts', function (Blueprint $table) { + $table->id(); + + $table->string("reel_id")->unique(); + $table->boolean("reposted")->default(false); + $table->integer("repost_tries")->default(0); + $table->foreignIdFor(InstagramAccount::class, "account_id") + ->constrained('instagram_repost_accounts') + ->cascadeOnDelete(); + + $table->timestamps(); + }); + + // Already reposted posts + $notDeadLmaoAccount = InstagramAccount::forceCreate([ + "username" => "notdeadlmao69", + ]); + $negusflexAccount = InstagramAccount::forceCreate([ + "username" => "negusflex", + ]); + InstagramRepost::forceCreate([ + "reel_id" => "DKbW7M_RWV7", + "reposted" => true, + "account_id" => $notDeadLmaoAccount->id, + ]); + InstagramRepost::forceCreate([ + "reel_id" => "DKccuTMTmP_", + "reposted" => true, + "account_id" => $negusflexAccount->id, + ]); + InstagramRepost::forceCreate([ + "reel_id" => "DJmUjhWSnqm", + "reposted" => true, + "account_id" => $negusflexAccount->id, + ]); + InstagramRepost::forceCreate([ + "reel_id" => "DKcdSGnv6uq", + "reposted" => true, + "account_id" => $negusflexAccount->id, + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Job::where("id", 4)->delete(); + JobInfo::where("job_id", 4)->delete(); + + Schema::dropIfExists('instagram_repost_accounts'); + Schema::dropIfExists('instagram_reposts'); + } +}; diff --git a/routes/console.php b/routes/console.php index f9caefa..80f4aef 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,6 +2,7 @@ use App\Browser\Jobs\Hellcase\HellcaseJob; use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob; +use App\Browser\Jobs\InstagramRepost\InstagramRepostJob; use App\Jobs\PruneOldJobRuns; use App\Services\BrowserJobsInstances; use Illuminate\Foundation\Inspiring; @@ -23,3 +24,4 @@ Schedule::job(new PruneOldJobRuns)->monthly()->onOneServer()->withoutOverlapping Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job'); // Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job'); Schedule::job(new HellcaseBattlesJob)->hourly()->onOneServer()->withoutOverlapping()->name('hellcase_battles')->description('Hellcase battles job'); +Schedule::job(new InstagramRepostJob())->everyThreeHours()->onOneServer()->withoutOverlapping()->name('instagram_reposts')->description('Intagrame reposts job');