mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat(music): add Music Track (kind 36787) and Music Playlist (kind 34139)
Adds quartz support for two new addressable Nostr event kinds, modeled after the NIP-88 poll structure, plus modern Compose renderers wired into both the feed (NoteCompose) and the thread/master view (ThreadFeedView). Quartz: - MusicTrackEvent (36787): title/artist/url and optional album, track_number, released, duration, format, bitrate, sample_rate, language, explicit, image, video. Each field is a dedicated *Tag class with parse/assemble, plus a TagArrayBuilder DSL and a typed build() factory. Auto-emits the "t music" hashtag and an NIP-31 alt description. - MusicPlaylistEvent (34139): title, image, description, ordered "a" track references to MusicTrackEvent, plus public/private/collaborative flags. - Both registered in EventFactory so LocalCache materializes them as typed events instead of generic Event. Amethyst UI: - MusicTrack.kt: square cover with overlaid play affordance, large title, artist row with note icon, meta row (album/track #/release/duration/explicit badge), embedded VideoView for audio (or video URL when present) with cover thumbnail, lyric/credit content via TranslatableRichTextViewer, and topic chips for extra t tags. - MusicPlaylist.kt: cover with track-count badge, title, count + collaborative /private chips, descriptions, and an ordered list of tracks resolved via LoadAddressableNote (clickable, falls back to "loading"/"unknown track" for missing references). Capped at 25 with a "+N more tracks" footer. - Track-count strings use <plurals> for correct CLDR pluralization. - Wired into NoteCompose `when` dispatch and ThreadFeedView header dispatch.
This commit is contained in:
@@ -148,6 +148,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
||||
@@ -215,6 +217,8 @@ import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
|
||||
@@ -918,6 +922,14 @@ private fun RenderNoteRow(
|
||||
RenderAudioHeader(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is MusicTrackEvent -> {
|
||||
RenderMusicTrack(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is MusicPlaylistEvent -> {
|
||||
RenderMusicPlaylist(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is DraftWrapEvent -> {
|
||||
RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
|
||||
private val PLAYLIST_COVER_ASPECT_RATIO = 1f
|
||||
private const val MAX_PREVIEW_TRACKS = 25
|
||||
|
||||
@Composable
|
||||
fun RenderMusicPlaylist(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? MusicPlaylistEvent ?: return
|
||||
|
||||
MusicPlaylistHeader(
|
||||
noteEvent = noteEvent,
|
||||
note = note,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MusicPlaylistHeader(
|
||||
noteEvent: MusicPlaylistEvent,
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val shortDescription = remember(noteEvent) { noteEvent.description() }
|
||||
val longDescription = remember(noteEvent) { noteEvent.content.ifBlank { null } }
|
||||
val trackAddresses = remember(noteEvent) { noteEvent.trackAddresses() }
|
||||
val isCollaborative = remember(noteEvent) { noteEvent.isCollaborative() }
|
||||
val isPrivate = remember(noteEvent) { noteEvent.isPrivate() }
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||
MusicPlaylistCover(image, note, trackAddresses.size, accountViewModel)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.PlaylistAdd,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.padding(start = 6.dp))
|
||||
val trackCount = trackAddresses.size
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.music_playlist_track_count, trackCount, trackCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
if (isCollaborative) {
|
||||
PlaylistTag(text = stringRes(R.string.music_playlist_collaborative))
|
||||
}
|
||||
if (isPrivate) {
|
||||
PlaylistTag(text = stringRes(R.string.music_playlist_private))
|
||||
}
|
||||
}
|
||||
|
||||
shortDescription?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
longDescription?.takeIf { !makeItShort }?.let {
|
||||
val tags = remember(noteEvent) { noteEvent.tags.toImmutableListOfLists() }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (trackAddresses.isNotEmpty()) {
|
||||
Spacer(Modifier.padding(top = 4.dp))
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
trackAddresses
|
||||
.take(MAX_PREVIEW_TRACKS)
|
||||
.forEachIndexed { index, address ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f),
|
||||
)
|
||||
}
|
||||
LoadAddressableNote(address, accountViewModel) { trackNote ->
|
||||
if (trackNote != null) {
|
||||
PlaylistTrackRow(
|
||||
position = index + 1,
|
||||
trackNote = trackNote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
MissingPlaylistTrackRow(position = index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trackAddresses.size > MAX_PREVIEW_TRACKS) {
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f))
|
||||
val remaining = trackAddresses.size - MAX_PREVIEW_TRACKS
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.music_playlist_more_tracks, remaining, remaining),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MusicPlaylistCover(
|
||||
image: String?,
|
||||
note: Note,
|
||||
trackCount: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val imageShape = RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)
|
||||
val imageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(PLAYLIST_COVER_ASPECT_RATIO)
|
||||
.clip(imageShape)
|
||||
|
||||
Box(imageModifier) {
|
||||
if (image != null) {
|
||||
MyAsyncImage(
|
||||
imageUrl = image,
|
||||
contentDescription = stringRes(R.string.preview_card_image_for, image),
|
||||
contentScale = ContentScale.Crop,
|
||||
mainImageModifier = Modifier.fillMaxSize(),
|
||||
loadedImageModifier = imageModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel, imageModifier) },
|
||||
onError = { DefaultImageHeader(note, accountViewModel, imageModifier) },
|
||||
)
|
||||
} else {
|
||||
DefaultImageHeader(note, accountViewModel, imageModifier)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(12.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.Black.copy(alpha = 0.55f))
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.PlaylistAdd,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.padding(start = 6.dp))
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.music_playlist_track_count, trackCount, trackCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaylistTrackRow(
|
||||
position: Int,
|
||||
trackNote: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val trackEvent = trackNote.event as? MusicTrackEvent
|
||||
val title = trackEvent?.title() ?: stringRes(R.string.music_playlist_unknown_track)
|
||||
val artist = trackEvent?.artist()
|
||||
val duration = trackEvent?.duration()
|
||||
val cover = trackEvent?.image()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { nav.nav(Route.Note(trackNote.idHex)) }
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = position.toString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier =
|
||||
Modifier
|
||||
.width(28.dp)
|
||||
.padding(end = 4.dp),
|
||||
)
|
||||
|
||||
if (cover != null) {
|
||||
MyAsyncImage(
|
||||
imageUrl = cover,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
mainImageModifier = Modifier.size(40.dp).clip(RoundedCornerShape(6.dp)),
|
||||
loadedImageModifier = Modifier.size(40.dp).clip(RoundedCornerShape(6.dp)),
|
||||
accountViewModel = accountViewModel,
|
||||
onLoadingBackground = {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)),
|
||||
)
|
||||
},
|
||||
onError = { TrackCoverPlaceholder() },
|
||||
)
|
||||
} else {
|
||||
TrackCoverPlaceholder()
|
||||
}
|
||||
|
||||
Spacer(Modifier.padding(start = 10.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
artist?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
duration?.let {
|
||||
Spacer(Modifier.padding(start = 6.dp))
|
||||
Text(
|
||||
text = formatTrackDuration(it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MissingPlaylistTrackRow(position: Int) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = position.toString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier =
|
||||
Modifier
|
||||
.width(28.dp)
|
||||
.padding(end = 4.dp),
|
||||
)
|
||||
TrackCoverPlaceholder()
|
||||
Spacer(Modifier.padding(start = 10.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.music_playlist_loading_track),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackCoverPlaceholder() {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaylistTag(text: String) {
|
||||
Spacer(Modifier.padding(start = 8.dp))
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f))
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTrackDuration(seconds: Int): String {
|
||||
val minutes = seconds / 60
|
||||
val secs = seconds % 60
|
||||
return "%d:%02d".format(minutes, secs)
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private val COVER_ASPECT_RATIO = 1f
|
||||
|
||||
@Composable
|
||||
fun RenderMusicTrack(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? MusicTrackEvent ?: return
|
||||
|
||||
MusicTrackHeader(
|
||||
noteEvent = noteEvent,
|
||||
note = note,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun MusicTrackHeader(
|
||||
noteEvent: MusicTrackEvent,
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val artist = remember(noteEvent) { noteEvent.artist() }
|
||||
val url = remember(noteEvent) { noteEvent.url() }
|
||||
val videoUrl = remember(noteEvent) { noteEvent.videoUrl() }
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val album = remember(noteEvent) { noteEvent.album() }
|
||||
val trackNumber = remember(noteEvent) { noteEvent.trackNumber() }
|
||||
val released = remember(noteEvent) { noteEvent.released() }
|
||||
val duration = remember(noteEvent) { noteEvent.duration() }
|
||||
val format = remember(noteEvent) { noteEvent.format() }
|
||||
val isExplicit = remember(noteEvent) { noteEvent.isExplicit() }
|
||||
val description = remember(noteEvent) { noteEvent.content.ifBlank { null } }
|
||||
val topics =
|
||||
remember(noteEvent) {
|
||||
noteEvent.tags
|
||||
.mapNotNull { if (it.size > 1 && it[0] == "t" && it[1] != MusicTrackEvent.GENRE_TAG) it[1] else null }
|
||||
.distinct()
|
||||
.take(4)
|
||||
.toImmutableList()
|
||||
}
|
||||
val playableUri = videoUrl ?: url
|
||||
val mimeType =
|
||||
remember(format, videoUrl) {
|
||||
when {
|
||||
videoUrl != null -> "video/${format ?: "mp4"}"
|
||||
format != null -> "audio/$format"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||
MusicTrackCover(image, note, accountViewModel)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
artist?.let {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.padding(start = 6.dp))
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (album != null || released != null || duration != null || isExplicit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
album?.let {
|
||||
val trackPart = trackNumber?.let { n -> " · #$n" } ?: ""
|
||||
MetaText(text = it + trackPart)
|
||||
}
|
||||
if (album != null && (released != null || duration != null || isExplicit)) MetaSeparator()
|
||||
released?.let { MetaText(text = it) }
|
||||
if (released != null && (duration != null || isExplicit)) MetaSeparator()
|
||||
duration?.let { MetaText(text = formatDuration(it)) }
|
||||
if (duration != null && isExplicit) MetaSeparator()
|
||||
if (isExplicit) ExplicitBadge()
|
||||
}
|
||||
}
|
||||
|
||||
if (playableUri != null) {
|
||||
Spacer(Modifier.padding(top = 4.dp))
|
||||
Box(Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))) {
|
||||
if (image != null) {
|
||||
LoadThumbAndThenVideoView(
|
||||
videoUri = playableUri,
|
||||
mimeType = mimeType,
|
||||
title = title,
|
||||
thumbUri = image,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
roundedCorner = true,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
nostrUriCallback = "nostr:${note.toNEvent()}",
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
VideoView(
|
||||
videoUri = playableUri,
|
||||
mimeType = mimeType,
|
||||
title = title,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
roundedCorner = true,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
nostrUriCallback = "nostr:${note.toNEvent()}",
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description?.takeIf { !makeItShort }?.let {
|
||||
Spacer(Modifier.padding(top = 4.dp))
|
||||
val tags = remember(noteEvent) { noteEvent.tags.toImmutableListOfLists() }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
DisplayUncitedHashtags(noteEvent, it, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (topics.isNotEmpty()) {
|
||||
Spacer(Modifier.padding(top = 4.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
topics.forEach { TopicChip(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MusicTrackCover(
|
||||
image: String?,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val imageShape = RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)
|
||||
val imageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(COVER_ASPECT_RATIO)
|
||||
.clip(imageShape)
|
||||
|
||||
Box(imageModifier) {
|
||||
if (image != null) {
|
||||
MyAsyncImage(
|
||||
imageUrl = image,
|
||||
contentDescription = stringRes(R.string.preview_card_image_for, image),
|
||||
contentScale = ContentScale.Crop,
|
||||
mainImageModifier = Modifier.fillMaxSize(),
|
||||
loadedImageModifier = imageModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel, imageModifier) },
|
||||
onError = { DefaultImageHeader(note, accountViewModel, imageModifier) },
|
||||
)
|
||||
} else {
|
||||
DefaultImageHeader(note, accountViewModel, imageModifier)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(12.dp)
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.92f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(32.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaText(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaSeparator() {
|
||||
Text(
|
||||
text = " · ",
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExplicitBadge() {
|
||||
Text(
|
||||
text = "E",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontSize = Font10SP,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(3.dp))
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f))
|
||||
.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopicChip(topic: String) {
|
||||
Text(
|
||||
text = "#$topic",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontSize = Font10SP,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f))
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatDuration(seconds: Int): String {
|
||||
val minutes = seconds / 60
|
||||
val secs = seconds % 60
|
||||
return "%d:%02d".format(minutes, secs)
|
||||
}
|
||||
+8
@@ -168,6 +168,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderOnchainZap
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
||||
@@ -228,6 +230,8 @@ import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
|
||||
@@ -638,6 +642,10 @@ private fun FullBleedNoteCompose(
|
||||
AudioTrackHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
} else if (noteEvent is AudioHeaderEvent) {
|
||||
AudioHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
} else if (noteEvent is MusicTrackEvent) {
|
||||
RenderMusicTrack(baseNote, makeItShort = false, canPreview = true, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is MusicPlaylistEvent) {
|
||||
RenderMusicPlaylist(baseNote, makeItShort = false, canPreview = true, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is CommunityPostApprovalEvent) {
|
||||
RenderPostApproval(
|
||||
baseNote,
|
||||
|
||||
@@ -1505,6 +1505,19 @@
|
||||
|
||||
<string name="long_form_reading_minutes">%1$d min read</string>
|
||||
|
||||
<plurals name="music_playlist_track_count">
|
||||
<item quantity="one">%1$d track</item>
|
||||
<item quantity="other">%1$d tracks</item>
|
||||
</plurals>
|
||||
<plurals name="music_playlist_more_tracks">
|
||||
<item quantity="one">+%1$d more track</item>
|
||||
<item quantity="other">+%1$d more tracks</item>
|
||||
</plurals>
|
||||
<string name="music_playlist_collaborative">Collaborative</string>
|
||||
<string name="music_playlist_private">Private</string>
|
||||
<string name="music_playlist_unknown_track">Unknown track</string>
|
||||
<string name="music_playlist_loading_track">Loading track…</string>
|
||||
|
||||
|
||||
<string name="loading_location">Loading location</string>
|
||||
<string name="lack_location_permissions">No Location Permissions</string>
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.CollaborativeTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.PrivateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.PublicTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Immutable
|
||||
class MusicPlaylistEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
fun isPublic() = tags.firstNotNullOfOrNull(PublicTag::parse) ?: true
|
||||
|
||||
fun isPrivate() = tags.firstNotNullOfOrNull(PrivateTag::parse) ?: false
|
||||
|
||||
fun isCollaborative() = tags.firstNotNullOfOrNull(CollaborativeTag::parse) ?: false
|
||||
|
||||
/**
|
||||
* Track references as `a` tags pointing to MusicTrackEvent (kind 36787) addressable events.
|
||||
* Order is preserved as it appears in the tag array.
|
||||
*/
|
||||
fun trackAddresses(): List<Address> =
|
||||
tags.mapNotNull { tag ->
|
||||
val address = ATag.parseAddress(tag) ?: return@mapNotNull null
|
||||
if (address.kind == MusicTrackEvent.KIND) address else null
|
||||
}
|
||||
|
||||
fun trackCount(): Int = trackAddresses().size
|
||||
|
||||
companion object {
|
||||
const val KIND = 34139
|
||||
const val ALT_DESCRIPTION_PREFIX = "Playlist"
|
||||
const val CATEGORY_TAG = "playlist"
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun build(
|
||||
title: String,
|
||||
description: String = "",
|
||||
image: String? = null,
|
||||
shortDescription: String? = null,
|
||||
tracks: List<Address> = emptyList(),
|
||||
isPrivate: Boolean = false,
|
||||
isCollaborative: Boolean = false,
|
||||
dTag: String = Uuid.random().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<MusicPlaylistEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, description, createdAt) {
|
||||
dTag(dTag)
|
||||
alt("$ALT_DESCRIPTION_PREFIX: $title")
|
||||
|
||||
title(title)
|
||||
hashtag(CATEGORY_TAG)
|
||||
|
||||
image?.let { image(it) }
|
||||
shortDescription?.let { description(it) }
|
||||
|
||||
tracks.forEach { trackAddress(it) }
|
||||
|
||||
if (isPrivate) {
|
||||
private(true)
|
||||
} else {
|
||||
public(true)
|
||||
}
|
||||
if (isCollaborative) collaborative(true)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.CollaborativeTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.PrivateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.PublicTag
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.description(description: String) = addUnique(DescriptionTag.assemble(description))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.public(public: Boolean = true) = addUnique(PublicTag.assemble(public))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.private(private: Boolean = true) = addUnique(PrivateTag.assemble(private))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.collaborative(collaborative: Boolean = true) = addUnique(CollaborativeTag.assemble(collaborative))
|
||||
|
||||
fun TagArrayBuilder<MusicPlaylistEvent>.trackAddress(address: Address) = add(ATag.assemble(address, null))
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class DescriptionTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "description"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(description: String) = arrayOf(TAG_NAME, description)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ImageTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "image"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(url: String) = arrayOf(TAG_NAME, url)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class TitleTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "title"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(title: String) = arrayOf(TAG_NAME, title)
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.playlist.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class PublicTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "public"
|
||||
const val TRUE_VALUE = "true"
|
||||
|
||||
fun parse(tag: Array<String>): Boolean? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].equals(TRUE_VALUE, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun assemble(public: Boolean = true) = arrayOf(TAG_NAME, if (public) TRUE_VALUE else "false")
|
||||
}
|
||||
}
|
||||
|
||||
class PrivateTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "private"
|
||||
const val TRUE_VALUE = "true"
|
||||
|
||||
fun parse(tag: Array<String>): Boolean? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].equals(TRUE_VALUE, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun assemble(private: Boolean = true) = arrayOf(TAG_NAME, if (private) TRUE_VALUE else "false")
|
||||
}
|
||||
}
|
||||
|
||||
class CollaborativeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "collaborative"
|
||||
const val TRUE_VALUE = "true"
|
||||
|
||||
fun parse(tag: Array<String>): Boolean? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].equals(TRUE_VALUE, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun assemble(collaborative: Boolean = true) = arrayOf(TAG_NAME, if (collaborative) TRUE_VALUE else "false")
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.AlbumTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ArtistTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.BitrateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.DurationTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ExplicitTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.FormatTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.LanguageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ReleasedTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.SampleRateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.TrackNumberTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.UrlTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.VideoUrlTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Immutable
|
||||
class MusicTrackEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun artist() = tags.firstNotNullOfOrNull(ArtistTag::parse)
|
||||
|
||||
fun url() = tags.firstNotNullOfOrNull(UrlTag::parse)
|
||||
|
||||
fun videoUrl() = tags.firstNotNullOfOrNull(VideoUrlTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun album() = tags.firstNotNullOfOrNull(AlbumTag::parse)
|
||||
|
||||
fun trackNumber() = tags.firstNotNullOfOrNull(TrackNumberTag::parse)
|
||||
|
||||
fun released() = tags.firstNotNullOfOrNull(ReleasedTag::parse)
|
||||
|
||||
fun duration() = tags.firstNotNullOfOrNull(DurationTag::parse)
|
||||
|
||||
fun format() = tags.firstNotNullOfOrNull(FormatTag::parse)
|
||||
|
||||
fun bitrate() = tags.firstNotNullOfOrNull(BitrateTag::parse)
|
||||
|
||||
fun sampleRate() = tags.firstNotNullOfOrNull(SampleRateTag::parse)
|
||||
|
||||
fun language() = tags.firstNotNullOfOrNull(LanguageTag::parse)
|
||||
|
||||
fun isExplicit() = tags.firstNotNullOfOrNull(ExplicitTag::parse) ?: false
|
||||
|
||||
companion object {
|
||||
const val KIND = 36787
|
||||
const val ALT_DESCRIPTION_PREFIX = "Music track"
|
||||
const val GENRE_TAG = "music"
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun build(
|
||||
title: String,
|
||||
artist: String,
|
||||
url: String,
|
||||
description: String = "",
|
||||
image: String? = null,
|
||||
videoUrl: String? = null,
|
||||
album: String? = null,
|
||||
trackNumber: Int? = null,
|
||||
released: String? = null,
|
||||
duration: Int? = null,
|
||||
format: String? = null,
|
||||
bitrate: String? = null,
|
||||
sampleRate: Int? = null,
|
||||
language: String? = null,
|
||||
explicit: Boolean = false,
|
||||
dTag: String = Uuid.random().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<MusicTrackEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, description, createdAt) {
|
||||
dTag(dTag)
|
||||
alt("$ALT_DESCRIPTION_PREFIX: $title by $artist")
|
||||
|
||||
title(title)
|
||||
artist(artist)
|
||||
url(url)
|
||||
hashtag(GENRE_TAG)
|
||||
|
||||
image?.let { image(it) }
|
||||
videoUrl?.let { videoUrl(it) }
|
||||
album?.let { album(it) }
|
||||
trackNumber?.let { trackNumber(it) }
|
||||
released?.let { released(it) }
|
||||
duration?.let { duration(it) }
|
||||
format?.let { format(it) }
|
||||
bitrate?.let { bitrate(it) }
|
||||
sampleRate?.let { sampleRate(it) }
|
||||
language?.let { language(it) }
|
||||
if (explicit) explicit(true)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.AlbumTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ArtistTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.BitrateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.DurationTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ExplicitTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.FormatTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.LanguageTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.ReleasedTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.SampleRateTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.TrackNumberTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.UrlTag
|
||||
import com.vitorpamplona.quartz.experimental.music.track.tags.VideoUrlTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.artist(artist: String) = addUnique(ArtistTag.assemble(artist))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.url(url: String) = addUnique(UrlTag.assemble(url))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.videoUrl(url: String) = addUnique(VideoUrlTag.assemble(url))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.album(album: String) = addUnique(AlbumTag.assemble(album))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.trackNumber(number: Int) = addUnique(TrackNumberTag.assemble(number))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.released(isoDate: String) = addUnique(ReleasedTag.assemble(isoDate))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.duration(seconds: Int) = addUnique(DurationTag.assemble(seconds))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.format(format: String) = addUnique(FormatTag.assemble(format))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.bitrate(bitrate: String) = addUnique(BitrateTag.assemble(bitrate))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.sampleRate(hz: Int) = addUnique(SampleRateTag.assemble(hz))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.language(iso639: String) = addUnique(LanguageTag.assemble(iso639))
|
||||
|
||||
fun TagArrayBuilder<MusicTrackEvent>.explicit(explicit: Boolean = true) = addUnique(ExplicitTag.assemble(explicit))
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class AlbumTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "album"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(album: String) = arrayOf(TAG_NAME, album)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ArtistTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "artist"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(artist: String) = arrayOf(TAG_NAME, artist)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class BitrateTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "bitrate"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(bitrate: String) = arrayOf(TAG_NAME, bitrate)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class DurationTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "duration"
|
||||
|
||||
fun parse(tag: Array<String>): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(seconds: Int) = arrayOf(TAG_NAME, seconds.toString())
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ExplicitTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "explicit"
|
||||
const val EXPLICIT_VALUE = "true"
|
||||
|
||||
fun parse(tag: Array<String>): Boolean? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].equals(EXPLICIT_VALUE, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun assemble(explicit: Boolean = true) = arrayOf(TAG_NAME, if (explicit) EXPLICIT_VALUE else "false")
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class FormatTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "format"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(format: String) = arrayOf(TAG_NAME, format)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ImageTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "image"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(url: String) = arrayOf(TAG_NAME, url)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class LanguageTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "language"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(iso639: String) = arrayOf(TAG_NAME, iso639)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ReleasedTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "released"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(isoDate: String) = arrayOf(TAG_NAME, isoDate)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class SampleRateTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "sample_rate"
|
||||
|
||||
fun parse(tag: Array<String>): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(hz: Int) = arrayOf(TAG_NAME, hz.toString())
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class TitleTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "title"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(title: String) = arrayOf(TAG_NAME, title)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class TrackNumberTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "track_number"
|
||||
|
||||
fun parse(tag: Array<String>): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(trackNumber: Int) = arrayOf(TAG_NAME, trackNumber.toString())
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class UrlTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "url"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(url: String) = arrayOf(TAG_NAME, url)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.music.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class VideoUrlTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "video"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(url: String) = arrayOf(TAG_NAME, url)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
|
||||
@@ -463,6 +465,8 @@ class EventFactory {
|
||||
MediaFollowListEvent.KIND -> MediaFollowListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MediaStarterPackEvent.KIND -> MediaStarterPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MusicPlaylistEvent.KIND -> MusicPlaylistEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MusicTrackEvent.KIND -> MusicTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
KeyPackageEvent.KIND -> KeyPackageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
KeyPackageRelayListEvent.KIND -> KeyPackageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user