Files
newznab-tmux/app/Console/Commands/CreateMediaIndexes.php
T
2026-02-11 14:02:26 +01:00

332 lines
12 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Manticoresearch\Client;
use Manticoresearch\Exceptions\ResponseException;
class CreateMediaIndexes extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nntmux:create-media-indexes
{--drop : Drop existing indexes before creating}
{--populate : Populate indexes after creation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create movies and tvshows search indexes for the active search driver';
/**
* Execute the console command.
*/
public function handle(): int
{
$driver = config('search.default', 'manticore');
$this->info("Creating media search indexes for driver: {$driver}");
if ($driver === 'manticore') {
return $this->createManticoreIndexes();
} elseif ($driver === 'elasticsearch') {
return $this->createElasticsearchIndexes();
} else {
$this->error("Unsupported search driver: {$driver}");
return Command::FAILURE;
}
}
/**
* Create Manticore indexes for movies and tvshows.
*/
private function createManticoreIndexes(): int
{
$host = config('search.drivers.manticore.host', '127.0.0.1');
$port = config('search.drivers.manticore.port', 9308);
$client = new Client([
'host' => $host,
'port' => $port,
]);
// Test connection
try {
$client->nodes()->status();
$this->info('Connected to ManticoreSearch successfully.');
} catch (\Exception $e) {
$this->error('Failed to connect to ManticoreSearch: '.$e->getMessage());
return Command::FAILURE;
}
$dropExisting = $this->option('drop');
// Define movies and tvshows indexes
$indexes = [
'movies_rt' => [
'settings' => [
'min_prefix_len' => 0,
'min_infix_len' => 2,
],
'columns' => [
'imdbid' => ['type' => 'integer'],
'tmdbid' => ['type' => 'integer'],
'traktid' => ['type' => 'integer'],
'title' => ['type' => 'text'],
'year' => ['type' => 'text'],
'genre' => ['type' => 'text'],
'actors' => ['type' => 'text'],
'director' => ['type' => 'text'],
'rating' => ['type' => 'text'],
'plot' => ['type' => 'text'],
],
],
'tvshows_rt' => [
'settings' => [
'min_prefix_len' => 0,
'min_infix_len' => 2,
],
'columns' => [
'title' => ['type' => 'text'],
'tvdb' => ['type' => 'integer'],
'trakt' => ['type' => 'integer'],
'tvmaze' => ['type' => 'integer'],
'tvrage' => ['type' => 'integer'],
'imdb' => ['type' => 'integer'],
'tmdb' => ['type' => 'integer'],
'started' => ['type' => 'text'],
'type' => ['type' => 'integer'],
],
],
];
$hasErrors = false;
foreach ($indexes as $indexName => $schema) {
if (! $this->createManticoreIndex($client, $indexName, $schema, $dropExisting)) {
$hasErrors = true;
}
}
if ($hasErrors) {
$this->error('Some errors occurred during index creation.');
return Command::FAILURE;
}
$this->info('Media indexes created successfully!');
// Optionally populate the indexes
if ($this->option('populate')) {
$this->info('Populating indexes...');
$this->call('nntmux:populate', [
'--manticore' => true,
'--movies' => true,
]);
$this->call('nntmux:populate', [
'--manticore' => true,
'--tvshows' => true,
]);
}
return Command::SUCCESS;
}
/**
* Create a single Manticore index.
*
* @param array<string, mixed> $schema
*/
private function createManticoreIndex(Client $client, string $indexName, array $schema, bool $dropExisting): bool
{
try {
// Check if index exists
$indexExists = false;
try {
$client->table($indexName)->describe();
$indexExists = true;
} catch (ResponseException $e) {
// Index doesn't exist
}
if ($indexExists) {
if ($dropExisting) {
$this->warn("Dropping existing index: {$indexName}");
$client->tables()->drop(['index' => $indexName, 'body' => ['silent' => true]]);
} else {
$this->info("Index {$indexName} already exists. Use --drop to recreate.");
return true;
}
}
// Create the index using the ManticoreSearch PHP client
$this->info("Creating index: {$indexName}");
// Use the same format as CreateManticoreIndexes - pass schema directly
$response = $client->tables()->create([
'index' => $indexName,
'body' => $schema,
]);
$this->info("Index {$indexName} created successfully.");
$this->line('Response: '.json_encode($response, JSON_PRETTY_PRINT));
return true;
} catch (ResponseException $e) {
// Check if the error is because the index already exists
if (str_contains($e->getMessage(), 'already exists')) {
$this->warn("Index {$indexName} already exists. Use --drop to recreate.");
return true;
}
// Check for data_dir configuration error
if (str_contains($e->getMessage(), 'data_dir')) {
$this->error("Failed to create index {$indexName}: ".$e->getMessage());
$this->newLine();
$this->warn('ManticoreSearch requires data_dir to be set in its configuration file.');
$this->info('To fix this, edit your ManticoreSearch config file (usually /etc/manticoresearch/manticore.conf):');
$this->line(' 1. Add or uncomment: data_dir = /var/lib/manticore');
$this->line(' 2. Ensure the directory exists and is writable by the manticore user');
$this->line(' 3. Restart ManticoreSearch: sudo systemctl restart manticore');
return false;
}
$this->error("Failed to create index {$indexName}: ".$e->getMessage());
return false;
} catch (\Throwable $e) {
$this->error("Unexpected error creating index {$indexName}: ".$e->getMessage());
return false;
}
}
/**
* Create Elasticsearch indexes for movies and tvshows.
*/
private function createElasticsearchIndexes(): int
{
$this->info('Creating Elasticsearch media indexes...');
$dropExisting = $this->option('drop');
// Movies index mapping
$moviesMapping = [
'mappings' => [
'properties' => [
'imdbid' => ['type' => 'integer'],
'tmdbid' => ['type' => 'integer'],
'traktid' => ['type' => 'integer'],
'title' => ['type' => 'text', 'analyzer' => 'standard'],
'year' => ['type' => 'keyword'],
'genre' => ['type' => 'text'],
'actors' => ['type' => 'text'],
'director' => ['type' => 'text'],
'rating' => ['type' => 'keyword'],
'plot' => ['type' => 'text'],
],
],
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0,
],
];
// TV shows index mapping
$tvshowsMapping = [
'mappings' => [
'properties' => [
'title' => ['type' => 'text', 'analyzer' => 'standard'],
'tvdb' => ['type' => 'integer'],
'trakt' => ['type' => 'integer'],
'tvmaze' => ['type' => 'integer'],
'tvrage' => ['type' => 'integer'],
'imdb' => ['type' => 'integer'],
'tmdb' => ['type' => 'integer'],
'started' => ['type' => 'keyword'],
'type' => ['type' => 'integer'],
],
],
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0,
],
];
try {
// Create Elasticsearch client directly
$esConfig = config('search.drivers.elasticsearch');
$esClient = \Elasticsearch\ClientBuilder::create()
->setHosts($esConfig['hosts'] ?? [['host' => 'localhost', 'port' => 9200]])
->build();
// Create movies index
$moviesIndex = config('search.drivers.elasticsearch.indexes.movies', 'movies');
if ($dropExisting && $esClient->indices()->exists(['index' => $moviesIndex])) {
$this->warn("Dropping existing index: {$moviesIndex}");
$esClient->indices()->delete(['index' => $moviesIndex]);
}
if (! $esClient->indices()->exists(['index' => $moviesIndex])) {
$this->info("Creating index: {$moviesIndex}");
$esClient->indices()->create([
'index' => $moviesIndex,
'body' => $moviesMapping,
]);
$this->info("Index {$moviesIndex} created successfully.");
} else {
$this->info("Index {$moviesIndex} already exists. Use --drop to recreate.");
}
// Create tvshows index
$tvshowsIndex = config('search.drivers.elasticsearch.indexes.tvshows', 'tvshows');
if ($dropExisting && $esClient->indices()->exists(['index' => $tvshowsIndex])) {
$this->warn("Dropping existing index: {$tvshowsIndex}");
$esClient->indices()->delete(['index' => $tvshowsIndex]);
}
if (! $esClient->indices()->exists(['index' => $tvshowsIndex])) {
$this->info("Creating index: {$tvshowsIndex}");
$esClient->indices()->create([
'index' => $tvshowsIndex,
'body' => $tvshowsMapping,
]);
$this->info("Index {$tvshowsIndex} created successfully.");
} else {
$this->info("Index {$tvshowsIndex} already exists. Use --drop to recreate.");
}
$this->info('Elasticsearch media indexes created successfully!');
// Optionally populate the indexes
if ($this->option('populate')) {
$this->info('Populating indexes...');
$this->call('nntmux:populate', [
'--elastic' => true,
'--movies' => true,
]);
$this->call('nntmux:populate', [
'--elastic' => true,
'--tvshows' => true,
]);
}
return Command::SUCCESS;
} catch (\Throwable $e) {
$this->error('Failed to create Elasticsearch indexes: '.$e->getMessage());
return Command::FAILURE;
}
}
}