Use manticore for mediainfo data

This commit is contained in:
DariusIII
2026-08-07 16:01:41 +02:00
parent 65ccb95449
commit e1d1c3cee7
21 changed files with 546 additions and 120 deletions
+38
View File
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Tests\Unit;
use App\Services\Search\Drivers\ElasticSearchDriver;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
final class ElasticSearchQueryTest extends TestCase
{
@@ -67,4 +69,40 @@ final class ElasticSearchQueryTest extends TestCase
$this->assertStringContainsString("'_source' => false", $methodSource);
$this->assertStringContainsString("'track_total_hits' => (bool)", $methodSource);
}
#[Test]
public function filtered_release_search_supports_media_info_text_and_numeric_filters(): void
{
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
$this->assertIsString($driverSource);
$this->assertStringContainsString("'media_video_codec' => ['media_video_codec']", $driverSource);
$this->assertStringContainsString("'term' => ['media_unique_id' => \$mediaUniqueId]", $driverSource);
$this->assertStringContainsString("'term' => ['has_media_info' =>", $driverSource);
$this->assertStringContainsString("'range' => ['media_video_width' => \$range]", $driverSource);
$this->assertStringContainsString("'range' => ['media_video_height' => \$range]", $driverSource);
}
#[Test]
public function media_info_unique_id_uses_an_exact_keyword_filter(): void
{
$reflection = new ReflectionClass(ElasticSearchDriver::class);
$driver = $reflection->newInstanceWithoutConstructor();
$method = $reflection->getMethod('buildElasticsearchReleaseFilters');
$filters = $method->invoke($driver, ['media_unique_id' => ' 0x123ABC ']);
$this->assertContains(['term' => ['media_unique_id' => '0x123ABC']], $filters);
}
#[Test]
public function incremental_release_updates_use_the_canonical_projection_and_document(): void
{
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
$this->assertIsString($driverSource);
$this->assertStringContainsString('ReleaseIndexProjection::forId((int) $releaseID)', $driverSource);
$this->assertStringContainsString('ReleaseSearchIndexDocument::normalizeForBulk($parameters)', $driverSource);
$this->assertStringContainsString("'body' => \$document", $driverSource);
}
}
@@ -29,6 +29,11 @@ final class ManticoreIndexRegistryTest extends TestCase
self::assertSame('bigint', $definitions['releases']['columns']['passwordstatus']['type']);
self::assertSame('bigint', $definitions['releases']['columns']['haspreview']['type']);
self::assertSame('text', $definitions['releases']['columns']['media_movie_name']['type']);
self::assertSame('string', $definitions['releases']['columns']['media_unique_id']['type']);
self::assertSame('integer', $definitions['releases']['columns']['media_video_width']['type']);
self::assertSame('text', $definitions['releases']['columns']['media_audio_language']['type']);
self::assertSame('integer', $definitions['releases']['columns']['has_media_info']['type']);
}
#[Test]
+41
View File
@@ -3,6 +3,7 @@
namespace Tests\Unit;
use App\Services\Search\Drivers\ManticoreSearchDriver;
use App\Services\Search\DTO\ReleaseSearchQuery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
@@ -263,6 +264,46 @@ class ManticoreSearchQueryTest extends TestCase
$this->assertStringContainsString("\$query->sort('id', \$order);", $driverSource);
}
#[Test]
public function release_search_query_normalizes_media_info_filters(): void
{
$criteria = ReleaseSearchQuery::fromCriteria([
'has_media_info' => true,
'media_unique_id' => ' 0x123ABC ',
'min_video_width' => -1,
'max_video_width' => 3840,
'min_video_height' => 720,
'max_video_height' => 2160,
], 25)->criteria();
$this->assertTrue($criteria['has_media_info']);
$this->assertSame('0x123ABC', $criteria['media_unique_id']);
$this->assertSame(0, $criteria['min_video_width']);
$this->assertSame(3840, $criteria['max_video_width']);
$this->assertSame(720, $criteria['min_video_height']);
$this->assertSame(2160, $criteria['max_video_height']);
$invalidCriteria = ReleaseSearchQuery::fromCriteria(['media_unique_id' => '-1'], 25)->criteria();
$this->assertNull($invalidCriteria['media_unique_id']);
}
#[Test]
public function filtered_release_search_allowlists_media_info_fields_and_filters(): void
{
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
$this->assertIsString($driverSource);
$this->assertStringContainsString("'media_video_codec',", $driverSource);
$this->assertStringContainsString('in_array($key, self::RELEASE_FIELD_SEARCH_FIELDS, true)', $driverSource);
$this->assertStringContainsString("\$query->filter('has_media_info', '='", $driverSource);
$this->assertStringContainsString("\$query->filter('media_unique_id', '='", $driverSource);
$this->assertStringContainsString("\$query->filter('media_video_width', 'gte'", $driverSource);
$this->assertStringContainsString("\$query->filter('media_video_height', 'lte'", $driverSource);
$reflection = new ReflectionClass(ManticoreSearchDriver::class);
$this->assertNotContains('media_unique_id', $reflection->getConstant('RELEASE_FIELD_SEARCH_FIELDS'));
}
#[Test]
public function search_releases_filtered_scopes_field_queries_with_grouping_parentheses(): void
{
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Facades\Search;
use App\Models\MediaInfo as MediaInfoRecord;
use App\Services\ReleaseExtraService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Mhor\MediaInfo\Container\MediaInfoContainer;
use Mhor\MediaInfo\Type\General;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
final class ReleaseExtraServiceTest extends TestCase
{
#[Test]
public function it_reindexes_the_release_once_after_media_info_persistence(): void
{
Search::shouldReceive('updateRelease')
->once()
->with(42);
(new ReleaseExtraService)->addFromXml(42, new MediaInfoContainer);
}
#[Test]
public function it_does_not_persist_invalid_media_unique_id_sentinels(): void
{
Schema::create('media_infos', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('releases_id');
$table->string('movie_name')->nullable();
$table->string('file_name')->nullable();
$table->string('unique_id')->nullable();
$table->timestamps();
});
$general = new General;
$general->set('movie_name', 'Example Movie');
$general->set('unique_id', '0x0');
$mediaInfo = new MediaInfoContainer;
$mediaInfo->setGeneral($general);
try {
MediaInfoRecord::addData(43, $mediaInfo);
$record = MediaInfoRecord::query()->where('releases_id', 43)->firstOrFail();
} finally {
Schema::drop('media_infos');
}
$this->assertSame('Example Movie', $record->movie_name);
$this->assertNull($record->unique_id);
}
}
@@ -30,6 +30,11 @@ final class ReleaseSearchIndexDocumentTest extends TestCase
'tv_episodes_id' => '456',
'passwordstatus' => '-1',
'comments' => '3',
'media_movie_name' => 'Example Movie',
'media_video_width' => '1920',
'media_video_height' => '1080',
'media_audio_language' => 'English French',
'has_media_info' => '1',
'postdate' => '2026-01-02 03:04:05',
'adddate' => '2026-01-03 04:05:06',
'nzb_password' => 'must-not-be-indexed',
@@ -40,6 +45,11 @@ final class ReleaseSearchIndexDocumentTest extends TestCase
self::assertSame(123, $document['tmdbid']);
self::assertSame(456, $document['tv_episodes_id']);
self::assertSame(-1, $document['passwordstatus']);
self::assertSame('Example Movie', $document['media_movie_name']);
self::assertSame(1920, $document['media_video_width']);
self::assertSame(1080, $document['media_video_height']);
self::assertSame('English French', $document['media_audio_language']);
self::assertSame(1, $document['has_media_info']);
self::assertGreaterThan(0, $document['postdate_ts']);
self::assertGreaterThan(0, $document['adddate_ts']);
self::assertArrayNotHasKey('nzb_password', $document);
@@ -5,7 +5,12 @@ declare(strict_types=1);
namespace Tests\Unit\Services\Search;
use App\Services\Search\Support\ReleaseIndexProjection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PHPUnit\Framework\Attributes\Test;
use ReflectionClass;
use Tests\TestCase;
final class ReleaseIndexProjectionTest extends TestCase
@@ -17,8 +22,68 @@ final class ReleaseIndexProjectionTest extends TestCase
self::assertStringContainsString('select group_concat(rf.name', $sql);
self::assertStringContainsString('where rf.releases_id = r.id', $sql);
self::assertStringContainsString('select group_concat(ad.audioformat', $sql);
self::assertStringContainsString('from release_subtitles rs', $sql);
self::assertStringContainsString('left join (select "selected_media_info"."releases_id"', $sql);
self::assertStringContainsString('select min(candidate_media_info.id)', $sql);
self::assertStringContainsString('left join "video_data" as "vd"', $sql);
self::assertStringNotContainsString('join "release_files"', $sql);
self::assertStringNotContainsString('join "audio_data"', $sql);
self::assertStringNotContainsString('join "release_subtitles"', $sql);
self::assertStringNotContainsString(' group by ', $sql);
self::assertStringContainsString('order by "r"."id" asc limit 5000', $sql);
}
#[Test]
public function media_info_projection_selects_one_deterministic_row_per_release(): void
{
Schema::create('media_infos', static function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('releases_id');
$table->string('movie_name')->nullable();
$table->string('file_name')->nullable();
$table->string('unique_id')->nullable();
});
try {
DB::table('media_infos')->insert([
[
'id' => 10,
'releases_id' => 1,
'movie_name' => 'First movie',
'file_name' => 'first.mkv',
'unique_id' => 'first-id',
],
[
'id' => 11,
'releases_id' => 1,
'movie_name' => 'Duplicate movie',
'file_name' => 'duplicate.mkv',
'unique_id' => 'duplicate-id',
],
]);
$method = (new ReflectionClass(ReleaseIndexProjection::class))->getMethod('mediaInfoQuery');
$query = $method->invoke(null);
self::assertInstanceOf(Builder::class, $query);
$rows = $query->get();
$row = (array) $rows->first();
self::assertCount(1, $rows);
self::assertSame('First movie', $row['movie_name']);
self::assertSame('first-id', $row['unique_id']);
} finally {
Schema::drop('media_infos');
}
}
#[Test]
public function both_search_engines_bulk_populate_from_the_canonical_projection(): void
{
$commandSource = file_get_contents(__DIR__.'/../../../../app/Console/Commands/NntmuxPopulateSearchIndexes.php');
self::assertIsString($commandSource);
self::assertGreaterThanOrEqual(2, substr_count($commandSource, 'ReleaseIndexProjection::query()->orderBy(\'r.id\')'));
self::assertGreaterThanOrEqual(2, substr_count($commandSource, 'ReleaseSearchIndexDocument::normalize((array) $item)'));
}
}