diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 6b37d1e..1f915c5 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -46,7 +46,7 @@ import { LastFMRecentTracksCardDefinition } from './media/LastFMCard/LastFMRecen import { LastFMTopTracksCardDefinition } from './media/LastFMCard/LastFMTopTracksCard'; import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard'; import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard'; -import { PlyrFMCardDefinition } from './media/PlyrFMCard'; +import { PlyrFMCardDefinition, PlyrFMCollectionCardDefinition } from './media/PlyrFMCard'; import { MarginCardDefinition } from './social/MarginCard'; import { SembleCollectionCardDefinition } from './social/SembleCollectionCard'; // import { Model3DCardDefinition } from './visual/Model3DCard'; @@ -101,6 +101,7 @@ export const AllCardDefinitions = [ LastFMTopAlbumsCardDefinition, LastFMProfileCardDefinition, PlyrFMCardDefinition, + PlyrFMCollectionCardDefinition, MarginCardDefinition, SembleCollectionCardDefinition ] as const; diff --git a/src/lib/cards/media/PlyrFMCard/CreatePlyrFMCardModal.svelte b/src/lib/cards/media/PlyrFMCard/CreatePlyrFMCardModal.svelte index 6480b12..9216a3b 100644 --- a/src/lib/cards/media/PlyrFMCard/CreatePlyrFMCardModal.svelte +++ b/src/lib/cards/media/PlyrFMCard/CreatePlyrFMCardModal.svelte @@ -2,7 +2,7 @@ import { Alert, Button, Input, Subheading } from '@foxui/core'; import Modal from '$lib/components/modal/Modal.svelte'; import type { CreationModalComponentProps } from '../../types'; - import { toPlyrFMEmbedUrl } from './index'; + import { toPlyrFMEmbedUrl, toPlyrFMCollectionEmbedUrl } from './index'; let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); @@ -14,21 +14,27 @@ const embedUrl = toPlyrFMEmbedUrl(item.cardData.href); if (!embedUrl) { - errorMessage = 'Please enter a valid plyr.fm track URL'; + errorMessage = 'Please enter a valid plyr.fm URL (track, playlist, or album)'; return false; } item.cardData.href = embedUrl; + // resize to collection dimensions if it's a playlist or album + if (toPlyrFMCollectionEmbedUrl(item.cardData.href)) { + item.h = 5; + item.mobileH = 10; + } + return true; } - Enter a Plyr.fm track URL + Enter a Plyr.fm URL { if (e.key === 'Enter' && checkUrl()) oncreate(); }} diff --git a/src/lib/cards/media/PlyrFMCard/PlyrFMCard.svelte b/src/lib/cards/media/PlyrFMCard/PlyrFMCard.svelte index 0592853..a51ba47 100644 --- a/src/lib/cards/media/PlyrFMCard/PlyrFMCard.svelte +++ b/src/lib/cards/media/PlyrFMCard/PlyrFMCard.svelte @@ -12,7 +12,7 @@ frameborder="0" allow="autoplay; encrypted-media" loading="lazy" - title="Plyr.fm track" + title="Plyr.fm" > {:else} diff --git a/src/lib/cards/media/PlyrFMCard/index.ts b/src/lib/cards/media/PlyrFMCard/index.ts index 025a41e..6062fb8 100644 --- a/src/lib/cards/media/PlyrFMCard/index.ts +++ b/src/lib/cards/media/PlyrFMCard/index.ts @@ -2,14 +2,18 @@ import type { CardDefinition } from '../../types'; import CreatePlyrFMCardModal from './CreatePlyrFMCardModal.svelte'; import PlyrFMCard from './PlyrFMCard.svelte'; -const cardType = 'plyr-fm'; +const musicIcon = ``; + +// ── Track card (existing) ──────────────────────────────────────────── + +const trackType = 'plyr-fm'; export const PlyrFMCardDefinition = { - type: cardType, + type: trackType, contentComponent: PlyrFMCard, creationModalComponent: CreatePlyrFMCardModal, createNew: (item) => { - item.cardType = cardType; + item.cardType = trackType; item.cardData = {}; item.w = 4; item.mobileW = 8; @@ -18,7 +22,7 @@ export const PlyrFMCardDefinition = { }, onUrlHandler: (url, item) => { - const embedUrl = toPlyrFMEmbedUrl(url); + const embedUrl = toPlyrFMTrackEmbedUrl(url); if (!embedUrl) return null; item.cardData.href = embedUrl; @@ -38,15 +42,61 @@ export const PlyrFMCardDefinition = { minW: 2, minH: 2, - keywords: ['music', 'song', 'plyr', 'plyrfm', 'audio'], + keywords: ['music', 'song', 'plyr', 'plyrfm', 'audio', 'track'], groups: ['Media'], - icon: `` -} as CardDefinition & { type: typeof cardType }; + icon: musicIcon +} as CardDefinition & { type: typeof trackType }; -// Match plyr.fm track URLs (both embed and regular) -// https://plyr.fm/embed/track/56 +// ── Collection card (playlists + albums) ───────────────────────────── + +const collectionType = 'plyr-fm-collection'; + +export const PlyrFMCollectionCardDefinition = { + type: collectionType, + contentComponent: PlyrFMCard, + creationModalComponent: CreatePlyrFMCardModal, + createNew: (item) => { + item.cardType = collectionType; + item.cardData = {}; + item.w = 4; + item.mobileW = 8; + item.h = 5; + item.mobileH = 10; + }, + + onUrlHandler: (url, item) => { + const embedUrl = toPlyrFMCollectionEmbedUrl(url); + if (!embedUrl) return null; + + item.cardData.href = embedUrl; + + item.w = 4; + item.mobileW = 8; + item.h = 5; + item.mobileH = 10; + + return item; + }, + + // higher priority so collection URLs are matched before the track handler + urlHandlerPriority: 3, + + name: 'Plyr.fm Playlist / Album', + canResize: true, + minW: 2, + minH: 3, + + keywords: ['music', 'playlist', 'album', 'plyr', 'plyrfm', 'collection'], + groups: ['Media'], + icon: musicIcon +} as CardDefinition & { type: typeof collectionType }; + +// ── URL matching ───────────────────────────────────────────────────── + +// Match plyr.fm track URLs // https://plyr.fm/track/595 -export function toPlyrFMEmbedUrl(url: string | undefined): string | null { +// https://plyr.fm/embed/track/56 +export function toPlyrFMTrackEmbedUrl(url: string | undefined): string | null { if (!url) return null; const match = url.match(/plyr\.fm\/(embed\/)?track\/(\d+)/); @@ -54,3 +104,39 @@ export function toPlyrFMEmbedUrl(url: string | undefined): string | null { return `https://plyr.fm/embed/track/${match[2]}`; } + +// Match plyr.fm playlist and album URLs +// +// Playlists: +// https://plyr.fm/playlist/abc-def-123 +// https://plyr.fm/embed/playlist/abc-def-123 +// +// Albums: +// https://plyr.fm/u/handle/album/slug +// https://plyr.fm/embed/album/handle/slug +export function toPlyrFMCollectionEmbedUrl(url: string | undefined): string | null { + if (!url) return null; + + // Playlist: /playlist/{uuid} or /embed/playlist/{uuid} + const playlistMatch = url.match(/plyr\.fm\/(embed\/)?playlist\/([a-f0-9-]+)/i); + if (playlistMatch) { + return `https://plyr.fm/embed/playlist/${playlistMatch[2]}`; + } + + // Album: /u/{handle}/album/{slug} or /embed/album/{handle}/{slug} + const albumEmbedMatch = url.match(/plyr\.fm\/embed\/album\/([^/?#]+)\/([^/?#]+)/); + if (albumEmbedMatch) { + return `https://plyr.fm/embed/album/${albumEmbedMatch[1]}/${albumEmbedMatch[2]}`; + } + const albumPageMatch = url.match(/plyr\.fm\/u\/([^/?#]+)\/album\/([^/?#]+)/); + if (albumPageMatch) { + return `https://plyr.fm/embed/album/${albumPageMatch[1]}/${albumPageMatch[2]}`; + } + + return null; +} + +// Accepts any plyr.fm URL (track, playlist, or album) +export function toPlyrFMEmbedUrl(url: string | undefined): string | null { + return toPlyrFMTrackEmbedUrl(url) ?? toPlyrFMCollectionEmbedUrl(url); +}