feat(*.php, *.psql): deduplicate API code + performance improvements
This commit is contained in:
parent
cf3dac8a46
commit
4bad005e58
31 changed files with 502 additions and 666 deletions
|
@ -1,72 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Classes;
|
namespace App\Classes;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
|
|
||||||
require __DIR__ . "/../../vendor/autoload.php";
|
require __DIR__ . "/BaseHandler.php";
|
||||||
|
|
||||||
abstract class ApiHandler
|
abstract class ApiHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->loadEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl =
|
|
||||||
$_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL") ?: "";
|
|
||||||
$this->postgrestApiKey =
|
|
||||||
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY") ?: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function ensureCliAccess(): void
|
protected function ensureCliAccess(): void
|
||||||
{
|
{
|
||||||
if (php_sapi_name() !== "cli" && $_SERVER["REQUEST_METHOD"] !== "POST") {
|
if (php_sapi_name() !== 'cli' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
$this->redirectNotFound();
|
$this->sendErrorResponse("Not Found", 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function redirectNotFound(): void
|
|
||||||
{
|
|
||||||
header("Location: /404", true, 302);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function fetchFromPostgREST(
|
|
||||||
string $endpoint,
|
|
||||||
string $query = "",
|
|
||||||
string $method = "GET",
|
|
||||||
?array $body = null
|
|
||||||
): array {
|
|
||||||
$url = "{$this->postgrestUrl}/{$endpoint}?{$query}";
|
|
||||||
$options = [
|
|
||||||
"headers" => [
|
|
||||||
"Content-Type" => "application/json",
|
|
||||||
"Authorization" => "Bearer {$this->postgrestApiKey}",
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($method === "POST" && $body) $options["json"] = $body;
|
|
||||||
|
|
||||||
$response = (new Client())->request($method, $url, $options);
|
|
||||||
|
|
||||||
return json_decode($response->getBody(), true) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sendResponse(string $message, int $statusCode): void
|
|
||||||
{
|
|
||||||
http_response_code($statusCode);
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
echo json_encode(["message" => $message]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sendErrorResponse(string $message, int $statusCode): void
|
|
||||||
{
|
|
||||||
$this->sendResponse($message, $statusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,60 +16,71 @@ abstract class BaseHandler
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->loadEnvironment();
|
$this->loadEnvironment();
|
||||||
|
$this->initializeCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
private function loadEnvironment(): void
|
||||||
{
|
{
|
||||||
$this->postgrestUrl =
|
$this->postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL") ?? "";
|
||||||
$_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL") ?: "";
|
$this->postgrestApiKey = $_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY") ?? "";
|
||||||
$this->postgrestApiKey =
|
|
||||||
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY") ?: "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function makeRequest(
|
protected function initializeCache(): void
|
||||||
string $method,
|
{
|
||||||
string $endpoint,
|
if (class_exists("Redis")) {
|
||||||
array $options = []
|
try {
|
||||||
): array {
|
$redis = new \Redis();
|
||||||
|
$redis->connect("127.0.0.1", 6379);
|
||||||
|
$this->cache = $redis;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Redis connection failed: " . $e->getMessage());
|
||||||
|
$this->cache = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("Redis extension not found — caching disabled.");
|
||||||
|
$this->cache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeRequest(string $method, string $endpoint, array $options = []): array
|
||||||
|
{
|
||||||
$client = new Client();
|
$client = new Client();
|
||||||
$url = rtrim($this->postgrestUrl, "/") . "/" . ltrim($endpoint, "/");
|
$url = rtrim($this->postgrestUrl, "/") . "/" . ltrim($endpoint, "/");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->request(
|
$response = $client->request($method, $url, array_merge_recursive([
|
||||||
$method,
|
"headers" => [
|
||||||
$url,
|
"Authorization" => "Bearer {$this->postgrestApiKey}",
|
||||||
array_merge($options, [
|
"Content-Type" => "application/json",
|
||||||
"headers" => [
|
]
|
||||||
"Authorization" => "Bearer {$this->postgrestApiKey}",
|
], $options));
|
||||||
"Content-Type" => "application/json",
|
|
||||||
],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
$responseBody = $response->getBody()->getContents();
|
$responseBody = $response->getBody()->getContents();
|
||||||
|
|
||||||
if (empty($responseBody)) return [];
|
if (empty($responseBody)) return [];
|
||||||
|
|
||||||
$responseData = json_decode($responseBody, true);
|
$data = json_decode($responseBody, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new \Exception("Invalid JSON: " . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) throw new \Exception("Invalid JSON response: {$responseBody}");
|
return $data;
|
||||||
|
|
||||||
return $responseData;
|
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
$response = $e->getResponse();
|
$response = $e->getResponse();
|
||||||
$statusCode = $response ? $response->getStatusCode() : "N/A";
|
$statusCode = $response ? $response->getStatusCode() : 'N/A';
|
||||||
$responseBody = $response
|
$responseBody = $response ? $response->getBody()->getContents() : 'No response';
|
||||||
? $response->getBody()->getContents()
|
|
||||||
: "No response body";
|
|
||||||
|
|
||||||
throw new \Exception(
|
throw new \Exception("HTTP {$method} {$url} failed with status {$statusCode}: {$responseBody}");
|
||||||
"Request to {$url} failed with status {$statusCode}. Response: {$responseBody}"
|
|
||||||
);
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
throw new \Exception("Request to {$url} failed: " . $e->getMessage());
|
throw new \Exception("Request error: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function fetchFromApi(string $endpoint, string $query = ""): array
|
||||||
|
{
|
||||||
|
$url = $endpoint . ($query ? "?{$query}" : "");
|
||||||
|
return $this->makeRequest("GET", $url);
|
||||||
|
}
|
||||||
|
|
||||||
protected function sendResponse(array $data, int $statusCode = 200): void
|
protected function sendResponse(array $data, int $statusCode = 200): void
|
||||||
{
|
{
|
||||||
http_response_code($statusCode);
|
http_response_code($statusCode);
|
||||||
|
@ -78,52 +89,8 @@ abstract class BaseHandler
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sendErrorResponse(
|
protected function sendErrorResponse(string $message, int $statusCode = 500): void
|
||||||
string $message,
|
{
|
||||||
int $statusCode = 500
|
|
||||||
): void {
|
|
||||||
$this->sendResponse(["error" => $message], $statusCode);
|
$this->sendResponse(["error" => $message], $statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fetchFromApi(string $endpoint, string $query): array
|
|
||||||
{
|
|
||||||
$client = new Client();
|
|
||||||
$url =
|
|
||||||
rtrim($this->postgrestUrl, "/") .
|
|
||||||
"/" .
|
|
||||||
ltrim($endpoint, "/") .
|
|
||||||
"?" .
|
|
||||||
$query;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = $client->request("GET", $url, [
|
|
||||||
"headers" => [
|
|
||||||
"Content-Type" => "application/json",
|
|
||||||
"Authorization" => "Bearer {$this->postgrestApiKey}",
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->getStatusCode() !== 200) throw new Exception("API call to {$url} failed with status code " . $response->getStatusCode());
|
|
||||||
|
|
||||||
return json_decode($response->getBody(), true);
|
|
||||||
} catch (RequestException $e) {
|
|
||||||
throw new Exception("Error fetching from API: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function initializeCache(): void
|
|
||||||
{
|
|
||||||
if (class_exists("Redis")) {
|
|
||||||
$redis = new \Redis();
|
|
||||||
try {
|
|
||||||
$redis->connect("127.0.0.1", 6379);
|
|
||||||
$this->cache = $redis;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("Redis connection failed: " . $e->getMessage());
|
|
||||||
$this->cache = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->cache = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,6 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
class ArtistImportHandler extends ApiHandler
|
class ArtistImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
private string $artistImportToken;
|
private string $artistImportToken;
|
||||||
private string $placeholderImageId = "4cef75db-831f-4f5d-9333-79eaa5bb55ee";
|
private string $placeholderImageId = "4cef75db-831f-4f5d-9333-79eaa5bb55ee";
|
||||||
private string $navidromeApiUrl;
|
private string $navidromeApiUrl;
|
||||||
|
@ -20,13 +17,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl = getenv("POSTGREST_URL");
|
|
||||||
$this->postgrestApiKey = getenv("POSTGREST_API_KEY");
|
|
||||||
$this->artistImportToken = getenv("ARTIST_IMPORT_TOKEN");
|
$this->artistImportToken = getenv("ARTIST_IMPORT_TOKEN");
|
||||||
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
||||||
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
||||||
|
@ -41,11 +32,13 @@ class ArtistImportHandler extends ApiHandler
|
||||||
$providedToken = $input["token"] ?? null;
|
$providedToken = $input["token"] ?? null;
|
||||||
$artistId = $input["artistId"] ?? null;
|
$artistId = $input["artistId"] ?? null;
|
||||||
|
|
||||||
if (!$providedToken || $providedToken !== $this->artistImportToken) {
|
if ($providedToken !== $this->artistImportToken) {
|
||||||
$this->sendJsonResponse("error", "Unauthorized access", 401);
|
$this->sendJsonResponse("error", "Unauthorized access", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$artistId) $this->sendJsonResponse("error", "Artist ID is required", 400);
|
if (!$artistId) {
|
||||||
|
$this->sendJsonResponse("error", "Artist ID is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$artistData = $this->fetchNavidromeArtist($artistId);
|
$artistData = $this->fetchNavidromeArtist($artistId);
|
||||||
|
@ -54,7 +47,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
if ($artistExists) $this->processAlbums($artistId, $artistData->name);
|
if ($artistExists) $this->processAlbums($artistId, $artistData->name);
|
||||||
|
|
||||||
$this->sendJsonResponse("message", "Artist and albums synced successfully", 200);
|
$this->sendJsonResponse("message", "Artist and albums synced successfully", 200);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->sendJsonResponse("error", "Error: " . $e->getMessage(), 500);
|
$this->sendJsonResponse("error", "Error: " . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +60,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchNavidromeArtist(string $artistId)
|
private function fetchNavidromeArtist(string $artistId): object
|
||||||
{
|
{
|
||||||
$client = new Client();
|
$client = new Client();
|
||||||
$response = $client->get("{$this->navidromeApiUrl}/api/artist/{$artistId}", [
|
$response = $client->get("{$this->navidromeApiUrl}/api/artist/{$artistId}", [
|
||||||
|
@ -77,7 +70,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return json_decode($response->getBody(), false);
|
return json_decode($response->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchNavidromeAlbums(string $artistId): array
|
private function fetchNavidromeAlbums(string $artistId): array
|
||||||
|
@ -103,16 +96,14 @@ class ArtistImportHandler extends ApiHandler
|
||||||
private function processArtist(object $artistData): bool
|
private function processArtist(object $artistData): bool
|
||||||
{
|
{
|
||||||
$artistName = $artistData->name ?? "";
|
$artistName = $artistData->name ?? "";
|
||||||
|
if (!$artistName) throw new \Exception("Artist name is missing.");
|
||||||
if (!$artistName) throw new Exception("Artist name is missing from Navidrome data.");
|
|
||||||
|
|
||||||
$existingArtist = $this->getArtistByName($artistName);
|
$existingArtist = $this->getArtistByName($artistName);
|
||||||
|
|
||||||
if ($existingArtist) return true;
|
if ($existingArtist) return true;
|
||||||
|
|
||||||
$artistKey = sanitizeMediaString($artistName);
|
$artistKey = sanitizeMediaString($artistName);
|
||||||
$slug = "/music/artists/{$artistKey}";
|
$slug = "/music/artists/{$artistKey}";
|
||||||
$description = strip_tags($artistData->biography) ?? "";
|
$description = strip_tags($artistData->biography ?? "");
|
||||||
$genre = $this->resolveGenreId($artistData->genres[0]->name ?? "");
|
$genre = $this->resolveGenreId($artistData->genres[0]->name ?? "");
|
||||||
$starred = $artistData->starred ?? false;
|
$starred = $artistData->starred ?? false;
|
||||||
|
|
||||||
|
@ -127,35 +118,34 @@ class ArtistImportHandler extends ApiHandler
|
||||||
"genres" => $genre,
|
"genres" => $genre,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->saveArtist($artistPayload);
|
$this->makeRequest("POST", "artists", ["json" => $artistPayload]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function processAlbums(string $artistId, string $artistName): void
|
private function processAlbums(string $artistId, string $artistName): void
|
||||||
{
|
{
|
||||||
$artist = $this->getArtistByName($artistName);
|
$artist = $this->getArtistByName($artistName);
|
||||||
|
if (!$artist) throw new \Exception("Artist not found after insert.");
|
||||||
if (!$artist) throw new Exception("Artist not found in the database.");
|
|
||||||
|
|
||||||
$existingAlbums = $this->getExistingAlbums($artist["id"]);
|
$existingAlbums = $this->getExistingAlbums($artist["id"]);
|
||||||
$existingAlbumKeys = array_column($existingAlbums, "key");
|
$existingAlbumKeys = array_column($existingAlbums, "key");
|
||||||
|
|
||||||
$navidromeAlbums = $this->fetchNavidromeAlbums($artistId);
|
$navidromeAlbums = $this->fetchNavidromeAlbums($artistId);
|
||||||
|
|
||||||
foreach ($navidromeAlbums as $album) {
|
foreach ($navidromeAlbums as $album) {
|
||||||
$albumName = $album["name"];
|
$albumName = $album["name"] ?? "";
|
||||||
$releaseYearRaw = $album["date"] ?? null;
|
$releaseYearRaw = $album["date"] ?? null;
|
||||||
$releaseYear = null;
|
$releaseYear = null;
|
||||||
|
|
||||||
if ($releaseYearRaw) {
|
if ($releaseYearRaw && preg_match('/^\d{4}/', $releaseYearRaw, $matches)) {
|
||||||
if (preg_match('/^\d{4}/', $releaseYearRaw, $matches)) $releaseYear = (int)$matches[0];
|
$releaseYear = (int)$matches[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$artistKey = sanitizeMediaString($artistName);
|
$artistKey = sanitizeMediaString($artistName);
|
||||||
$albumKey = $artistKey . "-" . sanitizeMediaString($albumName);
|
$albumKey = "{$artistKey}-" . sanitizeMediaString($albumName);
|
||||||
|
|
||||||
if (in_array($albumKey, $existingAlbumKeys)) {
|
if (in_array($albumKey, $existingAlbumKeys)) {
|
||||||
error_log("Skipping existing album: " . $albumName);
|
error_log("Skipping existing album: {$albumName}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,8 +160,8 @@ class ArtistImportHandler extends ApiHandler
|
||||||
"tentative" => true,
|
"tentative" => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->saveAlbum($albumPayload);
|
$this->makeRequest("POST", "albums", ["json" => $albumPayload]);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Error adding album '{$albumName}': " . $e->getMessage());
|
error_log("Error adding album '{$albumName}': " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,34 +169,19 @@ class ArtistImportHandler extends ApiHandler
|
||||||
|
|
||||||
private function getArtistByName(string $nameString): ?array
|
private function getArtistByName(string $nameString): ?array
|
||||||
{
|
{
|
||||||
$query = "name_string=eq." . urlencode($nameString);
|
$response = $this->fetchFromApi("artists", "name_string=eq." . urlencode($nameString));
|
||||||
$response = $this->fetchFromPostgREST("artists", $query, "GET");
|
|
||||||
|
|
||||||
return $response[0] ?? null;
|
return $response[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveArtist(array $artistPayload): void
|
|
||||||
{
|
|
||||||
$this->fetchFromPostgREST("artists", "", "POST", $artistPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function saveAlbum(array $albumPayload): void
|
|
||||||
{
|
|
||||||
$this->fetchFromPostgREST("albums", "", "POST", $albumPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveGenreId(string $genreName): ?string
|
|
||||||
{
|
|
||||||
$genres = $this->fetchFromPostgREST("genres", "name=eq." . urlencode(strtolower($genreName)), "GET");
|
|
||||||
|
|
||||||
if (!empty($genres)) return $genres[0]["id"];
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getExistingAlbums(string $artistId): array
|
private function getExistingAlbums(string $artistId): array
|
||||||
{
|
{
|
||||||
return $this->fetchFromPostgREST("albums", "artist=eq." . urlencode($artistId), "GET");
|
return $this->fetchFromApi("albums", "artist=eq." . urlencode($artistId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveGenreId(string $genreName): ?string
|
||||||
|
{
|
||||||
|
$genres = $this->fetchFromApi("genres", "name=eq." . urlencode(strtolower($genreName)));
|
||||||
|
return $genres[0]["id"] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,46 +7,39 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
class BookImportHandler extends ApiHandler
|
class BookImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
private string $bookImportToken;
|
private string $bookImportToken;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
$this->bookImportToken = $_ENV["BOOK_IMPORT_TOKEN"] ?? getenv("BOOK_IMPORT_TOKEN");
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL");
|
|
||||||
$this->postgrestApiKey =
|
|
||||||
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY");
|
|
||||||
$this->bookImportToken =
|
|
||||||
$_ENV["BOOK_IMPORT_TOKEN"] ?? getenv("BOOK_IMPORT_TOKEN");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRequest(): void
|
public function handleRequest(): void
|
||||||
{
|
{
|
||||||
$input = json_decode(file_get_contents("php://input"), true);
|
$input = json_decode(file_get_contents("php://input"), true);
|
||||||
|
|
||||||
if (!$input) $this->sendErrorResponse("Invalid or missing JSON body", 400);
|
if (!$input) {
|
||||||
|
$this->sendErrorResponse("Invalid or missing JSON body", 400);
|
||||||
|
}
|
||||||
|
|
||||||
$providedToken = $input["token"] ?? null;
|
$providedToken = $input["token"] ?? null;
|
||||||
$isbn = $input["isbn"] ?? null;
|
$isbn = $input["isbn"] ?? null;
|
||||||
|
|
||||||
if (!$providedToken || $providedToken !== $this->bookImportToken) $this->sendErrorResponse("Unauthorized access", 401);
|
if ($providedToken !== $this->bookImportToken) {
|
||||||
|
$this->sendErrorResponse("Unauthorized access", 401);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$isbn) $this->sendErrorResponse("isbn parameter is required", 400);
|
if (!$isbn) {
|
||||||
|
$this->sendErrorResponse("isbn parameter is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$bookData = $this->fetchBookData($isbn);
|
$bookData = $this->fetchBookData($isbn);
|
||||||
$this->processBook($bookData);
|
$this->processBook($bookData);
|
||||||
|
$this->sendResponse(["message" => "Book imported successfully"], 200);
|
||||||
$this->sendResponse("Book imported successfully", 200);
|
} catch (\Exception $e) {
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->sendErrorResponse("Error: " . $e->getMessage(), 500);
|
$this->sendErrorResponse("Error: " . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +59,9 @@ class BookImportHandler extends ApiHandler
|
||||||
$data = json_decode($response->getBody(), true);
|
$data = json_decode($response->getBody(), true);
|
||||||
$bookKey = "ISBN:{$isbn}";
|
$bookKey = "ISBN:{$isbn}";
|
||||||
|
|
||||||
if (empty($data[$bookKey])) throw new Exception("Book data not found for ISBN: {$isbn}");
|
if (empty($data[$bookKey])) {
|
||||||
|
throw new \Exception("Book data not found for ISBN: {$isbn}");
|
||||||
|
}
|
||||||
|
|
||||||
return $data[$bookKey];
|
return $data[$bookKey];
|
||||||
}
|
}
|
||||||
|
@ -80,11 +75,14 @@ class BookImportHandler extends ApiHandler
|
||||||
$author = $bookData["authors"][0]["name"] ?? null;
|
$author = $bookData["authors"][0]["name"] ?? null;
|
||||||
$description = $bookData["description"] ?? ($bookData["notes"] ?? "");
|
$description = $bookData["description"] ?? ($bookData["notes"] ?? "");
|
||||||
|
|
||||||
if (!$isbn || !$title || !$author) throw new Exception("Missing essential book data (title, author, or ISBN).");
|
if (!$isbn || !$title || !$author) {
|
||||||
|
throw new \Exception("Missing essential book data (title, author, or ISBN).");
|
||||||
|
}
|
||||||
|
|
||||||
$existingBook = $this->getBookByISBN($isbn);
|
$existingBook = $this->getBookByISBN($isbn);
|
||||||
|
if ($existingBook) {
|
||||||
if ($existingBook) throw new Exception("Book with ISBN {$isbn} already exists.");
|
throw new \Exception("Book with ISBN {$isbn} already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
$bookPayload = [
|
$bookPayload = [
|
||||||
"isbn" => $isbn,
|
"isbn" => $isbn,
|
||||||
|
@ -95,19 +93,12 @@ class BookImportHandler extends ApiHandler
|
||||||
"slug" => "/books/" . $isbn,
|
"slug" => "/books/" . $isbn,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->saveBook($bookPayload);
|
$this->makeRequest("POST", "books", ["json" => $bookPayload]);
|
||||||
}
|
|
||||||
|
|
||||||
private function saveBook(array $bookPayload): void
|
|
||||||
{
|
|
||||||
$this->fetchFromPostgREST("books", "", "POST", $bookPayload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getBookByISBN(string $isbn): ?array
|
private function getBookByISBN(string $isbn): ?array
|
||||||
{
|
{
|
||||||
$query = "isbn=eq." . urlencode($isbn);
|
$response = $this->fetchFromApi("books", "isbn=eq." . urlencode($isbn));
|
||||||
$response = $this->fetchFromPostgREST("books", $query, "GET");
|
|
||||||
|
|
||||||
return $response[0] ?? null;
|
return $response[0] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,9 @@ class ContactHandler extends BaseHandler
|
||||||
|
|
||||||
public function __construct(?Client $httpClient = null)
|
public function __construct(?Client $httpClient = null)
|
||||||
{
|
{
|
||||||
|
parent::__construct();
|
||||||
$this->httpClient = $httpClient ?? new Client();
|
$this->httpClient = $httpClient ?? new Client();
|
||||||
$this->loadEnvironment();
|
$this->forwardEmailApiKey = $_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY");
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL");
|
|
||||||
$this->postgrestApiKey =
|
|
||||||
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY");
|
|
||||||
$this->forwardEmailApiKey =
|
|
||||||
$_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRequest(): void
|
public function handleRequest(): void
|
||||||
|
@ -42,7 +34,7 @@ class ContactHandler extends BaseHandler
|
||||||
$rawBody = file_get_contents("php://input");
|
$rawBody = file_get_contents("php://input");
|
||||||
$formData = json_decode($rawBody, true);
|
$formData = json_decode($rawBody, true);
|
||||||
if (!$formData || !isset($formData["data"])) {
|
if (!$formData || !isset($formData["data"])) {
|
||||||
throw new Exception("Invalid JSON payload.");
|
throw new \Exception("Invalid JSON payload.");
|
||||||
}
|
}
|
||||||
$formData = $formData["data"];
|
$formData = $formData["data"];
|
||||||
} elseif (
|
} elseif (
|
||||||
|
@ -93,7 +85,7 @@ class ContactHandler extends BaseHandler
|
||||||
$this->saveToDatabase($contactData);
|
$this->saveToDatabase($contactData);
|
||||||
$this->sendNotificationEmail($contactData);
|
$this->sendNotificationEmail($contactData);
|
||||||
$this->sendRedirect("/contact/success");
|
$this->sendRedirect("/contact/success");
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Error handling contact form submission: " . $e->getMessage());
|
error_log("Error handling contact form submission: " . $e->getMessage());
|
||||||
$this->sendErrorResponse($e->getMessage(), 400);
|
$this->sendErrorResponse($e->getMessage(), 400);
|
||||||
}
|
}
|
||||||
|
@ -103,7 +95,7 @@ class ContactHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
$referer = $_SERVER["HTTP_REFERER"] ?? "";
|
$referer = $_SERVER["HTTP_REFERER"] ?? "";
|
||||||
$allowedDomain = "coryd.dev";
|
$allowedDomain = "coryd.dev";
|
||||||
if (!str_contains($referer, $allowedDomain)) throw new Exception("Invalid submission origin.");
|
if (!str_contains($referer, $allowedDomain)) throw new \Exception("Invalid submission origin.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkRateLimit(): void
|
private function checkRateLimit(): void
|
||||||
|
@ -132,7 +124,7 @@ class ContactHandler extends BaseHandler
|
||||||
|
|
||||||
private function enforceHttps(): void
|
private function enforceHttps(): void
|
||||||
{
|
{
|
||||||
if (empty($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] !== "on") throw new Exception("Secure connection required. Use HTTPS.");
|
if (empty($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] !== "on") throw new \Exception("Secure connection required. Use HTTPS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isBlockedDomain(string $email): bool
|
private function isBlockedDomain(string $email): bool
|
||||||
|
@ -171,7 +163,7 @@ class ContactHandler extends BaseHandler
|
||||||
|
|
||||||
if ($response->getStatusCode() >= 400) {
|
if ($response->getStatusCode() >= 400) {
|
||||||
$errorResponse = json_decode($response->getBody(), true);
|
$errorResponse = json_decode($response->getBody(), true);
|
||||||
throw new Exception(
|
throw new \Exception(
|
||||||
"PostgREST error: " . ($errorResponse["message"] ?? "Unknown error")
|
"PostgREST error: " . ($errorResponse["message"] ?? "Unknown error")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -204,7 +196,7 @@ class ContactHandler extends BaseHandler
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($response->getStatusCode() >= 400) throw new Exception("Failed to send email notification.");
|
if ($response->getStatusCode() >= 400) throw new \Exception("Failed to send email notification.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendRedirect(string $path): void
|
private function sendRedirect(string $path): void
|
||||||
|
@ -221,7 +213,7 @@ class ContactHandler extends BaseHandler
|
||||||
try {
|
try {
|
||||||
$handler = new ContactHandler();
|
$handler = new ContactHandler();
|
||||||
$handler->handleRequest();
|
$handler->handleRequest();
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Contact form error: " . $e->getMessage());
|
error_log("Contact form error: " . $e->getMessage());
|
||||||
echo json_encode(["error" => $e->getMessage()]);
|
echo json_encode(["error" => $e->getMessage()]);
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|
171
api/mastodon.php
171
api/mastodon.php
|
@ -7,12 +7,9 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
class MastodonPostHandler extends ApiHandler
|
class MastodonPostHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
private string $mastodonAccessToken;
|
private string $mastodonAccessToken;
|
||||||
private string $rssFeedUrl;
|
private string $rssFeedUrl = "https://www.coryd.dev/feeds/syndication.xml";
|
||||||
private string $baseUrl;
|
private string $baseUrl = "https://www.coryd.dev";
|
||||||
|
|
||||||
private const MASTODON_API_STATUS = "https://follow.coryd.dev/api/v1/statuses";
|
private const MASTODON_API_STATUS = "https://follow.coryd.dev/api/v1/statuses";
|
||||||
|
|
||||||
|
@ -22,21 +19,11 @@ class MastodonPostHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
|
||||||
$this->validateAuthorization();
|
|
||||||
$this->httpClient = $httpClient ?: new Client();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
$this->mastodonAccessToken = getenv("MASTODON_ACCESS_TOKEN") ?: $_ENV["MASTODON_ACCESS_TOKEN"] ?? "";
|
||||||
{
|
$this->httpClient = $httpClient ?: new Client();
|
||||||
$this->postgrestUrl =
|
|
||||||
getenv("POSTGREST_URL") ?: $_ENV["POSTGREST_URL"] ?? "";
|
$this->validateAuthorization();
|
||||||
$this->postgrestApiKey =
|
|
||||||
getenv("POSTGREST_API_KEY") ?: $_ENV["POSTGREST_API_KEY"] ?? "";
|
|
||||||
$this->mastodonAccessToken =
|
|
||||||
getenv("MASTODON_ACCESS_TOKEN") ?: $_ENV["MASTODON_ACCESS_TOKEN"] ?? "";
|
|
||||||
$this->rssFeedUrl = "https://www.coryd.dev/feeds/syndication.xml";
|
|
||||||
$this->baseUrl = "https://www.coryd.dev";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateAuthorization(): void
|
private function validateAuthorization(): void
|
||||||
|
@ -46,7 +33,7 @@ class MastodonPostHandler extends ApiHandler
|
||||||
|
|
||||||
if ($authHeader !== $expectedToken) {
|
if ($authHeader !== $expectedToken) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized."]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,16 +48,16 @@ class MastodonPostHandler extends ApiHandler
|
||||||
$latestItems = $this->fetchRSSFeed($this->rssFeedUrl);
|
$latestItems = $this->fetchRSSFeed($this->rssFeedUrl);
|
||||||
|
|
||||||
foreach (array_reverse($latestItems) as $item) {
|
foreach (array_reverse($latestItems) as $item) {
|
||||||
$existingPost = $this->fetchFromPostgREST("mastodon_posts", "link=eq." . urlencode($item["link"]));
|
$existing = $this->fetchFromApi("mastodon_posts", "link=eq." . urlencode($item["link"]));
|
||||||
|
if (!empty($existing)) continue;
|
||||||
if (!empty($existingPost)) continue;
|
|
||||||
|
|
||||||
$content = $this->truncateContent(
|
$content = $this->truncateContent(
|
||||||
$item["title"],
|
$item["title"],
|
||||||
str_replace(array("\n", "\r"), '', strip_tags($item["description"])),
|
strip_tags($item["description"]),
|
||||||
$item["link"],
|
$item["link"],
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
$timestamp = date("Y-m-d H:i:s");
|
$timestamp = date("Y-m-d H:i:s");
|
||||||
|
|
||||||
if (!$this->storeInDatabase($item["link"], $timestamp)) {
|
if (!$this->storeInDatabase($item["link"], $timestamp)) {
|
||||||
|
@ -78,15 +65,11 @@ class MastodonPostHandler extends ApiHandler
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mastodonPostUrl = $this->postToMastodon($content, $item["image"] ?? null);
|
$postedUrl = $this->postToMastodon($content, $item["image"] ?? null);
|
||||||
|
if ($postedUrl) {
|
||||||
if ($mastodonPostUrl) {
|
echo "Posted: {$postedUrl}\n";
|
||||||
if (strpos($item["link"], $this->baseUrl . "/posts") !== false) {
|
|
||||||
$slug = str_replace($this->baseUrl, "", $item["link"]);
|
|
||||||
echo "Posted and stored URL: {$item["link"]}\n";
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
echo "Failed to post to Mastodon. Skipping database update.\n";
|
echo "Failed to post to Mastodon for: {$item["link"]}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,16 +79,16 @@ class MastodonPostHandler extends ApiHandler
|
||||||
private function fetchRSSFeed(string $rssFeedUrl): array
|
private function fetchRSSFeed(string $rssFeedUrl): array
|
||||||
{
|
{
|
||||||
$rssText = file_get_contents($rssFeedUrl);
|
$rssText = file_get_contents($rssFeedUrl);
|
||||||
|
if (!$rssText) throw new \Exception("Failed to fetch RSS feed.");
|
||||||
if (!$rssText) throw new Exception("Failed to fetch RSS feed.");
|
|
||||||
|
|
||||||
$rss = new \SimpleXMLElement($rssText);
|
$rss = new \SimpleXMLElement($rssText);
|
||||||
$items = [];
|
$items = [];
|
||||||
|
|
||||||
foreach ($rss->channel->item as $item) {
|
foreach ($rss->channel->item as $item) {
|
||||||
$imageUrl = null;
|
$imageUrl = null;
|
||||||
|
if ($item->enclosure && isset($item->enclosure['url'])) {
|
||||||
if ($item->enclosure && isset($item->enclosure['url'])) $imageUrl = (string) $item->enclosure['url'];
|
$imageUrl = (string) $item->enclosure['url'];
|
||||||
|
}
|
||||||
|
|
||||||
$items[] = [
|
$items[] = [
|
||||||
"title" => (string) $item->title,
|
"title" => (string) $item->title,
|
||||||
|
@ -120,15 +103,13 @@ class MastodonPostHandler extends ApiHandler
|
||||||
|
|
||||||
private function uploadImageToMastodon(string $imageUrl): ?string
|
private function uploadImageToMastodon(string $imageUrl): ?string
|
||||||
{
|
{
|
||||||
$headers = [
|
|
||||||
"Authorization" => "Bearer {$this->mastodonAccessToken}"
|
|
||||||
];
|
|
||||||
|
|
||||||
$tempFile = tempnam(sys_get_temp_dir(), "mastodon_img");
|
$tempFile = tempnam(sys_get_temp_dir(), "mastodon_img");
|
||||||
file_put_contents($tempFile, file_get_contents($imageUrl));
|
file_put_contents($tempFile, file_get_contents($imageUrl));
|
||||||
|
|
||||||
$response = $this->httpClient->request("POST", "https://follow.coryd.dev/api/v2/media", [
|
$response = $this->httpClient->request("POST", "https://follow.coryd.dev/api/v2/media", [
|
||||||
"headers" => $headers,
|
"headers" => [
|
||||||
|
"Authorization" => "Bearer {$this->mastodonAccessToken}"
|
||||||
|
],
|
||||||
"multipart" => [
|
"multipart" => [
|
||||||
[
|
[
|
||||||
"name" => "file",
|
"name" => "file",
|
||||||
|
@ -140,13 +121,12 @@ class MastodonPostHandler extends ApiHandler
|
||||||
|
|
||||||
unlink($tempFile);
|
unlink($tempFile);
|
||||||
|
|
||||||
$statusCode = $response->getStatusCode();
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
throw new \Exception("Image upload failed with status {$response->getStatusCode()}");
|
||||||
|
}
|
||||||
|
|
||||||
if ($statusCode !== 200) throw new Exception("Image upload failed with status $statusCode.");
|
$json = json_decode($response->getBody(), true);
|
||||||
|
return $json["id"] ?? null;
|
||||||
$responseBody = json_decode($response->getBody()->getContents(), true);
|
|
||||||
|
|
||||||
return $responseBody["id"] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function postToMastodon(string $content, ?string $imageUrl = null): ?string
|
private function postToMastodon(string $content, ?string $imageUrl = null): ?string
|
||||||
|
@ -156,43 +136,42 @@ class MastodonPostHandler extends ApiHandler
|
||||||
"Content-Type" => "application/json",
|
"Content-Type" => "application/json",
|
||||||
];
|
];
|
||||||
|
|
||||||
$mediaIds = [];
|
$postData = ["status" => $content];
|
||||||
|
|
||||||
if ($imageUrl) {
|
if ($imageUrl) {
|
||||||
try {
|
try {
|
||||||
$mediaId = $this->uploadImageToMastodon($imageUrl);
|
$mediaId = $this->uploadImageToMastodon($imageUrl);
|
||||||
if ($mediaId) $mediaIds[] = $mediaId;
|
if ($mediaId) $postData["media_ids"] = [$mediaId];
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
echo "Image upload failed: " . $e->getMessage() . "\n";
|
echo "Image upload failed: {$e->getMessage()}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$postData = ["status" => $content];
|
$response = $this->httpClient->request("POST", self::MASTODON_API_STATUS, [
|
||||||
|
"headers" => $headers,
|
||||||
|
"json" => $postData
|
||||||
|
]);
|
||||||
|
|
||||||
if (!empty($mediaIds)) $postData["media_ids"] = $mediaIds;
|
if ($response->getStatusCode() >= 400) {
|
||||||
|
throw new \Exception("Mastodon post failed: {$response->getBody()}");
|
||||||
|
}
|
||||||
|
|
||||||
$response = $this->httpRequest(
|
$body = json_decode($response->getBody()->getContents(), true);
|
||||||
self::MASTODON_API_STATUS,
|
return $body["url"] ?? null;
|
||||||
"POST",
|
|
||||||
$headers,
|
|
||||||
$postData
|
|
||||||
);
|
|
||||||
|
|
||||||
return $response["url"] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function storeInDatabase(string $link, string $timestamp): bool
|
private function storeInDatabase(string $link, string $timestamp): bool
|
||||||
{
|
{
|
||||||
$data = [
|
|
||||||
"link" => $link,
|
|
||||||
"created_at" => $timestamp,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->fetchFromPostgREST("mastodon_posts", "", "POST", $data);
|
$this->makeRequest("POST", "mastodon_posts", [
|
||||||
|
"json" => [
|
||||||
|
"link" => $link,
|
||||||
|
"created_at" => $timestamp
|
||||||
|
]
|
||||||
|
]);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
echo "Error storing post in database: " . $e->getMessage() . "\n";
|
echo "Error storing post in DB: " . $e->getMessage() . "\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,70 +179,32 @@ class MastodonPostHandler extends ApiHandler
|
||||||
private function isDatabaseAvailable(): bool
|
private function isDatabaseAvailable(): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->fetchFromPostgREST("mastodon_posts", "limit=1");
|
$response = $this->fetchFromApi("mastodon_posts", "limit=1");
|
||||||
return is_array($response);
|
return is_array($response);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
echo "Database check failed: " . $e->getMessage() . "\n";
|
echo "Database check failed: " . $e->getMessage() . "\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function truncateContent(
|
private function truncateContent(string $title, string $description, string $link, int $maxLength): string
|
||||||
string $title,
|
{
|
||||||
string $description,
|
|
||||||
string $link,
|
|
||||||
int $maxLength
|
|
||||||
): string {
|
|
||||||
$baseLength = strlen("$title\n\n$link");
|
$baseLength = strlen("$title\n\n$link");
|
||||||
$availableSpace = $maxLength - $baseLength - 4;
|
$available = $maxLength - $baseLength - 4;
|
||||||
|
|
||||||
if (strlen($description) > $availableSpace) {
|
if (strlen($description) > $available) {
|
||||||
$description = substr($description, 0, $availableSpace);
|
$description = substr($description, 0, $available);
|
||||||
$description = preg_replace('/\s+\S*$/', "", $description) . "...";
|
$description = preg_replace('/\s+\S*$/', "", $description) . "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "$title\n\n$description\n\n$link";
|
return "$title\n\n$description\n\n$link";
|
||||||
}
|
}
|
||||||
|
|
||||||
private function httpRequest(
|
|
||||||
string $url,
|
|
||||||
string $method = "GET",
|
|
||||||
array $headers = [],
|
|
||||||
?array $data = null
|
|
||||||
): array {
|
|
||||||
$options = ["headers" => $headers];
|
|
||||||
|
|
||||||
if ($data) $options["json"] = $data;
|
|
||||||
|
|
||||||
$response = $this->httpClient->request($method, $url, $options);
|
|
||||||
$statusCode = $response->getStatusCode();
|
|
||||||
|
|
||||||
if ($statusCode >= 400) throw new Exception("HTTP error $statusCode: " . $response->getBody());
|
|
||||||
|
|
||||||
$responseBody = $response->getBody()->getContents();
|
|
||||||
|
|
||||||
if (empty($responseBody)) return [];
|
|
||||||
|
|
||||||
$decodedResponse = json_decode($responseBody, true);
|
|
||||||
|
|
||||||
if (!is_array($decodedResponse)) return [];
|
|
||||||
|
|
||||||
return $decodedResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPostgRESTHeaders(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"Authorization" => "Bearer {$this->postgrestApiKey}",
|
|
||||||
"Content-Type" => "application/json",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$handler = new MastodonPostHandler();
|
$handler = new MastodonPostHandler();
|
||||||
$handler->handlePost();
|
$handler->handlePost();
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(["error" => $e->getMessage()]);
|
echo json_encode(["error" => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ class LatestListenHandler extends BaseHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->sendResponse($latestListen);
|
$this->sendResponse($latestListen);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("LatestListenHandler Error: " . $e->getMessage());
|
error_log("LatestListenHandler Error: " . $e->getMessage());
|
||||||
$this->sendErrorResponse(
|
$this->sendErrorResponse(
|
||||||
"Internal Server Error: " . $e->getMessage(),
|
"Internal Server Error: " . $e->getMessage(),
|
||||||
|
|
90
api/proxy.php
Normal file
90
api/proxy.php
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Classes\BaseHandler;
|
||||||
|
|
||||||
|
require __DIR__ . '/../server/utils/init.php';
|
||||||
|
require __DIR__ . "/Classes/BaseHandler.php";
|
||||||
|
|
||||||
|
class ProxyHandler extends BaseHandler
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->ensureAllowedOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ensureAllowedOrigin(): void
|
||||||
|
{
|
||||||
|
$allowedHosts = ['coryd.dev', 'www.coryd.dev'];
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||||
|
|
||||||
|
$hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true);
|
||||||
|
|
||||||
|
if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden — invalid origin", 403);
|
||||||
|
|
||||||
|
$allowedSource = $origin ?: $referer;
|
||||||
|
$scheme = parse_url($allowedSource, PHP_URL_SCHEME) ?? 'https';
|
||||||
|
$host = parse_url($allowedSource, PHP_URL_HOST);
|
||||||
|
|
||||||
|
header("Access-Control-Allow-Origin: {$scheme}://{$host}");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type");
|
||||||
|
header("Access-Control-Allow-Methods: GET, POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRequest(): void
|
||||||
|
{
|
||||||
|
$data = $_GET['data'] ?? null;
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
$cacheDuration = intval($_GET['cacheDuration'] ?? 3600);
|
||||||
|
|
||||||
|
if (!$data) $this->sendErrorResponse("Missing 'data' parameter", 400);
|
||||||
|
|
||||||
|
$cacheKey = $this->buildCacheKey($data, $id);
|
||||||
|
|
||||||
|
if ($this->cache) {
|
||||||
|
$cached = $this->cache->get($cacheKey);
|
||||||
|
if ($cached) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $cached;
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $id ? "id=eq.$id" : "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->fetchFromApi($data, $query);
|
||||||
|
$markdownFields = $_GET['markdown'] ?? [];
|
||||||
|
$markdownFields = is_array($markdownFields)
|
||||||
|
? $markdownFields
|
||||||
|
: explode(',', $markdownFields);
|
||||||
|
$markdownFields = array_map('trim', array_filter($markdownFields));
|
||||||
|
|
||||||
|
if (!empty($response) && !empty($markdownFields)) {
|
||||||
|
foreach ($markdownFields as $field) {
|
||||||
|
if (!empty($response[0][$field])) $response[0]["{$field}_html"] = parseMarkdown($response[0][$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($response);
|
||||||
|
|
||||||
|
if ($this->cache) {
|
||||||
|
$this->cache->setex($cacheKey, $cacheDuration, $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $json;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->sendErrorResponse("PostgREST fetch failed: " . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCacheKey(string $data, ?string $id): string
|
||||||
|
{
|
||||||
|
return "proxy_{$data}" . ($id ? "_{$id}" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$handler = new ProxyHandler();
|
||||||
|
$handler->handleRequest();
|
124
api/scrobble.php
124
api/scrobble.php
|
@ -10,13 +10,8 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
|
||||||
$expectedToken = "Bearer " . getenv("NAVIDROME_SCROBBLE_TOKEN");
|
|
||||||
|
|
||||||
class NavidromeScrobbleHandler extends ApiHandler
|
class NavidromeScrobbleHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
private string $postgrestApiUrl;
|
|
||||||
private string $postgrestApiToken;
|
|
||||||
private string $navidromeApiUrl;
|
private string $navidromeApiUrl;
|
||||||
private string $navidromeAuthToken;
|
private string $navidromeAuthToken;
|
||||||
private string $forwardEmailApiKey;
|
private string $forwardEmailApiKey;
|
||||||
|
@ -28,14 +23,12 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
$this->loadExternalServiceKeys();
|
||||||
$this->validateAuthorization();
|
$this->validateAuthorization();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
private function loadExternalServiceKeys(): void
|
||||||
{
|
{
|
||||||
$this->postgrestApiUrl = getenv("POSTGREST_URL");
|
|
||||||
$this->postgrestApiToken = getenv("POSTGREST_API_KEY");
|
|
||||||
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
$this->navidromeApiUrl = getenv("NAVIDROME_API_URL");
|
||||||
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
$this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN");
|
||||||
$this->forwardEmailApiKey = getenv("FORWARDEMAIL_API_KEY");
|
$this->forwardEmailApiKey = getenv("FORWARDEMAIL_API_KEY");
|
||||||
|
@ -95,7 +88,7 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
private function isTrackAlreadyScrobbled(array $track): bool
|
private function isTrackAlreadyScrobbled(array $track): bool
|
||||||
{
|
{
|
||||||
$playDate = strtotime($track["playDate"]);
|
$playDate = strtotime($track["playDate"]);
|
||||||
$existingListen = $this->fetchFromPostgREST("listens", "listened_at=eq.{$playDate}&limit=1");
|
$existingListen = $this->fetchFromApi("listens", "listened_at=eq.{$playDate}&limit=1");
|
||||||
|
|
||||||
return !empty($existingListen);
|
return !empty($existingListen);
|
||||||
}
|
}
|
||||||
|
@ -121,61 +114,52 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
|
|
||||||
private function getOrCreateArtist(string $artistName): array
|
private function getOrCreateArtist(string $artistName): array
|
||||||
{
|
{
|
||||||
if (!$this->isDatabaseAvailable()) {
|
if (!$this->isDatabaseAvailable()) return [];
|
||||||
error_log("Skipping artist insert: database is unavailable.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($this->artistCache[$artistName])) return $this->artistCache[$artistName];
|
if (isset($this->artistCache[$artistName])) return $this->artistCache[$artistName];
|
||||||
|
|
||||||
$encodedArtist = rawurlencode($artistName);
|
$encodedArtist = rawurlencode($artistName);
|
||||||
$existingArtist = $this->fetchFromPostgREST("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
$existingArtist = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
||||||
|
|
||||||
if (!empty($existingArtist)) {
|
if (!empty($existingArtist)) {
|
||||||
$this->artistCache[$artistName] = $existingArtist[0];
|
return $this->artistCache[$artistName] = $existingArtist[0];
|
||||||
return $existingArtist[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fetchFromPostgREST("artists", "", "POST", [
|
$this->makeRequest("POST", "artists", [
|
||||||
"mbid" => "",
|
"json" => [
|
||||||
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
"mbid" => "",
|
||||||
"name_string" => $artistName,
|
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
||||||
"slug" => "/music",
|
"name_string" => $artistName,
|
||||||
"country" => "",
|
"slug" => "/music",
|
||||||
"description" => "",
|
"country" => "",
|
||||||
"tentative" => true,
|
"description" => "",
|
||||||
"favorite" => false,
|
"tentative" => true,
|
||||||
"tattoo" => false,
|
"favorite" => false,
|
||||||
"total_plays" => 0
|
"tattoo" => false,
|
||||||
|
"total_plays" => 0
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
$this->sendFailureEmail("New tentative artist record", "A new tentative artist record was inserted for: $artistName");
|
$this->sendFailureEmail("New tentative artist record", "A new tentative artist record was inserted for: $artistName");
|
||||||
|
|
||||||
$artistData = $this->fetchFromPostgREST("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
$artistData = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
||||||
|
|
||||||
$this->artistCache[$artistName] = $artistData[0] ?? [];
|
return $this->artistCache[$artistName] = $artistData[0] ?? [];
|
||||||
|
|
||||||
return $this->artistCache[$artistName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getOrCreateAlbum(string $albumName, array $artistData): array
|
private function getOrCreateAlbum(string $albumName, array $artistData): array
|
||||||
{
|
{
|
||||||
if (!$this->isDatabaseAvailable()) {
|
if (!$this->isDatabaseAvailable()) return [];
|
||||||
error_log("Skipping album insert: database is unavailable.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$albumKey = $this->generateAlbumKey($artistData["name_string"], $albumName);
|
$albumKey = $this->generateAlbumKey($artistData["name_string"], $albumName);
|
||||||
|
|
||||||
if (isset($this->albumCache[$albumKey])) return $this->albumCache[$albumKey];
|
if (isset($this->albumCache[$albumKey])) return $this->albumCache[$albumKey];
|
||||||
|
|
||||||
$encodedAlbumKey = rawurlencode($albumKey);
|
$encodedAlbumKey = rawurlencode($albumKey);
|
||||||
$existingAlbum = $this->fetchFromPostgREST("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
$existingAlbum = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
||||||
|
|
||||||
if (!empty($existingAlbum)) {
|
if (!empty($existingAlbum)) {
|
||||||
$this->albumCache[$albumKey] = $existingAlbum[0];
|
return $this->albumCache[$albumKey] = $existingAlbum[0];
|
||||||
return $existingAlbum[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$artistId = $artistData["id"] ?? null;
|
$artistId = $artistData["id"] ?? null;
|
||||||
|
@ -185,35 +169,37 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fetchFromPostgREST("albums", "", "POST", [
|
$this->makeRequest("POST", "albums", [
|
||||||
"mbid" => null,
|
"json" => [
|
||||||
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
"mbid" => null,
|
||||||
"key" => $albumKey,
|
"art" => "4cef75db-831f-4f5d-9333-79eaa5bb55ee",
|
||||||
"name" => $albumName,
|
"key" => $albumKey,
|
||||||
"tentative" => true,
|
"name" => $albumName,
|
||||||
"total_plays" => 0,
|
"tentative" => true,
|
||||||
"artist" => $artistId
|
"total_plays" => 0,
|
||||||
|
"artist" => $artistId
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->sendFailureEmail("New tentative album record", "A new tentative album record was inserted:\n\nAlbum: $albumName\nKey: $albumKey");
|
$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");
|
$albumData = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
||||||
|
|
||||||
$this->albumCache[$albumKey] = $albumData[0] ?? [];
|
return $this->albumCache[$albumKey] = $albumData[0] ?? [];
|
||||||
|
|
||||||
return $this->albumCache[$albumKey];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function insertListen(array $track, string $albumKey): void
|
private function insertListen(array $track, string $albumKey): void
|
||||||
{
|
{
|
||||||
$playDate = strtotime($track["playDate"]);
|
$playDate = strtotime($track["playDate"]);
|
||||||
|
|
||||||
$this->fetchFromPostgREST("listens", "", "POST", [
|
$this->makeRequest("POST", "listens", [
|
||||||
"artist_name" => $track["artist"],
|
"json" => [
|
||||||
"album_name" => $track["album"],
|
"artist_name" => $track["artist"],
|
||||||
"track_name" => $track["title"],
|
"album_name" => $track["album"],
|
||||||
"listened_at" => $playDate,
|
"track_name" => $track["title"],
|
||||||
"album_key" => $albumKey
|
"listened_at" => $playDate,
|
||||||
|
"album_key" => $albumKey
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,24 +207,18 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
$artistKey = sanitizeMediaString($artistName);
|
$artistKey = sanitizeMediaString($artistName);
|
||||||
$albumKey = sanitizeMediaString($albumName);
|
$albumKey = sanitizeMediaString($albumName);
|
||||||
|
|
||||||
return "{$artistKey}-{$albumKey}";
|
return "{$artistKey}-{$albumKey}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendFailureEmail(string $subject, string $message): void
|
private function sendFailureEmail(string $subject, string $message): void
|
||||||
{
|
{
|
||||||
if (!$this->isDatabaseAvailable()) {
|
if (!$this->isDatabaseAvailable()) return;
|
||||||
error_log("Skipping email: database is unavailable.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$authHeader = "Basic " . base64_encode($this->forwardEmailApiKey . ":");
|
$authHeader = "Basic " . base64_encode($this->forwardEmailApiKey . ":");
|
||||||
$client = new Client([
|
$client = new Client(["base_uri" => "https://api.forwardemail.net/"]);
|
||||||
"base_uri" => "https://api.forwardemail.net/",
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->post("v1/emails", [
|
$client->post("v1/emails", [
|
||||||
"headers" => [
|
"headers" => [
|
||||||
"Authorization" => $authHeader,
|
"Authorization" => $authHeader,
|
||||||
"Content-Type" => "application/x-www-form-urlencoded",
|
"Content-Type" => "application/x-www-form-urlencoded",
|
||||||
|
@ -250,12 +230,10 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
"text" => $message,
|
"text" => $message,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||||
error_log("Request Exception: " . $e->getMessage());
|
error_log("Request Exception: " . $e->getMessage());
|
||||||
if ($e->hasResponse()) {
|
if ($e->hasResponse()) {
|
||||||
$errorResponse = (string) $e->getResponse()->getBody();
|
error_log("Error Response: " . (string) $e->getResponse()->getBody());
|
||||||
error_log("Error Response: " . $errorResponse);
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("General Exception: " . $e->getMessage());
|
error_log("General Exception: " . $e->getMessage());
|
||||||
|
@ -265,9 +243,9 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
private function isDatabaseAvailable(): bool
|
private function isDatabaseAvailable(): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->fetchFromPostgREST("listens", "limit=1");
|
$response = $this->fetchFromApi("listens", "limit=1");
|
||||||
return is_array($response);
|
return is_array($response);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Database check failed: " . $e->getMessage());
|
error_log("Database check failed: " . $e->getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -277,7 +255,7 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
try {
|
try {
|
||||||
$handler = new NavidromeScrobbleHandler();
|
$handler = new NavidromeScrobbleHandler();
|
||||||
$handler->runScrobbleCheck();
|
$handler->runScrobbleCheck();
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(["error" => $e->getMessage()]);
|
echo json_encode(["error" => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class SearchHandler extends BaseHandler
|
||||||
],
|
],
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Search API Error: " . $e->getMessage());
|
error_log("Search API Error: " . $e->getMessage());
|
||||||
$this->sendErrorResponse("Invalid request. Please check your query and try again.", 400);
|
$this->sendErrorResponse("Invalid request. Please check your query and try again.", 400);
|
||||||
}
|
}
|
||||||
|
@ -55,15 +55,15 @@ class SearchHandler extends BaseHandler
|
||||||
|
|
||||||
private function validateAndSanitizeQuery(?string $query): string
|
private function validateAndSanitizeQuery(?string $query): string
|
||||||
{
|
{
|
||||||
if (empty($query) || !is_string($query)) throw new Exception("Invalid 'q' parameter. Must be a non-empty string.");
|
if (empty($query) || !is_string($query)) throw new \Exception("Invalid 'q' parameter. Must be a non-empty string.");
|
||||||
|
|
||||||
$query = trim($query);
|
$query = trim($query);
|
||||||
|
|
||||||
if (strlen($query) > 255) throw new Exception(
|
if (strlen($query) > 255) throw new \Exception(
|
||||||
"Invalid 'q' parameter. Exceeds maximum length of 255 characters."
|
"Invalid 'q' parameter. Exceeds maximum length of 255 characters."
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) throw new Exception(
|
if (!preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) throw new \Exception(
|
||||||
"Invalid 'q' parameter. Contains unsupported characters."
|
"Invalid 'q' parameter. Contains unsupported characters."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
class SeasonImportHandler extends ApiHandler
|
class SeasonImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
private string $tmdbApiKey;
|
private string $tmdbApiKey;
|
||||||
private string $seasonsImportToken;
|
private string $seasonsImportToken;
|
||||||
|
|
||||||
|
@ -17,62 +14,43 @@ class SeasonImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
|
||||||
$this->authenticateRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl = getenv("POSTGREST_URL") ?: $_ENV["POSTGREST_URL"];
|
|
||||||
$this->postgrestApiKey = getenv("POSTGREST_API_KEY") ?: $_ENV["POSTGREST_API_KEY"];
|
|
||||||
$this->tmdbApiKey = getenv("TMDB_API_KEY") ?: $_ENV["TMDB_API_KEY"];
|
$this->tmdbApiKey = getenv("TMDB_API_KEY") ?: $_ENV["TMDB_API_KEY"];
|
||||||
$this->seasonsImportToken = getenv("SEASONS_IMPORT_TOKEN") ?: $_ENV["SEASONS_IMPORT_TOKEN"];
|
$this->seasonsImportToken = getenv("SEASONS_IMPORT_TOKEN") ?: $_ENV["SEASONS_IMPORT_TOKEN"];
|
||||||
|
|
||||||
|
$this->authenticateRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function authenticateRequest(): void
|
private function authenticateRequest(): void
|
||||||
{
|
{
|
||||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
||||||
http_response_code(405);
|
$this->sendErrorResponse("Method Not Allowed", 405);
|
||||||
echo json_encode(["error" => "Method Not Allowed"]);
|
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
||||||
if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
|
if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
|
||||||
http_response_code(401);
|
$this->sendErrorResponse("Unauthorized", 401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$providedToken = trim($matches[1]);
|
$providedToken = trim($matches[1]);
|
||||||
if ($providedToken !== $this->seasonsImportToken) {
|
if ($providedToken !== $this->seasonsImportToken) {
|
||||||
http_response_code(403);
|
$this->sendErrorResponse("Forbidden", 403);
|
||||||
echo json_encode(["error" => "Forbidden"]);
|
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function importSeasons(): void
|
public function importSeasons(): void
|
||||||
{
|
{
|
||||||
$ongoingShows = $this->fetchOngoingShows();
|
$ongoingShows = $this->fetchFromApi("optimized_shows", "ongoing=eq.true");
|
||||||
|
|
||||||
if (empty($ongoingShows)) {
|
if (empty($ongoingShows)) {
|
||||||
http_response_code(200);
|
$this->sendResponse(["message" => "No ongoing shows to update"], 200);
|
||||||
echo json_encode(["message" => "No ongoing shows to update"]);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($ongoingShows as $show) {
|
foreach ($ongoingShows as $show) {
|
||||||
$this->processShowSeasons($show);
|
$this->processShowSeasons($show);
|
||||||
}
|
}
|
||||||
|
|
||||||
http_response_code(200);
|
$this->sendResponse(["message" => "Season import completed"], 200);
|
||||||
echo json_encode(["message" => "Season import completed"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchOngoingShows(): array
|
|
||||||
{
|
|
||||||
return $this->fetchFromPostgREST("optimized_shows", "ongoing=eq.true", "GET");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function processShowSeasons(array $show): void
|
private function processShowSeasons(array $show): void
|
||||||
|
@ -98,8 +76,7 @@ class SeasonImportHandler extends ApiHandler
|
||||||
|
|
||||||
private function shouldKeepOngoing(string $status): bool
|
private function shouldKeepOngoing(string $status): bool
|
||||||
{
|
{
|
||||||
$validStatuses = ["Returning Series", "In Production"];
|
return in_array($status, ["Returning Series", "In Production"]);
|
||||||
return in_array($status, $validStatuses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchShowDetails(string $tmdbId): array
|
private function fetchShowDetails(string $tmdbId): array
|
||||||
|
@ -117,49 +94,40 @@ class SeasonImportHandler extends ApiHandler
|
||||||
|
|
||||||
private function fetchWatchedEpisodes(int $showId): array
|
private function fetchWatchedEpisodes(int $showId): array
|
||||||
{
|
{
|
||||||
$watchedEpisodes = $this->fetchFromPostgREST(
|
$episodes = $this->fetchFromApi("optimized_last_watched_episodes", "show_id=eq.{$showId}&order=last_watched_at.desc&limit=1");
|
||||||
"optimized_last_watched_episodes",
|
|
||||||
"show_id=eq.{$showId}&order=last_watched_at.desc&limit=1",
|
|
||||||
"GET"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($watchedEpisodes)) return [];
|
if (empty($episodes)) return [];
|
||||||
|
|
||||||
$lastWatched = $watchedEpisodes[0] ?? null;
|
return [
|
||||||
|
"season_number" => (int) $episodes[0]["season_number"],
|
||||||
if ($lastWatched) return [
|
"episode_number" => (int) $episodes[0]["episode_number"],
|
||||||
"season_number" => (int) $lastWatched["season_number"],
|
];
|
||||||
"episode_number" => (int) $lastWatched["episode_number"]
|
|
||||||
];
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function processSeasonEpisodes(int $showId, string $tmdbId, array $season): void
|
private function processSeasonEpisodes(int $showId, string $tmdbId, array $season): void
|
||||||
{
|
{
|
||||||
$seasonNumber = $season["season_number"] ?? null;
|
$seasonNumber = $season["season_number"] ?? null;
|
||||||
|
|
||||||
if ($seasonNumber === null || $seasonNumber == 0) return;
|
if ($seasonNumber === null || $seasonNumber == 0) return;
|
||||||
|
|
||||||
$episodes = $this->fetchSeasonEpisodes($tmdbId, $seasonNumber);
|
$episodes = $this->fetchSeasonEpisodes($tmdbId, $seasonNumber);
|
||||||
|
|
||||||
if (empty($episodes)) return;
|
if (empty($episodes)) return;
|
||||||
|
|
||||||
$watchedEpisodes = $this->fetchWatchedEpisodes($showId);
|
$watched = $this->fetchWatchedEpisodes($showId);
|
||||||
$lastWatchedSeason = $watchedEpisodes["season_number"] ?? null;
|
$lastWatchedSeason = $watched["season_number"] ?? null;
|
||||||
$lastWatchedEpisode = $watchedEpisodes["episode_number"] ?? null;
|
$lastWatchedEpisode = $watched["episode_number"] ?? null;
|
||||||
$scheduledEpisodes = $this->fetchFromPostgREST(
|
|
||||||
"optimized_scheduled_episodes",
|
$scheduled = $this->fetchFromApi(
|
||||||
"show_id=eq.{$showId}&season_number=eq.{$seasonNumber}",
|
"optimized_scheduled_episodes",
|
||||||
"GET"
|
"show_id=eq.{$showId}&season_number=eq.{$seasonNumber}"
|
||||||
);
|
);
|
||||||
$scheduledEpisodeNumbers = array_column($scheduledEpisodes, "episode_number");
|
|
||||||
|
$scheduledEpisodeNumbers = array_column($scheduled, "episode_number");
|
||||||
|
|
||||||
foreach ($episodes as $episode) {
|
foreach ($episodes as $episode) {
|
||||||
$episodeNumber = $episode["episode_number"] ?? null;
|
$episodeNumber = $episode["episode_number"] ?? null;
|
||||||
|
|
||||||
if ($episodeNumber === null) continue;
|
if ($episodeNumber === null) continue;
|
||||||
if (in_array($episodeNumber, $scheduledEpisodeNumbers)) continue;
|
if (in_array($episodeNumber, $scheduledEpisodeNumbers)) continue;
|
||||||
|
|
||||||
if ($lastWatchedSeason !== null && $seasonNumber < $lastWatchedSeason) return;
|
if ($lastWatchedSeason !== null && $seasonNumber < $lastWatchedSeason) return;
|
||||||
if ($seasonNumber == $lastWatchedSeason && $episodeNumber <= $lastWatchedEpisode) continue;
|
if ($seasonNumber == $lastWatchedSeason && $episodeNumber <= $lastWatchedEpisode) continue;
|
||||||
|
|
||||||
|
@ -183,11 +151,10 @@ class SeasonImportHandler extends ApiHandler
|
||||||
private function addEpisodeToSchedule(int $showId, int $seasonNumber, array $episode): void
|
private function addEpisodeToSchedule(int $showId, int $seasonNumber, array $episode): void
|
||||||
{
|
{
|
||||||
$airDate = $episode["air_date"] ?? null;
|
$airDate = $episode["air_date"] ?? null;
|
||||||
|
|
||||||
if (!$airDate) return;
|
if (!$airDate) return;
|
||||||
|
|
||||||
$currentDate = date("Y-m-d");
|
$today = date("Y-m-d");
|
||||||
$status = ($airDate && $airDate < $currentDate) ? "aired" : "upcoming";
|
$status = ($airDate < $today) ? "aired" : "upcoming";
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
"show_id" => $showId,
|
"show_id" => $showId,
|
||||||
|
@ -197,7 +164,7 @@ class SeasonImportHandler extends ApiHandler
|
||||||
"status" => $status,
|
"status" => $status,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->fetchFromPostgREST("scheduled_episodes", "", "POST", $payload);
|
$this->makeRequest("POST", "scheduled_episodes", ["json" => $payload]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@ use GuzzleHttp\Client;
|
||||||
|
|
||||||
class WatchingImportHandler extends ApiHandler
|
class WatchingImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
|
||||||
protected string $postgrestApiKey;
|
|
||||||
|
|
||||||
private string $tmdbApiKey;
|
private string $tmdbApiKey;
|
||||||
private string $tmdbImportToken;
|
private string $tmdbImportToken;
|
||||||
|
|
||||||
|
@ -17,17 +14,9 @@ class WatchingImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->loadEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadEnvironment(): void
|
|
||||||
{
|
|
||||||
$this->postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL");
|
|
||||||
$this->postgrestApiKey =
|
|
||||||
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY");
|
|
||||||
$this->tmdbApiKey = $_ENV["TMDB_API_KEY"] ?? getenv("TMDB_API_KEY");
|
$this->tmdbApiKey = $_ENV["TMDB_API_KEY"] ?? getenv("TMDB_API_KEY");
|
||||||
$this->tmdbImportToken =
|
$this->tmdbImportToken = $_ENV["WATCHING_IMPORT_TOKEN"] ?? getenv("WATCHING_IMPORT_TOKEN");
|
||||||
$_ENV["WATCHING_IMPORT_TOKEN"] ?? getenv("WATCHING_IMPORT_TOKEN");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRequest(): void
|
public function handleRequest(): void
|
||||||
|
@ -37,19 +26,22 @@ class WatchingImportHandler extends ApiHandler
|
||||||
if (!$input) $this->sendErrorResponse("Invalid or missing JSON body", 400);
|
if (!$input) $this->sendErrorResponse("Invalid or missing JSON body", 400);
|
||||||
|
|
||||||
$providedToken = $input["token"] ?? null;
|
$providedToken = $input["token"] ?? null;
|
||||||
|
|
||||||
if (!$providedToken || $providedToken !== $this->tmdbImportToken) $this->sendErrorResponse("Unauthorized access", 401);
|
|
||||||
|
|
||||||
$tmdbId = $input["tmdb_id"] ?? null;
|
$tmdbId = $input["tmdb_id"] ?? null;
|
||||||
$mediaType = $input["media_type"] ?? null;
|
$mediaType = $input["media_type"] ?? null;
|
||||||
|
|
||||||
if (!$tmdbId || !$mediaType) $this->sendErrorResponse("tmdb_id and media_type are required", 400);
|
if ($providedToken !== $this->tmdbImportToken) {
|
||||||
|
$this->sendErrorResponse("Unauthorized access", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tmdbId || !$mediaType) {
|
||||||
|
$this->sendErrorResponse("tmdb_id and media_type are required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$mediaData = $this->fetchTMDBData($tmdbId, $mediaType);
|
$mediaData = $this->fetchTMDBData($tmdbId, $mediaType);
|
||||||
$this->processMedia($mediaData, $mediaType);
|
$this->processMedia($mediaData, $mediaType);
|
||||||
$this->sendResponse("Media imported successfully", 200);
|
$this->sendResponse(["message" => "Media imported successfully"], 200);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->sendErrorResponse("Error: " . $e->getMessage(), 500);
|
$this->sendErrorResponse("Error: " . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,8 +57,7 @@ class WatchingImportHandler extends ApiHandler
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = json_decode($response->getBody(), true);
|
$data = json_decode($response->getBody(), true);
|
||||||
|
if (empty($data)) throw new \Exception("No data found for TMDB ID: {$tmdbId}");
|
||||||
if (empty($data)) throw new Exception("No data found for TMDB ID: {$tmdbId}");
|
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
@ -75,18 +66,19 @@ class WatchingImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
$id = $mediaData["id"];
|
$id = $mediaData["id"];
|
||||||
$title = $mediaType === "movie" ? $mediaData["title"] : $mediaData["name"];
|
$title = $mediaType === "movie" ? $mediaData["title"] : $mediaData["name"];
|
||||||
$year =
|
$year = $mediaData["release_date"] ?? $mediaData["first_air_date"] ?? null;
|
||||||
$mediaData["release_date"] ?? ($mediaData["first_air_date"] ?? null);
|
|
||||||
$year = $year ? substr($year, 0, 4) : null;
|
$year = $year ? substr($year, 0, 4) : null;
|
||||||
$description = $mediaData["overview"] ?? "";
|
$description = $mediaData["overview"] ?? "";
|
||||||
|
|
||||||
$tags = array_map(
|
$tags = array_map(
|
||||||
fn($genre) => strtolower(trim($genre["name"])),
|
fn($genre) => strtolower(trim($genre["name"])),
|
||||||
$mediaData["genres"]
|
$mediaData["genres"] ?? []
|
||||||
);
|
);
|
||||||
$slug =
|
|
||||||
$mediaType === "movie"
|
$slug = $mediaType === "movie"
|
||||||
? "/watching/movies/{$id}"
|
? "/watching/movies/{$id}"
|
||||||
: "/watching/shows/{$id}";
|
: "/watching/shows/{$id}";
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
"title" => $title,
|
"title" => $title,
|
||||||
"year" => $year,
|
"year" => $year,
|
||||||
|
@ -94,80 +86,64 @@ class WatchingImportHandler extends ApiHandler
|
||||||
"tmdb_id" => $id,
|
"tmdb_id" => $id,
|
||||||
"slug" => $slug,
|
"slug" => $slug,
|
||||||
];
|
];
|
||||||
$response = $this->fetchFromPostgREST(
|
|
||||||
$mediaType === "movie" ? "movies" : "shows",
|
|
||||||
"",
|
|
||||||
"POST",
|
|
||||||
$payload
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($response["id"])) {
|
$table = $mediaType === "movie" ? "movies" : "shows";
|
||||||
$queryResponse = $this->fetchFromPostgREST(
|
|
||||||
$mediaType === "movie" ? "movies" : "shows",
|
try {
|
||||||
"tmdb_id=eq.{$id}",
|
$response = $this->makeRequest("POST", $table, ["json" => $payload]);
|
||||||
"GET"
|
} catch (\Exception $e) {
|
||||||
);
|
$response = $this->fetchFromApi($table, "tmdb_id=eq.{$id}")[0] ?? [];
|
||||||
$response = $queryResponse[0] ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($response["id"])) {
|
if (!empty($response["id"])) {
|
||||||
$mediaId = $response["id"];
|
$mediaId = $response["id"];
|
||||||
$existingTagMap = $this->getTagIds($tags);
|
$existingTagMap = $this->getTagIds($tags);
|
||||||
$updatedTagMap = $this->insertMissingTags($tags, $existingTagMap);
|
$updatedTagMap = $this->insertMissingTags($tags, $existingTagMap);
|
||||||
$this->associateTagsWithMedia(
|
$this->associateTagsWithMedia($mediaType, $mediaId, array_values($updatedTagMap));
|
||||||
$mediaType,
|
|
||||||
$mediaId,
|
|
||||||
array_values($updatedTagMap)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTagIds(array $tags): array
|
private function getTagIds(array $tags): array
|
||||||
{
|
{
|
||||||
$existingTagMap = [];
|
$map = [];
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
$query = "name=ilike." . urlencode($tag);
|
$response = $this->fetchFromApi("tags", "name=ilike." . urlencode($tag));
|
||||||
$existingTags = $this->fetchFromPostgREST("tags", $query, "GET");
|
if (!empty($response[0]["id"])) {
|
||||||
|
$map[strtolower($tag)] = $response[0]["id"];
|
||||||
if (!empty($existingTags[0]["id"])) $existingTagMap[strtolower($tag)] = $existingTags[0]["id"];
|
|
||||||
}
|
|
||||||
return $existingTagMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function insertMissingTags(array $tags, array $existingTagMap): array
|
|
||||||
{
|
|
||||||
$newTags = array_diff($tags, array_keys($existingTagMap));
|
|
||||||
foreach ($newTags as $newTag) {
|
|
||||||
try {
|
|
||||||
$response = $this->fetchFromPostgREST("tags", "", "POST", [
|
|
||||||
"name" => $newTag,
|
|
||||||
]);
|
|
||||||
if (!empty($response["id"])) $existingTagMap[$newTag] = $response["id"];
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$queryResponse = $this->fetchFromPostgREST(
|
|
||||||
"tags",
|
|
||||||
"name=eq.{$newTag}",
|
|
||||||
"GET"
|
|
||||||
);
|
|
||||||
if (!empty($queryResponse[0]["id"])) $existingTagMap[$newTag] = $queryResponse[0]["id"];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $existingTagMap;
|
|
||||||
|
return $map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function associateTagsWithMedia(
|
private function insertMissingTags(array $tags, array $existingMap): array
|
||||||
string $mediaType,
|
{
|
||||||
int $mediaId,
|
$newTags = array_diff($tags, array_keys($existingMap));
|
||||||
array $tagIds
|
|
||||||
): void {
|
foreach ($newTags as $tag) {
|
||||||
$junctionTable = $mediaType === "movie" ? "movies_tags" : "shows_tags";
|
try {
|
||||||
|
$created = $this->makeRequest("POST", "tags", ["json" => ["name" => $tag]]);
|
||||||
|
if (!empty($created["id"])) $existingMap[$tag] = $created["id"];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$fallback = $this->fetchFromApi("tags", "name=eq." . urlencode($tag));
|
||||||
|
if (!empty($fallback[0]["id"])) $existingMap[$tag] = $fallback[0]["id"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $existingMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function associateTagsWithMedia(string $mediaType, int $mediaId, array $tagIds): void
|
||||||
|
{
|
||||||
|
$junction = $mediaType === "movie" ? "movies_tags" : "shows_tags";
|
||||||
$mediaColumn = $mediaType === "movie" ? "movies_id" : "shows_id";
|
$mediaColumn = $mediaType === "movie" ? "movies_id" : "shows_id";
|
||||||
|
|
||||||
foreach ($tagIds as $tagId) {
|
foreach ($tagIds as $tagId) {
|
||||||
$this->fetchFromPostgREST($junctionTable, "", "POST", [
|
$this->makeRequest("POST", $junction, ["json" => [
|
||||||
$mediaColumn => $mediaId,
|
$mediaColumn => $mediaId,
|
||||||
"tags_id" => $tagId,
|
"tags_id" => $tagId
|
||||||
]);
|
]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
package-lock.json
generated
24
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "2.1.4",
|
"version": "3.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "2.1.4",
|
"version": "3.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
|
@ -184,9 +184,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@11ty/eleventy-plugin-bundle": {
|
"node_modules/@11ty/eleventy-plugin-bundle": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.5.tgz",
|
||||||
"integrity": "sha512-9Y9aLB5kwK7dkTC+Pfbt4EEs58TMQjuo1+EJ18dA/XKDxczHj2fAUZcETMgNQ17AmrMDj5HxJ0ezFNGpMcD7Vw==",
|
"integrity": "sha512-LfcXr5pvvFjA6k1u8o0vqxbFVY8elpxIeICvdJti9FWUbHyJlS6ydRkyUnijpa+NTsj7DrlcrD1r1uBrANHYeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -3191,13 +3191,13 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "7.2.1",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^4.5.0"
|
"entities": "^6.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
@ -3234,9 +3234,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parse5/node_modules/entities": {
|
"node_modules/parse5/node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "2.1.4",
|
"version": "3.0.3",
|
||||||
"description": "The source for my personal site. Built using 11ty (and other tools).",
|
"description": "The source for my personal site. Built using 11ty (and other tools).",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
CREATE OR REPLACE VIEW optimized_recent_activity AS
|
CREATE OR REPLACE VIEW optimized_recent_activity AS
|
||||||
WITH activity_data AS (
|
WITH activity_data AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
NULL::bigint AS id,
|
||||||
p.date AS content_date,
|
p.date AS content_date,
|
||||||
p.title,
|
p.title,
|
||||||
p.content AS description,
|
p.content AS description,
|
||||||
|
@ -22,6 +23,7 @@ WITH activity_data AS (
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
NULL::bigint AS id,
|
||||||
l.date AS content_date,
|
l.date AS content_date,
|
||||||
l.title,
|
l.title,
|
||||||
l.description,
|
l.description,
|
||||||
|
@ -43,6 +45,7 @@ WITH activity_data AS (
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
NULL::bigint AS id,
|
||||||
b.date_finished AS content_date,
|
b.date_finished AS content_date,
|
||||||
CONCAT(b.title,
|
CONCAT(b.title,
|
||||||
CASE WHEN b.rating IS NOT NULL THEN CONCAT(' (', b.rating, ')') ELSE '' END
|
CASE WHEN b.rating IS NOT NULL THEN CONCAT(' (', b.rating, ')') ELSE '' END
|
||||||
|
@ -67,6 +70,7 @@ WITH activity_data AS (
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
NULL::bigint AS id,
|
||||||
m.last_watched AS content_date,
|
m.last_watched AS content_date,
|
||||||
CONCAT(m.title,
|
CONCAT(m.title,
|
||||||
CASE WHEN m.rating IS NOT NULL THEN CONCAT(' (', m.rating, ')') ELSE '' END
|
CASE WHEN m.rating IS NOT NULL THEN CONCAT(' (', m.rating, ')') ELSE '' END
|
||||||
|
@ -91,6 +95,7 @@ WITH activity_data AS (
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
c.id,
|
||||||
c.date AS content_date,
|
c.date AS content_date,
|
||||||
CONCAT(c.artist->>'name', ' at ', c.venue->>'name_short') AS title,
|
CONCAT(c.artist->>'name', ' at ', c.venue->>'name_short') AS title,
|
||||||
c.concert_notes AS description,
|
c.concert_notes AS description,
|
||||||
|
@ -104,7 +109,7 @@ WITH activity_data AS (
|
||||||
c.venue->>'latitude' AS venue_lat,
|
c.venue->>'latitude' AS venue_lat,
|
||||||
c.venue->>'longitude' AS venue_lon,
|
c.venue->>'longitude' AS venue_lon,
|
||||||
c.venue->>'name_short' AS venue_name,
|
c.venue->>'name_short' AS venue_name,
|
||||||
c.notes AS notes,
|
c.concert_notes AS notes,
|
||||||
'concerts' AS type,
|
'concerts' AS type,
|
||||||
'Concert' AS label
|
'Concert' AS label
|
||||||
FROM optimized_concerts c
|
FROM optimized_concerts c
|
||||||
|
|
|
@ -2,7 +2,6 @@ CREATE OR REPLACE VIEW optimized_concerts AS
|
||||||
SELECT
|
SELECT
|
||||||
c.id,
|
c.id,
|
||||||
c.date,
|
c.date,
|
||||||
c.notes,
|
|
||||||
CASE WHEN c.artist IS NOT NULL THEN
|
CASE WHEN c.artist IS NOT NULL THEN
|
||||||
json_build_object('name', a.name_string, 'url', a.slug)
|
json_build_object('name', a.name_string, 'url', a.slug)
|
||||||
ELSE
|
ELSE
|
||||||
|
@ -16,4 +15,3 @@ FROM
|
||||||
LEFT JOIN venues v ON c.venue = v.id
|
LEFT JOIN venues v ON c.venue = v.id
|
||||||
ORDER BY
|
ORDER BY
|
||||||
c.date DESC;
|
c.date DESC;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require __DIR__ . "/../../vendor/autoload.php";
|
|
||||||
|
|
||||||
use Kaoken\MarkdownIt\MarkdownIt;
|
use Kaoken\MarkdownIt\MarkdownIt;
|
||||||
use Kaoken\MarkdownIt\Plugins\MarkdownItFootnote;
|
use Kaoken\MarkdownIt\Plugins\MarkdownItFootnote;
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,80 @@
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
// dialog controls
|
// dialog controls
|
||||||
(() => {
|
(() => {
|
||||||
if (document.querySelectorAll(".dialog-open").length) {
|
const dialogButtons = document.querySelectorAll(".dialog-open");
|
||||||
document.querySelectorAll(".dialog-open").forEach((button) => {
|
if (!dialogButtons.length) return;
|
||||||
const dialogId = button.getAttribute("data-dialog-trigger");
|
|
||||||
const dialog = document.getElementById(`dialog-${dialogId}`);
|
|
||||||
|
|
||||||
if (!dialog) return;
|
dialogButtons.forEach((button) => {
|
||||||
|
const dialogId = button.getAttribute("data-dialog-trigger");
|
||||||
|
const dialog = document.getElementById(`dialog-${dialogId}`);
|
||||||
|
if (!dialog) return;
|
||||||
|
|
||||||
const closeButton = dialog.querySelector(".dialog-close");
|
const closeButton = dialog.querySelector(".dialog-close");
|
||||||
|
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", async () => {
|
||||||
dialog.showModal();
|
const isDynamic = dialog.dataset.dynamic;
|
||||||
dialog.classList.remove("closing");
|
const isLoaded = dialog.dataset.loaded;
|
||||||
});
|
|
||||||
|
|
||||||
if (closeButton)
|
if (isDynamic && !isLoaded) {
|
||||||
closeButton.addEventListener("click", () => {
|
const markdownFields = dialog.dataset.markdown || "";
|
||||||
dialog.classList.add("closing");
|
try {
|
||||||
setTimeout(() => dialog.close(), 200);
|
const res = await fetch(`/api/proxy.php?data=${isDynamic}&id=${dialogId}&markdown=${encodeURIComponent(markdownFields)}`);
|
||||||
});
|
const [data] = await res.json();
|
||||||
|
const firstField = markdownFields.split(",")[0]?.trim();
|
||||||
|
const html = data?.[`${firstField}_html`] || "<p>No notes available.</p>";
|
||||||
|
|
||||||
dialog.addEventListener("click", (event) => {
|
dialog.querySelectorAll(".dialog-dynamic").forEach((el) => el.remove());
|
||||||
const rect = dialog.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (
|
const container = document.createElement("div");
|
||||||
event.clientX < rect.left ||
|
|
||||||
event.clientX > rect.right ||
|
container.classList.add("dialog-dynamic");
|
||||||
event.clientY < rect.top ||
|
container.innerHTML = html;
|
||||||
event.clientY > rect.bottom
|
dialog.appendChild(container);
|
||||||
) {
|
dialog.dataset.loaded = "true";
|
||||||
dialog.classList.add("closing");
|
} catch (err) {
|
||||||
setTimeout(() => dialog.close(), 200);
|
dialog.querySelectorAll(".dialog-dynamic").forEach((el) => el.remove());
|
||||||
|
|
||||||
|
const errorNode = document.createElement("div");
|
||||||
|
|
||||||
|
errorNode.classList.add("dialog-dynamic");
|
||||||
|
errorNode.textContent = "Failed to load content.";
|
||||||
|
dialog.appendChild(errorNode);
|
||||||
|
|
||||||
|
console.warn("Dialog content load error:", err);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
dialog.showModal();
|
||||||
|
dialog.classList.remove("closing");
|
||||||
|
});
|
||||||
|
|
||||||
dialog.addEventListener("cancel", (event) => {
|
if (closeButton) {
|
||||||
event.preventDefault();
|
closeButton.addEventListener("click", () => {
|
||||||
dialog.classList.add("closing");
|
dialog.classList.add("closing");
|
||||||
setTimeout(() => dialog.close(), 200);
|
setTimeout(() => dialog.close(), 200);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.addEventListener("click", (event) => {
|
||||||
|
const rect = dialog.getBoundingClientRect();
|
||||||
|
const outsideClick =
|
||||||
|
event.clientX < rect.left ||
|
||||||
|
event.clientX > rect.right ||
|
||||||
|
event.clientY < rect.top ||
|
||||||
|
event.clientY > rect.bottom;
|
||||||
|
|
||||||
|
if (outsideClick) {
|
||||||
|
dialog.classList.add("closing");
|
||||||
|
setTimeout(() => dialog.close(), 200);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
dialog.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dialog.classList.add("closing");
|
||||||
|
setTimeout(() => dialog.close(), 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// text toggle for media pages
|
// text toggle for media pages
|
||||||
|
@ -51,12 +83,9 @@ window.addEventListener("load", () => {
|
||||||
const content = document.querySelector("[data-toggle-content]");
|
const content = document.querySelector("[data-toggle-content]");
|
||||||
const text = document.querySelectorAll("[data-toggle-content] p");
|
const text = document.querySelectorAll("[data-toggle-content] p");
|
||||||
const minHeight = 500; // this needs to match the height set on [data-toggle-content].text-toggle-hidden in text-toggle.css
|
const minHeight = 500; // this needs to match the height set on [data-toggle-content].text-toggle-hidden in text-toggle.css
|
||||||
const interiorHeight = Array.from(text).reduce(
|
const interiorHeight = Array.from(text).reduce((acc, node) => acc + node.scrollHeight, 0);
|
||||||
(acc, node) => acc + node.scrollHeight,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!button || !content || !text) return;
|
if (!button || !content || !text.length) return;
|
||||||
|
|
||||||
if (interiorHeight < minHeight) {
|
if (interiorHeight < minHeight) {
|
||||||
content.classList.remove("text-toggle-hidden");
|
content.classList.remove("text-toggle-hidden");
|
||||||
|
|
|
@ -6,22 +6,6 @@
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Lexend';
|
|
||||||
src: url('/assets/fonts/ll.woff2') format('woff2');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Lexend';
|
|
||||||
src: url('/assets/fonts/lb.woff2') format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Space Grotesk";
|
font-family: "Space Grotesk";
|
||||||
src: url("/assets/fonts/sg.woff2") format("woff2");
|
src: url("/assets/fonts/sg.woff2") format("woff2");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-regular);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
--border-gray: 1px solid var(--gray-light);
|
--border-gray: 1px solid var(--gray-light);
|
||||||
|
|
||||||
/* fonts */
|
/* fonts */
|
||||||
--font-body: "Lexend", -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, Cantarell, Ubuntu, roboto, noto, helvetica, arial, sans-serif;
|
--font-body: Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
--font-heading: "Space Grotesk", "Arial Black", "Arial Bold", Gadget, sans-serif;
|
--font-heading: "Space Grotesk", "Arial Black", "Arial Bold", Gadget, sans-serif;
|
||||||
--font-code: "MonoLisa", SFMono-Regular, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
--font-code: "MonoLisa", SFMono-Regular, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@
|
||||||
--font-size-2xl: 1.45rem;
|
--font-size-2xl: 1.45rem;
|
||||||
--font-size-3xl: 1.6rem;
|
--font-size-3xl: 1.6rem;
|
||||||
|
|
||||||
--font-weight-light: 300;
|
|
||||||
--font-weight-regular: 400;
|
--font-weight-regular: 400;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ dialog {
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3 {
|
* {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,15 @@
|
||||||
<button class="dialog-open client-side" aria-label="{{ label }}" data-dialog-trigger="{{ dialogId }}" data-dialog-button>
|
<button class="dialog-open client-side" aria-label="{{ label }}" data-dialog-trigger="{{ dialogId }}" data-dialog-button>
|
||||||
{{ labelContent }}
|
{{ labelContent }}
|
||||||
</button>
|
</button>
|
||||||
<dialog id="dialog-{{ dialogId }}" class="client-side">
|
<dialog
|
||||||
|
id="dialog-{{ dialogId }}"
|
||||||
|
class="client-side"
|
||||||
|
{% if dynamic %}data-dynamic="{{ dynamic }}"{% endif %}
|
||||||
|
{% if markdown %}data-markdown="{{ markdown }}"{% endif %}>
|
||||||
<button class="dialog-close" aria-label="Close dialog" data-dialog-button>
|
<button class="dialog-close" aria-label="Close dialog" data-dialog-button>
|
||||||
{% tablericon "circle-x" %}
|
{% tablericon "circle-x" %}
|
||||||
</button>
|
</button>
|
||||||
|
{%- unless dynamic -%}
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
{%- endunless -%}
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
|
@ -15,12 +15,13 @@
|
||||||
• {{ item.label }}
|
• {{ item.label }}
|
||||||
<span class="client-side">
|
<span class="client-side">
|
||||||
{%- if item.notes -%}
|
{%- if item.notes -%}
|
||||||
{% assign notes = item.notes | prepend: "### Notes\n" | markdown %}
|
{% assign notes = item.notes | markdown %}
|
||||||
{% render "blocks/dialog.liquid",
|
{% render "blocks/dialog.liquid",
|
||||||
icon:"info-circle",
|
icon:"info-circle",
|
||||||
label:"View info about this concert"
|
label:"View info about this concert"
|
||||||
content:notes,
|
dynamic:"optimized_concerts",
|
||||||
id:item.content_date
|
markdown:"concert_notes",
|
||||||
|
id:item.id
|
||||||
%}
|
%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="preload" href="/assets/fonts/sg.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
<link rel="preload" href="/assets/fonts/sg.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="/assets/fonts/ll.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
|
||||||
<link rel="preload" href="/assets/fonts/lb.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
|
||||||
<link rel="preload" href="/assets/fonts/ml.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
<link rel="preload" href="/assets/fonts/ml.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||||
<noscript>
|
<noscript>
|
||||||
<link rel="stylesheet" href="/assets/styles/noscript.css?v={% appVersion %}" type="text/css" />
|
<link rel="stylesheet" href="/assets/styles/noscript.css?v={% appVersion %}" type="text/css" />
|
||||||
|
|
|
@ -84,7 +84,7 @@ schema: artist
|
||||||
$dialogId = "dialog-" . htmlspecialchars($concert["id"]);
|
$dialogId = "dialog-" . htmlspecialchars($concert["id"]);
|
||||||
?>
|
?>
|
||||||
<li class="concerts">
|
<li class="concerts">
|
||||||
On <mark><?php echo date("F j, Y", strtotime($concert["date"])); ?></mark>
|
On <mark><?php echo date("F j, Y", strtotime($concert["date"])); ?></mark> 
|
||||||
<?php if (!empty($venue)): ?>
|
<?php if (!empty($venue)): ?>
|
||||||
at <?php echo $venue; ?>
|
at <?php echo $venue; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
|
@ -41,7 +41,7 @@ schema: book
|
||||||
<span class="sub-meta tattoo">{% tablericon "needle" %} I have a tattoo inspired by this book!</span>
|
<span class="sub-meta tattoo">{% tablericon "needle" %} I have a tattoo inspired by this book!</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($book["status"] == 'finished'): ?>
|
<?php if ($book["status"] == 'finished'): ?>
|
||||||
<span class="sub-meta">Finished on: <mark><?= date('F j, Y', strtotime($book["date_finished"])) ?></mark></span>
|
<span class="sub-meta">Finished on: <mark><?= date('F j, Y', strtotime($book["date_finished"])) ?></mark></span>
|
||||||
<?php elseif ($book["status"] == 'started'): ?>
|
<?php elseif ($book["status"] == 'started'): ?>
|
||||||
<progress value="<?= htmlspecialchars($book["progress"]) ?>" max="100"><?php $book["progress"] . '%'; ?></progress>
|
<progress value="<?= htmlspecialchars($book["progress"]) ?>" max="100"><?php $book["progress"] . '%'; ?></progress>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
|
@ -8,7 +8,7 @@ schema: genre
|
||||||
<article class="genre-focus">
|
<article class="genre-focus">
|
||||||
<?php $artistCount = count($genre["artists"]); ?>
|
<?php $artistCount = count($genre["artists"]); ?>
|
||||||
<?php if ($artistCount > 0): ?>
|
<?php if ($artistCount > 0): ?>
|
||||||
<p>My top <mark><?= htmlspecialchars($genre["name"]) ?></mark> artists are
|
<p>My top <mark><?= htmlspecialchars($genre["name"]) ?></mark> artists are
|
||||||
<?php
|
<?php
|
||||||
$artists = array_slice($genre["artists"], 0, 5);
|
$artists = array_slice($genre["artists"], 0, 5);
|
||||||
$artistLinks = [];
|
$artistLinks = [];
|
||||||
|
@ -16,7 +16,7 @@ schema: genre
|
||||||
$artistLinks[] = '<a href="' . htmlspecialchars($artist["url"]) . '">' . htmlspecialchars($artist["name"]) . '</a>';
|
$artistLinks[] = '<a href="' . htmlspecialchars($artist["url"]) . '">' . htmlspecialchars($artist["name"]) . '</a>';
|
||||||
}
|
}
|
||||||
echo implode(', ', $artistLinks);
|
echo implode(', ', $artistLinks);
|
||||||
?>. I've listened to <mark><?= $genre["total_plays"] . ' ' . pluralize($genre["total_plays"], "play") ?></mark> tracks from this genre.</p>
|
?>. I've listened to <mark><?= $genre["total_plays"] . ' ' . pluralize($genre["total_plays"], "play") ?></mark> tracks from this genre.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
|
|
|
@ -33,10 +33,7 @@ schema: show
|
||||||
<span class="sub-meta tattoo">{% tablericon "needle" %} I have a tattoo inspired by this show!</span>
|
<span class="sub-meta tattoo">{% tablericon "needle" %} I have a tattoo inspired by this show!</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($show["episode"]["formatted_episode"])): ?>
|
<?php if (!empty($show["episode"]["formatted_episode"])): ?>
|
||||||
<span class="sub-meta">I last watched
|
<span class="sub-meta">I last watched <mark><?= htmlspecialchars($show["episode"]["formatted_episode"]) ?></mark> on <?= date('F j, Y', strtotime($show["episode"]["last_watched_at"])) ?>.</span>
|
||||||
<mark><?= htmlspecialchars($show["episode"]["formatted_episode"]) ?></mark> 
|
|
||||||
on <?= date('F j, Y', strtotime($show["episode"]["last_watched_at"])) ?>.
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($show["review"])): ?>
|
<?php if (!empty($show["review"])): ?>
|
||||||
|
|
|
@ -5,7 +5,7 @@ schema: tags
|
||||||
---
|
---
|
||||||
<a class="back-link" href="/tags" title="Go back to the tags index page">{% tablericon "arrow-left" %} Back to tags</a>
|
<a class="back-link" href="/tags" title="Go back to the tags index page">{% tablericon "arrow-left" %} Back to tags</a>
|
||||||
<h2 class="page-title">#<?= htmlspecialchars($tag) ?></h2>
|
<h2 class="page-title">#<?= htmlspecialchars($tag) ?></h2>
|
||||||
<p><mark><?= number_format($totalCount) ?> item<?= $totalCount === 1 ? '' : 's' ?></mark> items tagged with <mark>#<?= htmlspecialchars($tag) ?></mark>. <a class="search" href="/search">You can search my site as well</a>.</p>
|
<p><mark><?= number_format($totalCount) ?> item<?= $totalCount === 1 ? '' : 's' ?></mark> items tagged with <mark>#<?= htmlspecialchars($tag) ?></mark>. <a class="search" href="/search">You can search my site as well</a>.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<?php foreach ($tagged as $item): ?>
|
<?php foreach ($tagged as $item): ?>
|
||||||
<article class="<?= $item['type'] ?>">
|
<article class="<?= $item['type'] ?>">
|
||||||
|
@ -19,9 +19,9 @@ schema: tags
|
||||||
• <?= $item['label'] ?>
|
• <?= $item['label'] ?>
|
||||||
</aside>
|
</aside>
|
||||||
<h3>
|
<h3>
|
||||||
<a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?><?php if (!empty($item['rating'])): ?> (<?= $item['rating'] ?>)<?php endif; ?></a>
|
<a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?><?php if (!empty($item['rating'])): ?> (<?= $item['rating'] ?>)<?php endif; ?></a>
|
||||||
<?php if (!empty($item['author'])): ?>
|
<?php if (!empty($item['author'])): ?>
|
||||||
 via 
|
 via 
|
||||||
<?php if (!empty($item['author']['url'])): ?>
|
<?php if (!empty($item['author']['url'])): ?>
|
||||||
<a href="<?= $item['author']['url'] ?>">
|
<a href="<?= $item['author']['url'] ?>">
|
||||||
<?= $item['author']['name'] ?>
|
<?= $item['author']['name'] ?>
|
||||||
|
|
|
@ -33,12 +33,13 @@ permalink: "/music/concerts/{% if pagination.pageNumber > 0 %}{{ pagination.page
|
||||||
<strong>{{ artistName }}</strong> on {{ concert.date | date: "%B %e, %Y" }}
|
<strong>{{ artistName }}</strong> on {{ concert.date | date: "%B %e, %Y" }}
|
||||||
{% if venue %} at {{ venue }}{% endif %}
|
{% if venue %} at {{ venue }}{% endif %}
|
||||||
<span class="client-side" style="display:inline">
|
<span class="client-side" style="display:inline">
|
||||||
{%- if concert.notes -%}
|
{%- if concert.concert_notes -%}
|
||||||
{% assign notes = concert.notes | prepend: "### Notes\n" | markdown %}
|
{% assign notes = concert.concert_notes | markdown %}
|
||||||
{% render "blocks/dialog.liquid",
|
{% render "blocks/dialog.liquid",
|
||||||
icon:"info-circle",
|
icon:"info-circle",
|
||||||
label:"View info about this concert"
|
label:"View info about this concert"
|
||||||
content:notes,
|
dynamic:"optimized_concerts",
|
||||||
|
markdown:"concert_notes",
|
||||||
id:concert.id
|
id:concert.id
|
||||||
%}
|
%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue