Bandcamp embed (#9445)

This commit is contained in:
Samuel Newman
2026-02-13 13:30:10 +00:00
committed by GitHub
parent 795428971d
commit c83dddce57
4 changed files with 67 additions and 14 deletions
+24
View File
@@ -437,6 +437,13 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://www.flickr.com/groups/898944@N23/', 'https://www.flickr.com/groups/898944@N23/',
'https://www.flickr.com/groups', 'https://www.flickr.com/groups',
'https://maxblansjaar.bandcamp.com/album/false-comforts',
'https://grmnygrmny.bandcamp.com/track/fluid',
'https://sufjanstevens.bandcamp.com/',
'https://sufjanstevens.bandcamp.com',
'https://bandcamp.com/',
'https://bandcamp.com',
] ]
const outputs = [ const outputs = [
@@ -815,6 +822,23 @@ describe('parseEmbedPlayerFromUrl', () => {
undefined, undefined,
undefined, undefined,
{
type: 'bandcamp_album',
source: 'bandcamp',
playerUri:
'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fmaxblansjaar.bandcamp.com%2Falbum%2Ffalse-comforts/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
},
{
type: 'bandcamp_track',
source: 'bandcamp',
playerUri:
'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fgrmnygrmny.bandcamp.com%2Ftrack%2Ffluid/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
},
undefined,
undefined,
undefined,
undefined,
] ]
it('correctly grabs the correct id from uri', () => { it('correctly grabs the correct id from uri', () => {
+9 -14
View File
@@ -9,7 +9,8 @@ import {
externalEmbedLabels, externalEmbedLabels,
} from '#/lib/strings/embed-player' } from '#/lib/strings/embed-player'
import {useSetExternalEmbedPref} from '#/state/preferences' import {useSetExternalEmbedPref} from '#/state/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -24,9 +25,7 @@ export function EmbedConsentDialog({
onAccept: () => void onAccept: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const setExternalEmbedPref = useSetExternalEmbedPref() const setExternalEmbedPref = useSetExternalEmbedPref()
const {gtMobile} = useBreakpoints()
const onShowAllPress = useCallback(() => { const onShowAllPress = useCallback(() => {
for (const key of embedPlayerSources) { for (const key of embedPlayerSources) {
@@ -52,50 +51,46 @@ export function EmbedConsentDialog({
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`External Media`)} label={_(msg`External Media`)}
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> style={web({maxWidth: 400})}>
<View style={a.gap_sm}> <View style={a.gap_sm}>
<Text style={[a.text_2xl, a.font_semi_bold]}> <Text style={[a.text_2xl, a.font_bold]}>
<Trans>External Media</Trans> <Trans>External Media</Trans>
</Text> </Text>
<View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}> <View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}>
<Text> <Text style={[a.text_md, a.leading_snug]}>
<Trans> <Trans>
This content is hosted by {externalEmbedLabels[source]}. Do you This content is hosted by {externalEmbedLabels[source]}. Do you
want to enable external media? want to enable external media?
</Trans> </Trans>
</Text> </Text>
<Text style={t.atoms.text_contrast_medium}> <Admonition type="info">
<Trans> <Trans>
External media may allow websites to collect information about External media may allow websites to collect information about
you and your device. No information is sent or requested until you and your device. No information is sent or requested until
you press the "play" button. you press the "play" button.
</Trans> </Trans>
</Text> </Admonition>
</View> </View>
</View> </View>
<View style={a.gap_md}> <View style={a.gap_md}>
<Button <Button
style={gtMobile && a.flex_1}
label={_(msg`Enable external media`)} label={_(msg`Enable external media`)}
onPress={onShowAllPress} onPress={onShowAllPress}
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
color="primary" color="primary"
size="large" size="large">
variant="solid">
<ButtonText> <ButtonText>
<Trans>Enable external media</Trans> <Trans>Enable external media</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
<Button <Button
style={gtMobile && a.flex_1}
label={_(msg`Enable this source only`)} label={_(msg`Enable this source only`)}
onPress={onShowPress} onPress={onShowPress}
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
color="secondary" color="secondary"
size="large" size="large">
variant="solid">
<ButtonText> <ButtonText>
<Trans>Enable {externalEmbedLabels[source]} only</Trans> <Trans>Enable {externalEmbedLabels[source]} only</Trans>
</ButtonText> </ButtonText>
+33
View File
@@ -24,6 +24,7 @@ export const embedPlayerSources = [
'giphy', 'giphy',
'tenor', 'tenor',
'flickr', 'flickr',
'bandcamp',
] as const ] as const
export type EmbedPlayerSource = (typeof embedPlayerSources)[number] export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
@@ -44,6 +45,8 @@ export type EmbedPlayerType =
| 'giphy_gif' | 'giphy_gif'
| 'tenor_gif' | 'tenor_gif'
| 'flickr_album' | 'flickr_album'
| 'bandcamp_album'
| 'bandcamp_track'
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = { export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
youtube: 'YouTube', youtube: 'YouTube',
@@ -56,6 +59,7 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
appleMusic: 'Apple Music', appleMusic: 'Apple Music',
soundcloud: 'SoundCloud', soundcloud: 'SoundCloud',
flickr: 'Flickr', flickr: 'Flickr',
bandcamp: 'Bandcamp',
} }
export interface EmbedPlayerParams { export interface EmbedPlayerParams {
@@ -459,6 +463,32 @@ export function parseEmbedPlayerFromUrl(
return undefined return undefined
} }
} }
const bandcampRegex = /^[a-z\d][a-z\d-]{2,}[a-z\d]\.bandcamp\.com$/i
if (bandcampRegex.test(urlp.hostname)) {
const pathComponents = urlp.pathname.split('/')
switch (pathComponents[1]) {
case 'album':
return {
type: 'bandcamp_album',
source: 'bandcamp',
playerUri: `https://bandcamp.com/EmbeddedPlayer/url=${encodeURIComponent(
urlp.href,
)}/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/`,
}
case 'track':
return {
type: 'bandcamp_track',
source: 'bandcamp',
playerUri: `https://bandcamp.com/EmbeddedPlayer/url=${encodeURIComponent(
urlp.href,
)}/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/`,
}
default:
return undefined
}
}
} }
export function getPlayerAspect({ export function getPlayerAspect({
@@ -498,6 +528,9 @@ export function getPlayerAspect({
return {height: 165} return {height: 165}
case 'apple_music_song': case 'apple_music_song':
return {height: 150} return {height: 150}
case 'bandcamp_album':
case 'bandcamp_track':
return {aspectRatio: 1}
default: default:
return {aspectRatio: 16 / 9} return {aspectRatio: 16 / 9}
} }
+1
View File
@@ -106,6 +106,7 @@ const schema = z.object({
appleMusic: z.enum(externalEmbedOptions).optional(), appleMusic: z.enum(externalEmbedOptions).optional(),
soundcloud: z.enum(externalEmbedOptions).optional(), soundcloud: z.enum(externalEmbedOptions).optional(),
flickr: z.enum(externalEmbedOptions).optional(), flickr: z.enum(externalEmbedOptions).optional(),
bandcamp: z.enum(externalEmbedOptions).optional(),
}) })
.optional(), .optional(),
invites: z.object({ invites: z.object({