diff --git a/src/components/ContentPreview/MusicTrackPreview.tsx b/src/components/ContentPreview/MusicTrackPreview.tsx new file mode 100644 index 0000000..dba954f --- /dev/null +++ b/src/components/ContentPreview/MusicTrackPreview.tsx @@ -0,0 +1,24 @@ + +import { useTranslation } from 'react-i18next' +import { Event } from 'nostr-tools' + +export default function MusicTrackPreview({ event }: { event: Event }) { + const { t } = useTranslation() + + const title = event.tags.find(tag => tag[0] === 'title')?.[1] + const artist = event.tags.find(tag => tag[0] === 'artist')?.[1] + + return ( +
+ 🎵 + + {title || t('music.untitled')} + + {artist && ( + + {t('music.by')} {artist} + + )} +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index bc0f402..fbf1cfa 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -18,6 +18,7 @@ import PictureNotePreview from './PictureNotePreview' import PollPreview from './PollPreview' import ReactionPreview from './ReactionPreview' import VideoNotePreview from './VideoNotePreview' +import MusicTrackPreview from './MusicTrackPreview' export default function ContentPreview({ event, @@ -120,6 +121,10 @@ export default function ContentPreview({ return } + if (event.kind === ExtendedKind.MUSIC_TRACK) { + return + } + return (
[ diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index a22fa97..f745e64 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -20,6 +20,7 @@ const KIND_FILTER_OPTIONS = [ { kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' }, + { kindGroup: [ExtendedKind.MUSIC_TRACK], label: 'Music Posts' }, { kindGroup: [ ExtendedKind.VIDEO, diff --git a/src/components/Note/MusicTrackNote/index.tsx b/src/components/Note/MusicTrackNote/index.tsx new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/src/components/Note/MusicTrackNote/index.tsx @@ -0,0 +1,173 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import dayjs from 'dayjs' +import { Event } from 'nostr-tools' +import AudioPlayer from '@/components/AudioPlayer' + +interface MusicTrackNoteProps { + event: Event + className?: string +} + +export default function MusicTrackNote({ event, className }: MusicTrackNoteProps) { + const { t } = useTranslation() + + const metadata = useMemo(() => { + const getTagValue = (tagName: string) => { + const tag = event.tags.find(tag => tag[0] === tagName) + return tag?.[1] || null + } + + const getTagValues = (tagName: string) => { + return event.tags + .filter(tag => tag[0] === tagName) + .map(tag => tag[1]) + .filter(Boolean) + } + + + let lyrics = null + let credits = null + + if (event.content) { + const creditsMatch = event.content.match(/Credits:\s*\n([\s\S]*)/i) + if (creditsMatch) { + credits = creditsMatch[1].trim() + + const lyricsMatch = event.content.match(/^([\s\S]*?)Credits:/i) + lyrics = lyricsMatch ? lyricsMatch[1].trim() : null + } else { + + lyrics = event.content + } + } + + return { + title: getTagValue('title') || t('music.untitled'), + url: getTagValue('url'), + image: getTagValue('image'), + license: getTagValue('license'), + alt: getTagValue('alt'), + releaseDate: getTagValue('released'), + artist: getTagValue('artist'), + album: getTagValue('album'), + trackNumber: getTagValue('track_number'), + duration: getTagValue('duration'), + genres: getTagValues('t').filter(tag => + !['music', 'electronic', 'lofi pop'].includes(tag) + ), + lyrics, + credits + } + }, [event, t]) + + if (!metadata.url) { + return ( +
+ {t('music.noAudioUrl')} +
+ ) + } + + return ( +
+ {/* Main track container */} +
+ {/* Cover Art */} + {metadata.image && ( +
+ {metadata.alt +
+ )} + + {/* Track Info */} +
+

{metadata.title}

+ + {metadata.artist && ( +

+ {t('music.by')} {metadata.artist} +

+ )} + + {metadata.album && ( +

+ {t('music.album')}: {metadata.album} + {metadata.trackNumber && ` • ${t('music.track')} ${metadata.trackNumber}`} +

+ )} + + {metadata.releaseDate && ( +

+ {t('music.released')}: {dayjs(metadata.releaseDate).format('MMMM D, YYYY')} +

+ )} + + {/* Audio Player */} +
+ +
+ + {/* Show lyrics if they exist */} + {metadata.lyrics && ( +
+

+ {metadata.lyrics} +

+
+ )} + + {/* Show credits if they exist */} + {metadata.credits && ( +
+

+ {t('music.credits')}:
+ {metadata.credits} +

+
+ )} + + + +
+
+ + + {/* License and Metadata Footer */} +
+ {metadata.license && ( +

+ {t('music.license')}: {metadata.license} +

+ )} + + {/* Only show alt if it's different from what we've shown + {metadata.alt && !metadata.credits && ( +

+ {t('music.altText')}: {metadata.alt} +

+ )} + */} + {metadata.genres.length > 0 && ( +
+ {metadata.genres.map((genre, index) => ( + + {genre} + + ))} +
+ )} +
+
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 0d53b7a..5ba064f 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -38,6 +38,7 @@ import Reaction from './Reaction' import RelayReview from './RelayReview' import UnknownNote from './UnknownNote' import VideoNote from './VideoNote' +import MusicTrackNote from './MusicTrackNote' export default function Note({ event, @@ -124,6 +125,8 @@ export default function Note({ event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO ) { content = + } else if (event.kind === ExtendedKind.MUSIC_TRACK) { + content = } else if (event.kind === ExtendedKind.RELAY_REVIEW) { content = } else if (event.kind === kinds.Emojisets) { diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6e3c18f..8703149 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -686,6 +686,18 @@ export default { 'Allow insecure connections description': 'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.', 'reacted to': 'reacted to', - Reaction: 'Reaction' + Reaction: 'Reaction', + music: { + untitled: 'Untitled Track', + by: 'by', + album: 'Album', + track: 'Track', + released: 'Released', + license: 'License', + altText: 'Credit', + credits: 'Credits', // Add this + showLyrics: 'Show Lyrics', // Add this (optional) + noAudioUrl: 'No audio URL provided for this track', + } } }