diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 7c9b775..14e3399 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -38,6 +38,7 @@ import { CountdownCardDefinition } from './utilities/CountdownCard'; import { SpotifyCardDefinition } from './media/SpotifyCard'; import { AppleMusicCardDefinition } from './media/AppleMusicCard'; import { SoundCloudCardDefinition } from './media/SoundCloudCard'; +import { BandcampCardDefinition } from './media/BandcampCard'; import { ButtonCardDefinition } from './utilities/ButtonCard'; import { GuestbookCardDefinition } from './social/GuestbookCard'; import { FriendsCardDefinition } from './social/FriendsCard'; @@ -109,6 +110,7 @@ export const AllCardDefinitions = [ SpotifyCardDefinition, AppleMusicCardDefinition, SoundCloudCardDefinition, + BandcampCardDefinition, // Model3DCardDefinition FriendsCardDefinition, GitHubContributorsCardDefinition, diff --git a/src/lib/cards/media/BandcampCard/BandcampCard.svelte b/src/lib/cards/media/BandcampCard/BandcampCard.svelte new file mode 100644 index 0000000..ce721c2 --- /dev/null +++ b/src/lib/cards/media/BandcampCard/BandcampCard.svelte @@ -0,0 +1,28 @@ + + +{#if src} +
+ +
+{:else} +
+ Missing Bandcamp URL +
+{/if} diff --git a/src/lib/cards/media/BandcampCard/CreateBandcampCardModal.svelte b/src/lib/cards/media/BandcampCard/CreateBandcampCardModal.svelte new file mode 100644 index 0000000..7694c18 --- /dev/null +++ b/src/lib/cards/media/BandcampCard/CreateBandcampCardModal.svelte @@ -0,0 +1,49 @@ + + + + Enter a Bandcamp album or track URL + { + if (e.key === 'Enter' && checkUrl()) oncreate(); + }} + /> + + {#if errorMessage} + {errorMessage} + {/if} + +
+ + +
+
diff --git a/src/lib/cards/media/BandcampCard/index.ts b/src/lib/cards/media/BandcampCard/index.ts new file mode 100644 index 0000000..94ab49c --- /dev/null +++ b/src/lib/cards/media/BandcampCard/index.ts @@ -0,0 +1,47 @@ +import type { CardDefinition } from '../../types'; +import BandcampCard from './BandcampCard.svelte'; +import CreateBandcampCardModal from './CreateBandcampCardModal.svelte'; +import { isBandcampUrl } from './shared'; + +const cardType = 'bandcamp-embed'; + +export const BandcampCardDefinition = { + type: cardType, + contentComponent: BandcampCard, + creationModalComponent: CreateBandcampCardModal, + createNew: (item) => { + item.cardType = cardType; + item.cardData = {}; + item.w = 4; + item.mobileW = 8; + item.h = 5; + item.mobileH = 10; + }, + + onUrlHandler: (url, item) => { + if (!isBandcampUrl(url)) return null; + + item.cardData.href = url; + + item.w = 4; + item.mobileW = 8; + item.h = 5; + item.mobileH = 10; + + return item; + }, + + urlHandlerPriority: 2, + + canChange: (item) => isBandcampUrl(item.cardData?.href), + change: (item) => item, + + name: 'Bandcamp Embed', + canResize: true, + minW: 3, + minH: 4, + + keywords: ['music', 'album', 'track', 'bandcamp', 'audio', 'artist'], + groups: ['Media'], + icon: `` +} as CardDefinition & { type: typeof cardType }; diff --git a/src/lib/cards/media/BandcampCard/shared.ts b/src/lib/cards/media/BandcampCard/shared.ts new file mode 100644 index 0000000..7400223 --- /dev/null +++ b/src/lib/cards/media/BandcampCard/shared.ts @@ -0,0 +1,26 @@ +// Bandcamp's EmbeddedPlayer resolves a public album/track URL server-side via +// its `url=` param, so no scraping/ID lookup is needed (same approach as the +// first-party bluesky social-app embed player). + +// Match bandcamp.com subdomain stores only (matching bluesky's behavior). +// Custom domains are intentionally excluded to avoid hijacking unrelated +// `/album/` or `/track/` URLs. +const BANDCAMP_HOST = /^[a-z\d][a-z\d-]{2,}[a-z\d]\.bandcamp\.com$/i; + +export function isBandcampUrl(url: string | undefined): boolean { + if (!url) return false; + try { + const u = new URL(url); + if (!BANDCAMP_HOST.test(u.hostname)) return false; + const kind = u.pathname.split('/')[1]; + return kind === 'album' || kind === 'track'; + } catch { + return false; + } +} + +export function bandcampPlayerUri(url: string): string { + return `https://bandcamp.com/EmbeddedPlayer/url=${encodeURIComponent( + url + )}/size=large/bgcol=333333/linkcol=0f91ff/tracklist=true/transparent=true/`; +}