279 lines
7.9 KiB
PHP
279 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Handlers;
|
|
|
|
require __DIR__ . "/Classes/ApiHandler.php";
|
|
require __DIR__ . "/Utils/init.php";
|
|
|
|
use App\Classes\ApiHandler;
|
|
use GuzzleHttp\Client;
|
|
|
|
header("Content-Type: application/json");
|
|
|
|
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
|
$expectedToken = "Bearer " . getenv("NAVIDROME_SCROBBLE_TOKEN");
|
|
|
|
class NavidromeScrobbleHandler extends ApiHandler
|
|
{
|
|
private string $postgrestApiUrl;
|
|
private string $postgrestApiToken;
|
|
private string $navidromeApiUrl;
|
|
private string $navidromeAuthToken;
|
|
private string $forwardEmailApiKey;
|
|
|
|
private array $artistCache = [];
|
|
private array $albumCache = [];
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->ensureCliAccess();
|
|
$this->loadEnvironment();
|
|
$this->validateAuthorization();
|
|
}
|
|
|
|
private function loadEnvironment(): void
|
|
{
|
|
$this->postgrestApiUrl = getenv("POSTGREST_URL");
|
|
$this->postgrestApiToken = getenv("POSTGREST_API_KEY");
|
|
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
|
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
|
$this->forwardEmailApiKey = getenv("FORWARDEMAIL_API_KEY");
|
|
}
|
|
|
|
private function validateAuthorization(): void
|
|
{
|
|
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
|
$expectedToken = "Bearer " . getenv("NAVIDROME_SCROBBLE_TOKEN");
|
|
|
|
if ($authHeader !== $expectedToken) {
|
|
http_response_code(401);
|
|
echo json_encode(["error" => "Unauthorized."]);
|
|
exit();
|
|
}
|
|
}
|
|
|
|
public function runScrobbleCheck(): void
|
|
{
|
|
$recentTracks = $this->fetchRecentlyPlayed();
|
|
|
|
if (empty($recentTracks)) return;
|
|
|
|
foreach ($recentTracks as $track) {
|
|
if ($this->isTrackAlreadyScrobbled($track)) continue;
|
|
|
|
$this->handleTrackScrobble($track);
|
|
}
|
|
}
|
|
|
|
private function fetchRecentlyPlayed(): array
|
|
{
|
|
$client = new Client();
|
|
try {
|
|
$response = $client->request("GET", "{$this->navidromeApiUrl}/api/song", [
|
|
"query" => [
|
|
"_end" => 20,
|
|
"_order" => "DESC",
|
|
"_sort" => "play_date",
|
|
"_start" => 0,
|
|
"recently_played" => "true"
|
|
],
|
|
"headers" => [
|
|
"x-nd-authorization" => "Bearer {$this->navidromeAuthToken}",
|
|
"Accept" => "application/json"
|
|
]
|
|
]);
|
|
|
|
$data = json_decode($response->getBody()->getContents(), true);
|
|
return $data ?? [];
|
|
} catch (\Exception $e) {
|
|
error_log("Error fetching tracks: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function isTrackAlreadyScrobbled(array $track): bool
|
|
{
|
|
$playDate = strtotime($track["playDate"]);
|
|
$existingListen = $this->fetchFromPostgREST("listens", "listened_at=eq.{$playDate}&limit=1");
|
|
|
|
return !empty($existingListen);
|
|
}
|
|
|
|
private function handleTrackScrobble(array $track): void
|
|
{
|
|
$artistData = $this->getOrCreateArtist($track["artist"]);
|
|
|
|
if (empty($artistData)) {
|
|
error_log("Failed to retrieve or create artist: " . $track["artist"]);
|
|
return;
|
|
}
|
|
|
|
$albumData = $this->getOrCreateAlbum($track["album"], $artistData);
|
|
|
|
if (empty($albumData)) {
|
|
error_log("Failed to retrieve or create album: " . $track["album"]);
|
|
return;
|
|
}
|
|
|
|
$this->insertListen($track, $albumData["key"]);
|
|
}
|
|
|
|
private function getOrCreateArtist(string $artistName): array
|
|
{
|
|
if (!$this->isDatabaseAvailable()) {
|
|
error_log("Skipping artist insert: database is unavailable.");
|
|
return [];
|
|
}
|
|
|
|
if (isset($this->artistCache[$artistName])) return $this->artistCache[$artistName];
|
|
|
|
$encodedArtist = rawurlencode($artistName);
|
|
$existingArtist = $this->fetchFromPostgREST("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
|
|
|
if (!empty($existingArtist)) {
|
|
$this->artistCache[$artistName] = $existingArtist[0];
|
|
return $existingArtist[0];
|
|
}
|
|
|
|
$this->fetchFromPostgREST("artists", "", "POST", [
|
|
"mbid" => null,
|
|
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
|
"name" => $artistName,
|
|
"slug" => "/music",
|
|
"tentative" => true,
|
|
"total_plays" => 0
|
|
]);
|
|
|
|
|
|
$this->sendFailureEmail("New tentative artist record", "A new tentative artist record was inserted:\n\nArtist: $artistName");
|
|
|
|
$artistData = $this->fetchFromPostgREST("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
|
|
|
$this->artistCache[$artistName] = $artistData[0] ?? [];
|
|
|
|
return $this->artistCache[$artistName];
|
|
}
|
|
|
|
private function getOrCreateAlbum(string $albumName, array $artistData): array
|
|
{
|
|
if (!$this->isDatabaseAvailable()) {
|
|
error_log("Skipping album insert: database is unavailable.");
|
|
return [];
|
|
}
|
|
|
|
$albumKey = $this->generateAlbumKey($artistData["name_string"], $albumName);
|
|
|
|
if (isset($this->albumCache[$albumKey])) return $this->albumCache[$albumKey];
|
|
|
|
$encodedAlbumKey = rawurlencode($albumKey);
|
|
$existingAlbum = $this->fetchFromPostgREST("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
|
|
|
if (!empty($existingAlbum)) {
|
|
$this->albumCache[$albumKey] = $existingAlbum[0];
|
|
return $existingAlbum[0];
|
|
}
|
|
|
|
$artistId = $artistData["id"] ?? null;
|
|
|
|
if (!$artistId) {
|
|
error_log("Artist ID missing for album creation: " . $albumName);
|
|
return [];
|
|
}
|
|
|
|
$this->fetchFromPostgREST("albums", "", "POST", [
|
|
"mbid" => null,
|
|
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
|
"key" => $albumKey,
|
|
"name" => $albumName,
|
|
"tentative" => true,
|
|
"total_plays" => 0,
|
|
"artist" => $artistId
|
|
]);
|
|
|
|
$this->sendFailureEmail("New tentative album record", "A new tentative album record was inserted:\n\nAlbum: $albumName\nKey: $albumKey");
|
|
|
|
$albumData = $this->fetchFromPostgREST("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
|
|
|
$this->albumCache[$albumKey] = $albumData[0] ?? [];
|
|
|
|
return $this->albumCache[$albumKey];
|
|
}
|
|
|
|
private function insertListen(array $track, string $albumKey): void
|
|
{
|
|
$playDate = strtotime($track["playDate"]);
|
|
|
|
$this->fetchFromPostgREST("listens", "", "POST", [
|
|
"artist_name" => $track["artist"],
|
|
"album_name" => $track["album"],
|
|
"track_name" => $track["title"],
|
|
"listened_at" => $playDate,
|
|
"album_key" => $albumKey
|
|
]);
|
|
}
|
|
|
|
private function generateAlbumKey(string $artistName, string $albumName): string
|
|
{
|
|
$artistKey = sanitizeMediaString($artistName);
|
|
$albumKey = sanitizeMediaString($albumName);
|
|
|
|
return "{$artistKey}-{$albumKey}";
|
|
}
|
|
|
|
private function sendFailureEmail(string $subject, string $message): void
|
|
{
|
|
if (!$this->isDatabaseAvailable()) {
|
|
error_log("Skipping email: database is unavailable.");
|
|
return;
|
|
}
|
|
|
|
$authHeader = "Basic " . base64_encode($this->forwardEmailApiKey . ":");
|
|
$client = new Client([
|
|
"base_uri" => "https://api.forwardemail.net/",
|
|
]);
|
|
|
|
try {
|
|
$response = $client->post("v1/emails", [
|
|
"headers" => [
|
|
"Authorization" => $authHeader,
|
|
"Content-Type" => "application/x-www-form-urlencoded",
|
|
],
|
|
"form_params" => [
|
|
"from" => "coryd.dev <hi@admin.coryd.dev>",
|
|
"to" => "hi@coryd.dev",
|
|
"subject" => $subject,
|
|
"text" => $message,
|
|
],
|
|
]);
|
|
|
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
error_log("Request Exception: " . $e->getMessage());
|
|
if ($e->hasResponse()) {
|
|
$errorResponse = (string) $e->getResponse()->getBody();
|
|
error_log("Error Response: " . $errorResponse);
|
|
}
|
|
} catch (\Exception $e) {
|
|
error_log("General Exception: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function isDatabaseAvailable(): bool
|
|
{
|
|
try {
|
|
$response = $this->fetchFromPostgREST("listens", "limit=1");
|
|
return is_array($response);
|
|
} catch (Exception $e) {
|
|
error_log("Database check failed: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
$handler = new NavidromeScrobbleHandler();
|
|
$handler->runScrobbleCheck();
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => $e->getMessage()]);
|
|
}
|