chore(*): remove duplicate cache rule + cleanup cache headers; cleanup + formatting
This commit is contained in:
parent
425fed6ff6
commit
0e565970a5
42 changed files with 223 additions and 217 deletions
|
@ -17,13 +17,13 @@ To debug and develop php components, run `npm run php`. This will start the PHP
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `npm run start`: starts 11ty.
|
- `npm run start`: primary dev command that runs `watch` and `php` concurrently.
|
||||||
- `npm run start:quick`: starts 11ty a bit quicker (provided it's already been built).
|
- `npm run start:eleventy`: starts 11ty.
|
||||||
|
- `npm run start:eleventy:quick`: starts 11ty a bit quicker (provided it's already been built).
|
||||||
- `npm run debug`: runs 11ty with additional debug output.
|
- `npm run debug`: runs 11ty with additional debug output.
|
||||||
- `npm run watch`: watch and update when files change without running the web server.
|
- `npm run watch`: watch and update when files change without running the web server.
|
||||||
- `npm run build`: builds static site output.
|
- `npm run build`: builds static site output.
|
||||||
- `npm run php`: starts a PHP server for local development.
|
- `npm run php`: starts a PHP server for local development.
|
||||||
- `npm run dev`: primary dev command that runs `watch` and `php` concurrently.
|
|
||||||
- `npm run update:deps`: checks for dependency updates and updates 11ty.
|
- `npm run update:deps`: checks for dependency updates and updates 11ty.
|
||||||
- `npm run setup`: populates `.env` from 1Password and installs dependencies using `npm` and `composer`.
|
- `npm run setup`: populates `.env` from 1Password and installs dependencies using `npm` and `composer`.
|
||||||
- `npm run clean`: removes the `dist` and `.cache` folders.
|
- `npm run clean`: removes the `dist` and `.cache` folders.
|
||||||
|
|
|
@ -8,8 +8,6 @@ abstract class ApiHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
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->sendErrorResponse("Not Found", 404);
|
||||||
$this->sendErrorResponse("Not Found", 404);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,15 @@ class ArtistFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$cacheKey = "artist_" . md5($url);
|
$cacheKey = "artist_" . md5($url);
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$artist = $this->fetchSingleFromApi("optimized_artists", $url);
|
$artist = $this->fetchSingleFromApi("optimized_artists", $url);
|
||||||
|
|
||||||
if (!$artist) return null;
|
if (!$artist) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $artist);
|
$this->cacheSet($cacheKey, $artist);
|
||||||
|
|
||||||
return $artist;
|
return $artist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,13 +31,16 @@ abstract class BaseHandler
|
||||||
try {
|
try {
|
||||||
$redis = new \Redis();
|
$redis = new \Redis();
|
||||||
$redis->connect("127.0.0.1", 6379);
|
$redis->connect("127.0.0.1", 6379);
|
||||||
|
|
||||||
$this->cache = $redis;
|
$this->cache = $redis;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Redis connection failed: " . $e->getMessage());
|
error_log("Redis connection failed: " . $e->getMessage());
|
||||||
|
|
||||||
$this->cache = null;
|
$this->cache = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log("Redis extension not found — caching disabled.");
|
error_log("Redis extension not found — caching disabled.");
|
||||||
|
|
||||||
$this->cache = null;
|
$this->cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,12 +59,12 @@ abstract class BaseHandler
|
||||||
], $options));
|
], $options));
|
||||||
|
|
||||||
$responseBody = $response->getBody()->getContents();
|
$responseBody = $response->getBody()->getContents();
|
||||||
|
|
||||||
if (empty($responseBody)) return [];
|
if (empty($responseBody)) return [];
|
||||||
|
|
||||||
$data = 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: " . json_last_error_msg());
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
|
@ -78,6 +81,7 @@ abstract class BaseHandler
|
||||||
protected function fetchFromApi(string $endpoint, string $query = ""): array
|
protected function fetchFromApi(string $endpoint, string $query = ""): array
|
||||||
{
|
{
|
||||||
$url = $endpoint . ($query ? "?{$query}" : "");
|
$url = $endpoint . ($query ? "?{$query}" : "");
|
||||||
|
|
||||||
return $this->makeRequest("GET", $url);
|
return $this->makeRequest("GET", $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +89,9 @@ abstract class BaseHandler
|
||||||
{
|
{
|
||||||
http_response_code($statusCode);
|
http_response_code($statusCode);
|
||||||
header("Content-Type: application/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,15 @@ class BookFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$cacheKey = "book_" . md5($url);
|
$cacheKey = "book_" . md5($url);
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$book = $this->fetchSingleFromApi("optimized_books", $url);
|
$book = $this->fetchSingleFromApi("optimized_books", $url);
|
||||||
|
|
||||||
if (!$book) return null;
|
if (!$book) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $book);
|
$this->cacheSet($cacheKey, $book);
|
||||||
|
|
||||||
return $book;
|
return $book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,15 @@ class GenreFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$cacheKey = "genre_" . md5($url);
|
$cacheKey = "genre_" . md5($url);
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$genre = $this->fetchSingleFromApi("optimized_genres", $url);
|
$genre = $this->fetchSingleFromApi("optimized_genres", $url);
|
||||||
|
|
||||||
if (!$genre) return null;
|
if (!$genre) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $genre);
|
$this->cacheSet($cacheKey, $genre);
|
||||||
|
|
||||||
return $genre;
|
return $genre;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,15 @@ class MovieFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$cacheKey = "movie_" . md5($url);
|
$cacheKey = "movie_" . md5($url);
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$movie = $this->fetchSingleFromApi("optimized_movies", $url);
|
$movie = $this->fetchSingleFromApi("optimized_movies", $url);
|
||||||
|
|
||||||
if (!$movie) return null;
|
if (!$movie) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $movie);
|
$this->cacheSet($cacheKey, $movie);
|
||||||
|
|
||||||
return $movie;
|
return $movie;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ abstract class PageFetcher extends BaseHandler
|
||||||
protected function fetchSingleFromApi(string $endpoint, string $url): ?array
|
protected function fetchSingleFromApi(string $endpoint, string $url): ?array
|
||||||
{
|
{
|
||||||
$data = $this->fetchFromApi($endpoint, "url=eq./{$url}");
|
$data = $this->fetchFromApi($endpoint, "url=eq./{$url}");
|
||||||
|
|
||||||
return $data[0] ?? null;
|
return $data[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,15 @@ class ShowFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$cacheKey = "show_" . md5($url);
|
$cacheKey = "show_" . md5($url);
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$show = $this->fetchSingleFromApi("optimized_shows", $url);
|
$show = $this->fetchSingleFromApi("optimized_shows", $url);
|
||||||
|
|
||||||
if (!$show) return null;
|
if (!$show) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $show);
|
$this->cacheSet($cacheKey, $show);
|
||||||
|
|
||||||
return $show;
|
return $show;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ class TagFetcher extends PageFetcher
|
||||||
{
|
{
|
||||||
$offset = ($page - 1) * $pageSize;
|
$offset = ($page - 1) * $pageSize;
|
||||||
$cacheKey = "tag_" . md5("{$tag}_{$page}");
|
$cacheKey = "tag_" . md5("{$tag}_{$page}");
|
||||||
|
|
||||||
$cached = $this->cacheGet($cacheKey);
|
$cached = $this->cacheGet($cacheKey);
|
||||||
|
|
||||||
if ($cached) return $cached;
|
if ($cached) return $cached;
|
||||||
|
|
||||||
$results = $this->fetchPostRpc("rpc/get_tagged_content", [
|
$results = $this->fetchPostRpc("rpc/get_tagged_content", [
|
||||||
|
@ -21,6 +21,7 @@ class TagFetcher extends PageFetcher
|
||||||
if (!$results || count($results) === 0) return null;
|
if (!$results || count($results) === 0) return null;
|
||||||
|
|
||||||
$this->cacheSet($cacheKey, $results);
|
$this->cacheSet($cacheKey, $results);
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
function sanitizeMediaString(string $str): string
|
function sanitizeMediaString(string $str): string
|
||||||
{
|
{
|
||||||
$sanitizedString = preg_replace(
|
$sanitizedString = preg_replace("/[^a-zA-Z0-9\s-]/", "", iconv("UTF-8", "ASCII//TRANSLIT", $str));
|
||||||
"/[^a-zA-Z0-9\s-]/",
|
|
||||||
"",
|
|
||||||
iconv("UTF-8", "ASCII//TRANSLIT", $str)
|
|
||||||
);
|
|
||||||
|
|
||||||
return strtolower(
|
return strtolower(trim(preg_replace("/[\s-]+/", "-", $sanitizedString), "-"));
|
||||||
trim(preg_replace("/[\s-]+/", "-", $sanitizedString), "-")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -16,8 +16,8 @@ class ArtistImportHandler extends ApiHandler
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
|
||||||
|
|
||||||
|
$this->ensureCliAccess();
|
||||||
$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");
|
||||||
|
@ -32,13 +32,8 @@ class ArtistImportHandler extends ApiHandler
|
||||||
$providedToken = $input["token"] ?? null;
|
$providedToken = $input["token"] ?? null;
|
||||||
$artistId = $input["artistId"] ?? null;
|
$artistId = $input["artistId"] ?? null;
|
||||||
|
|
||||||
if ($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);
|
||||||
|
@ -56,7 +51,9 @@ class ArtistImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
http_response_code($statusCode);
|
http_response_code($statusCode);
|
||||||
header("Content-Type: application/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
echo json_encode([$key => $message]);
|
echo json_encode([$key => $message]);
|
||||||
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,9 +93,11 @@ 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.");
|
||||||
|
|
||||||
$existingArtist = $this->getArtistByName($artistName);
|
$existingArtist = $this->getArtistByName($artistName);
|
||||||
|
|
||||||
if ($existingArtist) return true;
|
if ($existingArtist) return true;
|
||||||
|
|
||||||
$artistKey = sanitizeMediaString($artistName);
|
$artistKey = sanitizeMediaString($artistName);
|
||||||
|
@ -106,7 +105,6 @@ class ArtistImportHandler extends ApiHandler
|
||||||
$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;
|
||||||
|
|
||||||
$artistPayload = [
|
$artistPayload = [
|
||||||
"name_string" => $artistName,
|
"name_string" => $artistName,
|
||||||
"slug" => $slug,
|
"slug" => $slug,
|
||||||
|
@ -119,17 +117,18 @@ class ArtistImportHandler extends ApiHandler
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->makeRequest("POST", "artists", ["json" => $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 after insert.");
|
||||||
|
|
||||||
$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) {
|
||||||
|
@ -137,9 +136,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
$releaseYearRaw = $album["date"] ?? null;
|
$releaseYearRaw = $album["date"] ?? null;
|
||||||
$releaseYear = null;
|
$releaseYear = null;
|
||||||
|
|
||||||
if ($releaseYearRaw && preg_match('/^\d{4}/', $releaseYearRaw, $matches)) {
|
if ($releaseYearRaw && 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);
|
||||||
|
@ -170,6 +167,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
private function getArtistByName(string $nameString): ?array
|
private function getArtistByName(string $nameString): ?array
|
||||||
{
|
{
|
||||||
$response = $this->fetchFromApi("artists", "name_string=eq." . urlencode($nameString));
|
$response = $this->fetchFromApi("artists", "name_string=eq." . urlencode($nameString));
|
||||||
|
|
||||||
return $response[0] ?? null;
|
return $response[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,6 +179,7 @@ class ArtistImportHandler extends ApiHandler
|
||||||
private function resolveGenreId(string $genreName): ?string
|
private function resolveGenreId(string $genreName): ?string
|
||||||
{
|
{
|
||||||
$genres = $this->fetchFromApi("genres", "name=eq." . urlencode(strtolower($genreName)));
|
$genres = $this->fetchFromApi("genres", "name=eq." . urlencode(strtolower($genreName)));
|
||||||
|
|
||||||
return $genres[0]["id"] ?? null;
|
return $genres[0]["id"] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ class BookImportHandler extends ApiHandler
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->ensureCliAccess();
|
$this->ensureCliAccess();
|
||||||
$this->bookImportToken = $_ENV["BOOK_IMPORT_TOKEN"] ?? getenv("BOOK_IMPORT_TOKEN");
|
$this->bookImportToken = $_ENV["BOOK_IMPORT_TOKEN"] ?? getenv("BOOK_IMPORT_TOKEN");
|
||||||
}
|
}
|
||||||
|
@ -20,20 +21,13 @@ class BookImportHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
$input = json_decode(file_get_contents("php://input"), true);
|
$input = json_decode(file_get_contents("php://input"), true);
|
||||||
|
|
||||||
if (!$input) {
|
if (!$input) $this->sendErrorResponse("Invalid or missing JSON body", 400);
|
||||||
$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 !== $this->bookImportToken) {
|
if ($providedToken !== $this->bookImportToken) $this->sendErrorResponse("Unauthorized access", 401);
|
||||||
$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);
|
||||||
|
@ -59,9 +53,7 @@ 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])) {
|
if (empty($data[$bookKey])) throw new \Exception("Book data not found for ISBN: {$isbn}");
|
||||||
throw new \Exception("Book data not found for ISBN: {$isbn}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data[$bookKey];
|
return $data[$bookKey];
|
||||||
}
|
}
|
||||||
|
@ -75,14 +67,11 @@ 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) {
|
if (!$isbn || !$title || !$author) throw new \Exception("Missing essential book data (title, author, or ISBN).");
|
||||||
throw new \Exception("Missing essential book data (title, author, or ISBN).");
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingBook = $this->getBookByISBN($isbn);
|
$existingBook = $this->getBookByISBN($isbn);
|
||||||
if ($existingBook) {
|
|
||||||
throw new \Exception("Book with ISBN {$isbn} already exists.");
|
if ($existingBook) throw new \Exception("Book with ISBN {$isbn} already exists.");
|
||||||
}
|
|
||||||
|
|
||||||
$bookPayload = [
|
$bookPayload = [
|
||||||
"isbn" => $isbn,
|
"isbn" => $isbn,
|
||||||
|
@ -99,6 +88,7 @@ class BookImportHandler extends ApiHandler
|
||||||
private function getBookByISBN(string $isbn): ?array
|
private function getBookByISBN(string $isbn): ?array
|
||||||
{
|
{
|
||||||
$response = $this->fetchFromApi("books", "isbn=eq." . urlencode($isbn));
|
$response = $this->fetchFromApi("books", "isbn=eq." . urlencode($isbn));
|
||||||
|
|
||||||
return $response[0] ?? null;
|
return $response[0] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,13 @@ class ContactHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
protected string $postgrestUrl;
|
protected string $postgrestUrl;
|
||||||
protected string $postgrestApiKey;
|
protected string $postgrestApiKey;
|
||||||
|
|
||||||
private string $forwardEmailApiKey;
|
private string $forwardEmailApiKey;
|
||||||
private Client $httpClient;
|
private Client $httpClient;
|
||||||
|
|
||||||
public function __construct(?Client $httpClient = null)
|
public function __construct(?Client $httpClient = null)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->httpClient = $httpClient ?? new Client();
|
$this->httpClient = $httpClient ?? new Client();
|
||||||
$this->forwardEmailApiKey = $_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY");
|
$this->forwardEmailApiKey = $_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY");
|
||||||
}
|
}
|
||||||
|
@ -33,19 +33,16 @@ class ContactHandler extends BaseHandler
|
||||||
if (strpos($contentType, "application/json") !== false) {
|
if (strpos($contentType, "application/json") !== false) {
|
||||||
$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"])) {
|
|
||||||
throw new \Exception("Invalid JSON payload.");
|
if (!$formData || !isset($formData["data"])) throw new \Exception("Invalid JSON payload.");
|
||||||
}
|
|
||||||
$formData = $formData["data"];
|
$formData = $formData["data"];
|
||||||
} elseif (
|
} elseif (
|
||||||
strpos($contentType, "application/x-www-form-urlencoded") !== false
|
strpos($contentType, "application/x-www-form-urlencoded") !== false
|
||||||
) {
|
) {
|
||||||
$formData = $_POST;
|
$formData = $_POST;
|
||||||
} else {
|
} else {
|
||||||
$this->sendErrorResponse(
|
$this->sendErrorResponse("Unsupported Content-Type. Use application/json or application/x-www-form-urlencoded.", 400);
|
||||||
"Unsupported Content-Type. Use application/json or application/x-www-form-urlencoded.",
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($formData["hp_name"])) $this->sendErrorResponse("Invalid submission.", 400);
|
if (!empty($formData["hp_name"])) $this->sendErrorResponse("Invalid submission.", 400);
|
||||||
|
@ -65,14 +62,8 @@ class ContactHandler extends BaseHandler
|
||||||
if (empty($name)) $this->sendErrorResponse("Name is required.", 400);
|
if (empty($name)) $this->sendErrorResponse("Name is required.", 400);
|
||||||
if (!$email) $this->sendErrorResponse("Valid email is required.", 400);
|
if (!$email) $this->sendErrorResponse("Valid email is required.", 400);
|
||||||
if (empty($message)) $this->sendErrorResponse("Message is required.", 400);
|
if (empty($message)) $this->sendErrorResponse("Message is required.", 400);
|
||||||
if (strlen($name) > 100) $this->sendErrorResponse(
|
if (strlen($name) > 100) $this->sendErrorResponse("Name is too long. Max 100 characters allowed.", 400);
|
||||||
"Name is too long. Max 100 characters allowed.",
|
if (strlen($message) > 1000) $this->sendErrorResponse("Message is too long. Max 1000 characters allowed.", 400);
|
||||||
400
|
|
||||||
);
|
|
||||||
if (strlen($message) > 1000) $this->sendErrorResponse(
|
|
||||||
"Message is too long. Max 1000 characters allowed.",
|
|
||||||
400
|
|
||||||
);
|
|
||||||
if ($this->isBlockedDomain($email)) $this->sendErrorResponse("Submission from blocked domain.", 400);
|
if ($this->isBlockedDomain($email)) $this->sendErrorResponse("Submission from blocked domain.", 400);
|
||||||
|
|
||||||
$contactData = [
|
$contactData = [
|
||||||
|
@ -87,6 +78,7 @@ class ContactHandler extends BaseHandler
|
||||||
$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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,6 +87,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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,13 +100,12 @@ class ContactHandler extends BaseHandler
|
||||||
|
|
||||||
if (file_exists($cacheFile)) {
|
if (file_exists($cacheFile)) {
|
||||||
$data = json_decode(file_get_contents($cacheFile), true);
|
$data = json_decode(file_get_contents($cacheFile), true);
|
||||||
if (
|
|
||||||
$data["timestamp"] + $rateLimitDuration > time() &&
|
if ($data["timestamp"] + $rateLimitDuration > time() && $data["count"] >= $maxRequests) {
|
||||||
$data["count"] >= $maxRequests
|
|
||||||
) {
|
|
||||||
header("Location: /429", true, 302);
|
header("Location: /429", true, 302);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$data["count"]++;
|
$data["count"]++;
|
||||||
} else {
|
} else {
|
||||||
$data = ["count" => 1, "timestamp" => time()];
|
$data = ["count" => 1, "timestamp" => time()];
|
||||||
|
@ -130,6 +122,7 @@ class ContactHandler extends BaseHandler
|
||||||
private function isBlockedDomain(string $email): bool
|
private function isBlockedDomain(string $email): bool
|
||||||
{
|
{
|
||||||
$domain = substr(strrchr($email, "@"), 1);
|
$domain = substr(strrchr($email, "@"), 1);
|
||||||
|
|
||||||
if (!$domain) return false;
|
if (!$domain) return false;
|
||||||
|
|
||||||
$response = $this->httpClient->get(
|
$response = $this->httpClient->get(
|
||||||
|
@ -145,7 +138,6 @@ class ContactHandler extends BaseHandler
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$blockedDomains = json_decode($response->getBody(), true);
|
$blockedDomains = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
return !empty($blockedDomains);
|
return !empty($blockedDomains);
|
||||||
|
@ -163,9 +155,8 @@ 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(
|
|
||||||
"PostgREST error: " . ($errorResponse["message"] ?? "Unknown error")
|
throw new \Exception("PostgREST error: " . ($errorResponse["message"] ?? "Unknown error"));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,6 +197,7 @@ class ContactHandler extends BaseHandler
|
||||||
$redirectUrl = "{$protocol}://{$host}{$path}";
|
$redirectUrl = "{$protocol}://{$host}{$path}";
|
||||||
|
|
||||||
header("Location: $redirectUrl", true, 302);
|
header("Location: $redirectUrl", true, 302);
|
||||||
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,6 +207,8 @@ try {
|
||||||
$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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,19 +10,16 @@ class MastodonPostHandler extends ApiHandler
|
||||||
private string $mastodonAccessToken;
|
private string $mastodonAccessToken;
|
||||||
private string $rssFeedUrl = "https://www.coryd.dev/feeds/syndication.xml";
|
private string $rssFeedUrl = "https://www.coryd.dev/feeds/syndication.xml";
|
||||||
private string $baseUrl = "https://www.coryd.dev";
|
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";
|
||||||
|
|
||||||
private Client $httpClient;
|
private Client $httpClient;
|
||||||
|
|
||||||
public function __construct(?Client $httpClient = null)
|
public function __construct(?Client $httpClient = null)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
|
||||||
|
|
||||||
|
$this->ensureCliAccess();
|
||||||
$this->mastodonAccessToken = getenv("MASTODON_ACCESS_TOKEN") ?: $_ENV["MASTODON_ACCESS_TOKEN"] ?? "";
|
$this->mastodonAccessToken = getenv("MASTODON_ACCESS_TOKEN") ?: $_ENV["MASTODON_ACCESS_TOKEN"] ?? "";
|
||||||
$this->httpClient = $httpClient ?: new Client();
|
$this->httpClient = $httpClient ?: new Client();
|
||||||
|
|
||||||
$this->validateAuthorization();
|
$this->validateAuthorization();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +46,7 @@ class MastodonPostHandler extends ApiHandler
|
||||||
|
|
||||||
foreach (array_reverse($latestItems) as $item) {
|
foreach (array_reverse($latestItems) as $item) {
|
||||||
$existing = $this->fetchFromApi("mastodon_posts", "link=eq." . urlencode($item["link"]));
|
$existing = $this->fetchFromApi("mastodon_posts", "link=eq." . urlencode($item["link"]));
|
||||||
|
|
||||||
if (!empty($existing)) continue;
|
if (!empty($existing)) continue;
|
||||||
|
|
||||||
$content = $this->truncateContent(
|
$content = $this->truncateContent(
|
||||||
|
@ -57,7 +55,6 @@ class MastodonPostHandler extends ApiHandler
|
||||||
$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)) {
|
||||||
|
@ -66,6 +63,7 @@ class MastodonPostHandler extends ApiHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
$postedUrl = $this->postToMastodon($content, $item["image"] ?? null);
|
$postedUrl = $this->postToMastodon($content, $item["image"] ?? null);
|
||||||
|
|
||||||
if ($postedUrl) {
|
if ($postedUrl) {
|
||||||
echo "Posted: {$postedUrl}\n";
|
echo "Posted: {$postedUrl}\n";
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,6 +77,7 @@ 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);
|
||||||
|
@ -106,9 +105,11 @@ class MastodonPostHandler extends ApiHandler
|
||||||
"headers" => $headers,
|
"headers" => $headers,
|
||||||
"json" => $postData
|
"json" => $postData
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($response->getStatusCode() >= 400) throw new \Exception("Mastodon post failed: {$response->getBody()}");
|
if ($response->getStatusCode() >= 400) throw new \Exception("Mastodon post failed: {$response->getBody()}");
|
||||||
|
|
||||||
$body = json_decode($response->getBody()->getContents(), true);
|
$body = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
return $body["url"] ?? null;
|
return $body["url"] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +122,11 @@ class MastodonPostHandler extends ApiHandler
|
||||||
"created_at" => $timestamp
|
"created_at" => $timestamp
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
echo "Error storing post in DB: " . $e->getMessage() . "\n";
|
echo "Error storing post in DB: " . $e->getMessage() . "\n";
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,9 +135,11 @@ class MastodonPostHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->fetchFromApi("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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,5 +163,6 @@ try {
|
||||||
$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()]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ class OembedHandler extends BaseHandler
|
||||||
|
|
||||||
if ($this->cache && $this->cache->exists($cacheKey)) {
|
if ($this->cache && $this->cache->exists($cacheKey)) {
|
||||||
$cachedItem = json_decode($this->cache->get($cacheKey), true);
|
$cachedItem = json_decode($this->cache->get($cacheKey), true);
|
||||||
|
|
||||||
$this->sendResponse($this->buildResponse(
|
$this->sendResponse($this->buildResponse(
|
||||||
$cachedItem['title'],
|
$cachedItem['title'],
|
||||||
$cachedItem['url'],
|
$cachedItem['url'],
|
||||||
|
|
|
@ -27,8 +27,10 @@
|
||||||
if ($httpCode !== 200 || $image === false || strpos($contentType, 'image/') !== 0) {
|
if ($httpCode !== 200 || $image === false || strpos($contentType, 'image/') !== 0) {
|
||||||
error_log("Failed to fetch image: $cdnUrl ($httpCode - $contentType)");
|
error_log("Failed to fetch image: $cdnUrl ($httpCode - $contentType)");
|
||||||
header("Location: /404", true, 302);
|
header("Location: /404", true, 302);
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Content-Type: $contentType");
|
header("Content-Type: $contentType");
|
||||||
|
|
||||||
echo $image;
|
echo $image;
|
||||||
|
|
|
@ -18,6 +18,7 @@ class LatestListenHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$cachedData = $this->cache ? $this->cache->get("latest_listen") : null;
|
$cachedData = $this->cache ? $this->cache->get("latest_listen") : null;
|
||||||
|
|
||||||
if ($cachedData) {
|
if ($cachedData) {
|
||||||
$this->sendResponse(json_decode($cachedData, true));
|
$this->sendResponse(json_decode($cachedData, true));
|
||||||
return;
|
return;
|
||||||
|
@ -41,17 +42,14 @@ 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(
|
|
||||||
"Internal Server Error: " . $e->getMessage(),
|
$this->sendErrorResponse("Internal Server Error: " . $e->getMessage(), 500);
|
||||||
500
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatLatestListen(array $latestListen): array
|
private function formatLatestListen(array $latestListen): array
|
||||||
{
|
{
|
||||||
$emoji =
|
$emoji = $latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧");
|
||||||
$latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧");
|
|
||||||
$trackName = htmlspecialchars(
|
$trackName = htmlspecialchars(
|
||||||
$latestListen["track_name"] ?? "Unknown Track",
|
$latestListen["track_name"] ?? "Unknown Track",
|
||||||
ENT_QUOTES,
|
ENT_QUOTES,
|
||||||
|
|
|
@ -18,7 +18,6 @@ class QueryHandler extends BaseHandler
|
||||||
$allowedHosts = ['coryd.dev', 'www.coryd.dev', 'localhost'];
|
$allowedHosts = ['coryd.dev', 'www.coryd.dev', 'localhost'];
|
||||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||||
|
|
||||||
$hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true);
|
$hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true);
|
||||||
|
|
||||||
if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden — invalid origin", 403);
|
if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden — invalid origin", 403);
|
||||||
|
@ -44,6 +43,7 @@ class QueryHandler extends BaseHandler
|
||||||
|
|
||||||
if ($this->cache) {
|
if ($this->cache) {
|
||||||
$cached = $this->cache->get($cacheKey);
|
$cached = $this->cache->get($cacheKey);
|
||||||
|
|
||||||
if ($cached) {
|
if ($cached) {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo $cached;
|
echo $cached;
|
||||||
|
|
|
@ -13,7 +13,6 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
private string $navidromeApiUrl;
|
private string $navidromeApiUrl;
|
||||||
private string $navidromeAuthToken;
|
private string $navidromeAuthToken;
|
||||||
private string $forwardEmailApiKey;
|
private string $forwardEmailApiKey;
|
||||||
|
|
||||||
private array $artistCache = [];
|
private array $artistCache = [];
|
||||||
private array $albumCache = [];
|
private array $albumCache = [];
|
||||||
|
|
||||||
|
@ -39,7 +38,9 @@ class NavidromeScrobbleHandler 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +61,7 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
private function fetchRecentlyPlayed(): array
|
private function fetchRecentlyPlayed(): array
|
||||||
{
|
{
|
||||||
$client = new Client();
|
$client = new Client();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->request("GET", "{$this->navidromeApiUrl}/api/song", [
|
$response = $client->request("GET", "{$this->navidromeApiUrl}/api/song", [
|
||||||
"query" => [
|
"query" => [
|
||||||
|
@ -76,9 +78,11 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = json_decode($response->getBody()->getContents(), true);
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
return $data ?? [];
|
return $data ?? [];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("Error fetching tracks: " . $e->getMessage());
|
error_log("Error fetching tracks: " . $e->getMessage());
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,15 +117,12 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
private function getOrCreateArtist(string $artistName): array
|
private function getOrCreateArtist(string $artistName): array
|
||||||
{
|
{
|
||||||
if (!$this->isDatabaseAvailable()) return [];
|
if (!$this->isDatabaseAvailable()) 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->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
$existingArtist = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
||||||
|
|
||||||
if (!empty($existingArtist)) {
|
if (!empty($existingArtist)) return $this->artistCache[$artistName] = $existingArtist[0];
|
||||||
return $this->artistCache[$artistName] = $existingArtist[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->makeRequest("POST", "artists", [
|
$this->makeRequest("POST", "artists", [
|
||||||
"json" => [
|
"json" => [
|
||||||
|
@ -137,7 +138,6 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
"total_plays" => 0
|
"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->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
$artistData = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1");
|
||||||
|
@ -156,9 +156,7 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
$encodedAlbumKey = rawurlencode($albumKey);
|
$encodedAlbumKey = rawurlencode($albumKey);
|
||||||
$existingAlbum = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
$existingAlbum = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1");
|
||||||
|
|
||||||
if (!empty($existingAlbum)) {
|
if (!empty($existingAlbum)) return $this->albumCache[$albumKey] = $existingAlbum[0];
|
||||||
return $this->albumCache[$albumKey] = $existingAlbum[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$artistId = $artistData["id"] ?? null;
|
$artistId = $artistData["id"] ?? null;
|
||||||
|
|
||||||
|
@ -205,6 +203,7 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
$artistKey = sanitizeMediaString($artistName);
|
$artistKey = sanitizeMediaString($artistName);
|
||||||
$albumKey = sanitizeMediaString($albumName);
|
$albumKey = sanitizeMediaString($albumName);
|
||||||
|
|
||||||
return "{$artistKey}-{$albumKey}";
|
return "{$artistKey}-{$albumKey}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,9 +229,8 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
]);
|
]);
|
||||||
} 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()) {
|
|
||||||
error_log("Error Response: " . (string) $e->getResponse()->getBody());
|
if ($e->hasResponse()) error_log("Error Response: " . (string) $e->getResponse()->getBody());
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
error_log("General Exception: " . $e->getMessage());
|
error_log("General Exception: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
@ -242,9 +240,11 @@ class NavidromeScrobbleHandler extends ApiHandler
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->fetchFromApi("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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,5 +255,6 @@ try {
|
||||||
$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()]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,7 @@ class SearchHandler extends BaseHandler
|
||||||
$offset = ($page - 1) * $pageSize;
|
$offset = ($page - 1) * $pageSize;
|
||||||
$cacheKey = $this->generateCacheKey($query, $types, $page, $pageSize);
|
$cacheKey = $this->generateCacheKey($query, $types, $page, $pageSize);
|
||||||
$results = [];
|
$results = [];
|
||||||
|
$results = $this->getCachedResults($cacheKey) ?? $this->fetchSearchResults($query, $types, $pageSize, $offset);
|
||||||
$results =
|
|
||||||
$this->getCachedResults($cacheKey) ??
|
|
||||||
$this->fetchSearchResults($query, $types, $pageSize, $offset);
|
|
||||||
|
|
||||||
if (empty($results) || empty($results["data"])) {
|
if (empty($results) || empty($results["data"])) {
|
||||||
$this->sendResponse(["results" => [], "total" => 0, "page" => $page, "pageSize" => $pageSize], 200);
|
$this->sendResponse(["results" => [], "total" => 0, "page" => $page, "pageSize" => $pageSize], 200);
|
||||||
|
@ -35,7 +32,6 @@ class SearchHandler extends BaseHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->cacheResults($cacheKey, $results);
|
$this->cacheResults($cacheKey, $results);
|
||||||
|
|
||||||
$this->sendResponse(
|
$this->sendResponse(
|
||||||
[
|
[
|
||||||
"results" => $results["data"],
|
"results" => $results["data"],
|
||||||
|
@ -57,13 +53,8 @@ class SearchHandler extends BaseHandler
|
||||||
|
|
||||||
$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("Invalid 'q' parameter. Contains unsupported characters.");
|
||||||
);
|
|
||||||
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) throw new \Exception(
|
|
||||||
"Invalid 'q' parameter. Contains unsupported characters."
|
|
||||||
);
|
|
||||||
|
|
||||||
$query = preg_replace("/\s+/", " ", $query);
|
$query = preg_replace("/\s+/", " ", $query);
|
||||||
|
|
||||||
|
@ -84,10 +75,7 @@ class SearchHandler extends BaseHandler
|
||||||
);
|
);
|
||||||
$invalidTypes = array_diff($types, $allowedTypes);
|
$invalidTypes = array_diff($types, $allowedTypes);
|
||||||
|
|
||||||
if (!empty($invalidTypes)) throw new Exception(
|
if (!empty($invalidTypes)) throw new Exception("Invalid 'type' parameter. Unsupported types: " . implode(", ", $invalidTypes));
|
||||||
"Invalid 'type' parameter. Unsupported types: " .
|
|
||||||
implode(", ", $invalidTypes)
|
|
||||||
);
|
|
||||||
|
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
@ -98,17 +86,14 @@ class SearchHandler extends BaseHandler
|
||||||
int $pageSize,
|
int $pageSize,
|
||||||
int $offset
|
int $offset
|
||||||
): array {
|
): array {
|
||||||
$typesParam =
|
$typesParam = $types && count($types) > 0 ? "%7B" . implode(",", $types) . "%7D" : "";
|
||||||
$types && count($types) > 0 ? "%7B" . implode(",", $types) . "%7D" : "";
|
|
||||||
$endpoint = "rpc/search_optimized_index";
|
$endpoint = "rpc/search_optimized_index";
|
||||||
$queryString =
|
$queryString =
|
||||||
"search_query=" .
|
"search_query=" .
|
||||||
urlencode($query) .
|
urlencode($query) .
|
||||||
"&page_size={$pageSize}&page_offset={$offset}" .
|
"&page_size={$pageSize}&page_offset={$offset}" .
|
||||||
($typesParam ? "&types={$typesParam}" : "");
|
($typesParam ? "&types={$typesParam}" : "");
|
||||||
|
|
||||||
$data = $this->makeRequest("GET", "{$endpoint}?{$queryString}");
|
$data = $this->makeRequest("GET", "{$endpoint}?{$queryString}");
|
||||||
|
|
||||||
$total = count($data) > 0 ? $data[0]["total_count"] : 0;
|
$total = count($data) > 0 ? $data[0]["total_count"] : 0;
|
||||||
$results = array_map(function ($item) {
|
$results = array_map(function ($item) {
|
||||||
unset($item["total_count"]);
|
unset($item["total_count"]);
|
||||||
|
@ -125,6 +110,7 @@ class SearchHandler extends BaseHandler
|
||||||
int $pageSize
|
int $pageSize
|
||||||
): string {
|
): string {
|
||||||
$typesKey = $types ? implode(",", $types) : "all";
|
$typesKey = $types ? implode(",", $types) : "all";
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
"search:%s:types:%s:page:%d:pageSize:%d",
|
"search:%s:types:%s:page:%d:pageSize:%d",
|
||||||
md5($query),
|
md5($query),
|
||||||
|
@ -138,6 +124,7 @@ class SearchHandler extends BaseHandler
|
||||||
{
|
{
|
||||||
if ($this->cache instanceof \Redis) {
|
if ($this->cache instanceof \Redis) {
|
||||||
$cachedData = $this->cache->get($cacheKey);
|
$cachedData = $this->cache->get($cacheKey);
|
||||||
|
|
||||||
return $cachedData ? json_decode($cachedData, true) : null;
|
return $cachedData ? json_decode($cachedData, true) : null;
|
||||||
} elseif (is_array($this->cache)) {
|
} elseif (is_array($this->cache)) {
|
||||||
return $this->cache[$cacheKey] ?? null;
|
return $this->cache[$cacheKey] ?? null;
|
||||||
|
|
|
@ -13,38 +13,31 @@ class SeasonImportHandler extends ApiHandler
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->ensureCliAccess();
|
|
||||||
|
|
||||||
|
$this->ensureCliAccess();
|
||||||
$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();
|
$this->authenticateRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function authenticateRequest(): void
|
private function authenticateRequest(): void
|
||||||
{
|
{
|
||||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
if ($_SERVER["REQUEST_METHOD"] !== "POST") $this->sendErrorResponse("Method Not Allowed", 405);
|
||||||
$this->sendErrorResponse("Method Not Allowed", 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
$authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
|
||||||
if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
|
|
||||||
$this->sendErrorResponse("Unauthorized", 401);
|
if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) $this->sendErrorResponse("Unauthorized", 401);
|
||||||
}
|
|
||||||
|
|
||||||
$providedToken = trim($matches[1]);
|
$providedToken = trim($matches[1]);
|
||||||
if ($providedToken !== $this->seasonsImportToken) {
|
|
||||||
$this->sendErrorResponse("Forbidden", 403);
|
if ($providedToken !== $this->seasonsImportToken) $this->sendErrorResponse("Forbidden", 403);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function importSeasons(): void
|
public function importSeasons(): void
|
||||||
{
|
{
|
||||||
$ongoingShows = $this->fetchFromApi("optimized_shows", "ongoing=eq.true");
|
$ongoingShows = $this->fetchFromApi("optimized_shows", "ongoing=eq.true");
|
||||||
|
|
||||||
if (empty($ongoingShows)) {
|
if (empty($ongoingShows)) $this->sendResponse(["message" => "No ongoing shows to update"], 200);
|
||||||
$this->sendResponse(["message" => "No ongoing shows to update"], 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($ongoingShows as $show) {
|
foreach ($ongoingShows as $show) {
|
||||||
$this->processShowSeasons($show);
|
$this->processShowSeasons($show);
|
||||||
|
@ -86,6 +79,7 @@ class SeasonImportHandler extends ApiHandler
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->get($url, ["headers" => ["Accept" => "application/json"]]);
|
$response = $client->get($url, ["headers" => ["Accept" => "application/json"]]);
|
||||||
|
|
||||||
return json_decode($response->getBody(), true) ?? [];
|
return json_decode($response->getBody(), true) ?? [];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -107,9 +101,11 @@ class SeasonImportHandler extends ApiHandler
|
||||||
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;
|
||||||
|
|
||||||
$watched = $this->fetchWatchedEpisodes($showId);
|
$watched = $this->fetchWatchedEpisodes($showId);
|
||||||
|
@ -125,9 +121,9 @@ class SeasonImportHandler extends ApiHandler
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
@ -142,6 +138,7 @@ class SeasonImportHandler extends ApiHandler
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->get($url, ["headers" => ["Accept" => "application/json"]]);
|
$response = $client->get($url, ["headers" => ["Accept" => "application/json"]]);
|
||||||
|
|
||||||
return json_decode($response->getBody(), true)["episodes"] ?? [];
|
return json_decode($response->getBody(), true)["episodes"] ?? [];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -151,11 +148,11 @@ 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;
|
||||||
|
|
||||||
$today = date("Y-m-d");
|
$today = date("Y-m-d");
|
||||||
$status = ($airDate < $today) ? "aired" : "upcoming";
|
$status = ($airDate < $today) ? "aired" : "upcoming";
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
"show_id" => $showId,
|
"show_id" => $showId,
|
||||||
"season_number" => $seasonNumber,
|
"season_number" => $seasonNumber,
|
||||||
|
|
|
@ -54,7 +54,6 @@ if ($method === 'GET' && preg_match('#^/utils\.js$#', $forwardPath)) {
|
||||||
|
|
||||||
http_response_code($code);
|
http_response_code($code);
|
||||||
header('Content-Type: application/javascript; charset=UTF-8');
|
header('Content-Type: application/javascript; charset=UTF-8');
|
||||||
header('Cache-Control: public, max-age=60');
|
|
||||||
echo $js;
|
echo $js;
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,7 @@ export const albumReleasesCalendar = (collection) => {
|
||||||
url: albumUrl,
|
url: albumUrl,
|
||||||
uid: `${album.release_timestamp}-${album.artist.name}-${album.title}`,
|
uid: `${album.release_timestamp}-${album.artist.name}-${album.title}`,
|
||||||
};
|
};
|
||||||
})
|
}).filter((event) => event !== null);
|
||||||
.filter((event) => event !== null);
|
|
||||||
|
|
||||||
const { error, value } = ics.createEvents(events, {
|
const { error, value } = ics.createEvents(events, {
|
||||||
calName: "Album releases calendar • coryd.dev",
|
calName: "Album releases calendar • coryd.dev",
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { minify } from "terser";
|
||||||
|
|
||||||
export const minifyJsComponents = async () => {
|
export const minifyJsComponents = async () => {
|
||||||
const scriptsDir = "dist/assets/scripts";
|
const scriptsDir = "dist/assets/scripts";
|
||||||
|
|
||||||
const minifyJsFilesInDir = async (dir) => {
|
const minifyJsFilesInDir = async (dir) => {
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
for (const fileName of files) {
|
for (const fileName of files) {
|
||||||
const filePath = path.join(dir, fileName);
|
const filePath = path.join(dir, fileName);
|
||||||
const stat = fs.statSync(filePath);
|
const stat = fs.statSync(filePath);
|
||||||
|
@ -16,6 +16,7 @@ export const minifyJsComponents = async () => {
|
||||||
} else if (fileName.endsWith(".js")) {
|
} else if (fileName.endsWith(".js")) {
|
||||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||||
const minified = await minify(fileContent);
|
const minified = await minify(fileContent);
|
||||||
|
|
||||||
if (minified.error) {
|
if (minified.error) {
|
||||||
console.error(`Error minifying ${filePath}:`, minified.error);
|
console.error(`Error minifying ${filePath}:`, minified.error);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
export default {
|
|
||||||
stringToRFC822Date: (dateString) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
|
|
||||||
if (isNaN(date.getTime())) return "";
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
timeZone: "America/Los_Angeles",
|
|
||||||
weekday: "short",
|
|
||||||
day: "2-digit",
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
timeZoneName: "short",
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat("en-US", options).format(date);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -30,9 +30,13 @@ export default {
|
||||||
getRemoteFileSize: async (url) => {
|
getRemoteFileSize: async (url) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { method: "HEAD" });
|
const response = await fetch(url, { method: "HEAD" });
|
||||||
|
|
||||||
if (!response.ok) return 0;
|
if (!response.ok) return 0;
|
||||||
|
|
||||||
const contentLength = response.headers.get("content-length");
|
const contentLength = response.headers.get("content-length");
|
||||||
|
|
||||||
if (!contentLength) return 0;
|
if (!contentLength) return 0;
|
||||||
|
|
||||||
return parseInt(contentLength, 10);
|
return parseInt(contentLength, 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
import truncateHtml from "truncate-html";
|
import truncateHtml from "truncate-html";
|
||||||
import { shuffleArray } from "../utilities/index.js";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
encodeAmp: (string) => {
|
encodeAmp: (string) => {
|
||||||
if (!string) return;
|
if (!string) return;
|
||||||
|
|
||||||
const pattern = /&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/g;
|
const pattern = /&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/g;
|
||||||
const replacement = "&";
|
const replacement = "&";
|
||||||
|
|
||||||
return string.replace(pattern, replacement);
|
return string.replace(pattern, replacement);
|
||||||
},
|
},
|
||||||
replaceQuotes: (string) => string.replace(/"/g, """),
|
replaceQuotes: (string) => string.replace(/"/g, """),
|
||||||
htmlTruncate: (content, limit = 50) =>
|
htmlTruncate: (content, limit = 50) => truncateHtml(content, limit, {
|
||||||
truncateHtml(content, limit, {
|
|
||||||
byWords: true,
|
byWords: true,
|
||||||
ellipsis: "...",
|
ellipsis: "...",
|
||||||
}),
|
}),
|
||||||
shuffleArray,
|
shuffleArray: (array) => {
|
||||||
mergeArray: (a, b) =>
|
const shuffled = [...array];
|
||||||
Array.isArray(a) && Array.isArray(b) ? [...new Set([...a, ...b])] : [],
|
|
||||||
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
|
let j = Math.floor(Math.random() * (i + 1));
|
||||||
|
let temp = shuffled[i];
|
||||||
|
shuffled[i] = shuffled[j];
|
||||||
|
shuffled[j] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shuffled;
|
||||||
|
},
|
||||||
|
mergeArray: (a, b) => Array.isArray(a) && Array.isArray(b) ? [...new Set([...a, ...b])] : [],
|
||||||
pluralize: (count, string, trailing) => {
|
pluralize: (count, string, trailing) => {
|
||||||
const countStr = String(count).replace(/,/g, "");
|
const countStr = String(count).replace(/,/g, "");
|
||||||
|
|
||||||
if (parseInt(countStr, 10) === 1) return string;
|
if (parseInt(countStr, 10) === 1) return string;
|
||||||
|
|
||||||
return `${string}s${trailing ? `${trailing}` : ''}`;
|
return `${string}s${trailing ? `${trailing}` : ''}`;
|
||||||
},
|
},
|
||||||
jsonEscape: (string) => JSON.stringify(string),
|
jsonEscape: (string) => JSON.stringify(string),
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import dates from "./dates.js";
|
|
||||||
import feeds from "./feeds.js"
|
import feeds from "./feeds.js"
|
||||||
import general from "./general.js";
|
import general from "./general.js";
|
||||||
import media from "./media.js";
|
import media from "./media.js";
|
||||||
import navigation from "./navigation.js";
|
import navigation from "./navigation.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...dates,
|
|
||||||
...feeds,
|
...feeds,
|
||||||
...general,
|
...general,
|
||||||
...media,
|
...media,
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
export default {
|
export default {
|
||||||
filterBooksByStatus: (books, status) =>
|
filterBooksByStatus: (books, status) => books.filter((book) => book.status === status),
|
||||||
books.filter((book) => book.status === status),
|
findFavoriteBooks: (books) => books.filter((book) => book.favorite === true),
|
||||||
findFavoriteBooks: (books) =>
|
bookYearLinks: (years) => years.sort((a, b) => b.value - a.value).map((year, index) =>
|
||||||
books.filter((book) => book.favorite === true),
|
|
||||||
bookYearLinks: (years) =>
|
|
||||||
years
|
|
||||||
.sort((a, b) => b.value - a.value)
|
|
||||||
.map(
|
|
||||||
(year, index) =>
|
|
||||||
`<a href="/reading/years/${year.value}">${year.value}</a>${
|
`<a href="/reading/years/${year.value}">${year.value}</a>${
|
||||||
index < years.length - 1 ? " • " : ""
|
index < years.length - 1 ? " • " : ""
|
||||||
}`
|
}`).join(""),
|
||||||
)
|
|
||||||
.join(""),
|
|
||||||
mediaLinks: (data, type, count = 10) => {
|
mediaLinks: (data, type, count = 10) => {
|
||||||
if (!data || !type) return "";
|
if (!data || !type) return "";
|
||||||
|
|
||||||
const dataSlice = data.slice(0, count);
|
const dataSlice = data.slice(0, count);
|
||||||
|
|
||||||
if (dataSlice.length === 0) return null;
|
if (dataSlice.length === 0) return null;
|
||||||
|
|
||||||
const buildLink = (item) => {
|
const buildLink = (item) => {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export default {
|
export default {
|
||||||
isLinkActive: (category, page) =>
|
isLinkActive: (category, page) => page.includes(category) && page.split("/").filter((a) => a !== "").length <= 1,
|
||||||
page.includes(category) &&
|
|
||||||
page.split("/").filter((a) => a !== "").length <= 1,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export const shuffleArray = (array) => {
|
|
||||||
const shuffled = [...array];
|
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
||||||
let j = Math.floor(Math.random() * (i + 1));
|
|
||||||
let temp = shuffled[i];
|
|
||||||
shuffled[i] = shuffled[j];
|
|
||||||
shuffled[j] = temp;
|
|
||||||
}
|
|
||||||
return shuffled;
|
|
||||||
};
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -1,15 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "6.0.3",
|
"version": "6.0.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "6.0.3",
|
"version": "6.0.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier-terser": "7.2.0",
|
|
||||||
"minisearch": "^7.1.2",
|
"minisearch": "^7.1.2",
|
||||||
"youtube-video-element": "^1.5.1"
|
"youtube-video-element": "^1.5.1"
|
||||||
},
|
},
|
||||||
|
@ -20,6 +19,7 @@
|
||||||
"concurrently": "9.1.2",
|
"concurrently": "9.1.2",
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.0.7",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.5.0",
|
||||||
|
"html-minifier-terser": "7.2.0",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
@ -402,6 +402,7 @@
|
||||||
"version": "0.3.8",
|
"version": "0.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
|
@ -416,6 +417,7 @@
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
@ -425,6 +427,7 @@
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
@ -434,6 +437,7 @@
|
||||||
"version": "0.3.6",
|
"version": "0.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
@ -444,12 +448,14 @@
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
|
@ -586,6 +592,7 @@
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
|
@ -860,12 +867,14 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/camel-case": {
|
"node_modules/camel-case": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
||||||
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pascal-case": "^3.1.2",
|
"pascal-case": "^3.1.2",
|
||||||
|
@ -1143,6 +1152,7 @@
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||||
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
|
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"source-map": "~0.6.0"
|
"source-map": "~0.6.0"
|
||||||
|
@ -1260,6 +1270,7 @@
|
||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
|
@ -1703,6 +1714,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"no-case": "^3.0.4",
|
"no-case": "^3.0.4",
|
||||||
|
@ -2200,6 +2212,7 @@
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||||
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
|
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"camel-case": "^4.1.2",
|
"camel-case": "^4.1.2",
|
||||||
|
@ -2221,6 +2234,7 @@
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
|
@ -2674,6 +2688,7 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
|
@ -2951,6 +2966,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lower-case": "^2.0.2",
|
"lower-case": "^2.0.2",
|
||||||
|
@ -3106,6 +3122,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||||
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dot-case": "^3.0.4",
|
"dot-case": "^3.0.4",
|
||||||
|
@ -3176,6 +3193,7 @@
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||||
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"no-case": "^3.0.4",
|
"no-case": "^3.0.4",
|
||||||
|
@ -3955,6 +3973,7 @@
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
||||||
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
|
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
|
@ -4227,6 +4246,7 @@
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -4246,6 +4266,7 @@
|
||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
|
@ -4489,6 +4510,7 @@
|
||||||
"version": "5.39.2",
|
"version": "5.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||||
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
|
@ -4507,6 +4529,7 @@
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tiny-case": {
|
"node_modules/tiny-case": {
|
||||||
|
@ -4633,6 +4656,7 @@
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "6.0.3",
|
"version": "6.0.9",
|
||||||
"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": {
|
||||||
|
@ -32,7 +32,6 @@
|
||||||
"author": "Cory Dransfeldt",
|
"author": "Cory Dransfeldt",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier-terser": "7.2.0",
|
|
||||||
"minisearch": "^7.1.2",
|
"minisearch": "^7.1.2",
|
||||||
"youtube-video-element": "^1.5.1"
|
"youtube-video-element": "^1.5.1"
|
||||||
},
|
},
|
||||||
|
@ -43,6 +42,7 @@
|
||||||
"concurrently": "9.1.2",
|
"concurrently": "9.1.2",
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.0.7",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.5.0",
|
||||||
|
"html-minifier-terser": "7.2.0",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
|
|
@ -15,7 +15,6 @@ function renderPaginator(array $pagination, int $totalPages): void {
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span><?= getTablerIcon('arrow-left') ?></span>
|
<span><?= getTablerIcon('arrow-left') ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<select-pagination data-base-index="1">
|
<select-pagination data-base-index="1">
|
||||||
<select class="client-side" aria-label="Page selection">
|
<select class="client-side" aria-label="Page selection">
|
||||||
<?php foreach ($pagination['pages'] as $i): ?>
|
<?php foreach ($pagination['pages'] as $i): ?>
|
||||||
|
@ -28,7 +27,6 @@ function renderPaginator(array $pagination, int $totalPages): void {
|
||||||
<p><span aria-current="page"><?= $pagination['pageNumber'] ?></span> of <?= $totalPages ?></p>
|
<p><span aria-current="page"><?= $pagination['pageNumber'] ?></span> of <?= $totalPages ?></p>
|
||||||
</noscript>
|
</noscript>
|
||||||
</select-pagination>
|
</select-pagination>
|
||||||
|
|
||||||
<?php if (!empty($pagination['href']['next'])): ?>
|
<?php if (!empty($pagination['href']['next'])): ?>
|
||||||
<a href="<?= $pagination['href']['next'] ?>" aria-label="Next page">
|
<a href="<?= $pagination['href']['next'] ?>" aria-label="Next page">
|
||||||
<?= getTablerIcon('arrow-right') ?>
|
<?= getTablerIcon('arrow-right') ?>
|
||||||
|
|
|
@ -4,9 +4,11 @@ function renderTags(array $tags): void {
|
||||||
if (empty($tags)) return;
|
if (empty($tags)) return;
|
||||||
|
|
||||||
echo '<div class="tags">';
|
echo '<div class="tags">';
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
$slug = strtolower(trim($tag));
|
$slug = strtolower(trim($tag));
|
||||||
echo '<a href="/tags/' . htmlspecialchars($slug) . '">#' . htmlspecialchars($slug) . '</a>';
|
echo '<a href="/tags/' . htmlspecialchars($slug) . '">#' . htmlspecialchars($slug) . '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
|
@ -19,7 +19,7 @@
|
||||||
src: url("/assets/fonts/dmi.woff2") format("woff2");
|
src: url("/assets/fonts/dmi.woff2") format("woff2");
|
||||||
font-weight: 100 700;
|
font-weight: 100 700;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: optional;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
@ -8,7 +8,7 @@ excludeFromSitemap: true
|
||||||
<opml version="1.0">
|
<opml version="1.0">
|
||||||
<head>
|
<head>
|
||||||
<title>OPML for all feeds in {{ globals.site_name }}'s blogroll</title>
|
<title>OPML for all feeds in {{ globals.site_name }}'s blogroll</title>
|
||||||
<dateCreated>{{ page.date | stringToRFC822Date }}</dateCreated>
|
<dateCreated>{{ page.date | date: "%a, %d %b %Y %H:%M:%S %Z" }}</dateCreated>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{%- for blog in blogroll -%}
|
{%- for blog in blogroll -%}
|
||||||
|
|
|
@ -101,11 +101,11 @@ RewriteRule .* /403/index.html [L,R=403]
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<IfModule mod_deflate.c>
|
<IfModule mod_deflate.c>
|
||||||
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/font-woff2
|
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_brotli.c>
|
<IfModule mod_brotli.c>
|
||||||
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json application/font-woff2
|
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_expires.c>
|
<IfModule mod_expires.c>
|
||||||
|
@ -113,25 +113,44 @@ RewriteRule .* /403/index.html [L,R=403]
|
||||||
ExpiresDefault "access plus 1 month"
|
ExpiresDefault "access plus 1 month"
|
||||||
ExpiresByType application/font-woff2 "access plus 1 year"
|
ExpiresByType application/font-woff2 "access plus 1 year"
|
||||||
ExpiresByType text/html "access plus 1 hour"
|
ExpiresByType text/html "access plus 1 hour"
|
||||||
ExpiresByType text/css "access plus 1 week"
|
ExpiresByType text/css "access plus 1 year"
|
||||||
ExpiresByType application/javascript "access plus 1 week"
|
ExpiresByType application/javascript "access plus 1 year"
|
||||||
ExpiresByType image/jpeg "access plus 1 month"
|
ExpiresByType image/jpeg "access plus 1 year"
|
||||||
ExpiresByType image/png "access plus 1 month"
|
ExpiresByType image/png "access plus 1 year"
|
||||||
ExpiresByType image/gif "access plus 1 month"
|
ExpiresByType image/gif "access plus 1 year"
|
||||||
ExpiresByType image/svg+xml "access plus 1 month"
|
ExpiresByType image/svg+xml "access plus 1 year"
|
||||||
ExpiresByType image/x-icon "access plus 1 year"
|
ExpiresByType image/x-icon "access plus 1 year"
|
||||||
ExpiresByType application/json "access plus 1 week"
|
ExpiresByType application/json "access plus 1 week"
|
||||||
ExpiresByType application/octet-stream "access plus 1 month"
|
ExpiresByType application/octet-stream "access plus 1 year"
|
||||||
ExpiresByType font/ttf "access plus 1 year"
|
ExpiresByType font/ttf "access plus 1 year"
|
||||||
ExpiresByType font/otf "access plus 1 year"
|
ExpiresByType font/otf "access plus 1 year"
|
||||||
ExpiresByType application/wasm "access plus 1 year"
|
ExpiresByType application/wasm "access plus 1 year"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
<FilesMatch "\.(css|js|woff2)$">
|
Header append Vary "Accept-Encoding"
|
||||||
Header set Cache-Control "public, max-age=31536000"
|
|
||||||
</FilesMatch>
|
<FilesMatch "\.(js|css|woff2|ttf|otf|png|jpg|jpeg|gif|svg|wasm|ico)$">
|
||||||
<FilesMatch "\.(html|htm)$">
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||||
Header set Cache-Control "public, max-age=3600, must-revalidate"
|
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
AddType font/woff2 .woff2
|
||||||
|
AddType font/ttf .ttf
|
||||||
|
AddType font/otf .otf
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# use brotli if available
|
||||||
|
RewriteCond %{HTTP:Accept-Encoding} br
|
||||||
|
RewriteCond %{REQUEST_FILENAME}.br -f
|
||||||
|
RewriteRule ^(.*)$ $1.br [QSA,L]
|
||||||
|
|
||||||
|
# fallback to gzip
|
||||||
|
RewriteCond %{HTTP:Accept-Encoding} gzip
|
||||||
|
RewriteCond %{REQUEST_FILENAME}.gz -f
|
||||||
|
RewriteRule ^(.*)$ $1.gz [QSA,L]
|
||||||
|
</IfModule>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue