gutenbergId = $gutenbergId; $this->generateSummary = $generateSummary; $this->onQueue('gutendex-import'); } /** * Execute the job. */ public function handle(GutendexClient $gutendexClient): void { Log::info('ImportBookFromGutendex: Starting import', [ 'gutenberg_id' => $this->gutenbergId, ]); try { // Check if book already exists $existingBook = Book::where('gutenberg_id', $this->gutenbergId)->first(); if ($existingBook) { Log::info('ImportBookFromGutendex: Book already exists, skipping', [ 'gutenberg_id' => $this->gutenbergId, 'book_id' => $existingBook->id, ]); return; } // Fetch book metadata from Gutendex $gutendexBook = $gutendexClient->getBook($this->gutenbergId); if (!$gutendexBook) { Log::error('ImportBookFromGutendex: Failed to fetch book from Gutendex', [ 'gutenberg_id' => $this->gutenbergId, ]); $this->fail(new \Exception("Failed to fetch book {$this->gutenbergId} from Gutendex")); return; } // Only import public domain books if (!$gutendexBook->isPublicDomain()) { Log::warning('ImportBookFromGutendex: Book is not public domain, skipping', [ 'gutenberg_id' => $this->gutenbergId, 'copyright' => $gutendexBook->copyright, ]); return; } // Check if full text is available if (!$gutendexBook->getPlainTextUrl()) { Log::warning('ImportBookFromGutendex: No plain text URL available, skipping', [ 'gutenberg_id' => $this->gutenbergId, ]); return; } DB::beginTransaction(); try { // Find or create category $category = $this->findOrCreateCategory($gutendexBook->suggestCategory()); // Create book record $bookData = $gutendexBook->toBookArray(); $bookData['category_id'] = $category->id; $bookData['slug'] = $this->generateUniqueSlug($gutendexBook->title); $book = Book::create($bookData); // Map authors to the authors table $this->syncAuthors($book, $gutendexBook->getAuthorNames()); DB::commit(); Log::info('ImportBookFromGutendex: Book imported successfully', [ 'gutenberg_id' => $this->gutenbergId, 'book_id' => $book->id, 'title' => $book->title, ]); // Dispatch summary generation job if ($this->generateSummary) { GenerateSummary::dispatch($book->id) ->delay(now()->addSeconds(5)); // Small delay to spread load } } catch (\Exception $e) { DB::rollBack(); throw $e; } } catch (\Exception $e) { Log::error('ImportBookFromGutendex: Import failed', [ 'gutenberg_id' => $this->gutenbergId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } } /** * Find or create a category. * * @param string|null $categorySlug * @return Category */ protected function findOrCreateCategory(?string $categorySlug): Category { if (!$categorySlug) { $categorySlug = 'uncategorized'; } $category = Category::where('slug', $categorySlug)->first(); if (!$category) { $categoryName = Str::title(str_replace('-', ' ', $categorySlug)); $category = Category::create([ 'name' => $categoryName, 'slug' => $categorySlug, 'description' => "Books in the {$categoryName} category", ]); Log::info('ImportBookFromGutendex: Created new category', [ 'category_id' => $category->id, 'slug' => $categorySlug, ]); } return $category; } /** * Generate a unique slug for the book. * * @param string $title * @return string */ protected function generateUniqueSlug(string $title): string { $slug = Str::slug($title); $originalSlug = $slug; $counter = 1; while (Book::where('slug', $slug)->exists()) { $slug = $originalSlug . '-' . $counter; $counter++; } return $slug; } /** * Sync authors to the book. * * @param Book $book * @param array $authorNames * @return void */ protected function syncAuthors(Book $book, array $authorNames): void { $authorIds = []; foreach ($authorNames as $authorName) { $author = Author::firstOrCreate( ['name' => $authorName], ['slug' => Str::slug($authorName)] ); $authorIds[] = $author->id; } $book->authors()->sync($authorIds); Log::info('ImportBookFromGutendex: Authors synced', [ 'book_id' => $book->id, 'author_count' => count($authorIds), ]); } /** * Handle a job failure. */ public function failed(\Throwable $exception): void { Log::error('ImportBookFromGutendex: Job failed permanently', [ 'gutenberg_id' => $this->gutenbergId, 'error' => $exception->getMessage(), ]); } }