chore(*.php): use pint for php formatting
This commit is contained in:
parent
bd1855a65e
commit
753f3433ce
40 changed files with 2261 additions and 1900 deletions
494
api/scrobble.php
494
api/scrobble.php
|
@ -1,274 +1,312 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
require_once __DIR__.'/../bootstrap.php';
|
||||
|
||||
use App\Classes\ApiHandler;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
header("Content-Type: application/json");
|
||||
header('Content-Type: application/json');
|
||||
|
||||
class NavidromeScrobbleHandler extends ApiHandler
|
||||
{
|
||||
private string $navidromeApiUrl;
|
||||
private string $navidromeAuthToken;
|
||||
private string $forwardEmailApiKey;
|
||||
private array $artistCache = [];
|
||||
private array $albumCache = [];
|
||||
private string $navidromeApiUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->ensureCliAccess();
|
||||
$this->loadExternalServiceKeys();
|
||||
$this->validateAuthorization();
|
||||
}
|
||||
private string $navidromeAuthToken;
|
||||
|
||||
private function loadExternalServiceKeys(): void
|
||||
{
|
||||
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
||||
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
||||
$this->forwardEmailApiKey = getenv("FORWARDEMAIL_API_KEY");
|
||||
}
|
||||
private string $forwardEmailApiKey;
|
||||
|
||||
private function validateAuthorization(): void
|
||||
{
|
||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
||||
$expectedToken = "Bearer " . getenv("NAVIDROME_SCROBBLE_TOKEN");
|
||||
private array $artistCache = [];
|
||||
|
||||
if ($authHeader !== $expectedToken) {
|
||||
http_response_code(401);
|
||||
private array $albumCache = [];
|
||||
|
||||
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
|
||||
{
|
||||
$playDateString = $track["playDate"] ?? null;
|
||||
|
||||
if (!$playDateString) return false;
|
||||
|
||||
$playDate = strtotime($playDateString);
|
||||
|
||||
if ($playDate === false) return false;
|
||||
|
||||
$existingListen = $this->fetchFromApi("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;
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->ensureCliAccess();
|
||||
$this->loadExternalServiceKeys();
|
||||
$this->validateAuthorization();
|
||||
}
|
||||
|
||||
$albumData = $this->getOrCreateAlbum($track["album"], $artistData);
|
||||
|
||||
if (empty($albumData)) {
|
||||
error_log("Failed to retrieve or create album: " . $track["album"]);
|
||||
return;
|
||||
private function loadExternalServiceKeys(): void
|
||||
{
|
||||
$this->navidromeApiUrl = getenv('NAVIDROME_API_URL');
|
||||
$this->navidromeAuthToken = getenv('NAVIDROME_API_TOKEN');
|
||||
$this->forwardEmailApiKey = getenv('FORWARDEMAIL_API_KEY');
|
||||
}
|
||||
|
||||
$this->insertListen($track, $albumData["key"]);
|
||||
}
|
||||
private function validateAuthorization(): void
|
||||
{
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
$expectedToken = 'Bearer '.getenv('NAVIDROME_SCROBBLE_TOKEN');
|
||||
|
||||
private function getOrCreateArtist(string $artistName): array
|
||||
{
|
||||
if (!$this->isDatabaseAvailable()) return [];
|
||||
if (isset($this->artistCache[$artistName])) return $this->artistCache[$artistName];
|
||||
if ($authHeader !== $expectedToken) {
|
||||
http_response_code(401);
|
||||
|
||||
$encodedArtist = rawurlencode($artistName);
|
||||
$existingArtist = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
||||
echo json_encode(['error' => 'Unauthorized.']);
|
||||
|
||||
if (!empty($existingArtist)) return $this->artistCache[$artistName] = $existingArtist[0];
|
||||
|
||||
$response = $this->makeRequest("POST", "artists", [
|
||||
"json" => [
|
||||
"mbid" => "",
|
||||
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
||||
"name_string" => $artistName,
|
||||
"slug" => "/music",
|
||||
"country" => "",
|
||||
"description" => "",
|
||||
"tentative" => true,
|
||||
"favorite" => false,
|
||||
"tattoo" => false,
|
||||
"total_plays" => 0
|
||||
],
|
||||
"headers" => ["Prefer" => "return=representation"]
|
||||
]);
|
||||
|
||||
$inserted = $response[0] ?? null;
|
||||
if ($inserted) $this->sendFailureEmail("New tentative artist record", "A new tentative artist record was inserted for: $artistName");
|
||||
|
||||
return $this->artistCache[$artistName] = $inserted ?? [];
|
||||
}
|
||||
|
||||
private function getOrCreateAlbum(string $albumName, array $artistData): array
|
||||
{
|
||||
if (!$this->isDatabaseAvailable()) return [];
|
||||
|
||||
$albumKey = $this->generateAlbumKey($artistData["name_string"], $albumName);
|
||||
|
||||
if (isset($this->albumCache[$albumKey])) return $this->albumCache[$albumKey];
|
||||
|
||||
$encodedAlbumKey = rawurlencode($albumKey);
|
||||
$existingAlbum = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
||||
|
||||
if (!empty($existingAlbum)) return $this->albumCache[$albumKey] = $existingAlbum[0];
|
||||
|
||||
$artistId = $artistData["id"] ?? null;
|
||||
|
||||
if (!$artistId) {
|
||||
error_log("Artist ID missing for album creation: " . $albumName);
|
||||
return [];
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->makeRequest("POST", "albums", [
|
||||
"json" => [
|
||||
"mbid" => null,
|
||||
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
||||
"key" => $albumKey,
|
||||
"name" => $albumName,
|
||||
"tentative" => true,
|
||||
"total_plays" => 0,
|
||||
"artist" => $artistId
|
||||
],
|
||||
"headers" => ["Prefer" => "return=representation"]
|
||||
]);
|
||||
public function runScrobbleCheck(): void
|
||||
{
|
||||
$recentTracks = $this->fetchRecentlyPlayed();
|
||||
|
||||
$inserted = $response[0] ?? null;
|
||||
if ($inserted) $this->sendFailureEmail("New tentative album record", "A new tentative album record was inserted:\n\nAlbum: $albumName\nKey: $albumKey");
|
||||
if (empty($recentTracks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->albumCache[$albumKey] = $inserted ?? [];
|
||||
}
|
||||
foreach ($recentTracks as $track) {
|
||||
if ($this->isTrackAlreadyScrobbled($track)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private function insertListen(array $track, string $albumKey): void
|
||||
{
|
||||
$payload = [
|
||||
"artist_name" => $track["artist"],
|
||||
"album_name" => $track["album"],
|
||||
"track_name" => $track["title"],
|
||||
"album_key" => $albumKey
|
||||
];
|
||||
|
||||
if (!empty($track["playDate"])) {
|
||||
$playDate = strtotime($track["playDate"]);
|
||||
if ($playDate !== false) $payload["listened_at"] = $playDate;
|
||||
$this->handleTrackScrobble($track);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($payload["listened_at"])) {
|
||||
error_log("Skipping track due to missing or invalid listened_at: " . json_encode($track));
|
||||
return;
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
$this->makeRequest("POST", "listens", ["json" => $payload]);
|
||||
}
|
||||
private function isTrackAlreadyScrobbled(array $track): bool
|
||||
{
|
||||
$playDateString = $track['playDate'] ?? null;
|
||||
|
||||
private function generateAlbumKey(string $artistName, string $albumName): string
|
||||
{
|
||||
$artistKey = sanitizeMediaString($artistName);
|
||||
$albumKey = sanitizeMediaString($albumName);
|
||||
if (! $playDateString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return "{$artistKey}-{$albumKey}";
|
||||
}
|
||||
$playDate = strtotime($playDateString);
|
||||
|
||||
private function sendFailureEmail(string $subject, string $message): void
|
||||
{
|
||||
if (!$this->isDatabaseAvailable()) return;
|
||||
if ($playDate === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authHeader = "Basic " . base64_encode($this->forwardEmailApiKey . ":");
|
||||
$client = new Client(["base_uri" => "https://api.forwardemail.net/"]);
|
||||
$existingListen = $this->fetchFromApi('listens', "listened_at=eq.{$playDate}&limit=1");
|
||||
|
||||
try {
|
||||
$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()) error_log("Error Response: " . (string) $e->getResponse()->getBody());
|
||||
} catch (\Exception $e) {
|
||||
error_log("General Exception: " . $e->getMessage());
|
||||
return ! empty($existingListen);
|
||||
}
|
||||
}
|
||||
|
||||
private function isDatabaseAvailable(): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->fetchFromApi("listens", "limit=1");
|
||||
private function handleTrackScrobble(array $track): void
|
||||
{
|
||||
$artistData = $this->getOrCreateArtist($track['artist']);
|
||||
|
||||
return is_array($response);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Database check failed: " . $e->getMessage());
|
||||
if (empty($artistData)) {
|
||||
error_log('Failed to retrieve or create artist: '.$track['artist']);
|
||||
|
||||
return false;
|
||||
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()) {
|
||||
return [];
|
||||
}
|
||||
if (isset($this->artistCache[$artistName])) {
|
||||
return $this->artistCache[$artistName];
|
||||
}
|
||||
|
||||
$encodedArtist = rawurlencode($artistName);
|
||||
$existingArtist = $this->fetchFromApi('artists', "name_string=eq.{$encodedArtist}&limit=1");
|
||||
|
||||
if (! empty($existingArtist)) {
|
||||
return $this->artistCache[$artistName] = $existingArtist[0];
|
||||
}
|
||||
|
||||
$response = $this->makeRequest('POST', 'artists', [
|
||||
'json' => [
|
||||
'mbid' => '',
|
||||
'art' => '4cef75db-831f-4f5d-9333-79eaa5bb55ee',
|
||||
'name_string' => $artistName,
|
||||
'slug' => '/music',
|
||||
'country' => '',
|
||||
'description' => '',
|
||||
'tentative' => true,
|
||||
'favorite' => false,
|
||||
'tattoo' => false,
|
||||
'total_plays' => 0,
|
||||
],
|
||||
'headers' => ['Prefer' => 'return=representation'],
|
||||
]);
|
||||
|
||||
$inserted = $response[0] ?? null;
|
||||
if ($inserted) {
|
||||
$this->sendFailureEmail('New tentative artist record', "A new tentative artist record was inserted for: $artistName");
|
||||
}
|
||||
|
||||
return $this->artistCache[$artistName] = $inserted ?? [];
|
||||
}
|
||||
|
||||
private function getOrCreateAlbum(string $albumName, array $artistData): array
|
||||
{
|
||||
if (! $this->isDatabaseAvailable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$albumKey = $this->generateAlbumKey($artistData['name_string'], $albumName);
|
||||
|
||||
if (isset($this->albumCache[$albumKey])) {
|
||||
return $this->albumCache[$albumKey];
|
||||
}
|
||||
|
||||
$encodedAlbumKey = rawurlencode($albumKey);
|
||||
$existingAlbum = $this->fetchFromApi('albums', "key=eq.{$encodedAlbumKey}&limit=1");
|
||||
|
||||
if (! empty($existingAlbum)) {
|
||||
return $this->albumCache[$albumKey] = $existingAlbum[0];
|
||||
}
|
||||
|
||||
$artistId = $artistData['id'] ?? null;
|
||||
|
||||
if (! $artistId) {
|
||||
error_log('Artist ID missing for album creation: '.$albumName);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$response = $this->makeRequest('POST', 'albums', [
|
||||
'json' => [
|
||||
'mbid' => null,
|
||||
'art' => '4cef75db-831f-4f5d-9333-79eaa5bb55ee',
|
||||
'key' => $albumKey,
|
||||
'name' => $albumName,
|
||||
'tentative' => true,
|
||||
'total_plays' => 0,
|
||||
'artist' => $artistId,
|
||||
],
|
||||
'headers' => ['Prefer' => 'return=representation'],
|
||||
]);
|
||||
|
||||
$inserted = $response[0] ?? null;
|
||||
if ($inserted) {
|
||||
$this->sendFailureEmail('New tentative album record', "A new tentative album record was inserted:\n\nAlbum: $albumName\nKey: $albumKey");
|
||||
}
|
||||
|
||||
return $this->albumCache[$albumKey] = $inserted ?? [];
|
||||
}
|
||||
|
||||
private function insertListen(array $track, string $albumKey): void
|
||||
{
|
||||
$payload = [
|
||||
'artist_name' => $track['artist'],
|
||||
'album_name' => $track['album'],
|
||||
'track_name' => $track['title'],
|
||||
'album_key' => $albumKey,
|
||||
];
|
||||
|
||||
if (! empty($track['playDate'])) {
|
||||
$playDate = strtotime($track['playDate']);
|
||||
if ($playDate !== false) {
|
||||
$payload['listened_at'] = $playDate;
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($payload['listened_at'])) {
|
||||
error_log('Skipping track due to missing or invalid listened_at: '.json_encode($track));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->makeRequest('POST', 'listens', ['json' => $payload]);
|
||||
}
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authHeader = 'Basic '.base64_encode($this->forwardEmailApiKey.':');
|
||||
$client = new Client(['base_uri' => 'https://api.forwardemail.net/']);
|
||||
|
||||
try {
|
||||
$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()) {
|
||||
error_log('Error Response: '.(string) $e->getResponse()->getBody());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('General Exception: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function isDatabaseAvailable(): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->fetchFromApi('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();
|
||||
$handler = new NavidromeScrobbleHandler();
|
||||
$handler->runScrobbleCheck();
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
http_response_code(500);
|
||||
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue