diff --git a/app/Services/Releases/ReleaseSearchService.php b/app/Services/Releases/ReleaseSearchService.php index 5c191fe09..962a85832 100644 --- a/app/Services/Releases/ReleaseSearchService.php +++ b/app/Services/Releases/ReleaseSearchService.php @@ -1636,6 +1636,16 @@ class ReleaseSearchService $query->where($field, 'LIKE', '%'.$term.'%'); } } + + if ($field === 'searchname' && count($terms) > 1) { + $normalizedPhrase = $this->normalizeMysqlFallbackPhrase($value); + if ($normalizedPhrase !== '') { + $query->whereRaw( + "LOWER(REPLACE(REPLACE(REPLACE($field, '.', ' '), '-', ' '), '_', ' ')) LIKE ?", + [$normalizedPhrase.'%'] + ); + } + } } } @@ -1655,6 +1665,15 @@ class ReleaseSearchService } } + private function normalizeMysqlFallbackPhrase(string $value): string + { + $normalized = strtolower(trim($value)); + $normalized = str_replace(['.', '-', '_'], ' ', $normalized); + $normalized = preg_replace('/\s+/', ' ', $normalized); + + return trim((string) $normalized); + } + /** * @param array $siteIdArr */ diff --git a/tests/Feature/MovieSearchApiTest.php b/tests/Feature/MovieSearchApiTest.php index bc84ccab4..dbda9fcc1 100644 --- a/tests/Feature/MovieSearchApiTest.php +++ b/tests/Feature/MovieSearchApiTest.php @@ -102,6 +102,39 @@ final class MovieSearchApiTest extends TestCase $this->assertSame('Resurrection.2025.1080p.WEB-DL.TEST', $results[0]->searchname); } + #[Test] + public function movies_search_mysql_fallback_excludes_unrelated_2025_and_resurrection_releases(): void + { + config(['nntmux.mysql_search_fallback' => true]); + + $mock = Mockery::mock(SearchService::class, [$this->app]); + $mock->shouldReceive('searchReleasesByExternalId')->never(); + $mock->shouldReceive('searchReleases')->once()->with(['searchname' => 'Resurrection 2025'], 1000)->andReturn([]); + $mock->shouldReceive('searchReleasesWithFuzzy')->never(); + $mock->shouldReceive('isAvailable')->andReturn(false); + + $this->app->instance(SearchService::class, $mock); + + $service = new ReleaseSearchService; + $results = $service->moviesSearch( + '', + -1, + -1, + 0, + 100, + 'Resurrection 2025', + [2030], + -1, + 0, + [], + 'posted_desc' + ); + + $this->assertCount(1, $results); + $this->assertSame(['Resurrection.2025.1080p.WEB-DL.TEST'], $results->pluck('searchname')->all()); + $this->assertNotContains('Titanic.The.Digital.Resurrection.2025.720p.WEBRip-LAMA', $results->pluck('searchname')->all()); + } + private function registerSqliteConcatIfNeeded(): void { if (DB::getDriverName() !== 'sqlite') { @@ -215,6 +248,20 @@ final class MovieSearchApiTest extends TestCase 'traktid' => 2002, 'title' => 'Resurrection Road', ], + [ + 'id' => 3, + 'imdbid' => '2468135', + 'tmdbid' => 1003, + 'traktid' => 2003, + 'title' => 'Drop', + ], + [ + 'id' => 4, + 'imdbid' => '1123581', + 'tmdbid' => 1004, + 'traktid' => 2004, + 'title' => 'Titanic: The Digital Resurrection', + ], ]); $now = now()->toDateTimeString(); @@ -252,6 +299,38 @@ final class MovieSearchApiTest extends TestCase 'imdbid' => '7654321', 'movieinfo_id' => 2, ], + [ + 'id' => 3, + 'searchname' => 'Drop.2025.2160p.BluRay.TEST', + 'guid' => 'movie-guid-3', + 'postdate' => $now, + 'adddate' => $now, + 'categories_id' => 2030, + 'groups_id' => 1, + 'size' => 1000, + 'totalpart' => 1, + 'passwordstatus' => 0, + 'grabs' => 0, + 'comments' => 0, + 'imdbid' => '2468135', + 'movieinfo_id' => 3, + ], + [ + 'id' => 4, + 'searchname' => 'Titanic.The.Digital.Resurrection.2025.720p.WEBRip-LAMA', + 'guid' => 'movie-guid-4', + 'postdate' => $now, + 'adddate' => $now, + 'categories_id' => 2030, + 'groups_id' => 1, + 'size' => 1000, + 'totalpart' => 1, + 'passwordstatus' => 0, + 'grabs' => 0, + 'comments' => 0, + 'imdbid' => '1123581', + 'movieinfo_id' => 4, + ], ]); Release::clearBootedModels(); diff --git a/tests/Feature/TvSearchApiTest.php b/tests/Feature/TvSearchApiTest.php index c2fabafbf..7297040f7 100644 --- a/tests/Feature/TvSearchApiTest.php +++ b/tests/Feature/TvSearchApiTest.php @@ -151,6 +151,40 @@ final class TvSearchApiTest extends TestCase $this->assertSame('Simpsons.S06E24.Test', $results[0]->searchname); } + #[Test] + public function tv_search_mysql_fallback_excludes_mid_title_false_positives_for_multi_word_queries(): void + { + config(['nntmux.mysql_search_fallback' => true]); + + $mock = Mockery::mock(SearchService::class, [$this->app]); + $mock->shouldReceive('searchReleasesByExternalId')->never(); + $mock->shouldReceive('searchReleases')->once()->with(['searchname' => 'The Simpsons'], 1000)->andReturn([]); + $mock->shouldReceive('searchReleasesWithFuzzy')->never(); + $mock->shouldReceive('isAvailable')->andReturn(false); + + $this->app->instance(SearchService::class, $mock); + + $service = new ReleaseSearchService; + $results = $service->tvSearch( + [], + '', + '', + '', + 0, + 100, + 'The Simpsons', + [5030], + -1, + 0, + [], + 'posted_desc' + ); + + $this->assertCount(1, $results); + $this->assertSame(['The.Simpsons.S06E24.Test'], $results->pluck('searchname')->all()); + $this->assertNotContains('Titanic.The.Digital.The.Simpsons.2025.Special', $results->pluck('searchname')->all()); + } + private function bindSearchIndexMockReturningBothReleases(): void { $mock = Mockery::mock(SearchService::class, [$this->app]); @@ -462,6 +496,38 @@ final class TvSearchApiTest extends TestCase 'videos_id' => 1, 'tv_episodes_id' => 2, ], + [ + 'id' => 3, + 'searchname' => 'The.Simpsons.S06E24.Test', + 'guid' => 'guid-the-s06e24', + 'postdate' => $now, + 'adddate' => $now, + 'categories_id' => 5030, + 'groups_id' => 1, + 'size' => 1500, + 'totalpart' => 1, + 'passwordstatus' => 0, + 'grabs' => 0, + 'comments' => 0, + 'videos_id' => 1, + 'tv_episodes_id' => 1, + ], + [ + 'id' => 4, + 'searchname' => 'Titanic.The.Digital.The.Simpsons.2025.Special', + 'guid' => 'guid-titanic-simpsons-special', + 'postdate' => $now, + 'adddate' => $now, + 'categories_id' => 5030, + 'groups_id' => 1, + 'size' => 2500, + 'totalpart' => 1, + 'passwordstatus' => 0, + 'grabs' => 0, + 'comments' => 0, + 'videos_id' => 1, + 'tv_episodes_id' => 2, + ], ]); Release::clearBootedModels();