feat: upgrade to SvelteKit 3 and adapter-bun
Some checks failed
Test and deploy staging / staging (push) Failing after 43s
Some checks failed
Test and deploy staging / staging (push) Failing after 43s
Run the sveltekit-3 migration and finish its manual tasks:
- Move kit config into the sveltekit() Vite plugin; drop svelte.config.js
- Replace $lib with #lib subpath imports
- Declare env vars in src/env.ts and import them from $app/env/{private,public}
- Consolidate param matchers into src/params.ts
- Use error(status, message), Response.json, refreshAll and goto reset/shallow
- Fall back to a full page load when goto rejects a login return path
Switch from adapter-node to adapter-bun: the build script runs Vite under
Bun (the adapter requires it) and the image starts with `bun ./build`.
Runtime env names (PORT, BODY_SIZE_LIMIT, ADDRESS_HEADER, ...) are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,29 +1,37 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { building, dev } from '$app/environment';
|
||||
import {
|
||||
BETTER_AUTH_SECRET,
|
||||
BETTER_AUTH_URL,
|
||||
ORIGIN,
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
OSU_CLIENT_ID,
|
||||
OSU_CLIENT_SECRET
|
||||
} from '$app/env/private';
|
||||
import { building, dev } from '$app/env';
|
||||
import { betterAuth } from 'better-auth/minimal';
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { genericOAuth } from 'better-auth/plugins';
|
||||
import { APIError, createAuthMiddleware } from 'better-auth/api';
|
||||
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||
import { getRequestEvent } from '$app/server';
|
||||
import { db } from '$lib/server/db';
|
||||
import * as schema from '$lib/server/db/schema';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import * as schema from '#lib/server/db/schema/index.js';
|
||||
import { osuProfile } from './osu-profile';
|
||||
export { isAdmin, requireAdmin, requireUser } from './authorization';
|
||||
|
||||
if (!building && (!env.BETTER_AUTH_SECRET || env.BETTER_AUTH_SECRET.length < 32)) {
|
||||
if (!building && (!BETTER_AUTH_SECRET || BETTER_AUTH_SECRET.length < 32)) {
|
||||
throw new Error('Set BETTER_AUTH_SECRET to a random secret of at least 32 characters');
|
||||
}
|
||||
if (!building && !dev && !env.BETTER_AUTH_URL && !env.ORIGIN) {
|
||||
if (!building && !dev && !BETTER_AUTH_URL && !ORIGIN) {
|
||||
throw new Error('Set BETTER_AUTH_URL to the public site origin');
|
||||
}
|
||||
export const authProviders = {
|
||||
google: !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET),
|
||||
osu: !!(env.OSU_CLIENT_ID && env.OSU_CLIENT_SECRET)
|
||||
google: !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET),
|
||||
osu: !!(OSU_CLIENT_ID && OSU_CLIENT_SECRET)
|
||||
};
|
||||
export const auth = betterAuth({
|
||||
baseURL: env.BETTER_AUTH_URL || env.ORIGIN || 'http://localhost:5173',
|
||||
secret: env.BETTER_AUTH_SECRET || 'build-only-placeholder-not-a-runtime-secret',
|
||||
baseURL: BETTER_AUTH_URL || ORIGIN || 'http://localhost:5173',
|
||||
secret: BETTER_AUTH_SECRET || 'build-only-placeholder-not-a-runtime-secret',
|
||||
database: drizzleAdapter(db, { provider: 'pg', schema }),
|
||||
emailAndPassword: { enabled: true, minPasswordLength: 12, maxPasswordLength: 128 },
|
||||
user: {
|
||||
@@ -42,7 +50,7 @@ export const auth = betterAuth({
|
||||
rateLimit: { enabled: true },
|
||||
socialProviders: authProviders.google
|
||||
? {
|
||||
google: { clientId: env.GOOGLE_CLIENT_ID!, clientSecret: env.GOOGLE_CLIENT_SECRET! }
|
||||
google: { clientId: GOOGLE_CLIENT_ID!, clientSecret: GOOGLE_CLIENT_SECRET! }
|
||||
}
|
||||
: {},
|
||||
hooks: {
|
||||
@@ -63,8 +71,8 @@ export const auth = betterAuth({
|
||||
? [
|
||||
{
|
||||
providerId: 'osu',
|
||||
clientId: env.OSU_CLIENT_ID!,
|
||||
clientSecret: env.OSU_CLIENT_SECRET!,
|
||||
clientId: OSU_CLIENT_ID!,
|
||||
clientSecret: OSU_CLIENT_SECRET!,
|
||||
authorizationUrl: 'https://osu.ppy.sh/oauth/authorize',
|
||||
tokenUrl: 'https://osu.ppy.sh/oauth/token',
|
||||
scopes: ['identify'],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { ANTHROPIC_API_KEY } from '$app/env/private';
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: env.ANTHROPIC_API_KEY
|
||||
apiKey: ANTHROPIC_API_KEY
|
||||
});
|
||||
|
||||
export interface BreedDetectionResult {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { DB_URL } from '$app/env/private';
|
||||
import { relations } from './relations';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
|
||||
const client = postgres(env.DB_URL, { prepare: false });
|
||||
const client = postgres(DB_URL, { prepare: false });
|
||||
|
||||
export const db = drizzle({
|
||||
relations,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// One file per domain. Everything is re-exported here so the rest of the app
|
||||
// (and drizzle-kit / relations) only ever imports `$lib/server/db/schema`.
|
||||
// (and drizzle-kit / relations) only ever imports `#lib/server/db/schema/index.js`.
|
||||
export * from './auth';
|
||||
export * from './blog';
|
||||
export * from './progress';
|
||||
|
||||
@@ -20,10 +20,12 @@ export function logApiError(error: unknown) {
|
||||
: { type: 'unexpected' }
|
||||
);
|
||||
}
|
||||
// Only the call signature: Bun's global fetch type also carries `preconnect`.
|
||||
export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
const cooldowns = new Map<string, number>();
|
||||
export async function requestJson(
|
||||
fetchFn: typeof fetch,
|
||||
fetchFn: Fetch,
|
||||
provider: string,
|
||||
url: string,
|
||||
init: RequestInit = {},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: { SPOTIFY_CLIENT_ID: 'test', SPOTIFY_CLIENT_SECRET: 'test' }
|
||||
}));
|
||||
vi.mock('$app/env/private', () => ({ SPOTIFY_CLIENT_ID: 'test', SPOTIFY_CLIENT_SECRET: 'test' }));
|
||||
import { createTokenCache, mapConcurrent, requestJson } from './finder-http';
|
||||
import { createRateLimiter, RATE_WINDOW_MS } from './finder-limit';
|
||||
import { getSpotifyMetadata, playlistTracks, MAX_PLAYLIST_TRACKS } from './spotify';
|
||||
@@ -127,7 +125,7 @@ describe('playlists', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
const result = await getSpotifyMetadata(fetchFn as typeof fetch, {
|
||||
const result = await getSpotifyMetadata(fetchFn, {
|
||||
kind: 'playlist',
|
||||
id: id(1)
|
||||
});
|
||||
@@ -158,7 +156,7 @@ it('paginates album tracks and attaches shared album metadata', async () => {
|
||||
tracks: { items: [track(1)], next: 'next', total: 2 }
|
||||
})
|
||||
);
|
||||
const result = await getSpotifyMetadata(fetchFn as typeof fetch, { kind: 'album', id: id(1) });
|
||||
const result = await getSpotifyMetadata(fetchFn, { kind: 'album', id: id(1) });
|
||||
expect(result.collection).toMatchObject({ kind: 'album', name: 'Album name', owner: 'Artist' });
|
||||
expect(result.tracks.map((t) => t.id)).toEqual([id(1), id(2)]);
|
||||
expect(result.tracks[1].album.name).toBe('Album name');
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
// House tracker ingest: the home worker POSTs what it scraped, this stores it
|
||||
// and sends alerts. Nothing here talks to the listing sites.
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import {
|
||||
HOUSE_WORKER_TOKEN,
|
||||
HOUSE_DISCORD_WEBHOOK_URL,
|
||||
HOUSE_NTFY_URL,
|
||||
HOUSE_NTFY_TOKEN
|
||||
} from '$app/env/private';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { and, desc, eq, inArray, isNull, lt, sql } from 'drizzle-orm';
|
||||
import { db } from '$lib/server/db';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import {
|
||||
houseAlertRules,
|
||||
houseListings,
|
||||
housePriceHistory,
|
||||
houseSearches,
|
||||
houseWorkerRuns
|
||||
} from '$lib/server/db/schema';
|
||||
} from '#lib/server/db/schema/index.js';
|
||||
import {
|
||||
alertMessage,
|
||||
matchesRule,
|
||||
type AlertMessage,
|
||||
type AlertRule
|
||||
} from '$lib/house-tracker/alerts';
|
||||
import { listingRef } from '$lib/house-tracker/pararius';
|
||||
} from '#lib/house-tracker/alerts.js';
|
||||
import { listingRef } from '#lib/house-tracker/pararius.js';
|
||||
import type {
|
||||
DetailsRequest,
|
||||
ListingCard,
|
||||
ReportRequest,
|
||||
SyncRequest,
|
||||
SyncResponse
|
||||
} from '$lib/house-tracker/types';
|
||||
} from '#lib/house-tracker/types.js';
|
||||
|
||||
export function requireWorker(request: Request) {
|
||||
const expected = env.HOUSE_WORKER_TOKEN ?? '';
|
||||
const expected = HOUSE_WORKER_TOKEN ?? '';
|
||||
if (expected.length < 32) error(503, 'HOUSE_WORKER_TOKEN is not configured');
|
||||
const given = (request.headers.get('authorization') ?? '').replace(/^Bearer\s+/i, '');
|
||||
const a = Buffer.from(given);
|
||||
@@ -307,9 +312,9 @@ export async function recordReport(input: ReportRequest) {
|
||||
/** Discord webhook and/or ntfy. Returns true when at least one channel accepted it. */
|
||||
export async function sendAlert(message: AlertMessage): Promise<boolean> {
|
||||
const jobs: Promise<boolean>[] = [];
|
||||
if (env.HOUSE_DISCORD_WEBHOOK_URL) {
|
||||
if (HOUSE_DISCORD_WEBHOOK_URL) {
|
||||
jobs.push(
|
||||
fetch(env.HOUSE_DISCORD_WEBHOOK_URL, {
|
||||
fetch(HOUSE_DISCORD_WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -330,15 +335,15 @@ export async function sendAlert(message: AlertMessage): Promise<boolean> {
|
||||
)
|
||||
);
|
||||
}
|
||||
if (env.HOUSE_NTFY_URL) {
|
||||
const url = new URL(env.HOUSE_NTFY_URL);
|
||||
if (HOUSE_NTFY_URL) {
|
||||
const url = new URL(HOUSE_NTFY_URL);
|
||||
const topic = url.pathname.replace(/^\/+|\/+$/g, '');
|
||||
jobs.push(
|
||||
fetch(url.origin, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(env.HOUSE_NTFY_TOKEN ? { authorization: `Bearer ${env.HOUSE_NTFY_TOKEN}` } : {})
|
||||
...(HOUSE_NTFY_TOKEN ? { authorization: `Bearer ${HOUSE_NTFY_TOKEN}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
topic,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, it, vi } from 'vitest';
|
||||
vi.mock('$env/dynamic/private', () => ({ env: { OSU_CLIENT_ID: '1', OSU_CLIENT_SECRET: 'test' } }));
|
||||
vi.mock('$app/env/private', () => ({ OSU_CLIENT_ID: '1', OSU_CLIENT_SECRET: 'test' }));
|
||||
import { getOsuMaps } from './osu';
|
||||
it('falls back deliberately, merges IDs, reuses OAuth and caches matching results', async () => {
|
||||
const fetchFn = vi.fn(async (url: string | URL | Request) => {
|
||||
@@ -22,14 +22,11 @@ it('falls back deliberately, merges IDs, reuses OAuth and caches matching result
|
||||
artists: [{ name: 'Artist' }],
|
||||
album: { name: '', images: [] }
|
||||
};
|
||||
const results = await Promise.all([
|
||||
getOsuMaps(fetchFn as typeof fetch, track),
|
||||
getOsuMaps(fetchFn as typeof fetch, track)
|
||||
]);
|
||||
const results = await Promise.all([getOsuMaps(fetchFn, track), getOsuMaps(fetchFn, track)]);
|
||||
expect(results[0]).toHaveLength(1);
|
||||
expect(results[0][0].score).toBe(100);
|
||||
expect(results[1]).toEqual(results[0]);
|
||||
await getOsuMaps(fetchFn as typeof fetch, track);
|
||||
await getOsuMaps(fetchFn, track);
|
||||
expect(fetchFn).toHaveBeenCalledTimes(3);
|
||||
expect(fetchFn.mock.calls.filter(([url]) => String(url).includes('/oauth/token'))).toHaveLength(
|
||||
1
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { OsuBeatmapset, ScoredBeatmapset, SpotifyTrack } from '$lib/map-finder/types';
|
||||
import { cleanTitle, normalize, rankCandidates, MEDIUM_CONFIDENCE } from '$lib/map-finder/matching';
|
||||
import { ApiError, createTokenCache, requestJson } from './finder-http';
|
||||
import { OSU_CLIENT_ID, OSU_CLIENT_SECRET } from '$app/env/private';
|
||||
import type { OsuBeatmapset, ScoredBeatmapset, SpotifyTrack } from '#lib/map-finder/types.js';
|
||||
import {
|
||||
cleanTitle,
|
||||
normalize,
|
||||
rankCandidates,
|
||||
MEDIUM_CONFIDENCE
|
||||
} from '#lib/map-finder/matching.js';
|
||||
import { ApiError, createTokenCache, requestJson, type Fetch } from './finder-http';
|
||||
const token = createTokenCache();
|
||||
const cache = new Map<string, { expires: number; result: ScoredBeatmapset[] }>();
|
||||
const pending = new Map<string, Promise<ScoredBeatmapset[]>>();
|
||||
@@ -20,16 +25,16 @@ async function limited<T>(work: () => Promise<T>): Promise<T> {
|
||||
else active--;
|
||||
}
|
||||
}
|
||||
async function search(fetchFn: typeof fetch, query: string): Promise<OsuBeatmapset[]> {
|
||||
async function search(fetchFn: Fetch, query: string): Promise<OsuBeatmapset[]> {
|
||||
const access = await token(() => {
|
||||
if (!env.OSU_CLIENT_ID || !env.OSU_CLIENT_SECRET)
|
||||
if (!OSU_CLIENT_ID || !OSU_CLIENT_SECRET)
|
||||
throw new ApiError('osu!', 503, 'osu! is not configured on this server.');
|
||||
return requestJson(fetchFn, 'osu!', 'https://osu.ppy.sh/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
client_id: Number(env.OSU_CLIENT_ID),
|
||||
client_secret: env.OSU_CLIENT_SECRET,
|
||||
client_id: Number(OSU_CLIENT_ID),
|
||||
client_secret: OSU_CLIENT_SECRET,
|
||||
grant_type: 'client_credentials',
|
||||
scope: 'public'
|
||||
})
|
||||
@@ -63,10 +68,7 @@ async function search(fetchFn: typeof fetch, query: string): Promise<OsuBeatmaps
|
||||
beatmaps: Array.isArray(item.beatmaps) ? item.beatmaps.map(() => ({ id: 0 })) : undefined
|
||||
}));
|
||||
}
|
||||
export async function getOsuMaps(
|
||||
fetchFn: typeof fetch,
|
||||
track: SpotifyTrack
|
||||
): Promise<ScoredBeatmapset[]> {
|
||||
export async function getOsuMaps(fetchFn: Fetch, track: SpotifyTrack): Promise<ScoredBeatmapset[]> {
|
||||
const key = JSON.stringify([normalize(track.name), track.artists.map((a) => normalize(a.name))]);
|
||||
for (const [key, entry] of cache) if (entry.expires <= Date.now()) cache.delete(key);
|
||||
const hit = cache.get(key);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { and, asc, eq, inArray } from 'drizzle-orm';
|
||||
import { pokemonResearch, pokemonGameResearch, pokemonLearnsets, pokemonMoves } from './db/schema';
|
||||
import { db } from '$lib/server/db';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import {
|
||||
games,
|
||||
pokemon,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
pokemonEvolutions,
|
||||
pokemonGameEntries,
|
||||
pokemonLocations
|
||||
} from '$lib/server/db/schema';
|
||||
} from '#lib/server/db/schema/index.js';
|
||||
|
||||
export type PokemonDetail = Awaited<ReturnType<typeof pokemonDetail>>;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
pokemonGameEntries,
|
||||
userEntries
|
||||
} from './db/schema';
|
||||
import { boundedInteger, isStage, obtainedMethods, validDate } from '$lib/game-tracker/journal';
|
||||
import { boundedInteger, isStage, obtainedMethods, validDate } from '#lib/game-tracker/journal.js';
|
||||
|
||||
// Serialize mutations for one user's game, including competing party assignments.
|
||||
async function lockGame(
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import {
|
||||
R2_ACCESS_KEY_ID,
|
||||
R2_SECRET_ACCESS_KEY,
|
||||
R2_ACCOUNT_ID,
|
||||
R2_BUCKET,
|
||||
R2_PUBLIC_URL
|
||||
} from '$app/env/private';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
function isConfigured(): boolean {
|
||||
return !!(
|
||||
env.R2_ACCESS_KEY_ID &&
|
||||
env.R2_SECRET_ACCESS_KEY &&
|
||||
env.R2_ACCOUNT_ID &&
|
||||
env.R2_BUCKET &&
|
||||
env.R2_PUBLIC_URL
|
||||
R2_ACCESS_KEY_ID &&
|
||||
R2_SECRET_ACCESS_KEY &&
|
||||
R2_ACCOUNT_ID &&
|
||||
R2_BUCKET &&
|
||||
R2_PUBLIC_URL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,10 +25,10 @@ function getClient(): S3Client {
|
||||
if (!isConfigured()) throw new Error('R2 is not configured — check your .env');
|
||||
_client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: env.R2_ACCESS_KEY_ID!,
|
||||
secretAccessKey: env.R2_SECRET_ACCESS_KEY!
|
||||
accessKeyId: R2_ACCESS_KEY_ID!,
|
||||
secretAccessKey: R2_SECRET_ACCESS_KEY!
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -53,13 +59,13 @@ export async function uploadImage(
|
||||
const key = `${prefix}/${randomUUID()}.${EXT[mimeType]}`;
|
||||
await getClient().send(
|
||||
new PutObjectCommand({
|
||||
Bucket: env.R2_BUCKET,
|
||||
Bucket: R2_BUCKET,
|
||||
Key: key,
|
||||
ContentType: mimeType,
|
||||
Body: new Uint8Array(await file.arrayBuffer())
|
||||
})
|
||||
);
|
||||
return { key, publicUrl: `${env.R2_PUBLIC_URL!.replace(/\/$/, '')}/${key}` };
|
||||
return { key, publicUrl: `${R2_PUBLIC_URL!.replace(/\/$/, '')}/${key}` };
|
||||
}
|
||||
|
||||
export function listImageUrl(key: string): string | null {
|
||||
@@ -69,19 +75,19 @@ export function listImageUrl(key: string): string | null {
|
||||
)
|
||||
)
|
||||
return null;
|
||||
if (!env.R2_PUBLIC_URL) return null;
|
||||
return `${env.R2_PUBLIC_URL.replace(/\/$/, '')}/${key}`;
|
||||
if (!R2_PUBLIC_URL) return null;
|
||||
return `${R2_PUBLIC_URL.replace(/\/$/, '')}/${key}`;
|
||||
}
|
||||
|
||||
/** Public URL of an uploaded Pokémon location picture, or null for foreign keys. */
|
||||
export function locationImageUrl(key: string | null): string | null {
|
||||
if (!key || !/^locations\/[0-9a-f-]{36}\.(jpg|png|webp)$/.test(key) || !env.R2_PUBLIC_URL)
|
||||
if (!key || !/^locations\/[0-9a-f-]{36}\.(jpg|png|webp)$/.test(key) || !R2_PUBLIC_URL)
|
||||
return null;
|
||||
return `${env.R2_PUBLIC_URL.replace(/\/$/, '')}/${key}`;
|
||||
return `${R2_PUBLIC_URL.replace(/\/$/, '')}/${key}`;
|
||||
}
|
||||
|
||||
export function listImageKey(url: string): string | null {
|
||||
const base = env.R2_PUBLIC_URL?.replace(/\/$/, '');
|
||||
const base = R2_PUBLIC_URL?.replace(/\/$/, '');
|
||||
if (!base || !url.startsWith(`${base}/`)) return null;
|
||||
const key = url.slice(base.length + 1);
|
||||
return listImageUrl(key) === url ? key : null;
|
||||
@@ -91,5 +97,5 @@ export function listImageKey(url: string): string | null {
|
||||
* Delete a photo from R2 by its key.
|
||||
*/
|
||||
export async function deleteObject(key: string): Promise<void> {
|
||||
await getClient().send(new DeleteObjectCommand({ Bucket: env.R2_BUCKET, Key: key }));
|
||||
await getClient().send(new DeleteObjectCommand({ Bucket: R2_BUCKET, Key: key }));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { blogArticles, lists } from '$lib/server/db/schema';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import { blogArticles, lists } from '#lib/server/db/schema/index.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export function slugify(title: string, fallback = 'article'): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { SpotifyInput } from '$lib/map-finder/input';
|
||||
import type { SpotifyTrack, SpotifyCollection } from '$lib/map-finder/types';
|
||||
import { ApiError, createTokenCache, requestJson } from './finder-http';
|
||||
import { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } from '$app/env/private';
|
||||
import type { SpotifyInput } from '#lib/map-finder/input.js';
|
||||
import type { SpotifyTrack, SpotifyCollection } from '#lib/map-finder/types.js';
|
||||
import { ApiError, createTokenCache, requestJson, type Fetch } from './finder-http';
|
||||
export const MAX_PLAYLIST_TRACKS = 50;
|
||||
export const MAX_PLAYLIST_PAGES = 10;
|
||||
const token = createTokenCache();
|
||||
@@ -55,14 +55,14 @@ export function playlistTracks(items: unknown[], seen = new Set<string>()): Spot
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function spotify(fetchFn: typeof fetch, path: string) {
|
||||
async function spotify(fetchFn: Fetch, path: string) {
|
||||
const access = await token(() => {
|
||||
if (!env.SPOTIFY_CLIENT_ID || !env.SPOTIFY_CLIENT_SECRET)
|
||||
if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET)
|
||||
throw new ApiError('Spotify', 503, 'Spotify is not configured on this server.');
|
||||
return requestJson(fetchFn, 'Spotify', 'https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(`${env.SPOTIFY_CLIENT_ID}:${env.SPOTIFY_CLIENT_SECRET}`)}`,
|
||||
Authorization: `Basic ${btoa(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`)}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({ grant_type: 'client_credentials' })
|
||||
@@ -75,7 +75,7 @@ async function spotify(fetchFn: typeof fetch, path: string) {
|
||||
);
|
||||
}
|
||||
export async function getSpotifyMetadata(
|
||||
fetchFn: typeof fetch,
|
||||
fetchFn: Fetch,
|
||||
input:
|
||||
| Extract<SpotifyInput, { kind: 'track' | 'playlist' | 'album' }>
|
||||
| Extract<SpotifyInput, { kind: 'search' }>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq, count } from 'drizzle-orm';
|
||||
import { db } from './db';
|
||||
import { pokemonProgress } from '$lib/game-tracker/progress';
|
||||
import { pokemonProgress } from '#lib/game-tracker/progress.js';
|
||||
import {
|
||||
games,
|
||||
pokemon,
|
||||
|
||||
Reference in New Issue
Block a user