Compare commits

..

No commits in common. "main" and "tiles/weather" have entirely different histories.

12 changed files with 15 additions and 210 deletions

View file

@ -1,2 +0,0 @@
LASTFM_API_KEY=
LASTFM_USER=

View file

@ -33,9 +33,5 @@
"typescript-eslint": "^8.47.0", "typescript-eslint": "^8.47.0",
"vite": "^7.2.2", "vite": "^7.2.2",
"vite-plugin-devtools-json": "^1.0.0" "vite-plugin-devtools-json": "^1.0.0"
},
"dependencies": {
"lastfm-ts-api": "^2.2.0",
"node-cache": "^5.1.2"
} }
} }

26
pnpm-lock.yaml generated
View file

@ -7,13 +7,6 @@ settings:
importers: importers:
.: .:
dependencies:
lastfm-ts-api:
specifier: ^2.2.0
version: 2.2.0
node-cache:
specifier: ^5.1.2
version: 5.1.2
devDependencies: devDependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^1.4.0 specifier: ^1.4.0
@ -686,10 +679,6 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -928,9 +917,6 @@ packages:
known-css-properties@0.37.0: known-css-properties@0.37.0:
resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
lastfm-ts-api@2.2.0:
resolution: {integrity: sha512-RGceA84ImKtPtFKZaAE9FbTI9hchjxAwi3ZJAr5nlrfVjvSQvpoEFtamrgJY+JYY9PdJusHil8eki00ll4ILZg==}
levn@0.4.1: levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1048,10 +1034,6 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-cache@5.1.2:
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
engines: {node: '>= 8.0.0'}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1819,8 +1801,6 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
clone@2.1.2: {}
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
@ -2067,8 +2047,6 @@ snapshots:
known-css-properties@0.37.0: {} known-css-properties@0.37.0: {}
lastfm-ts-api@2.2.0: {}
levn@0.4.1: levn@0.4.1:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@ -2155,10 +2133,6 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-cache@5.1.2:
dependencies:
clone: 2.1.2
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -266,7 +266,7 @@ Metro-like tile. Must be in a group to display correctly.
grid-column-start: {column}; grid-column-start: {column};
" "
onmousemove={active ? updateDynamicMouseValues : null} onmousemove={active ? updateDynamicMouseValues : null}
href={active ? link : "#"} href="#h"
> >
<!-- Tile pages --> <!-- Tile pages -->
<div <div

View file

@ -1,85 +0,0 @@
<!--
@component
Music tile.
-->
<script lang="ts">
import type { RecentTrack } from "$lib/server/clients/last-fm";
import { onDestroy, onMount } from "svelte";
import Tile from "../common/Tile.svelte";
import TileImagePage from "../pages/TileImagePage.svelte";
import TileTextPage from "../pages/TileTextPage.svelte";
import iconLastFm from "$lib/assets/icons/last-fm.webp";
import TileIconPage from "../pages/TileIconPage.svelte";
/**
* Update interval delay (in milliseconds).
*/
const UPDATE_INTERVAL_DELAY = 1 * 60 * 1000;
/**
* Link to the Last.FM website.
*/
const LASTFM_URL = "https://last.fm";
/**
* Properties.
*/
let {
row,
column,
size,
track: initialTrack,
}: {
row: number;
column: number;
size: "small" | "medium" | "wide" | "large";
track?: RecentTrack;
} = $props();
/**
* Current track data.
*/
let track = $state(initialTrack);
/**
* Update interval.
*/
let updateInterval: NodeJS.Timeout;
/**
* Fetch the current track and put it in state.
*/
async function updateTrack() {
const response = await fetch("/api/music");
if (!response.ok) {
return; // NOTE: if non-ok status, don't update
}
track = await response.json();
}
// NOTE: update interval set/clear functions
onMount(() => {
updateInterval = setInterval(updateTrack, UPDATE_INTERVAL_DELAY);
});
onDestroy(() => clearInterval(updateInterval));
</script>
<Tile {row} {column} {size} icon={iconLastFm} link={track?.url ?? LASTFM_URL}>
{#if track}
<TileImagePage image={track.cover} />
<TileTextPage
title={track.title}
subtitle={track.artist}
text={track.album}
/>
{:else}
<TileIconPage name="Last.FM" />
{/if}
</Tile>

View file

@ -20,14 +20,14 @@ Text page for a tile. Must be in a tile to display correctly.
<div class="w-full flex flex-col" style="height: var(--tile-page-height);"> <div class="w-full flex flex-col" style="height: var(--tile-page-height);">
<div class="w-full flex flex-col gap-1 p-2 select-none text-white"> <div class="w-full flex flex-col gap-1 p-2 select-none text-white">
<h1 class="font-bold text-md truncate">{title}</h1> <h1 class="font-bold text-md">{title}</h1>
{#if subtitle} {#if subtitle}
<h2 class="text-md truncate">{subtitle}</h2> <h2 class="text-md">{subtitle}</h2>
{/if} {/if}
{#if text} {#if text}
<p class="text-sm truncate">{text}</p> <p class="text-sm">{text}</p>
{/if} {/if}
</div> </div>

View file

@ -1,64 +0,0 @@
import { LASTFM_API_KEY, LASTFM_USER } from "$env/static/private";
import { LastFMUser } from "lastfm-ts-api";
/**
* Last.fm API client.
*/
const client = new LastFMUser(LASTFM_API_KEY);
/**
* Recent track.
*/
export interface RecentTrack {
/**
* Song title.
*/
title: string;
/**
* Song artist.
*/
artist: string;
/**
* Album title.
*/
album: string;
/**
* Cover URL.
*/
cover: string;
/**
* Song URL.
*/
url: string;
}
/**
* Get the most recent track from a listening history.
* @returns Recent track.
*/
export async function getRecentTrack(): Promise<RecentTrack> {
// NOTE: fetch the recent tracks
const {
recenttracks: {
track: tracks,
},
} = await client.getRecentTracks({
user: LASTFM_USER,
});
// NOTE: get the first track and return it
const track = tracks.shift()!;
return {
title: track.name,
artist: track.artist["#text"],
album: track.album["#text"],
// NOTE: the most HQ cover is always last in an array
cover: track.image.pop()!["#text"],
url: track.url,
};
}

View file

@ -1,7 +0,0 @@
import type { PageServerLoad } from './$types';
import { getRecentTrack } from '$lib/server/clients/last-fm';
export const load = (async () => {
return { track: await getRecentTrack() };
}) satisfies PageServerLoad;

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script>
import type { PageProps } from "./$types";
import iconSynzr from "$lib/assets/icons/synzr.webp"; import iconSynzr from "$lib/assets/icons/synzr.webp";
import imageTestRectangle from "$lib/assets/images/test-rectangle.webp"; import imageTestRectangle from "$lib/assets/images/test-rectangle.webp";
@ -8,12 +6,9 @@
import Tile from "$lib/components/tiles/common/Tile.svelte"; import Tile from "$lib/components/tiles/common/Tile.svelte";
import TileGroup from "$lib/components/tiles/common/TileGroup.svelte"; import TileGroup from "$lib/components/tiles/common/TileGroup.svelte";
import MusicTile from "$lib/components/tiles/music/MusicTile.svelte";
import TileIconPage from "$lib/components/tiles/pages/TileIconPage.svelte"; import TileIconPage from "$lib/components/tiles/pages/TileIconPage.svelte";
import TileImagePage from "$lib/components/tiles/pages/TileImagePage.svelte"; import TileImagePage from "$lib/components/tiles/pages/TileImagePage.svelte";
import TileTextPage from "$lib/components/tiles/pages/TileTextPage.svelte"; import TileTextPage from "$lib/components/tiles/pages/TileTextPage.svelte";
const { data }: PageProps = $props();
</script> </script>
<TileGroup title="a lot of mikhails" rows={3} columns={3}> <TileGroup title="a lot of mikhails" rows={3} columns={3}>
@ -28,7 +23,16 @@
<TileTextPage title="Title" subtitle="Subtitle" text="Text" /> <TileTextPage title="Title" subtitle="Subtitle" text="Text" />
</Tile> </Tile>
<MusicTile row={3} column={5} size="medium" track={data.track} /> <Tile
size="medium"
row={3}
column={5}
icon={iconSynzr}
link="https://example.com"
>
<TileImagePage image={imageTestSquare} />
<TileTextPage title="Title" subtitle="Subtitle" text="Text" />
</Tile>
<Tile <Tile
size="wide" size="wide"

View file

@ -1,11 +0,0 @@
import { getRecentTrack } from '$lib/server/clients/last-fm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return Response.json(
await getRecentTrack(),
{
headers: { 'cache-control': 'maxage=60' },
},
);
};