326 lines
12 KiB
PHP
326 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Site;
|
|
use App\Models\Topic;
|
|
use App\Models\Article;
|
|
use App\Helpers\Logger;
|
|
|
|
class PublisherService
|
|
{
|
|
private TopicBalancer $topicBalancer;
|
|
private OpenAIService $openAI;
|
|
private InternalLinkService $internalLinkService;
|
|
private ImageService $imageService;
|
|
private WordPressService $wordpress;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->topicBalancer = new TopicBalancer();
|
|
$this->openAI = new OpenAIService();
|
|
$this->internalLinkService = new InternalLinkService();
|
|
$this->imageService = new ImageService();
|
|
$this->wordpress = new WordPressService();
|
|
}
|
|
|
|
public function publishNext(): array
|
|
{
|
|
Logger::info('Rozpoczynam automatyczna publikacje', 'publish');
|
|
|
|
$sites = Site::findDueForPublishing();
|
|
|
|
if (empty($sites)) {
|
|
Logger::info('Brak stron do publikacji', 'publish');
|
|
return ['success' => false, 'message' => 'Brak stron wymagajacych publikacji.'];
|
|
}
|
|
|
|
$site = $sites[0];
|
|
return $this->publishForSite($site);
|
|
}
|
|
|
|
public function publishForSite(array $site): array
|
|
{
|
|
Logger::info("Publikacja dla strony: {$site['name']} (ID: {$site['id']})", 'publish');
|
|
|
|
// 0. Najpierw uzupelnij obrazki w juz opublikowanych artykulach bez miniatury.
|
|
$publishedWithoutImage = Article::findNextPublishedWithoutImageBySite((int) $site['id']);
|
|
if ($publishedWithoutImage) {
|
|
return $this->attachMissingImageToPublishedArticle($site, $publishedWithoutImage);
|
|
}
|
|
|
|
// 1. Najpierw publikuj gotowe, nieopublikowane artykuly.
|
|
$retryArticle = Article::findNextRetryableBySite((int) $site['id']);
|
|
if ($retryArticle) {
|
|
$topic = Topic::find((int) $retryArticle['topic_id']);
|
|
if (!$topic) {
|
|
Logger::error("Nie znaleziono tematu dla artykulu ID {$retryArticle['id']}", 'publish');
|
|
return ['success' => false, 'message' => 'Nie znaleziono tematu dla oczekujacego artykulu.'];
|
|
}
|
|
|
|
Logger::info("Ponowna proba publikacji artykulu ID {$retryArticle['id']}: {$retryArticle['title']}", 'publish');
|
|
Article::markRetryAttempt((int) $retryArticle['id']);
|
|
|
|
return $this->publishPreparedArticle(
|
|
$site,
|
|
$topic,
|
|
[
|
|
'title' => (string) $retryArticle['title'],
|
|
'content' => (string) $retryArticle['content'],
|
|
'model' => $retryArticle['ai_model'] ?? null,
|
|
'prompt' => $retryArticle['prompt_used'] ?? null,
|
|
],
|
|
(int) $retryArticle['id']
|
|
);
|
|
}
|
|
|
|
// 2. Gdy brak zaleglych, generuj nowy artykul.
|
|
$topic = $this->topicBalancer->getNextTopic($site['id']);
|
|
if (!$topic) {
|
|
Logger::error("Brak aktywnych tematow dla strony {$site['name']}", 'publish');
|
|
return ['success' => false, 'message' => "Brak aktywnych tematow dla strony {$site['name']}."];
|
|
}
|
|
|
|
Logger::info("Wybrany temat: {$topic['name']} (ID: {$topic['id']})", 'publish');
|
|
|
|
$existingTitles = Article::getRecentTitlesByTopic((int) $topic['id'], 20);
|
|
|
|
$article = $this->openAI->generateArticle(
|
|
$topic['name'],
|
|
$topic['description'] ?? '',
|
|
$existingTitles
|
|
);
|
|
|
|
if (!$article) {
|
|
$this->saveFailedArticle($site, $topic, 'Nie udalo sie wygenerowac artykulu przez OpenAI.');
|
|
return ['success' => false, 'message' => 'Blad generowania artykulu przez AI.'];
|
|
}
|
|
|
|
Logger::info("Wygenerowano artykul: {$article['title']}", 'publish');
|
|
|
|
$article['title'] = $this->normalizeArticleTitle((string) ($article['title'] ?? ''), (string) $topic['name']);
|
|
|
|
return $this->publishPreparedArticle($site, $topic, $article);
|
|
}
|
|
|
|
private function publishPreparedArticle(array $site, array $topic, array $article, ?int $existingArticleId = null): array
|
|
{
|
|
$linkingResult = $this->internalLinkService->enrichContentWithInternalLinks(
|
|
$site,
|
|
(string) ($article['title'] ?? ''),
|
|
(string) ($article['content'] ?? '')
|
|
);
|
|
$article['content'] = (string) ($linkingResult['content'] ?? (string) ($article['content'] ?? ''));
|
|
Logger::info(
|
|
'Internal linking mode=' . (string) ($linkingResult['mode'] ?? 'unknown')
|
|
. ', links_added=' . (int) ($linkingResult['links_added'] ?? 0),
|
|
'publish'
|
|
);
|
|
|
|
$imageUrl = null;
|
|
$mediaId = null;
|
|
$image = $this->imageService->generate((string) $article['title'], (string) $topic['name']);
|
|
|
|
if ($image) {
|
|
$mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']);
|
|
if ($mediaId) {
|
|
$imageUrl = 'wp_media:' . $mediaId;
|
|
Logger::info("Upload obrazka: media_id={$mediaId}", 'publish');
|
|
}
|
|
} else {
|
|
Logger::warning('Nie udalo sie wygenerowac obrazka, publikacja bez obrazka', 'publish');
|
|
}
|
|
|
|
$wpPostId = $this->wordpress->createPost(
|
|
$site,
|
|
(string) $article['title'],
|
|
(string) $article['content'],
|
|
$topic['wp_category_id'],
|
|
$mediaId,
|
|
$this->buildExcerpt((string) $article['content'])
|
|
);
|
|
|
|
if (!$wpPostId) {
|
|
$this->saveFailedArticle(
|
|
$site,
|
|
$topic,
|
|
'Nie udalo sie opublikowac posta na WordPress.',
|
|
$article,
|
|
$existingArticleId
|
|
);
|
|
return ['success' => false, 'message' => 'Blad publikacji na WordPress.'];
|
|
}
|
|
|
|
Logger::info("Opublikowano post: wp_post_id={$wpPostId}", 'publish');
|
|
$wpPostUrl = $this->wordpress->getPostLink($site, (int) $wpPostId);
|
|
|
|
if ($existingArticleId !== null) {
|
|
Article::update($existingArticleId, [
|
|
'title' => (string) $article['title'],
|
|
'content' => (string) $article['content'],
|
|
'wp_post_id' => $wpPostId,
|
|
'wp_post_url' => $wpPostUrl,
|
|
'image_url' => $imageUrl,
|
|
'status' => 'published',
|
|
'ai_model' => $article['model'] ?? null,
|
|
'prompt_used' => $article['prompt'] ?? null,
|
|
'error_message' => null,
|
|
'published_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
} else {
|
|
Article::create([
|
|
'site_id' => $site['id'],
|
|
'topic_id' => $topic['id'],
|
|
'title' => (string) $article['title'],
|
|
'content' => (string) $article['content'],
|
|
'wp_post_id' => $wpPostId,
|
|
'wp_post_url' => $wpPostUrl,
|
|
'image_url' => $imageUrl,
|
|
'status' => 'published',
|
|
'ai_model' => $article['model'] ?? null,
|
|
'prompt_used' => $article['prompt'] ?? null,
|
|
'published_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
Topic::incrementArticleCount((int) $topic['id']);
|
|
Site::updateLastPublished((int) $site['id']);
|
|
|
|
$message = "Opublikowano: \"{$article['title']}\" na {$site['name']}";
|
|
Logger::info($message, 'publish');
|
|
|
|
return ['success' => true, 'message' => $message];
|
|
}
|
|
|
|
private function normalizeArticleTitle(string $title, string $topicName): string
|
|
{
|
|
$title = trim($title);
|
|
$topicName = trim($topicName);
|
|
|
|
if ($title === '' || $topicName === '') {
|
|
return $title;
|
|
}
|
|
|
|
// Remove "<topic name>: " / "<topic name> - " prefixes from generated titles.
|
|
$topicPattern = preg_quote($topicName, '/');
|
|
$normalized = preg_replace('/^' . $topicPattern . '\s*[:\-\x{2013}\x{2014}|]\s*/iu', '', $title);
|
|
$normalized = is_string($normalized) ? trim($normalized) : $title;
|
|
|
|
return $normalized !== '' ? $normalized : $title;
|
|
}
|
|
|
|
private function saveFailedArticle(
|
|
array $site,
|
|
array $topic,
|
|
string $error,
|
|
?array $article = null,
|
|
?int $existingArticleId = null
|
|
): void {
|
|
if ($existingArticleId !== null) {
|
|
Article::update($existingArticleId, [
|
|
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
|
'content' => $article['content'] ?? '',
|
|
'status' => 'failed',
|
|
'ai_model' => $article['model'] ?? null,
|
|
'prompt_used' => $article['prompt'] ?? null,
|
|
'error_message' => $error,
|
|
]);
|
|
} else {
|
|
Article::create([
|
|
'site_id' => $site['id'],
|
|
'topic_id' => $topic['id'],
|
|
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
|
'content' => $article['content'] ?? '',
|
|
'status' => 'failed',
|
|
'ai_model' => $article['model'] ?? null,
|
|
'prompt_used' => $article['prompt'] ?? null,
|
|
'error_message' => $error,
|
|
]);
|
|
}
|
|
|
|
Logger::error("Publikacja nieudana: {$error}", 'publish');
|
|
}
|
|
|
|
private function attachMissingImageToPublishedArticle(array $site, array $article): array
|
|
{
|
|
$articleId = (int) $article['id'];
|
|
$wpPostId = (int) ($article['wp_post_id'] ?? 0);
|
|
|
|
if ($wpPostId <= 0) {
|
|
Logger::warning("Artykul ID {$articleId} oznaczony jako published bez wp_post_id - pomijam", 'publish');
|
|
return ['success' => false, 'message' => 'Brak wp_post_id dla opublikowanego artykulu.'];
|
|
}
|
|
|
|
$existingFeaturedMediaId = $this->wordpress->getPostFeaturedMedia($site, $wpPostId);
|
|
if ($existingFeaturedMediaId !== null) {
|
|
Article::update($articleId, ['image_url' => 'wp_media:' . $existingFeaturedMediaId]);
|
|
$message = "Artykul ID {$articleId} juz mial miniaturke (media_id={$existingFeaturedMediaId}) - zaktualizowano marker lokalny.";
|
|
Logger::info($message, 'publish');
|
|
return ['success' => true, 'message' => $message];
|
|
}
|
|
|
|
$topic = Topic::find((int) ($article['topic_id'] ?? 0));
|
|
$topicName = $topic['name'] ?? (string) $article['title'];
|
|
$title = (string) ($article['title'] ?? 'Artykul bez tytulu');
|
|
|
|
Article::markRetryAttempt($articleId);
|
|
|
|
Logger::info("Proba uzupelnienia obrazka dla artykulu ID {$articleId}: {$title}", 'publish');
|
|
|
|
$image = $this->imageService->generate($title, (string) $topicName);
|
|
if (!$image) {
|
|
$message = "Nie udalo sie wygenerowac obrazka dla opublikowanego artykulu ID {$articleId}.";
|
|
Logger::warning($message, 'publish');
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
$mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']);
|
|
if (!$mediaId) {
|
|
$message = "Nie udalo sie wyslac obrazka do WordPress dla artykulu ID {$articleId}.";
|
|
Logger::warning($message, 'publish');
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
$updated = $this->wordpress->updatePostFeaturedMedia($site, $wpPostId, $mediaId);
|
|
if (!$updated) {
|
|
$this->wordpress->deleteMedia($site, $mediaId);
|
|
$message = "Nie udalo sie podpiac miniaturki (media_id={$mediaId}) do wpisu wp_post_id={$wpPostId}.";
|
|
Logger::warning($message, 'publish');
|
|
return ['success' => false, 'message' => $message];
|
|
}
|
|
|
|
Article::update($articleId, [
|
|
'image_url' => 'wp_media:' . $mediaId,
|
|
'error_message' => null,
|
|
]);
|
|
|
|
$message = "Uzupelniono miniaturke dla artykulu ID {$articleId} (wp_post_id={$wpPostId}, media_id={$mediaId}).";
|
|
Logger::info($message, 'publish');
|
|
|
|
return ['success' => true, 'message' => $message];
|
|
}
|
|
|
|
private function buildExcerpt(string $htmlContent): string
|
|
{
|
|
$plain = trim((string) preg_replace('/\s+/u', ' ', strip_tags($htmlContent)));
|
|
if ($plain === '') {
|
|
return '';
|
|
}
|
|
|
|
$maxLength = 155;
|
|
if (mb_strlen($plain) <= $maxLength) {
|
|
return $plain;
|
|
}
|
|
|
|
$cut = mb_substr($plain, 0, $maxLength + 1);
|
|
$lastSpace = mb_strrpos($cut, ' ');
|
|
if ($lastSpace !== false && $lastSpace > 80) {
|
|
$cut = mb_substr($cut, 0, $lastSpace);
|
|
} else {
|
|
$cut = mb_substr($cut, 0, $maxLength);
|
|
}
|
|
|
|
return rtrim($cut, " \t\n\r\0\x0B.,;:!?") . '.';
|
|
}
|
|
}
|