tests/Plugin/AmpacheAudioMuseTest.php
Rama develop · réplica de lectura del repositorio original.
<?php
declare(strict_types=1);
/**
* vim:set softtabstop=4 shiftwidth=4 expandtab:
*
* LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
* Copyright Ampache.org, 2001-2026
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Ampache\Plugin;
use Ampache\Module\Api\OpenSubsonic_Api;
use Ampache\Module\Util\UrlValidatorInterface;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
/**
* Covers the id translation and score normalization in the AudioMuse sonic-analysis plugin.
*
* AudioMuse has no Ampache connector — it is pointed at Ampache through its Navidrome connector, which is a plain
* Subsonic API client. So it indexes tracks under the prefixed `so-<id>` form Ampache gives Subsonic clients, not
* the bare row id. Getting that translation wrong silently returns nothing, which is why it is pinned here.
*/
class AmpacheAudioMuseTest extends TestCase
{
/**
* A bare id must not go through getAmpacheId(): the legacy old-style Subsonic ranges read a small integer as a
* catalog id, which would drop every native-connector row.
*/
public function testBareIdsAreNotMistakenForOtherObjectTypes(): void
{
self::assertSame('catalog', OpenSubsonic_Api::getAmpacheType('1'));
self::assertSame([['id' => 1, 'similarity' => 1.0]], $this->toMatches([['item_id' => '1', 'distance' => 0.0]]));
}
/**
* AudioMuse reports whichever id the connector it scanned with used: its Ampache connector reports Ampache's own
* row id, its Navidrome connector the Subsonic `so-` form. Both have to resolve, or half the setups return nothing.
*/
public function testMatchesAcceptEitherConnectorsIdForm(): void
{
self::assertSame(
[
['id' => 1, 'similarity' => 0.9],
['id' => 3001, 'similarity' => 0.9],
],
$this->toMatches([
['item_id' => '1', 'distance' => 0.1],
['item_id' => 'so-3001', 'distance' => 0.1],
])
);
}
/**
* AudioMuse can report an angular distance above 1, which must not become a negative similarity.
*/
public function testMatchesClampSimilarityIntoRange(): void
{
$matches = $this->toMatches([
['item_id' => 'so-2', 'distance' => 1.9],
['item_id' => 'so-4', 'distance' => -0.5],
]);
self::assertSame(0.0, $matches[0]['similarity']);
self::assertSame(1.0, $matches[1]['similarity']);
}
/**
* A row that is not a song id belongs to some other server's index, so it is dropped rather than guessed at.
*/
public function testMatchesDropRowsThatAreNotSongIds(): void
{
self::assertSame(
[],
$this->toMatches([
['item_id' => 'al-7', 'distance' => 0.1],
['item_id' => 'ar-2', 'distance' => 0.1],
['item_id' => '', 'distance' => 0.1],
['distance' => 0.1],
])
);
}
public function testMatchesTranslateSubsonicIdsBackToAmpacheIds(): void
{
self::assertSame(
[
['id' => 1, 'similarity' => 1.0],
['id' => 3001, 'similarity' => 0.58],
],
$this->toMatches([
['item_id' => 'so-1', 'distance' => 0.0],
['item_id' => 'so-3001', 'distance' => 0.42],
])
);
}
/**
* The id sent out has to be the one AudioMuse indexed, which is the Subsonic form.
*/
public function testOutgoingIdsUseTheSubsonicForm(): void
{
self::assertSame('so-1', OpenSubsonic_Api::getSongSubId(1));
self::assertSame('so-3001', OpenSubsonic_Api::getSongSubId(3001));
}
/**
* find_path scores the whole route, not each hop, so its rows arrive with no distance at all. Treating that as
* distance 0 would report every song in the path as a perfect match; -1 is what the spec reserves for it.
*/
public function testRowsWithNoDistanceReportSimilarityAsUnsupported(): void
{
self::assertSame(
[
['id' => 1, 'similarity' => -1.0],
['id' => 3001, 'similarity' => -1.0],
],
$this->toMatches([
['item_id' => 'so-1', 'title' => 'Pasadinha'],
['item_id' => 'so-3001', 'title' => 'Empty Phases'],
])
);
}
/**
* @param array<int, array<string, mixed>> $rows
* @return list<array{'id': int, 'similarity': float}>
*/
private function toMatches(array $rows): array
{
$method = new ReflectionMethod(AmpacheAudioMuse::class, '_toMatches');
$urlValidator = new class implements UrlValidatorInterface {
public function isPublicHttpUrl(string $url): bool
{
return true;
}
public function resolvePinnedTarget(string $url): array
{
return ['host' => 'some-host', 'port' => 80, 'address' => '203.0.113.10'];
}
};
/** @var list<array{'id': int, 'similarity': float}> $result */
$result = $method->invoke(new AmpacheAudioMuse($urlValidator), $rows);
return $result;
}
}