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 (
+
[
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 && (
+
+

+
+ )}
+
+ {/* 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',
+ }
}
}