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 ", "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()]); }