chore: run prettier on all src/ files to fix formatting

Co-Authored-By: Justin Edmund <justin@jedmund.com>
This commit is contained in:
Devin AI 2025-11-23 12:12:02 +00:00
parent d60eba6e90
commit 8cc5cedc9d
65 changed files with 1917 additions and 1681 deletions

View file

@ -67,12 +67,21 @@ export async function request<TResponse = unknown, TBody = unknown>(
export const api = {
get: <T = unknown>(url: string, opts: Omit<RequestOptions, 'method' | 'body'> = {}) =>
request<T>(url, { ...opts, method: 'GET' }),
post: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'POST', body }),
put: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'PUT', body }),
patch: <T = unknown, B = unknown>(url: string, body: B, opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}) =>
request<T, B>(url, { ...opts, method: 'PATCH', body }),
post: <T = unknown, B = unknown>(
url: string,
body: B,
opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}
) => request<T, B>(url, { ...opts, method: 'POST', body }),
put: <T = unknown, B = unknown>(
url: string,
body: B,
opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}
) => request<T, B>(url, { ...opts, method: 'PUT', body }),
patch: <T = unknown, B = unknown>(
url: string,
body: B,
opts: Omit<RequestOptions<B>, 'method' | 'body'> = {}
) => request<T, B>(url, { ...opts, method: 'PATCH', body }),
delete: <T = unknown>(url: string, opts: Omit<RequestOptions, 'method' | 'body'> = {}) =>
request<T>(url, { ...opts, method: 'DELETE' })
}

View file

@ -127,37 +127,53 @@ export function createListFilters<T>(
*/
export const commonSorts = {
/** Sort by date field, newest first */
dateDesc: <T>(field: keyof T) => (a: T, b: T) =>
dateDesc:
<T>(field: keyof T) =>
(a: T, b: T) =>
new Date(b[field] as string).getTime() - new Date(a[field] as string).getTime(),
/** Sort by date field, oldest first */
dateAsc: <T>(field: keyof T) => (a: T, b: T) =>
dateAsc:
<T>(field: keyof T) =>
(a: T, b: T) =>
new Date(a[field] as string).getTime() - new Date(b[field] as string).getTime(),
/** Sort by string field, A-Z */
stringAsc: <T>(field: keyof T) => (a: T, b: T) =>
stringAsc:
<T>(field: keyof T) =>
(a: T, b: T) =>
String(a[field] || '').localeCompare(String(b[field] || '')),
/** Sort by string field, Z-A */
stringDesc: <T>(field: keyof T) => (a: T, b: T) =>
stringDesc:
<T>(field: keyof T) =>
(a: T, b: T) =>
String(b[field] || '').localeCompare(String(a[field] || '')),
/** Sort by number field, ascending */
numberAsc: <T>(field: keyof T) => (a: T, b: T) =>
numberAsc:
<T>(field: keyof T) =>
(a: T, b: T) =>
Number(a[field]) - Number(b[field]),
/** Sort by number field, descending */
numberDesc: <T>(field: keyof T) => (a: T, b: T) =>
numberDesc:
<T>(field: keyof T) =>
(a: T, b: T) =>
Number(b[field]) - Number(a[field]),
/** Sort by status field, published first */
statusPublishedFirst: <T>(field: keyof T) => (a: T, b: T) => {
statusPublishedFirst:
<T>(field: keyof T) =>
(a: T, b: T) => {
if (a[field] === b[field]) return 0
return a[field] === 'published' ? -1 : 1
},
/** Sort by status field, draft first */
statusDraftFirst: <T>(field: keyof T) => (a: T, b: T) => {
statusDraftFirst:
<T>(field: keyof T) =>
(a: T, b: T) => {
if (a[field] === b[field]) return 0
return a[field] === 'draft' ? -1 : 1
}

View file

@ -137,7 +137,8 @@
{#if searchError}
<div class="error-message">
<strong>Error:</strong> {searchError}
<strong>Error:</strong>
{searchError}
</div>
{/if}
@ -152,13 +153,7 @@
<h3>Results</h3>
<div class="result-tabs">
<button
class="tab"
class:active={true}
onclick={() => {}}
>
Raw JSON
</button>
<button class="tab" class:active={true} onclick={() => {}}> Raw JSON </button>
<button
class="copy-btn"
onclick={async () => {
@ -277,7 +272,8 @@
margin-bottom: $unit-half;
}
input, select {
input,
select {
width: 100%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);

View file

@ -12,7 +12,6 @@
let isBlinking = $state(false)
let isPlayingMusic = $state(forcePlayingMusic)
const scale = new Spring(1, {
stiffness: 0.1,
damping: 0.125

View file

@ -51,18 +51,25 @@
connected = state.connected
// Flash indicator when update is received
if (state.lastUpdate && (!lastUpdate || state.lastUpdate.getTime() !== lastUpdate.getTime())) {
if (
state.lastUpdate &&
(!lastUpdate || state.lastUpdate.getTime() !== lastUpdate.getTime())
) {
updateFlash = true
setTimeout(() => updateFlash = false, 500)
setTimeout(() => (updateFlash = false), 500)
}
lastUpdate = state.lastUpdate
// Calculate smart interval based on track remaining time
const nowPlayingAlbum = state.albums.find(a => a.isNowPlaying)
if (nowPlayingAlbum?.nowPlayingTrack && nowPlayingAlbum.appleMusicData?.tracks && nowPlayingAlbum.lastScrobbleTime) {
const nowPlayingAlbum = state.albums.find((a) => a.isNowPlaying)
if (
nowPlayingAlbum?.nowPlayingTrack &&
nowPlayingAlbum.appleMusicData?.tracks &&
nowPlayingAlbum.lastScrobbleTime
) {
const track = nowPlayingAlbum.appleMusicData.tracks.find(
t => t.name === nowPlayingAlbum.nowPlayingTrack
(t) => t.name === nowPlayingAlbum.nowPlayingTrack
)
if (track?.durationMs) {
@ -109,7 +116,7 @@
// Calculate initial remaining time
const calculateRemaining = () => {
const elapsed = Date.now() - lastUpdate.getTime()
const remaining = (updateInterval * 1000) - elapsed
const remaining = updateInterval * 1000 - elapsed
return Math.max(0, Math.ceil(remaining / 1000))
}
@ -213,7 +220,7 @@
{#if dev}
<div class="debug-panel" class:minimized={isMinimized}>
<div class="debug-header" onclick={() => isMinimized = !isMinimized}>
<div class="debug-header" onclick={() => (isMinimized = !isMinimized)}>
<h3>Debug Panel</h3>
<button class="minimize-btn" aria-label={isMinimized ? 'Expand' : 'Minimize'}>
{isMinimized ? '▲' : '▼'}
@ -226,21 +233,21 @@
<button
class="tab"
class:active={activeTab === 'nowplaying'}
onclick={() => activeTab = 'nowplaying'}
onclick={() => (activeTab = 'nowplaying')}
>
Now Playing
</button>
<button
class="tab"
class:active={activeTab === 'albums'}
onclick={() => activeTab = 'albums'}
onclick={() => (activeTab = 'albums')}
>
Albums
</button>
<button
class="tab"
class:active={activeTab === 'cache'}
onclick={() => activeTab = 'cache'}
onclick={() => (activeTab = 'cache')}
>
Cache
</button>
@ -251,13 +258,21 @@
<div class="section">
<h4>Connection</h4>
<p class="status" class:connected>
Status: {#if connected}<CheckIcon class="icon status-icon success" /> Connected{:else}<XIcon class="icon status-icon error" /> Disconnected{/if}
Status: {#if connected}<CheckIcon class="icon status-icon success" /> Connected{:else}<XIcon
class="icon status-icon error"
/> Disconnected{/if}
</p>
<p class:flash={updateFlash}>
Last Update: {lastUpdate ? lastUpdate.toLocaleTimeString() : 'Never'}
</p>
<p>Next Update: {formatTime(nextUpdateIn)}</p>
<p>Interval: {updateInterval}s {trackRemainingTime > 0 ? `(smart mode)` : nowPlaying ? '(fast mode)' : '(normal)'}</p>
<p>
Interval: {updateInterval}s {trackRemainingTime > 0
? `(smart mode)`
: nowPlaying
? '(fast mode)'
: '(normal)'}
</p>
{#if trackRemainingTime > 0}
<p>Track Remaining: {formatTime(trackRemainingTime)}</p>
{/if}
@ -274,7 +289,10 @@
{/if}
{#if nowPlaying.album.appleMusicData}
<p class="preview">
<span>Preview:</span> {#if nowPlaying.album.appleMusicData.previewUrl}<CheckIcon class="icon success" /> Available{:else}<XIcon class="icon error" /> Not found{/if}
<span>Preview:</span>
{#if nowPlaying.album.appleMusicData.previewUrl}<CheckIcon
class="icon success"
/> Available{:else}<XIcon class="icon error" /> Not found{/if}
</p>
{/if}
</div>
@ -290,8 +308,16 @@
<div class="albums-list">
{#each albums as album}
{@const albumId = `${album.artist.name}:${album.name}`}
<div class="album-item" class:playing={album.isNowPlaying} class:expanded={expandedAlbumId === albumId}>
<div class="album-header" onclick={() => expandedAlbumId = expandedAlbumId === albumId ? null : albumId}>
<div
class="album-item"
class:playing={album.isNowPlaying}
class:expanded={expandedAlbumId === albumId}
>
<div
class="album-header"
onclick={() =>
(expandedAlbumId = expandedAlbumId === albumId ? null : albumId)}
>
<div class="album-content">
<div class="album-info">
<span class="name">{album.name}</span>
@ -306,7 +332,9 @@
{album.appleMusicData.tracks?.length || 0} tracks
</span>
<span class="meta-item">
{#if album.appleMusicData.previewUrl}<CheckIcon class="icon success inline" /> Preview{:else}<XIcon class="icon error inline" /> No preview{/if}
{#if album.appleMusicData.previewUrl}<CheckIcon
class="icon success inline"
/> Preview{:else}<XIcon class="icon error inline" /> No preview{/if}
</span>
{:else}
<span class="meta-item">No Apple Music data</span>
@ -315,7 +343,10 @@
</div>
<button
class="clear-cache-btn"
onclick={(e) => { e.stopPropagation(); clearAlbumCache(album) }}
onclick={(e) => {
e.stopPropagation()
clearAlbumCache(album)
}}
disabled={clearingAlbums.has(albumId)}
title="Clear Apple Music cache for this album"
>
@ -333,9 +364,18 @@
{#if album.appleMusicData.searchMetadata}
<h5>Search Information</h5>
<div class="search-metadata">
<p><strong>Search Query:</strong> <code>{album.appleMusicData.searchMetadata.searchQuery}</code></p>
<p><strong>Search Time:</strong> {new Date(album.appleMusicData.searchMetadata.searchTime).toLocaleString()}</p>
<p><strong>Status:</strong>
<p>
<strong>Search Query:</strong>
<code>{album.appleMusicData.searchMetadata.searchQuery}</code>
</p>
<p>
<strong>Search Time:</strong>
{new Date(
album.appleMusicData.searchMetadata.searchTime
).toLocaleString()}
</p>
<p>
<strong>Status:</strong>
{#if album.appleMusicData.searchMetadata.found}
<CheckIcon class="icon success inline" /> Found
{:else}
@ -343,14 +383,22 @@
{/if}
</p>
{#if album.appleMusicData.searchMetadata.error}
<p><strong>Error:</strong> <span class="error-text">{album.appleMusicData.searchMetadata.error}</span></p>
<p>
<strong>Error:</strong>
<span class="error-text"
>{album.appleMusicData.searchMetadata.error}</span
>
</p>
{/if}
</div>
{/if}
{#if album.appleMusicData.appleMusicId}
<h5>Apple Music Details</h5>
<p><strong>Apple Music ID:</strong> {album.appleMusicData.appleMusicId}</p>
<p>
<strong>Apple Music ID:</strong>
{album.appleMusicData.appleMusicId}
</p>
{/if}
{#if album.appleMusicData.releaseDate}
@ -366,7 +414,10 @@
{/if}
{#if album.appleMusicData.previewUrl}
<p><strong>Preview URL:</strong> <code>{album.appleMusicData.previewUrl}</code></p>
<p>
<strong>Preview URL:</strong>
<code>{album.appleMusicData.previewUrl}</code>
</p>
{/if}
{#if album.appleMusicData.tracks?.length}
@ -378,7 +429,11 @@
<span class="track-number">{i + 1}.</span>
<span class="track-name">{track.name}</span>
{#if track.durationMs}
<span class="track-duration">{Math.floor(track.durationMs / 60000)}:{String(Math.floor((track.durationMs % 60000) / 1000)).padStart(2, '0')}</span>
<span class="track-duration"
>{Math.floor(track.durationMs / 60000)}:{String(
Math.floor((track.durationMs % 60000) / 1000)
).padStart(2, '0')}</span
>
{/if}
{#if track.previewUrl}
<CheckIcon class="icon success inline" title="Has preview" />
@ -395,7 +450,9 @@
</div>
{:else}
<h5>No Apple Music Data</h5>
<p class="no-data">This album was not searched in Apple Music or the search is pending.</p>
<p class="no-data">
This album was not searched in Apple Music or the search is pending.
</p>
{/if}
</div>
{/if}
@ -426,11 +483,7 @@
</div>
<div class="cache-actions">
<button
onclick={clearAllMusicCache}
disabled={isClearing}
class="clear-all-btn"
>
<button onclick={clearAllMusicCache} disabled={isClearing} class="clear-all-btn">
{isClearing ? 'Clearing...' : 'Clear All Music Cache'}
</button>
@ -463,10 +516,7 @@
{isClearing ? 'Clearing...' : 'Clear Not Found Cache'}
</button>
<button
onclick={() => searchModal?.open()}
class="search-btn"
>
<button onclick={() => searchModal?.open()} class="search-btn">
Test Apple Music Search
</button>
</div>
@ -912,7 +962,8 @@
flex-wrap: wrap;
gap: $unit;
.clear-all-btn, .clear-not-found-btn {
.clear-all-btn,
.clear-not-found-btn {
flex: 1;
min-width: 140px;
padding: $unit * 1.5;

View file

@ -250,8 +250,8 @@
background: $gray-95;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
monospace;
font-family:
'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: $text-color;
}

View file

@ -8,11 +8,13 @@
let { album, getAlbumArtwork }: Props = $props()
const trackText = $derived(`${album.artist.name} — ${album.name}${
const trackText = $derived(
`${album.artist.name} — ${album.name}${
album.appleMusicData?.releaseDate
? ` (${new Date(album.appleMusicData.releaseDate).getFullYear()})`
: ''
} — ${album.nowPlayingTrack || album.name}`)
} — ${album.nowPlayingTrack || album.name}`
)
</script>
<nav class="now-playing-bar">

View file

@ -229,8 +229,8 @@
.metadata-value {
font-size: 0.875rem;
color: $gray-10;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
monospace;
font-family:
'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}
}

View file

@ -275,10 +275,7 @@
</div>
<div class="header-actions">
{#if !isLoading}
<AutoSaveStatus
status="idle"
lastSavedAt={album?.updatedAt}
/>
<AutoSaveStatus status="idle" lastSavedAt={album?.updatedAt} />
{/if}
</div>
</header>

View file

@ -38,7 +38,14 @@
ondelete?: (event: CustomEvent<{ album: Album; event: MouseEvent }>) => void
}
let { album, isDropdownActive = false, ontoggledropdown, onedit, ontogglepublish, ondelete }: Props = $props()
let {
album,
isDropdownActive = false,
ontoggledropdown,
onedit,
ontogglepublish,
ondelete
}: Props = $props()
function formatRelativeTime(dateString: string): string {
const date = new Date(dateString)

View file

@ -112,6 +112,8 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
</style>

View file

@ -11,7 +11,8 @@
<div class="draft-banner">
<div class="draft-banner-content">
<span class="draft-banner-text">
Unsaved draft found{#if timeAgo} (saved {timeAgo}){/if}.
Unsaved draft found{#if timeAgo}
(saved {timeAgo}){/if}.
</span>
<div class="draft-banner-actions">
<button class="draft-banner-button" type="button" onclick={onRestore}>Restore</button>

View file

@ -50,7 +50,9 @@
let showDraftPrompt = $state(false)
let draftTimestamp = $state<number | null>(null)
let timeTicker = $state(0)
const draftTimeText = $derived.by(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
const draftTimeText = $derived.by(() =>
draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null
)
function buildPayload() {
return {
@ -65,7 +67,8 @@ function buildPayload() {
}
// Autosave store (edit mode only)
let autoSave = mode === 'edit' && postId
let autoSave =
mode === 'edit' && postId
? createAutoSaveStore({
debounceMs: 2000,
getPayload: () => (hasLoaded ? buildPayload() : null),
@ -126,7 +129,12 @@ $effect(() => {
// Trigger autosave when form data changes
$effect(() => {
title; slug; status; content; tags; activeTab
title
slug
status
content
tags
activeTab
if (hasLoaded && autoSave) {
autoSave.schedule()
}
@ -311,7 +319,6 @@ $effect(() => {
isSaving = false
}
}
</script>
<AdminPage>
@ -341,7 +348,8 @@ $effect(() => {
<div class="draft-banner">
<div class="draft-banner-content">
<span class="draft-banner-text">
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
Unsaved draft found{#if draftTimeText}
(saved {draftTimeText}){/if}.
</span>
<div class="draft-banner-actions">
<button class="draft-banner-button" onclick={restoreDraft}>Restore</button>
@ -381,11 +389,7 @@ $effect(() => {
<Input label="Slug" bind:value={slug} placeholder="essay-url-slug" />
<DropdownSelectField
label="Status"
bind:value={status}
options={statusOptions}
/>
<DropdownSelectField label="Status" bind:value={status} options={statusOptions} />
<div class="tags-field">
<label class="input-label">Tags</label>

View file

@ -1,6 +1,12 @@
<script lang="ts">
import Button from './Button.svelte'
import { formatFileSize, getFileType, isVideoFile, formatDuration, formatBitrate } from '$lib/utils/mediaHelpers'
import {
formatFileSize,
getFileType,
isVideoFile,
formatDuration,
formatBitrate
} from '$lib/utils/mediaHelpers'
import type { Media } from '@prisma/client'
interface Props {

View file

@ -47,7 +47,9 @@
let showDraftPrompt = $state(false)
let draftTimestamp = $state<number | null>(null)
let timeTicker = $state(0)
const draftTimeText = $derived.by(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
const draftTimeText = $derived.by(() =>
draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null
)
function buildPayload() {
return {
@ -68,7 +70,8 @@ function buildPayload() {
}
// Autosave store (edit mode only)
let autoSave = mode === 'edit' && postId
let autoSave =
mode === 'edit' && postId
? createAutoSaveStore({
debounceMs: 2000,
getPayload: () => (hasLoaded ? buildPayload() : null),
@ -101,7 +104,11 @@ let autoSave = mode === 'edit' && postId
// Trigger autosave when form data changes
$effect(() => {
title; status; content; featuredImage; tags
title
status
content
featuredImage
tags
if (hasLoaded && autoSave) {
autoSave.schedule()
}
@ -397,7 +404,8 @@ $effect(() => {
<div class="draft-banner">
<div class="draft-banner-content">
<span class="draft-banner-text">
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
Unsaved draft found{#if draftTimeText}
(saved {draftTimeText}){/if}.
</span>
<div class="draft-banner-actions">
<button class="draft-banner-button" onclick={restoreDraft}>Restore</button>

View file

@ -43,7 +43,11 @@
}
</script>
<div class="dropdown-container" use:clickOutside={{ enabled: isOpen }} onclickoutside={handleClickOutside}>
<div
class="dropdown-container"
use:clickOutside={{ enabled: isOpen }}
onclickoutside={handleClickOutside}
>
<Button
bind:this={buttonRef}
variant="primary"

View file

@ -165,9 +165,7 @@
{#if isDropdownOpen}
<div class="dropdown-menu">
<button class="dropdown-item" type="button" onclick={handleEdit}>
Edit post
</button>
<button class="dropdown-item" type="button" onclick={handleEdit}> Edit post </button>
<button class="dropdown-item" type="button" onclick={handleTogglePublish}>
{post.status === 'published' ? 'Unpublish' : 'Publish'} post
</button>

View file

@ -81,7 +81,9 @@
const hasFeaturedImage = $derived(
!!(formData.featuredImage && featuredImageMedia) || !!featuredImageMedia
)
const hasBackgroundColor = $derived(!!(formData.backgroundColor && formData.backgroundColor?.trim()))
const hasBackgroundColor = $derived(
!!(formData.backgroundColor && formData.backgroundColor?.trim())
)
const hasLogo = $derived(!!(formData.logoUrl && logoMedia) || !!logoMedia)
// Auto-disable toggles when content is removed

View file

@ -43,7 +43,8 @@
const draftKey = $derived(mode === 'edit' && project ? makeDraftKey('project', project.id) : null)
// Autosave (edit mode only)
const autoSave = mode === 'edit'
const autoSave =
mode === 'edit'
? createAutoSaveStore({
debounceMs: 2000,
getPayload: () => (hasLoaded ? formStore.buildPayload() : null),
@ -89,7 +90,8 @@
// Trigger autosave when formData changes (edit mode)
$effect(() => {
// Establish dependencies on fields
formStore.fields; activeTab
formStore.fields
activeTab
if (mode === 'edit' && hasLoaded && autoSave) {
autoSave.schedule()
}
@ -143,9 +145,9 @@
let savedProject: Project
if (mode === 'edit') {
savedProject = await api.put(`/api/projects/${project?.id}`, payload) as Project
savedProject = (await api.put(`/api/projects/${project?.id}`, payload)) as Project
} else {
savedProject = await api.post('/api/projects', payload) as Project
savedProject = (await api.post('/api/projects', payload)) as Project
}
toast.dismiss(loadingToastId)
@ -168,8 +170,6 @@
isSaving = false
}
}
</script>
<AdminPage>
@ -225,7 +225,11 @@
handleSave()
}}
>
<ProjectMetadataForm bind:formData={formStore.fields} validationErrors={formStore.validationErrors} onSave={handleSave} />
<ProjectMetadataForm
bind:formData={formStore.fields}
validationErrors={formStore.validationErrors}
onSave={handleSave}
/>
</form>
</div>
</div>
@ -239,7 +243,11 @@
handleSave()
}}
>
<ProjectBrandingForm bind:formData={formStore.fields} validationErrors={formStore.validationErrors} onSave={handleSave} />
<ProjectBrandingForm
bind:formData={formStore.fields}
validationErrors={formStore.validationErrors}
onSave={handleSave}
/>
</form>
</div>
</div>

View file

@ -131,9 +131,7 @@
{#if isDropdownOpen}
<div class="dropdown-menu">
<button class="dropdown-item" type="button" onclick={handleEdit}>
Edit project
</button>
<button class="dropdown-item" type="button" onclick={handleEdit}> Edit project </button>
<button class="dropdown-item" type="button" onclick={handleTogglePublish}>
{project.status === 'published' ? 'Unpublish' : 'Publish'} project
</button>

View file

@ -62,7 +62,9 @@ const draftKey = $derived(makeDraftKey('post', postId ?? 'new'))
let showDraftPrompt = $state(false)
let draftTimestamp = $state<number | null>(null)
let timeTicker = $state(0)
const draftTimeText = $derived.by(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
const draftTimeText = $derived.by(() =>
draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null
)
function buildPayload() {
const payload: any = {
@ -82,7 +84,8 @@ function buildPayload() {
}
// Autosave store (edit mode only)
let autoSave = mode === 'edit' && postId
let autoSave =
mode === 'edit' && postId
? createAutoSaveStore({
debounceMs: 2000,
getPayload: () => (hasLoaded ? buildPayload() : null),
@ -115,7 +118,11 @@ $effect(() => {
// Trigger autosave when form data changes
$effect(() => {
status; content; linkUrl; linkDescription; title
status
content
linkUrl
linkDescription
title
if (hasLoaded && autoSave) {
autoSave.schedule()
}
@ -336,7 +343,8 @@ $effect(() => {
<div class="draft-banner">
<div class="draft-banner-content">
<span class="draft-banner-text">
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
Unsaved draft found{#if draftTimeText}
(saved {draftTimeText}){/if}.
</span>
<div class="draft-banner-actions">
<button class="draft-banner-button" onclick={restoreDraft}>Restore</button>

View file

@ -66,14 +66,7 @@
>
<span class="status-dot"></span>
<span class="status-label">{currentConfig.label}</span>
<svg
class="chevron"
class:open={isOpen}
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<svg class="chevron" class:open={isOpen} width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M3 4.5L6 7.5L9 4.5"
stroke="currentColor"
@ -96,12 +89,7 @@
{#if viewUrl && currentStatus === 'published'}
<div class="dropdown-divider"></div>
<a
href={viewUrl}
target="_blank"
rel="noopener noreferrer"
class="dropdown-item view-link"
>
<a href={viewUrl} target="_blank" rel="noopener noreferrer" class="dropdown-item view-link">
View on site
</a>
{/if}

View file

@ -225,7 +225,6 @@
// Short delay to prevent flicker
await new Promise((resolve) => setTimeout(resolve, 500))
let url = `/api/media?page=${page}&limit=24`
if (filterType !== 'all') {

View file

@ -375,10 +375,7 @@
const afterPos = nodePos + actualNode.nodeSize
// Insert the duplicated node
editor.chain()
.focus()
.insertContentAt(afterPos, nodeCopy)
.run()
editor.chain().focus().insertContentAt(afterPos, nodeCopy).run()
isMenuOpen = false
}

View file

@ -33,7 +33,10 @@ export async function adminFetch(
let detail: string | undefined
try {
const json = await response.clone().json()
detail = typeof json === 'object' && json !== null && 'error' in json ? String(json.error) : undefined
detail =
typeof json === 'object' && json !== null && 'error' in json
? String(json.error)
: undefined
} catch {
try {
detail = await response.clone().text()

View file

@ -340,11 +340,14 @@ export async function findAlbum(artist: string, album: string): Promise<AppleMus
// Log all songs for debugging
songs.forEach((s, index) => {
logger.music('debug', `Song ${index + 1}: "${s.attributes?.name}" by "${s.attributes?.artistName}" on "${s.attributes?.albumName}"`)
logger.music(
'debug',
`Song ${index + 1}: "${s.attributes?.name}" by "${s.attributes?.artistName}" on "${s.attributes?.albumName}"`
)
})
// Find matching song
const matchingSong = songs.find(s => {
const matchingSong = songs.find((s) => {
const songName = s.attributes?.name || ''
const artistName = s.attributes?.artistName || ''
const albumName = s.attributes?.albumName || ''
@ -357,7 +360,8 @@ export async function findAlbum(artist: string, album: string): Promise<AppleMus
const artistSearchLower = artist.toLowerCase()
// Check if the song name matches what we're looking for
const songMatches = songNameLower === albumSearchLower ||
const songMatches =
songNameLower === albumSearchLower ||
songNameLower.includes(albumSearchLower) ||
albumSearchLower.includes(songNameLower)
@ -365,7 +369,8 @@ export async function findAlbum(artist: string, album: string): Promise<AppleMus
const artistNameNormalized = artistNameLower.replace(/\s+/g, '')
const artistSearchNormalized = artistSearchLower.replace(/\s+/g, '')
const artistMatches = artistNameLower === artistSearchLower ||
const artistMatches =
artistNameLower === artistSearchLower ||
artistNameNormalized === artistSearchNormalized ||
artistNameLower.includes(artistSearchLower) ||
artistSearchLower.includes(artistNameLower) ||
@ -373,7 +378,10 @@ export async function findAlbum(artist: string, album: string): Promise<AppleMus
artistSearchNormalized.includes(artistNameNormalized)
if (songMatches && artistMatches) {
logger.music('debug', `Found matching song: "${songName}" by "${artistName}" on album "${albumName}"`)
logger.music(
'debug',
`Found matching song: "${songName}" by "${artistName}" on album "${albumName}"`
)
return true
}
@ -397,7 +405,10 @@ export async function findAlbum(artist: string, album: string): Promise<AppleMus
}
// If no album found, create a synthetic album from the song
logger.music('debug', `Creating synthetic album from single: "${matchingSong.attributes?.name}"`)
logger.music(
'debug',
`Creating synthetic album from single: "${matchingSong.attributes?.name}"`
)
return {
id: `single-${matchingSong.id}`,
type: 'albums' as const,
@ -449,11 +460,13 @@ export async function transformAlbumData(appleMusicAlbum: AppleMusicAlbum) {
if ((attributes as any).isSingle && (attributes as any)._singleSongPreview) {
logger.music('debug', 'Processing synthetic single album')
previewUrl = (attributes as any)._singleSongPreview
tracks = [{
tracks = [
{
name: attributes.name,
previewUrl: (attributes as any)._singleSongPreview,
durationMs: undefined // We'd need to fetch the song details for duration
}]
}
]
}
// Always fetch tracks to get preview URLs
else if (appleMusicAlbum.id) {

View file

@ -9,12 +9,38 @@ export interface CacheConfig {
export class CacheManager {
private static cacheTypes: Map<string, CacheConfig> = new Map([
['lastfm-recent', { prefix: 'lastfm:recent:', defaultTTL: 30, description: 'Last.fm recent tracks' }],
['lastfm-album', { prefix: 'lastfm:albuminfo:', defaultTTL: 3600, description: 'Last.fm album info' }],
['apple-album', { prefix: 'apple:album:', defaultTTL: 86400, description: 'Apple Music album data' }],
['apple-notfound', { prefix: 'notfound:apple-music:', defaultTTL: 3600, description: 'Apple Music not found records' }],
['apple-failure', { prefix: 'failure:apple-music:', defaultTTL: 86400, description: 'Apple Music API failures' }],
['apple-ratelimit', { prefix: 'ratelimit:apple-music:', defaultTTL: 3600, description: 'Apple Music rate limit state' }]
[
'lastfm-recent',
{ prefix: 'lastfm:recent:', defaultTTL: 30, description: 'Last.fm recent tracks' }
],
[
'lastfm-album',
{ prefix: 'lastfm:albuminfo:', defaultTTL: 3600, description: 'Last.fm album info' }
],
[
'apple-album',
{ prefix: 'apple:album:', defaultTTL: 86400, description: 'Apple Music album data' }
],
[
'apple-notfound',
{
prefix: 'notfound:apple-music:',
defaultTTL: 3600,
description: 'Apple Music not found records'
}
],
[
'apple-failure',
{ prefix: 'failure:apple-music:', defaultTTL: 86400, description: 'Apple Music API failures' }
],
[
'apple-ratelimit',
{
prefix: 'ratelimit:apple-music:',
defaultTTL: 3600,
description: 'Apple Music rate limit state'
}
]
])
/**
@ -118,7 +144,10 @@ export class CacheManager {
}
}
logger.music('info', `Cleared ${totalDeleted} cache entries for album "${album}" by "${artist}"`)
logger.music(
'info',
`Cleared ${totalDeleted} cache entries for album "${album}" by "${artist}"`
)
return totalDeleted
}
@ -152,14 +181,21 @@ export class CacheManager {
export const cache = {
lastfm: {
getRecent: (username: string) => CacheManager.get('lastfm-recent', username),
setRecent: (username: string, data: string) => CacheManager.set('lastfm-recent', username, data),
getAlbum: (artist: string, album: string) => CacheManager.get('lastfm-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string) => CacheManager.set('lastfm-album', `${artist}:${album}`, data)
setRecent: (username: string, data: string) =>
CacheManager.set('lastfm-recent', username, data),
getAlbum: (artist: string, album: string) =>
CacheManager.get('lastfm-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string) =>
CacheManager.set('lastfm-album', `${artist}:${album}`, data)
},
apple: {
getAlbum: (artist: string, album: string) => CacheManager.get('apple-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string, ttl?: number) => CacheManager.set('apple-album', `${artist}:${album}`, data, ttl),
isNotFound: (artist: string, album: string) => CacheManager.get('apple-notfound', `${artist}:${album}`),
markNotFound: (artist: string, album: string, ttl?: number) => CacheManager.set('apple-notfound', `${artist}:${album}`, '1', ttl)
getAlbum: (artist: string, album: string) =>
CacheManager.get('apple-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string, ttl?: number) =>
CacheManager.set('apple-album', `${artist}:${album}`, data, ttl),
isNotFound: (artist: string, album: string) =>
CacheManager.get('apple-notfound', `${artist}:${album}`),
markNotFound: (artist: string, album: string, ttl?: number) =>
CacheManager.set('apple-notfound', `${artist}:${album}`, '1', ttl)
}
}

View file

@ -95,8 +95,8 @@ export async function uploadFileLocally(
}
// Extract video metadata
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
const audioStream = metadata.streams.find(s => s.codec_type === 'audio')
const videoStream = metadata.streams.find((s) => s.codec_type === 'video')
const audioStream = metadata.streams.find((s) => s.codec_type === 'audio')
if (videoStream) {
width = videoStream.width || 0

View file

@ -50,7 +50,7 @@ function createMusicStream() {
nowPlaying: nowPlayingAlbum
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
: 'none',
albums: albums.map(a => ({
albums: albums.map((a) => ({
name: a.name,
artist: a.artist.name,
isNowPlaying: a.isNowPlaying,
@ -144,11 +144,13 @@ function createMusicStream() {
albums: derived({ subscribe }, ($state) => $state.albums) as Readable<Album[]>,
// Helper to check if any album is playing
nowPlaying: derived({ subscribe }, ($state) => {
const playing = $state.albums.find(a => a.isNowPlaying)
return playing ? {
const playing = $state.albums.find((a) => a.isNowPlaying)
return playing
? {
album: playing,
track: playing.nowPlayingTrack
} : null
}
: null
}) as Readable<{ album: Album; track?: string } | null>
}
}

View file

@ -9,9 +9,7 @@ export function createProjectFormStore(initialProject?: Project | null) {
let original = $state<ProjectFormData | null>(null)
// Derived state using $derived rune
const isDirty = $derived(
original ? JSON.stringify(fields) !== JSON.stringify(original) : false
)
const isDirty = $derived(original ? JSON.stringify(fields) !== JSON.stringify(original) : false)
// Initialize from project if provided
if (initialProject) {
@ -96,7 +94,8 @@ export function createProjectFormStore(initialProject?: Project | null) {
role: fields.role,
projectType: fields.projectType,
externalUrl: fields.externalUrl,
featuredImage: fields.featuredImage && fields.featuredImage !== '' ? fields.featuredImage : null,
featuredImage:
fields.featuredImage && fields.featuredImage !== '' ? fields.featuredImage : null,
logoUrl: fields.logoUrl && fields.logoUrl !== '' ? fields.logoUrl : null,
backgroundColor: fields.backgroundColor,
highlightColor: fields.highlightColor,

View file

@ -63,7 +63,10 @@ export class LastfmStreamManager {
}
// Check for now playing updates for non-recent albums
const nowPlayingUpdates = await this.getNowPlayingUpdatesForNonRecentAlbums(enrichedAlbums, freshData)
const nowPlayingUpdates = await this.getNowPlayingUpdatesForNonRecentAlbums(
enrichedAlbums,
freshData
)
if (nowPlayingUpdates.length > 0) {
update.nowPlayingUpdates = nowPlayingUpdates
}

View file

@ -163,13 +163,19 @@ export class NowPlayingDetector {
for (const track of tracks) {
if (track.nowPlaying) {
hasOfficialNowPlaying = true
logger.music('debug', `Last.fm reports "${track.name}" by ${track.artist.name} as now playing`)
logger.music(
'debug',
`Last.fm reports "${track.name}" by ${track.artist.name} as now playing`
)
break
}
}
if (!hasOfficialNowPlaying) {
logger.music('debug', 'No official now playing from Last.fm, will use duration-based detection')
logger.music(
'debug',
'No official now playing from Last.fm, will use duration-based detection'
)
}
// Process all tracks
@ -230,8 +236,11 @@ export class NowPlayingDetector {
this.updateRecentTracks(newRecentTracks)
// Log summary
const nowPlayingCount = Array.from(albums.values()).filter(a => a.isNowPlaying).length
logger.music('debug', `Detected ${nowPlayingCount} album(s) as now playing out of ${albums.size} recent albums`)
const nowPlayingCount = Array.from(albums.values()).filter((a) => a.isNowPlaying).length
logger.music(
'debug',
`Detected ${nowPlayingCount} album(s) as now playing out of ${albums.size} recent albums`
)
// Ensure only one album is marked as now playing
return this.ensureSingleNowPlaying(albums, newRecentTracks)

View file

@ -34,7 +34,10 @@ export class SimpleLastfmStreamManager {
limit: 50,
extended: true
})
logger.music('debug', `📊 Got ${recentTracksResponse.tracks?.length || 0} tracks from Last.fm`)
logger.music(
'debug',
`📊 Got ${recentTracksResponse.tracks?.length || 0} tracks from Last.fm`
)
// Cache for other uses but always use fresh for now playing
await this.albumEnricher.cacheRecentTracks(this.username, recentTracksResponse)
@ -64,7 +67,7 @@ export class SimpleLastfmStreamManager {
// Check if anything changed
const currentState = JSON.stringify(
enrichedAlbums.map(a => ({
enrichedAlbums.map((a) => ({
key: `${a.artist.name}:${a.name}`,
isNowPlaying: a.isNowPlaying,
track: a.nowPlayingTrack

View file

@ -15,7 +15,10 @@ export class SimpleNowPlayingDetector {
const isPlaying = elapsed >= 0 && elapsed <= maxPlayTime
logger.music('debug', `Track playing check: elapsed=${Math.round(elapsed/1000)}s, duration=${Math.round(durationMs/1000)}s, maxPlay=${Math.round(maxPlayTime/1000)}s, isPlaying=${isPlaying}`)
logger.music(
'debug',
`Track playing check: elapsed=${Math.round(elapsed / 1000)}s, duration=${Math.round(durationMs / 1000)}s, maxPlay=${Math.round(maxPlayTime / 1000)}s, isPlaying=${isPlaying}`
)
// Track is playing if we're within the duration + buffer
return isPlaying
@ -30,16 +33,22 @@ export class SimpleNowPlayingDetector {
recentTracks: any[],
appleMusicDataLookup: (artistName: string, albumName: string) => Promise<any>
): Promise<Album[]> {
logger.music('debug', `Processing ${albums.length} albums with ${recentTracks.length} recent tracks`)
logger.music(
'debug',
`Processing ${albums.length} albums with ${recentTracks.length} recent tracks`
)
// First check if Last.fm reports anything as officially playing
const officialNowPlaying = recentTracks.find(track => track.nowPlaying)
const officialNowPlaying = recentTracks.find((track) => track.nowPlaying)
if (officialNowPlaying) {
// Trust Last.fm's official now playing status
logger.music('debug', `✅ Last.fm official now playing: "${officialNowPlaying.name}" by ${officialNowPlaying.artist.name}`)
logger.music(
'debug',
`✅ Last.fm official now playing: "${officialNowPlaying.name}" by ${officialNowPlaying.artist.name}`
)
return albums.map(album => ({
return albums.map((album) => ({
...album,
isNowPlaying:
album.name === officialNowPlaying.album.name &&
@ -74,14 +83,17 @@ export class SimpleNowPlayingDetector {
if (!mostRecentTrack) {
// No recent tracks, nothing is playing
logger.music('debug', '❌ No recent tracks found, nothing is playing')
return albums.map(album => ({
return albums.map((album) => ({
...album,
isNowPlaying: false,
nowPlayingTrack: undefined
}))
}
logger.music('debug', `Most recent track: "${mostRecentTrack.name}" by ${mostRecentTrack.artist.name} from ${mostRecentTrack.album.name}`)
logger.music(
'debug',
`Most recent track: "${mostRecentTrack.name}" by ${mostRecentTrack.artist.name} from ${mostRecentTrack.album.name}`
)
logger.music('debug', `Scrobbled at: ${mostRecentTrack.date}`)
// Check if the most recent track is still playing
@ -112,28 +124,39 @@ export class SimpleNowPlayingDetector {
logger.music('debug', `⚠️ No duration found for track "${mostRecentTrack.name}"`)
// Fallback: assume track is playing if scrobbled within last 5 minutes
const timeSinceScrobble = Date.now() - mostRecentTrack.date.getTime()
if (timeSinceScrobble < 5 * 60 * 1000) { // 5 minutes
if (timeSinceScrobble < 5 * 60 * 1000) {
// 5 minutes
isPlaying = true
playingTrack = mostRecentTrack.name
logger.music('debug', `⏰ Using time-based fallback: track scrobbled ${Math.round(timeSinceScrobble/1000)}s ago, assuming still playing`)
logger.music(
'debug',
`⏰ Using time-based fallback: track scrobbled ${Math.round(timeSinceScrobble / 1000)}s ago, assuming still playing`
)
}
}
}
} catch (error) {
logger.error('Error checking track duration:', error as Error, undefined, 'music')
logger.music('debug', `❌ Failed to get Apple Music data for ${mostRecentTrack.artist.name} - ${mostRecentTrack.album.name}`)
logger.music(
'debug',
`❌ Failed to get Apple Music data for ${mostRecentTrack.artist.name} - ${mostRecentTrack.album.name}`
)
// Fallback when Apple Music lookup fails
const timeSinceScrobble = Date.now() - mostRecentTrack.date.getTime()
if (timeSinceScrobble < 5 * 60 * 1000) { // 5 minutes
if (timeSinceScrobble < 5 * 60 * 1000) {
// 5 minutes
isPlaying = true
playingTrack = mostRecentTrack.name
logger.music('debug', `⏰ Using time-based fallback after Apple Music error: track scrobbled ${Math.round(timeSinceScrobble/1000)}s ago`)
logger.music(
'debug',
`⏰ Using time-based fallback after Apple Music error: track scrobbled ${Math.round(timeSinceScrobble / 1000)}s ago`
)
}
}
// Update albums with the result
return albums.map(album => {
return albums.map((album) => {
const key = `${album.artist.name}:${album.name}`
const isThisAlbumPlaying = isPlaying && key === albumKey
return {

View file

@ -1,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/stores'
</script>
<div class="error-container">

View file

@ -1,6 +1,10 @@
import { fail, redirect } from '@sveltejs/kit'
import type { Actions, PageServerLoad } from './$types'
import { clearSessionCookie, setSessionCookie, validateAdminPassword } from '$lib/server/admin/session'
import {
clearSessionCookie,
setSessionCookie,
validateAdminPassword
} from '$lib/server/admin/session'
export const load = (async ({ cookies }) => {
// Ensure we start with a clean session when hitting the login page

View file

@ -454,11 +454,7 @@
>
Manage Albums
</button>
<button
onclick={handleBulkDelete}
class="btn btn-danger btn-small"
disabled={isDeleting}
>
<button onclick={handleBulkDelete} class="btn btn-danger btn-small" disabled={isDeleting}>
{isDeleting
? 'Deleting...'
: `Delete ${selectedMediaIds.size} file${selectedMediaIds.size > 1 ? 's' : ''}`}
@ -533,9 +529,7 @@
Alt
</span>
{:else}
<span class="indicator-pill no-alt-text" title="No description">
No Alt
</span>
<span class="indicator-pill no-alt-text" title="No description"> No Alt </span>
{/if}
</div>
<span class="filesize">{formatFileSize(item.size)}</span>

View file

@ -49,7 +49,9 @@
let showCleanupModal = $state(false)
let cleaningUp = $state(false)
const allSelected = $derived(auditData && selectedFiles.size >= Math.min(20, auditData.orphanedFiles.length))
const allSelected = $derived(
auditData && selectedFiles.size >= Math.min(20, auditData.orphanedFiles.length)
)
const hasSelection = $derived(selectedFiles.size > 0)
const selectedSize = $derived(
auditData?.orphanedFiles

View file

@ -37,8 +37,8 @@
function addFiles(newFiles: File[]) {
// Filter for supported file types (images and videos)
const supportedFiles = newFiles.filter((file) =>
file.type.startsWith('image/') || file.type.startsWith('video/')
const supportedFiles = newFiles.filter(
(file) => file.type.startsWith('image/') || file.type.startsWith('video/')
)
if (supportedFiles.length !== newFiles.length) {
@ -305,7 +305,9 @@
</div>
<h3>Drop media files here</h3>
<p>or click to browse and select files</p>
<p class="upload-hint">Images: JPG, PNG, GIF, WebP, SVG | Videos: WebM, MP4, OGG, MOV, AVI</p>
<p class="upload-hint">
Images: JPG, PNG, GIF, WebP, SVG | Videos: WebM, MP4, OGG, MOV, AVI
</p>
{:else}
<div class="compact-content">
<svg

View file

@ -120,9 +120,7 @@ const statusFilterOptions = [
<AdminPage>
<AdminHeader title="Universe" slot="header">
{#snippet actions()}
<Button variant="primary" buttonSize="medium" onclick={handleNewEssay}>
New Essay
</Button>
<Button variant="primary" buttonSize="medium" onclick={handleNewEssay}>New Essay</Button>
{/snippet}
</AdminHeader>

View file

@ -39,7 +39,9 @@ const draftKey = $derived(makeDraftKey('post', $page.params.id))
let showDraftPrompt = $state(false)
let draftTimestamp = $state<number | null>(null)
let timeTicker = $state(0)
const draftTimeText = $derived.by(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
const draftTimeText = $derived.by(() =>
draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null
)
const postTypeConfig = {
post: { icon: '💭', label: 'Post', showTitle: false, showContent: true },
@ -353,7 +355,13 @@ onMount(async () => {
// Trigger autosave when form data changes
$effect(() => {
// Establish dependencies
title; slug; status; content; tags; excerpt; postType
title
slug
status
content
tags
excerpt
postType
if (hasLoaded) {
autoSave.schedule()
}
@ -521,7 +529,8 @@ $effect(() => {
<div class="draft-banner">
<div class="draft-banner-content">
<span class="draft-banner-text">
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
Unsaved draft found{#if draftTimeText}
(saved {draftTimeText}){/if}.
</span>
<div class="draft-banner-actions">
<button class="draft-banner-button" onclick={restoreDraft}>Restore</button>

View file

@ -114,11 +114,7 @@
<AdminPage>
<AdminHeader title="Work" slot="header">
{#snippet actions()}
<Button
variant="primary"
buttonSize="medium"
onclick={() => goto('/admin/projects/new')}
>
<Button variant="primary" buttonSize="medium" onclick={() => goto('/admin/projects/new')}>
New project
</Button>
{/snippet}

View file

@ -23,11 +23,14 @@ export const POST: RequestHandler = async ({ request }) => {
})
} catch (error) {
console.error('Apple Music search error:', error)
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error'
}), {
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
)
}
}

View file

@ -36,13 +36,16 @@ export const POST: RequestHandler = async ({ request }) => {
deleted = await redis.del(key)
}
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
success: true,
deleted,
key: key || pattern
}), {
}),
{
headers: { 'Content-Type': 'application/json' }
})
}
)
} catch (error) {
logger.error('Failed to clear cache:', error as Error)
return new Response('Internal server error', { status: 500 })

View file

@ -27,13 +27,16 @@ export const GET: RequestHandler = async ({ url }) => {
})
)
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
total: keys.length,
showing: keysWithValues.length,
keys: keysWithValues
}), {
}),
{
headers: { 'Content-Type': 'application/json' }
})
}
)
} catch (error) {
console.error('Failed to get Redis keys:', error)
return new Response('Internal server error', { status: 500 })

View file

@ -14,20 +14,26 @@ export const GET: RequestHandler = async ({ url }) => {
try {
const result = await findAlbum(artist, album)
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
artist,
album,
found: !!result,
result
}), {
}),
{
headers: { 'Content-Type': 'application/json' }
})
}
)
} catch (error) {
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error'
}), {
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
)
}
}

View file

@ -20,36 +20,45 @@ export const GET: RequestHandler = async () => {
const jpSongs = jpResults.results?.songs?.data || []
const usSongs = usResults.results?.songs?.data || []
const hachiko = [...jpSongs, ...usSongs].find(s =>
const hachiko = [...jpSongs, ...usSongs].find(
(s) =>
s.attributes?.name?.toLowerCase() === 'hachikō' &&
s.attributes?.artistName?.includes('藤井')
)
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
searchQuery,
jpSongsFound: jpSongs.length,
usSongsFound: usSongs.length,
hachikoFound: !!hachiko,
hachikoDetails: hachiko ? {
hachikoDetails: hachiko
? {
name: hachiko.attributes?.name,
artist: hachiko.attributes?.artistName,
album: hachiko.attributes?.albumName,
preview: hachiko.attributes?.previews?.[0]?.url
} : null,
allSongs: [...jpSongs, ...usSongs].map(s => ({
}
: null,
allSongs: [...jpSongs, ...usSongs].map((s) => ({
name: s.attributes?.name,
artist: s.attributes?.artistName,
album: s.attributes?.albumName
}))
}), {
}),
{
headers: { 'Content-Type': 'application/json' }
})
}
)
} catch (error) {
return new Response(JSON.stringify({
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error'
}), {
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
)
}
}

View file

@ -265,7 +265,12 @@ async function searchAppleMusicForAlbum(album: Album): Promise<Album> {
searchMetadata,
error: true
}
await redis.set(`apple:album:${album.artist.name}:${album.name}`, JSON.stringify(errorData), 'EX', 1800)
await redis.set(
`apple:album:${album.artist.name}:${album.name}`,
JSON.stringify(errorData),
'EX',
1800
)
}
// Return album with search metadata if Apple Music search fails

View file

@ -53,7 +53,7 @@ export const GET: RequestHandler = async ({ request }) => {
let remainingMs = 0
if (nowPlayingAlbum?.nowPlayingTrack && nowPlayingAlbum.appleMusicData?.tracks) {
const track = nowPlayingAlbum.appleMusicData.tracks.find(
t => t.name === nowPlayingAlbum.nowPlayingTrack
(t) => t.name === nowPlayingAlbum.nowPlayingTrack
)
if (track?.durationMs && nowPlayingAlbum.lastScrobbleTime) {
@ -68,7 +68,7 @@ export const GET: RequestHandler = async ({ request }) => {
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
: 'none',
remainingMs: remainingMs,
albumsWithStatus: update.albums.map(a => ({
albumsWithStatus: update.albums.map((a) => ({
name: a.name,
artist: a.artist.name,
isNowPlaying: a.isNowPlaying,
@ -100,7 +100,10 @@ export const GET: RequestHandler = async ({ request }) => {
// Apply new interval if it changed significantly (more than 1 second difference)
if (Math.abs(targetInterval - currentInterval) > 1000) {
currentInterval = targetInterval
logger.music('debug', `Adjusting interval to ${currentInterval}ms (playing: ${isPlaying}, remaining: ${Math.round(remainingMs/1000)}s)`)
logger.music(
'debug',
`Adjusting interval to ${currentInterval}ms (playing: ${isPlaying}, remaining: ${Math.round(remainingMs / 1000)}s)`
)
// Reset interval with new timing
if (intervalId) {

View file

@ -139,7 +139,10 @@ export const POST: RequestHandler = async (event) => {
const allowedTypes = [...allowedImageTypes, ...allowedVideoTypes]
if (!allowedTypes.includes(file.type)) {
return errorResponse('Invalid file type. Allowed types: Images (JPEG, PNG, WebP, GIF, SVG) and Videos (WebM, MP4, OGG, MOV, AVI)', 400)
return errorResponse(
'Invalid file type. Allowed types: Images (JPEG, PNG, WebP, GIF, SVG) and Videos (WebM, MP4, OGG, MOV, AVI)',
400
)
}
// Validate file size - different limits for images and videos

View file

@ -191,7 +191,8 @@ export const PATCH: RequestHandler = async (event) => {
if (data.content !== undefined) updateData.content = data.content
if (data.featuredImage !== undefined) updateData.featuredImage = data.featuredImage
if (data.attachedPhotos !== undefined)
updateData.attachments = data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null
updateData.attachments =
data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null
if (data.tags !== undefined) updateData.tags = data.tags
if (data.publishedAt !== undefined) updateData.publishedAt = data.publishedAt

View file

@ -320,7 +320,10 @@
if (isMobile) {
const viewport = document.querySelector('meta[name="viewport"]')
if (viewport) {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes')
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes'
)
}
}

View file

@ -89,7 +89,8 @@ export const GET: RequestHandler = async (event) => {
section: 'universe',
id: post.id.toString(),
title:
post.title || new Date(post.publishedAt || post.createdAt).toLocaleDateString('en-US', {
post.title ||
new Date(post.publishedAt || post.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
@ -177,7 +178,7 @@ ${
item.type === 'album' && item.coverPhoto
? `
<enclosure url="${item.coverPhoto.url.startsWith('http') ? item.coverPhoto.url : event.url.origin + item.coverPhoto.url}" type="image/jpeg" length="${item.coverPhoto.size || 0}"/>
<media:thumbnail url="${(item.coverPhoto.thumbnailUrl || item.coverPhoto.url).startsWith('http') ? (item.coverPhoto.thumbnailUrl || item.coverPhoto.url) : event.url.origin + (item.coverPhoto.thumbnailUrl || item.coverPhoto.url)}"/>
<media:thumbnail url="${(item.coverPhoto.thumbnailUrl || item.coverPhoto.url).startsWith('http') ? item.coverPhoto.thumbnailUrl || item.coverPhoto.url : event.url.origin + (item.coverPhoto.thumbnailUrl || item.coverPhoto.url)}"/>
<media:content url="${item.coverPhoto.url.startsWith('http') ? item.coverPhoto.url : event.url.origin + item.coverPhoto.url}" type="image/jpeg"/>`
: item.type === 'post' && item.featuredImage
? `
@ -215,9 +216,9 @@ ${item.location ? `<category domain="location">${escapeXML(item.location)}</cate
'Content-Type': 'application/rss+xml; charset=utf-8',
'Cache-Control': 'public, max-age=300, s-maxage=600, stale-while-revalidate=86400',
'Last-Modified': lastBuildDate,
'ETag': etag,
ETag: etag,
'X-Content-Type-Options': 'nosniff',
'Vary': 'Accept-Encoding',
Vary: 'Accept-Encoding',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Max-Age': '86400'