Issues (1012)

app/Console/Commands/CreateMediaIndexes.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace App\Console\Commands;
4
5
use Illuminate\Console\Command;
6
use Manticoresearch\Client;
7
use Manticoresearch\Exceptions\ResponseException;
8
9
class CreateMediaIndexes extends Command
10
{
11
    /**
12
     * The name and signature of the console command.
13
     *
14
     * @var string
15
     */
16
    protected $signature = 'nntmux:create-media-indexes
17
                                        {--drop : Drop existing indexes before creating}
18
                                        {--populate : Populate indexes after creation}';
19
20
    /**
21
     * The console command description.
22
     *
23
     * @var string
24
     */
25
    protected $description = 'Create movies and tvshows search indexes for the active search driver';
26
27
    /**
28
     * Execute the console command.
29
     */
30
    public function handle(): int
31
    {
32
        $driver = config('search.default', 'manticore');
33
        $this->info("Creating media search indexes for driver: {$driver}");
34
35
        if ($driver === 'manticore') {
36
            return $this->createManticoreIndexes();
37
        } elseif ($driver === 'elasticsearch') {
38
            return $this->createElasticsearchIndexes();
39
        } else {
40
            $this->error("Unsupported search driver: {$driver}");
41
42
            return Command::FAILURE;
43
        }
44
    }
45
46
    /**
47
     * Create Manticore indexes for movies and tvshows.
48
     */
49
    private function createManticoreIndexes(): int
50
    {
51
        $host = config('search.drivers.manticore.host', '127.0.0.1');
52
        $port = config('search.drivers.manticore.port', 9308);
53
54
        $client = new Client([
55
            'host' => $host,
56
            'port' => $port,
57
        ]);
58
59
        // Test connection
60
        try {
61
            $client->nodes()->status();
62
            $this->info('Connected to ManticoreSearch successfully.');
63
        } catch (\Exception $e) {
64
            $this->error('Failed to connect to ManticoreSearch: '.$e->getMessage());
65
66
            return Command::FAILURE;
67
        }
68
69
        $dropExisting = $this->option('drop');
70
71
        // Define movies and tvshows indexes
72
        $indexes = [
73
            'movies_rt' => [
74
                'settings' => [
75
                    'min_prefix_len' => 0,
76
                    'min_infix_len' => 2,
77
                ],
78
                'columns' => [
79
                    'imdbid' => ['type' => 'integer'],
80
                    'tmdbid' => ['type' => 'integer'],
81
                    'traktid' => ['type' => 'integer'],
82
                    'title' => ['type' => 'text'],
83
                    'year' => ['type' => 'text'],
84
                    'genre' => ['type' => 'text'],
85
                    'actors' => ['type' => 'text'],
86
                    'director' => ['type' => 'text'],
87
                    'rating' => ['type' => 'text'],
88
                    'plot' => ['type' => 'text'],
89
                ],
90
            ],
91
            'tvshows_rt' => [
92
                'settings' => [
93
                    'min_prefix_len' => 0,
94
                    'min_infix_len' => 2,
95
                ],
96
                'columns' => [
97
                    'title' => ['type' => 'text'],
98
                    'tvdb' => ['type' => 'integer'],
99
                    'trakt' => ['type' => 'integer'],
100
                    'tvmaze' => ['type' => 'integer'],
101
                    'tvrage' => ['type' => 'integer'],
102
                    'imdb' => ['type' => 'integer'],
103
                    'tmdb' => ['type' => 'integer'],
104
                    'started' => ['type' => 'text'],
105
                    'type' => ['type' => 'integer'],
106
                ],
107
            ],
108
        ];
109
110
        $hasErrors = false;
111
112
        foreach ($indexes as $indexName => $schema) {
113
            if (! $this->createManticoreIndex($client, $indexName, $schema, $dropExisting)) {
0 ignored issues
show
$dropExisting of type string is incompatible with the type boolean expected by parameter $dropExisting of App\Console\Commands\Cre...:createManticoreIndex(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
            if (! $this->createManticoreIndex($client, $indexName, $schema, /** @scrutinizer ignore-type */ $dropExisting)) {
Loading history...
114
                $hasErrors = true;
115
            }
116
        }
117
118
        if ($hasErrors) {
119
            $this->error('Some errors occurred during index creation.');
120
121
            return Command::FAILURE;
122
        }
123
124
        $this->info('Media indexes created successfully!');
125
126
        // Optionally populate the indexes
127
        if ($this->option('populate')) {
128
            $this->info('Populating indexes...');
129
            $this->call('nntmux:populate', [
130
                '--manticore' => true,
131
                '--movies' => true,
132
            ]);
133
            $this->call('nntmux:populate', [
134
                '--manticore' => true,
135
                '--tvshows' => true,
136
            ]);
137
        }
138
139
        return Command::SUCCESS;
140
    }
141
142
    /**
143
     * Create a single Manticore index.
144
     */
145
    private function createManticoreIndex(Client $client, string $indexName, array $schema, bool $dropExisting): bool
146
    {
147
        try {
148
            // Check if index exists
149
            $indexExists = false;
150
            try {
151
                $client->table($indexName)->describe();
152
                $indexExists = true;
153
            } catch (ResponseException $e) {
154
                // Index doesn't exist
155
            }
156
157
            if ($indexExists) {
158
                if ($dropExisting) {
159
                    $this->warn("Dropping existing index: {$indexName}");
160
                    $client->tables()->drop(['index' => $indexName, 'body' => ['silent' => true]]);
161
                } else {
162
                    $this->info("Index {$indexName} already exists. Use --drop to recreate.");
163
164
                    return true;
165
                }
166
            }
167
168
            // Create the index using the ManticoreSearch PHP client
169
            $this->info("Creating index: {$indexName}");
170
171
            // Use the same format as CreateManticoreIndexes - pass schema directly
172
            $response = $client->tables()->create([
173
                'index' => $indexName,
174
                'body' => $schema,
175
            ]);
176
177
            $this->info("Index {$indexName} created successfully.");
178
            $this->line('Response: '.json_encode($response, JSON_PRETTY_PRINT));
179
180
            return true;
181
182
        } catch (ResponseException $e) {
183
            // Check if the error is because the index already exists
184
            if (str_contains($e->getMessage(), 'already exists')) {
185
                $this->warn("Index {$indexName} already exists. Use --drop to recreate.");
186
187
                return true;
188
            }
189
            // Check for data_dir configuration error
190
            if (str_contains($e->getMessage(), 'data_dir')) {
191
                $this->error("Failed to create index {$indexName}: ".$e->getMessage());
192
                $this->newLine();
193
                $this->warn('ManticoreSearch requires data_dir to be set in its configuration file.');
194
                $this->info('To fix this, edit your ManticoreSearch config file (usually /etc/manticoresearch/manticore.conf):');
195
                $this->line('  1. Add or uncomment: data_dir = /var/lib/manticore');
196
                $this->line('  2. Ensure the directory exists and is writable by the manticore user');
197
                $this->line('  3. Restart ManticoreSearch: sudo systemctl restart manticore');
198
199
                return false;
200
            }
201
            $this->error("Failed to create index {$indexName}: ".$e->getMessage());
202
203
            return false;
204
        } catch (\Throwable $e) {
205
            $this->error("Unexpected error creating index {$indexName}: ".$e->getMessage());
206
207
            return false;
208
        }
209
    }
210
211
    /**
212
     * Create Elasticsearch indexes for movies and tvshows.
213
     */
214
    private function createElasticsearchIndexes(): int
215
    {
216
        $this->info('Creating Elasticsearch media indexes...');
217
218
        $dropExisting = $this->option('drop');
219
220
        // Movies index mapping
221
        $moviesMapping = [
222
            'mappings' => [
223
                'properties' => [
224
                    'imdbid' => ['type' => 'integer'],
225
                    'tmdbid' => ['type' => 'integer'],
226
                    'traktid' => ['type' => 'integer'],
227
                    'title' => ['type' => 'text', 'analyzer' => 'standard'],
228
                    'year' => ['type' => 'keyword'],
229
                    'genre' => ['type' => 'text'],
230
                    'actors' => ['type' => 'text'],
231
                    'director' => ['type' => 'text'],
232
                    'rating' => ['type' => 'keyword'],
233
                    'plot' => ['type' => 'text'],
234
                ],
235
            ],
236
            'settings' => [
237
                'number_of_shards' => 1,
238
                'number_of_replicas' => 0,
239
            ],
240
        ];
241
242
        // TV shows index mapping
243
        $tvshowsMapping = [
244
            'mappings' => [
245
                'properties' => [
246
                    'title' => ['type' => 'text', 'analyzer' => 'standard'],
247
                    'tvdb' => ['type' => 'integer'],
248
                    'trakt' => ['type' => 'integer'],
249
                    'tvmaze' => ['type' => 'integer'],
250
                    'tvrage' => ['type' => 'integer'],
251
                    'imdb' => ['type' => 'integer'],
252
                    'tmdb' => ['type' => 'integer'],
253
                    'started' => ['type' => 'keyword'],
254
                    'type' => ['type' => 'integer'],
255
                ],
256
            ],
257
            'settings' => [
258
                'number_of_shards' => 1,
259
                'number_of_replicas' => 0,
260
            ],
261
        ];
262
263
        try {
264
            // Create Elasticsearch client directly
265
            $esConfig = config('search.drivers.elasticsearch');
266
            $esClient = \Elasticsearch\ClientBuilder::create()
267
                ->setHosts($esConfig['hosts'] ?? [['host' => 'localhost', 'port' => 9200]])
268
                ->build();
269
270
            // Create movies index
271
            $moviesIndex = config('search.drivers.elasticsearch.indexes.movies', 'movies');
272
            if ($dropExisting && $esClient->indices()->exists(['index' => $moviesIndex])) {
273
                $this->warn("Dropping existing index: {$moviesIndex}");
274
                $esClient->indices()->delete(['index' => $moviesIndex]);
275
            }
276
277
            if (! $esClient->indices()->exists(['index' => $moviesIndex])) {
278
                $this->info("Creating index: {$moviesIndex}");
279
                $esClient->indices()->create([
280
                    'index' => $moviesIndex,
281
                    'body' => $moviesMapping,
282
                ]);
283
                $this->info("Index {$moviesIndex} created successfully.");
284
            } else {
285
                $this->info("Index {$moviesIndex} already exists. Use --drop to recreate.");
286
            }
287
288
            // Create tvshows index
289
            $tvshowsIndex = config('search.drivers.elasticsearch.indexes.tvshows', 'tvshows');
290
            if ($dropExisting && $esClient->indices()->exists(['index' => $tvshowsIndex])) {
291
                $this->warn("Dropping existing index: {$tvshowsIndex}");
292
                $esClient->indices()->delete(['index' => $tvshowsIndex]);
293
            }
294
295
            if (! $esClient->indices()->exists(['index' => $tvshowsIndex])) {
296
                $this->info("Creating index: {$tvshowsIndex}");
297
                $esClient->indices()->create([
298
                    'index' => $tvshowsIndex,
299
                    'body' => $tvshowsMapping,
300
                ]);
301
                $this->info("Index {$tvshowsIndex} created successfully.");
302
            } else {
303
                $this->info("Index {$tvshowsIndex} already exists. Use --drop to recreate.");
304
            }
305
306
            $this->info('Elasticsearch media indexes created successfully!');
307
308
            // Optionally populate the indexes
309
            if ($this->option('populate')) {
310
                $this->info('Populating indexes...');
311
                $this->call('nntmux:populate', [
312
                    '--elastic' => true,
313
                    '--movies' => true,
314
                ]);
315
                $this->call('nntmux:populate', [
316
                    '--elastic' => true,
317
                    '--tvshows' => true,
318
                ]);
319
            }
320
321
            return Command::SUCCESS;
322
323
        } catch (\Throwable $e) {
324
            $this->error('Failed to create Elasticsearch indexes: '.$e->getMessage());
325
326
            return Command::FAILURE;
327
        }
328
    }
329
}
330