feat: initial commit

This commit is contained in:
Cory Dransfeldt 2024-11-16 16:43:07 -08:00
commit 0ff7457679
No known key found for this signature in database
192 changed files with 24379 additions and 0 deletions

View file

@ -0,0 +1,46 @@
import { parseISO, format, isValid } from "date-fns";
import { toZonedTime } from "date-fns-tz";
import { createEvents } from "ics";
const nowUTC = toZonedTime(new Date(), "UTC");
const formattedDate = format(nowUTC, "yyyyMMdd'T'HHmmss'Z'");
export async function albumReleasesCalendar(albumReleases) {
if (!albumReleases || albumReleases.length === 0) return "";
const events = albumReleases
.map((album) => {
const date = parseISO(album["release_date"]);
if (!isValid(date)) return null;
return {
start: [
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
],
startInputType: "local",
startOutputType: "local",
title: `Release: ${album["artist"]["name"]} - ${album["title"]}`,
description: `Check out this new album release: ${album["url"]}. Read more about ${album["artist"]["name"]} at https://coryd.dev${album["artist"]["url"]}`,
url: album["url"],
uid: `${format(date, "yyyyMMdd")}-${album["artist"]["name"]}-${album["title"]}@coryd.dev`,
timestamp: formattedDate,
};
})
.filter((event) => event !== null);
const { error, value } = createEvents(events, {
calName: "Album releases calendar / coryd.dev",
});
if (error) {
console.error("Error creating events: ", error);
events.forEach((event, index) => {
console.error(`Event ${index}:`, event);
});
return "";
}
return value;
}

View file

@ -0,0 +1 @@
export const CACHE_DURATION = 120 * 1000;

View file

@ -0,0 +1,38 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedActivity = null;
let lastFetchTime = 0;
export async function fetchActivity() {
const now = Date.now();
if (cachedActivity && now - lastFetchTime < CACHE_DURATION)
return cachedActivity;
try {
const { data, error } = await supabase
.from("optimized_all_activity")
.select("feed");
if (error) {
console.error("Error fetching activity data:", error);
return cachedActivity || [];
}
const [{ feed } = {}] = data || [];
const filteredFeed = feed?.filter((item) => item.feed !== null) || [];
cachedActivity = filteredFeed;
lastFetchTime = now;
return filteredFeed;
} catch (error) {
console.error("Error in fetchActivity:", error);
return cachedActivity || [];
}
}

View file

@ -0,0 +1,56 @@
import { createClient } from "@supabase/supabase-js";
import { format, startOfDay, isAfter, getTime } from "date-fns";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedAlbumReleases = null;
let lastFetchTime = 0;
export async function fetchAlbumReleases() {
const now = Date.now();
if (cachedAlbumReleases && now - lastFetchTime < CACHE_DURATION)
return cachedAlbumReleases;
const today = getTime(startOfDay(new Date()));
try {
const { data, error } = await supabase
.from("optimized_album_releases")
.select("*");
if (error) {
console.error("Error fetching album releases:", error);
return { all: [], upcoming: [] };
}
const all = data
.map((album) => {
const releaseDate = startOfDay(
new Date(album.release_timestamp * 1000)
);
return {
...album,
description: album.artist.description,
date: format(releaseDate, "PPPP"),
timestamp: getTime(releaseDate) / 1000,
};
})
.sort((a, b) => a.timestamp - b.timestamp);
const upcoming = all.filter((album) =>
isAfter(new Date(album.release_timestamp * 1000), new Date(today))
);
cachedAlbumReleases = { all, upcoming };
lastFetchTime = now;
return { all, upcoming };
} catch (error) {
console.error("Error in fetchAlbumReleases:", error);
return cachedAlbumReleases || { all: [], upcoming: [] };
}
}

View file

@ -0,0 +1,43 @@
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedPages = null;
let lastFetchTime = 0;
export async function fetchAnalyticsData() {
const now = Date.now();
if (cachedPages && now - lastFetchTime < CACHE_DURATION) return cachedPages;
const API_KEY_PLAUSIBLE = import.meta.env.API_KEY_PLAUSIBLE;
const url =
"https://plausible.io/api/v1/stats/breakdown?site_id=coryd.dev&period=6mo&property=event:page&limit=30";
try {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${API_KEY_PLAUSIBLE}`,
},
});
if (res.status === 429) {
console.warn("Rate limit reached: Too Many Requests");
throw new Error("Too many requests. Please try again later.");
}
if (!res.ok) {
console.error(`Error fetching Plausible data: ${res.statusText}`);
return [];
}
const pages = await res.json();
const filteredPages = pages.results.filter((p) => p.page.includes("posts"));
cachedPages = filteredPages;
lastFetchTime = now;
return filteredPages;
} catch (error) {
console.error("Error fetching Plausible data:", error);
return cachedPages || [];
}
}

53
src/utils/data/artists.js Normal file
View file

@ -0,0 +1,53 @@
import { createClient } from "@supabase/supabase-js";
import { parseCountryField } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedArtists = null;
let lastFetchTime = 0;
export async function fetchArtists() {
const now = Date.now();
if (cachedArtists && now - lastFetchTime < CACHE_DURATION)
return cachedArtists;
const PAGE_SIZE = 1000;
let artists = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from("optimized_artists")
.select("*")
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching artists:", error);
break;
}
artists = artists.concat(
data.map((artist) => ({
...artist,
country: parseCountryField(artist["country"]),
}))
);
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
cachedArtists = artists;
lastFetchTime = now;
return artists;
} catch (error) {
console.error("Error in fetchArtists:", error);
return cachedArtists || [];
}
}

View file

@ -0,0 +1,41 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedBlogroll = null;
let lastFetchTime = 0;
export async function fetchBlogroll() {
const now = Date.now();
if (cachedBlogroll && now - lastFetchTime < CACHE_DURATION)
return cachedBlogroll;
try {
const { data, error } = await supabase
.from("authors")
.select("*")
.eq("blogroll", true)
.order("name", { ascending: true });
if (error) {
console.error("Error fetching blogroll:", error);
return [];
}
const sortedData = data.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
cachedBlogroll = sortedData;
lastFetchTime = now;
return sortedData;
} catch (error) {
console.error("Error in fetchBlogroll:", error);
return cachedBlogroll || [];
}
}

79
src/utils/data/books.js Normal file
View file

@ -0,0 +1,79 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedBooks = null;
let lastFetchTime = 0;
export async function fetchBooks() {
const now = Date.now();
if (cachedBooks && now - lastFetchTime < CACHE_DURATION) return cachedBooks;
const PAGE_SIZE = 1000;
let books = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from("optimized_books")
.select("*")
.order("date_finished", { ascending: false })
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching books:", error);
break;
}
books = books.concat(data);
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
const years = {};
books.forEach((book) => {
const year = book.year;
if (!years[year]) {
years[year] = { value: year, data: [book] };
} else {
years[year].data.push(book);
}
});
const sortedByYear = Object.values(years).filter(
(year) => year.value > 2017
);
const currentYear = new Date().getFullYear();
const booksForCurrentYear =
sortedByYear
.find((yearGroup) => yearGroup.value === currentYear)
?.data.filter((book) => book["status"] === "finished") || [];
const result = {
all: books,
years: sortedByYear,
currentYear: booksForCurrentYear,
feed: books.filter((book) => book.feed),
};
cachedBooks = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchBooks:", error);
return (
cachedBooks || {
all: [],
years: [],
currentYear: [],
feed: [],
}
);
}
}

View file

@ -0,0 +1,51 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedConcerts = null;
let lastFetchTime = 0;
export async function fetchConcerts() {
const now = Date.now();
if (cachedConcerts && now - lastFetchTime < CACHE_DURATION)
return cachedConcerts;
const PAGE_SIZE = 1000;
let concerts = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from("optimized_concerts")
.select("*")
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching concerts:", error);
break;
}
concerts = concerts.concat(data);
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
const result = concerts.map((concert) => ({
...concert,
artist: concert.artist || { name: concert.artist_name_string, url: null },
}));
cachedConcerts = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchConcerts:", error);
return cachedConcerts || [];
}
}

View file

@ -0,0 +1,33 @@
import { createClient } from "@supabase/supabase-js";
import { removeTrailingSlash } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const cache = {};
const cacheTimestamps = {};
export async function fetchArtistByUrl(env, url) {
const normalizedUrl = removeTrailingSlash(url);
const now = Date.now();
if (
cache[normalizedUrl] &&
now - cacheTimestamps[normalizedUrl] < CACHE_DURATION
)
return cache[normalizedUrl];
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data: artist, error } = await supabase
.from("optimized_artists")
.select("*")
.eq("url", normalizedUrl)
.limit(1);
if (error || !artist.length) return null;
cache[normalizedUrl] = artist[0];
cacheTimestamps[normalizedUrl] = now;
return artist[0];
}

View file

@ -0,0 +1,33 @@
import { createClient } from "@supabase/supabase-js";
import { removeTrailingSlash } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const cache = {};
const cacheTimestamps = {};
export async function fetchBookByUrl(env, url) {
const normalizedUrl = removeTrailingSlash(url);
const now = Date.now();
if (
cache[normalizedUrl] &&
now - cacheTimestamps[normalizedUrl] < CACHE_DURATION
)
return cache[normalizedUrl];
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data: book, error } = await supabase
.from("optimized_books")
.select("*")
.eq("url", normalizedUrl)
.limit(1);
if (error || !book.length) return null;
cache[normalizedUrl] = book[0];
cacheTimestamps[normalizedUrl] = now;
return book[0];
}

View file

@ -0,0 +1,32 @@
import { createClient } from "@supabase/supabase-js";
import { removeTrailingSlash } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedGenres = {};
let lastFetchTime = {};
export async function fetchGenreByUrl(env, url) {
const normalizedUrl = removeTrailingSlash(url);
if (
cachedGenres[normalizedUrl] &&
Date.now() - lastFetchTime[normalizedUrl] < CACHE_DURATION
)
return cachedGenres[normalizedUrl];
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data: genre, error } = await supabase
.from("optimized_genres")
.select("*")
.eq("url", normalizedUrl)
.limit(1);
if (error || !genre?.length) return null;
cachedGenres[normalizedUrl] = genre[0];
lastFetchTime[normalizedUrl] = Date.now();
return genre[0];
}

View file

@ -0,0 +1,32 @@
import { createClient } from "@supabase/supabase-js";
import { removeTrailingSlash } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedMovies = {};
let lastFetchTime = {};
export async function fetchMovieByUrl(env, url) {
const normalizedUrl = removeTrailingSlash(url);
if (
cachedMovies[normalizedUrl] &&
Date.now() - lastFetchTime[normalizedUrl] < CACHE_DURATION
)
return cachedMovies[normalizedUrl];
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data: movie, error } = await supabase
.from("optimized_movies")
.select("*")
.eq("url", normalizedUrl)
.limit(1);
if (error || !movie?.length) return null;
cachedMovies[normalizedUrl] = movie[0];
lastFetchTime[normalizedUrl] = Date.now();
return movie[0];
}

View file

@ -0,0 +1,32 @@
import { createClient } from "@supabase/supabase-js";
import { removeTrailingSlash } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedShows = {};
let lastFetchTime = {};
export async function fetchShowByUrl(env, url) {
const normalizedUrl = removeTrailingSlash(url);
if (
cachedShows[normalizedUrl] &&
Date.now() - lastFetchTime[normalizedUrl] < CACHE_DURATION
)
return cachedShows[normalizedUrl];
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data: tv, error } = await supabase
.from("optimized_shows")
.select("*")
.eq("url", normalizedUrl)
.limit(1);
if (error || !tv?.length) return null;
cachedShows[normalizedUrl] = tv[0];
lastFetchTime[normalizedUrl] = Date.now();
return tv[0];
}

32
src/utils/data/genres.js Normal file
View file

@ -0,0 +1,32 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedGenres = null;
let lastFetchTime = 0;
export async function fetchGenres() {
const now = Date.now();
if (cachedGenres && now - lastFetchTime < CACHE_DURATION) return cachedGenres;
try {
const { data, error } = await supabase.from("optimized_genres").select("*");
if (error) {
console.error("Error fetching genres:", error);
return [];
}
cachedGenres = data;
lastFetchTime = now;
return data;
} catch (error) {
console.error("Error in fetchGenres:", error);
return cachedGenres || [];
}
}

View file

@ -0,0 +1,79 @@
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchNavigation } from "@utils/data/nav.js";
import { fetchArtistByUrl } from "@utils/data/dynamic/artistByUrl.js";
import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js";
import { fetchGenreByUrl } from "@utils/data/dynamic/genreByUrl.js";
import { fetchMovieByUrl } from "@utils/data/dynamic/movieByUrl.js";
import { fetchShowByUrl } from "@utils/data/dynamic/showByUrl.js";
import { isbnRegex } from "@utils/helpers/media.js";
import { isExcludedPath } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedGlobals = null;
let cachedNav = null;
let lastFetchTimeGlobals = 0;
let lastFetchTimeNav = 0;
export async function fetchGlobalData(Astro, urlPath) {
const now = Date.now();
if (Astro?.locals) {
const data = {
globals: Astro.locals.globals,
nav: Astro.locals.nav,
};
if (urlPath?.startsWith("/music/artists/"))
data.artist = Astro.locals.artist;
if (isbnRegex.test(urlPath)) data.book = Astro.locals.book;
if (urlPath?.startsWith("/music/genres/")) data.genre = Astro.locals.genre;
if (
urlPath?.startsWith("/watching/movies/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
)
data.movie = Astro.locals.movie;
if (
urlPath?.startsWith("/watching/shows/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
)
data.show = Astro.locals.show;
return data;
}
const globals =
cachedGlobals && now - lastFetchTimeGlobals < CACHE_DURATION
? cachedGlobals
: ((cachedGlobals = await fetchGlobals()), (lastFetchTimeGlobals = now));
const nav =
cachedNav && now - lastFetchTimeNav < CACHE_DURATION
? cachedNav
: ((cachedNav = await fetchNavigation()), (lastFetchTimeNav = now));
let artist, book, genre, movie, show;
try {
if (urlPath?.startsWith("/music/artists/")) {
artist = await fetchArtistByUrl(urlPath);
} else if (isbnRegex.test(urlPath)) {
book = await fetchBookByUrl(urlPath);
} else if (urlPath?.startsWith("/music/genres/")) {
genre = await fetchGenreByUrl(urlPath);
} else if (
urlPath?.startsWith("/watching/movies/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
) {
movie = await fetchMovieByUrl(urlPath);
} else if (
urlPath?.startsWith("/watching/shows/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
) {
show = await fetchShowByUrl(urlPath);
}
} catch (error) {
console.error("Error fetching data:", error);
}
return { globals, nav, artist, book, genre, movie, show };
}

35
src/utils/data/globals.js Normal file
View file

@ -0,0 +1,35 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedGlobals = null;
let lastFetchTime = 0;
export async function fetchGlobals(env) {
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const now = Date.now();
if (cachedGlobals && now - lastFetchTime < CACHE_DURATION)
return cachedGlobals;
try {
const { data, error } = await supabase
.from("optimized_globals")
.select("*")
.single();
if (error) {
console.error("Error fetching globals:", error);
return {};
}
cachedGlobals = data;
lastFetchTime = now;
return data;
} catch (error) {
console.error("Error in fetchGlobals:", error);
return cachedGlobals || {};
}
}

47
src/utils/data/links.js Normal file
View file

@ -0,0 +1,47 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedLinks = null;
let lastFetchTime = 0;
export async function fetchLinks() {
const now = Date.now();
if (cachedLinks && now - lastFetchTime < CACHE_DURATION) return cachedLinks;
const PAGE_SIZE = 1000;
let links = [];
let page = 0;
let fetchMore = true;
try {
while (fetchMore) {
const { data, error } = await supabase
.from("optimized_links")
.select("*")
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
if (error) {
console.error("Error fetching links:", error);
return cachedLinks || links;
}
if (data.length < PAGE_SIZE) fetchMore = false;
links = links.concat(data);
page++;
}
cachedLinks = links;
lastFetchTime = now;
return links;
} catch (error) {
console.error("Error in fetchLinks:", error);
return cachedLinks || [];
}
}

80
src/utils/data/movies.js Normal file
View file

@ -0,0 +1,80 @@
import { createClient } from "@supabase/supabase-js";
import { parseISO, subMonths } from "date-fns";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedMovies = null;
let lastFetchTime = 0;
export async function fetchMovies() {
const now = Date.now();
if (cachedMovies && now - lastFetchTime < CACHE_DURATION) return cachedMovies;
const PAGE_SIZE = 1000;
let movies = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from("optimized_movies")
.select("*")
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching movies:", error);
return (
cachedMovies || {
movies: [],
watchHistory: [],
recentlyWatched: [],
favorites: [],
feed: [],
}
);
}
movies = movies.concat(data);
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
const favoriteMovies = movies.filter((movie) => movie.favorite);
const recentlyWatchedMovies = movies.filter((movie) => {
if (!movie.last_watched) return false;
const lastWatchedDate = parseISO(movie.last_watched);
const threeMonthsAgo = subMonths(new Date(), 3);
return lastWatchedDate >= threeMonthsAgo;
});
const result = {
movies,
watchHistory: movies.filter((movie) => movie.last_watched),
recentlyWatched: recentlyWatchedMovies,
favorites: favoriteMovies.sort((a, b) => a.title.localeCompare(b.title)),
feed: movies.filter((movie) => movie.feed),
};
cachedMovies = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchMovies:", error);
return (
cachedMovies || {
movies: [],
watchHistory: [],
recentlyWatched: [],
favorites: [],
feed: [],
}
);
}
}

View file

@ -0,0 +1,59 @@
import { createClient } from "@supabase/supabase-js";
import { fetchDataFromView, calculateTotalPlays } from "@utils/data/music/utils.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedMonthData = null;
let lastFetchTimeMonth = 0;
export async function fetchMusicMonth() {
const now = Date.now();
if (cachedMonthData && now - lastFetchTimeMonth < CACHE_DURATION)
return cachedMonthData;
try {
const [
monthTracks,
monthArtists,
monthAlbums,
monthGenres,
] = await Promise.all([
fetchDataFromView("month_tracks", supabase),
fetchDataFromView("month_artists", supabase),
fetchDataFromView("month_albums", supabase),
fetchDataFromView("month_genres", supabase),
]);
const result = {
month: {
tracks: monthTracks,
artists: monthArtists,
albums: monthAlbums,
genres: monthGenres,
totalTracks: calculateTotalPlays(monthTracks),
},
};
cachedMonthData = result;
lastFetchTimeMonth = now;
return result;
} catch (error) {
console.error("Error in fetchMonthData:", error);
return (
cachedMonthData || {
month: {
tracks: [],
artists: [],
albums: [],
genres: [],
totalTracks: "0",
},
}
);
}
}

View file

@ -0,0 +1,28 @@
const PAGE_SIZE = 1000;
export const fetchDataFromView = async (viewName, supabase) => {
let rows = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from(viewName)
.select("*")
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error || data.length === 0) break;
rows = [...rows, ...data];
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
} catch (error) {
console.error(`Error fetching data from view: ${viewName}`, error);
}
return rows;
};
export const calculateTotalPlays = (tracks) =>
tracks.reduce((acc, track) => acc + track.plays, 0).toLocaleString("en-US");

View file

@ -0,0 +1,58 @@
import { createClient } from "@supabase/supabase-js";
import { fetchDataFromView, calculateTotalPlays } from "@utils/data/music/utils.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedWeekData = null;
let lastFetchTimeWeek = 0;
export async function fetchMusicWeek() {
const now = Date.now();
if (cachedWeekData && now - lastFetchTimeWeek < CACHE_DURATION)
return cachedWeekData;
try {
const [recentTracks, weekTracks, weekArtists, weekAlbums, weekGenres] =
await Promise.all([
fetchDataFromView("recent_tracks", supabase),
fetchDataFromView("week_tracks", supabase),
fetchDataFromView("week_artists", supabase),
fetchDataFromView("week_albums", supabase),
fetchDataFromView("week_genres", supabase),
]);
const result = {
recent: recentTracks,
week: {
tracks: weekTracks,
artists: weekArtists,
albums: weekAlbums,
genres: weekGenres,
totalTracks: calculateTotalPlays(weekTracks),
},
};
cachedWeekData = result;
lastFetchTimeWeek = now;
return result;
} catch (error) {
console.error("Error in fetchWeekData:", error);
return (
cachedWeekData || {
recent: [],
week: {
tracks: [],
artists: [],
albums: [],
genres: [],
totalTracks: "0",
},
}
);
}
}

51
src/utils/data/nav.js Normal file
View file

@ -0,0 +1,51 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedNavigation = null;
let lastFetchTime = 0;
export async function fetchNavigation(env) {
const SUPABASE_URL = import.meta.env?.SUPABASE_URL || env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env?.SUPABASE_KEY || env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const now = Date.now();
if (cachedNavigation && now - lastFetchTime < CACHE_DURATION)
return cachedNavigation;
try {
const { data, error } = await supabase
.from("optimized_navigation")
.select("*");
if (error) {
console.error("Error fetching navigation:", error);
return {};
}
const menu = data.reduce((acc, item) => {
const menuItem = {
title: item["title"] || item["page_title"],
permalink: item["permalink"] || item["page_permalink"],
icon: item["icon"],
sort: item["sort"],
};
if (!acc[item["menu_location"]]) acc[item["menu_location"]] = [menuItem];
else acc[item["menu_location"]].push(menuItem);
return acc;
}, {});
Object.keys(menu).forEach((location) => {
menu[location].sort((a, b) => a["sort"] - b["sort"]);
});
cachedNavigation = menu;
lastFetchTime = now;
return menu;
} catch (error) {
console.error("Error in fetchNavigation:", error);
return cachedNavigation || {};
}
}

32
src/utils/data/pages.js Normal file
View file

@ -0,0 +1,32 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedPages = null;
let lastFetchTime = 0;
export async function fetchPages() {
const now = Date.now();
if (cachedPages && now - lastFetchTime < CACHE_DURATION) return cachedPages;
try {
const { data, error } = await supabase.from("optimized_pages").select("*");
if (error) {
console.error("Error fetching pages:", error);
return cachedPages || [];
}
cachedPages = data;
lastFetchTime = now;
return data;
} catch (error) {
console.error("Error in fetchPages:", error);
return cachedPages || [];
}
}

68
src/utils/data/posts.js Normal file
View file

@ -0,0 +1,68 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedPosts = null;
let lastFetchTime = 0;
export async function fetchAllPosts() {
const now = Date.now();
if (cachedPosts && now - lastFetchTime < CACHE_DURATION) return cachedPosts;
const PAGE_SIZE = 1000;
let posts = [];
let page = 0;
let fetchMore = true;
while (fetchMore) {
const { data, error } = await supabase
.from("optimized_posts")
.select("*")
.order("date", { ascending: false })
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
if (error) return posts;
if (data.length < PAGE_SIZE) fetchMore = false;
posts = posts.concat(data);
page++;
}
cachedPosts = posts;
lastFetchTime = now;
return posts;
}
let cachedPostByUrl = {};
let lastFetchTimeByUrl = {};
export async function fetchPostByUrl(url) {
const now = Date.now();
if (
cachedPostByUrl[url] &&
lastFetchTimeByUrl[url] &&
now - lastFetchTimeByUrl[url] < CACHE_DURATION
) {
return cachedPostByUrl[url];
}
const { data: post, error } = await supabase
.from("optimized_posts")
.select("*")
.eq("url", url)
.limit(1);
if (error || !post.length) return null;
cachedPostByUrl[url] = post[0];
lastFetchTimeByUrl[url] = now;
return post[0];
}

47
src/utils/data/robots.js Normal file
View file

@ -0,0 +1,47 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedRobots = null;
let lastFetchTime = 0;
export async function fetchAllRobots() {
const now = Date.now();
if (cachedRobots && now - lastFetchTime < CACHE_DURATION) return cachedRobots;
const PAGE_SIZE = 500;
let robots = [];
let from = 0;
try {
while (true) {
const { data, error } = await supabase
.from("robots")
.select("user_agent")
.range(from, from + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching robots:", error);
return cachedRobots || [];
}
robots = robots.concat(data);
if (data.length < PAGE_SIZE) break;
from += PAGE_SIZE;
}
const result = robots.map((robot) => robot["user_agent"]).sort();
cachedRobots = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchAllRobots:", error);
return cachedRobots || [];
}
}

View file

@ -0,0 +1,39 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedSyndication = null;
let lastFetchTime = 0;
export async function fetchSyndication() {
const now = Date.now();
if (cachedSyndication && now - lastFetchTime < CACHE_DURATION)
return cachedSyndication;
try {
const { data, error } = await supabase
.from("optimized_syndication")
.select("syndication");
if (error) {
console.error("Error fetching syndication:", error);
return cachedSyndication || [];
}
const [{ syndication } = {}] = data;
const result =
syndication?.filter((item) => item.syndication !== null) || [];
cachedSyndication = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchSyndication:", error);
return cachedSyndication || [];
}
}

80
src/utils/data/tv.js Normal file
View file

@ -0,0 +1,80 @@
import { createClient } from "@supabase/supabase-js";
import { CACHE_DURATION } from "@utils/constants/index.js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
let cachedShows = null;
let lastFetchTime = 0;
export const fetchShows = async () => {
const now = Date.now();
if (cachedShows && now - lastFetchTime < CACHE_DURATION) return cachedShows;
const PAGE_SIZE = 1000;
let shows = [];
let rangeStart = 0;
try {
while (true) {
const { data, error } = await supabase
.from("optimized_shows")
.select("*")
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
if (error) {
console.error("Error fetching shows:", error);
return (
cachedShows || {
shows: [],
recentlyWatched: [],
favorites: [],
}
);
}
shows = shows.concat(data);
if (data.length < PAGE_SIZE) break;
rangeStart += PAGE_SIZE;
}
const watchedShows = shows.filter(
(show) => show["last_watched_at"] !== null
);
const episodes = watchedShows.map((show) => ({
title: show["episode"]["title"],
year: show["year"],
formatted_episode: show["episode"]["formatted_episode"],
url: show["episode"]["url"],
image: show["episode"]["image"],
backdrop: show["episode"]["backdrop"],
last_watched_at: show["episode"]["last_watched_at"],
grid: show["grid"],
type: "tv",
}));
const result = {
shows,
recentlyWatched: episodes.slice(0, 75),
favorites: shows
.filter((show) => show.favorite)
.sort((a, b) => a.title.localeCompare(b.title)),
};
cachedShows = result;
lastFetchTime = now;
return result;
} catch (error) {
console.error("Error in fetchShows:", error);
return (
cachedShows || {
shows: [],
recentlyWatched: [],
favorites: [],
}
);
}
};

View file

@ -0,0 +1,13 @@
export function getPopularPosts(posts, analytics) {
const filteredPosts = posts.filter((post) =>
analytics.some((p) => p.page.includes(post.url))
);
const sortedPosts = filteredPosts.sort((a, b) => {
const visitors = (page) =>
analytics.find((p) => p.page.includes(page.url))?.visitors || 0;
return visitors(b) - visitors(a);
});
return sortedPosts;
}

View file

@ -0,0 +1,111 @@
import { convert } from "html-to-text";
import { format } from "date-fns-tz";
import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import markdownItFootnote from "markdown-it-footnote";
import hljs from "highlight.js";
import "highlight.js/styles/github-dark.min.css";
import truncateHtml from "truncate-html";
// arrays
export const shuffleArray = (array) => {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
};
// countries
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
export const getCountryName = (countryCode) => {
try {
return regionNames.of(countryCode.trim()) || countryCode.trim();
} catch {
return countryCode.trim();
}
};
export const parseCountryField = (countryField) => {
if (!countryField) return null;
const delimiters = [",", "/", "&", "and"];
return delimiters
.reduce(
(countries, delimiter) =>
countries.flatMap((country) => country.split(delimiter)),
[countryField]
)
.map(getCountryName)
.join(", ");
};
// html
export const htmlTruncate = (content, limit = 50) =>
truncateHtml(content, limit, {
byWords: true,
ellipsis: "...",
});
export const htmlToText = (html) =>
convert(html, {
wordwrap: false,
selectors: [
{ selector: "a", options: { ignoreHref: true } },
{ selector: "h1", options: { uppercase: false } },
{ selector: "h2", options: { uppercase: false } },
{ selector: "h3", options: { uppercase: false } },
{ selector: "*", format: "block" },
],
});
export const escapeHtml = (input) =>
typeof input === "string"
? input.replace(/[&<>"']/g, (char) => {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
return map[char];
})
: "";
// markdown
const markdown = markdownIt({
html: true,
linkify: true,
breaks: true,
highlight: (code, lang) => {
if (lang && hljs.getLanguage(lang))
return hljs.highlight(code, { language: lang }).value;
return hljs.highlightAuto(code).value;
},
});
markdown
.use(markdownItAnchor, {
level: [1, 2],
permalink: markdownItAnchor.permalink.headerLink({
safariReaderFix: true,
}),
})
.use(markdownItFootnote);
export const md = (string) => markdown.render(string);
// urls
export const encodeAmp = (url) => url.replace(/&/g, "&amp;");
export const removeTrailingSlash = (url) => url.replace(/\/$/, "");
export const isExcludedPath = (path, exclusions) =>
exclusions.some((exclusion) => path.includes(exclusion));
// dates
export const dateToRFC822 = (date) =>
format(date, "EEE, dd MMM yyyy HH:mm:ss XXX", {
timeZone: "America/Los_Angeles",
});

View file

@ -0,0 +1,73 @@
import countries from "i18n-iso-countries";
import enLocale from "i18n-iso-countries/langs/en.json";
countries.registerLocale(enLocale);
export const filterBooksByStatus = (books, status) =>
books.filter((book) => book["status"] === status);
export const findFavoriteBooks = (books) =>
books.filter((book) => book["favorite"] === true);
export const bookYearLinks = (years) =>
years
.sort((a, b) => b["value"] - a["value"])
.map(
(year, index) =>
`<a href="/books/years/${year["value"]}">${year["value"]}</a>${
index < years.length - 1 ? " / " : ""
}`
)
.join("");
export const mediaLinks = (data, type, count = 10) => {
if (!data || !type) return "";
const dataSlice = data.slice(0, count);
if (dataSlice.length === 0) return null;
const buildLink = (item) => {
switch (type) {
case "genre":
return `<a href="${item["genre_url"]}">${item["genre_name"]}</a>`;
case "artist":
return `<a href="${item["url"]}">${item["name"]}</a>`;
case "book":
return `<a href="${item["url"]}">${item["title"]}</a>`;
default:
return "";
}
};
if (dataSlice.length === 1) return buildLink(dataSlice[0]);
const links = dataSlice.map(buildLink);
const allButLast = links.slice(0, -1).join(", ");
const last = links[links.length - 1];
return `${allButLast} and ${last}`;
};
export const parseCountries = (input) => {
if (!input) return null;
const countryCodes = input
.split(/\s+and\s+|,\s*|\s+/)
.map((code) => code.trim().toUpperCase())
.filter((code) => code.length > 0);
const countryNames = countryCodes.map((code) => {
const countryName = countries.getName(code, "en");
if (!countryName) {
console.warn(`Country code "${code}" is not valid.`);
return null;
}
return countryName;
});
return countryNames.filter((name) => name !== null).join(", ");
};
export const isbnRegex = /^\/books\/(\d{10}|\d{13})$/;