acquire() ) { return false; } $status = Settings::STATUS_ERROR; try { $status = $this->execute( $trigger ); } finally { $lock->release(); Settings::record_run( $status ); } return true; } /** * Der eigentliche Lauf innerhalb des Locks. * * @param string $trigger Ausloeser des Laufs. * @return string Einer der Settings::STATUS_-Werte. */ private function execute( string $trigger ): string { $repository = new JobRepository(); $jobs = array_values( array_filter( $repository->all(), static function ( Job $job ): bool { return AivorClient::is_uuid( $job->aivor_job_id ); } ) ); $manual = self::TRIGGER_MANUAL === $trigger; if ( array() === $jobs ) { if ( $manual ) { EventLog::add( EventLog::TYPE_RUN, __( 'Nothing to do: no content job is set up yet.', 'aivor-content-import' ) ); } return Settings::STATUS_SKIPPED; } if ( '' === Settings::api_key() ) { if ( $manual ) { EventLog::add( EventLog::TYPE_RUN, __( 'Nothing to do: no Aivor API key is set.', 'aivor-content-import' ), array( 'level' => EventLog::LEVEL_WARNING ) ); } return Settings::STATUS_SKIPPED; } $client = AivorClient::from_settings(); $writer = new PostWriter(); $budget = new TimeBudget(); $back_channel = new BackChannel( $client, new PatchApplier( $writer ), $budget ); $importer = new Importer( $client, $writer, $back_channel, $repository ); // Schritte 1 und 2 sind best effort und duerfen die Abholung nie // verhindern: Wiederholte Abbrueche wuerden genau die Artikel // aufhalten, auf die der Betreiber wartet. try { $back_channel->report_pending(); $back_channel->process_sites( $jobs ); SeoAdapters::migrate(); } catch ( \Throwable $error ) { EventLog::add( EventLog::TYPE_RUN, sprintf( /* translators: %s: error message */ __( 'Reporting to Aivor was interrupted: %s', 'aivor-content-import' ), $error->getMessage() ), array( 'level' => EventLog::LEVEL_ERROR ) ); } $result = $this->import_jobs( $jobs, $importer, $repository, $budget ); $counts = $back_channel->counts(); $worked = $result['imported'] > 0 || $result['handled'] > 0 || $counts['reported'] > 0 || $counts['patched'] > 0; $failed = $result['errors'] > 0 || $counts['errors'] > 0; if ( $result['more'] ) { self::schedule_follow_up(); } // Ein Lauf von Hand bekommt immer eine Rueckmeldung — aber die // Wochengrenze nicht doppelt, wenn sie gerade schon protokolliert wurde. if ( $manual && ! $worked && ! $failed && ! $result['limit_logged'] ) { EventLog::add( EventLog::TYPE_RUN, $result['limited'] ? self::limit_message() : __( 'Nothing to do: Aivor has no new articles for your content jobs right now.', 'aivor-content-import' ) ); } if ( $failed ) { return Settings::STATUS_ERROR; } return $worked ? Settings::STATUS_OK : Settings::STATUS_SKIPPED; } /** * Holt je Job hoechstens einen Artikel ab. * * Die Jobs werden reihum begonnen, damit bei knappem Zeitbudget nicht * immer derselbe Job zuerst drankommt. * * @param Job[] $jobs Die Jobs. * @param Importer $importer Importer. * @param JobRepository $repository Job-Ablage. * @param TimeBudget $budget Zeitbudget. * @return array{imported: int, handled: int, errors: int, more: bool, limited: bool, limit_logged: bool} */ private function import_jobs( array $jobs, Importer $importer, JobRepository $repository, TimeBudget $budget ): array { $result = array( 'imported' => 0, 'handled' => 0, 'errors' => 0, 'more' => false, 'limited' => false, 'limit_logged' => false, ); $count = count( $jobs ); $offset = (int) Settings::get( 'next_job_offset', 0 ) % $count; Settings::update( array( 'next_job_offset' => $offset + 1 ) ); for ( $index = 0; $index < $count; $index++ ) { $job = $jobs[ ( $offset + $index ) % $count ]; // Ein Abruf kann bis zu 30 Sekunden dauern, ein Bild ebenso. Ohne // genug Reserve lieber in der naechsten Runde. if ( $budget->remaining() < 15 ) { $result['more'] = true; break; } if ( self::weekly_limit_reached() ) { $result['limited'] = true; $result['limit_logged'] = self::log_limit_once_a_day(); break; } try { $outcome = $importer->import_next( $job ); } catch ( ApiException $error ) { ++$result['errors']; $this->store_error( $repository, $job, $error->getMessage() ); continue; } catch ( \Throwable $error ) { ++$result['errors']; $this->store_error( $repository, $job, $error->getMessage() ); continue; } if ( Importer::NOTHING === $outcome ) { if ( '' !== $job->error ) { $repository->update_fields( $job->id, array( 'error' => '' ) ); } continue; } // Eine erweiterte Fassung ist eine Lieferung, aber kein neuer Artikel: // Sie zaehlt hier mit, gegen die Wochengrenze nicht (kein IMPORTED_TIME). if ( Importer::IMPORTED === $outcome || Importer::EXTENDED === $outcome ) { ++$result['imported']; } if ( Importer::REDELIVERED === $outcome || Importer::DISCARDED === $outcome ) { ++$result['handled']; } // Wer etwas geliefert hat, hat vielleicht noch mehr. $result['more'] = true; } return $result; } /** * Haelt einen Fehler am Job fest und protokolliert ihn. * * @param JobRepository $repository Job-Ablage. * @param Job $job Der Job. * @param string $message Meldung. * @return void */ private function store_error( JobRepository $repository, Job $job, string $message ): void { $repository->update_fields( $job->id, array( 'error' => mb_substr( $message, 0, 500 ) ) ); EventLog::add( EventLog::TYPE_API_ERROR, $message, array( 'level' => EventLog::LEVEL_ERROR, 'job_id' => $job->id, ) ); } /** * Ob die Wochengrenze erreicht ist. * * @return bool */ public static function weekly_limit_reached(): bool { return DraftMapping::count_imported_since( time() - WEEK_IN_SECONDS ) >= Settings::weekly_limit(); } /** * Meldung zur erreichten Wochengrenze. * * @return string */ public static function limit_message(): string { $oldest = DraftMapping::oldest_import_since( time() - WEEK_IN_SECONDS ); $next = $oldest > 0 ? $oldest + WEEK_IN_SECONDS : time(); return sprintf( /* translators: 1: weekly limit, 2: date and time */ __( 'The weekly limit of %1$d new articles is reached. Aivor keeps the next articles ready; the plugin fetches them again from %2$s.', 'aivor-content-import' ), Settings::weekly_limit(), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next ) ); } /** * Protokolliert die Wochengrenze hoechstens einmal am Tag. * * @return bool Ob jetzt protokolliert wurde. */ private static function log_limit_once_a_day(): bool { if ( time() - (int) Settings::get( 'limit_logged_at', 0 ) < DAY_IN_SECONDS ) { return false; } Settings::update( array( 'limit_logged_at' => time() ) ); EventLog::add( EventLog::TYPE_RUN, self::limit_message() ); return true; } /** * Plant einen zusaetzlichen Lauf, damit wartende Artikel nicht eine Stunde warten. * * @return void */ private static function schedule_follow_up(): void { $args = array( self::TRIGGER_FOLLOW_UP ); if ( ! wp_next_scheduled( AIVOR_CONTENT_CRON_HOOK, $args ) ) { wp_schedule_single_event( time() + self::FOLLOW_UP_DELAY, AIVOR_CONTENT_CRON_HOOK, $args ); } } }