package protocol import ( "runtime" "git.smesh.lol/nostr/pkg/lol/errorf" ) // Studio credits a company or room by activity: ["studio", "recording", "Name, // City"]. The activity is the same lowercase word the role vocabulary uses // without the -engineer suffix. const ( StudioRecording = "recording" StudioMix = "mix" StudioMastering = "mastering" ) // Studio is a studio tag: which room, for which activity. type Studio struct { Activity string // recording, mix, mastering Name string // free text: "Studio Name, City" } func (s *Studio) Tag() (t []string) { if s == nil { return nil } t = []string{:3} t[0] = TagStudio t[1] = s.Activity t[2] = s.Name return } // ReleaseCredits is the payload of a kind 32214 event: the full participant // list for a release, each with a role, plus the studios it was made in. // // Credits are wiki-style and validated socially (confirmations, attestations, // contributions), so nothing here is authoritative on its own; this is the // authored claim. type ReleaseCredits struct { D string Album *Address // 32211 album address Title string Participants []Participant Studios []Studio Content string Extra [][]string } func (c *ReleaseCredits) Tags() (tags [][]string) { tags = appendKV(nil, TagD, c.D) tags = appendTag(tags, c.Album.Tag()) tags = appendKV(tags, TagTitle, c.Title) for i := range c.Participants { tags = appendTag(tags, c.Participants[i].Tag()) } for i := range c.Studios { tags = appendTag(tags, c.Studios[i].Tag()) } tags = appendTags(tags, c.Extra) return } func ParseReleaseCredits(tags [][]string, content string) (c *ReleaseCredits, err error) { c = &ReleaseCredits{Content: content} for i := range tags { tg := tags[i] if len(tg) < 1 { continue } switch tg[0] { case TagD: c.D = elem(tg, 1) case TagA: a, aerr := AddressFromTag(tg) if aerr != nil { err = aerr return } c.Album = a case TagTitle: c.Title = elem(tg, 1) case TagP: p, perr := parseParticipant(tg) if perr != nil { err = perr return } c.Participants = push(c.Participants, p) case TagStudio: c.Studios = push(c.Studios, Studio{Activity: elem(tg, 1), Name: elem(tg, 2)}) default: c.Extra = appendTag(c.Extra, tg) } } return } func (c *ReleaseCredits) Validate() (err error) { if c == nil { err = errorf.E("nil release credits") return } if c.D == "" { err = errorf.E("release credits: empty d tag") return } if c.Album != nil && c.Album.Kind != KindAlbum { err = errorf.E("release credits: target is kind %d, want %d", c.Album.Kind, KindAlbum) return } for i := range c.Participants { if !IsHex64(c.Participants[i].Pubkey) { err = errorf.E("release credits: participant %d pubkey is not 64 hex characters", i) return } if c.Participants[i].Role == "" { err = errorf.E("release credits: participant %d has no role", i) return } } for i := range c.Studios { if c.Studios[i].Activity == "" || c.Studios[i].Name == "" { err = errorf.E("release credits: studio %d is incomplete", i) return } } return } func (c *ReleaseCredits) Warnings() (w []string) { if c.Album == nil { w = push(w, "no album address") } if len(c.Participants) == 0 { w = push(w, "no credited participants") } return } // TrackCredits is the payload of a kind 32215 event: per-track credits that // inherit from the release and override only the roles they state. type TrackCredits struct { D string Track *Address // 32210 track address Release *Address // 32214 release credits address, the inheritance link Participants []Participant Content string Extra [][]string } func (c *TrackCredits) Tags() (tags [][]string) { tags = appendKV(nil, TagD, c.D) tags = appendTag(tags, c.Track.Tag()) tags = appendTag(tags, c.Release.Tag()) for i := range c.Participants { tags = appendTag(tags, c.Participants[i].Tag()) } tags = appendTags(tags, c.Extra) return } func ParseTrackCredits(tags [][]string, content string) (c *TrackCredits, err error) { c = &TrackCredits{Content: content} for i := range tags { tg := tags[i] if len(tg) < 1 { continue } switch tg[0] { case TagD: c.D = elem(tg, 1) case TagA: a, aerr := AddressFromTag(tg) if aerr != nil { err = aerr return } // The two a tags are told apart by kind: track first, release // credits second, as the spec writes them. if a.Kind == KindTrack && c.Track == nil { c.Track = a } else if a.Kind == KindReleaseCredits && c.Release == nil { c.Release = a } else if c.Track == nil { c.Track = a } else { c.Release = a } case TagP: p, perr := parseParticipant(tg) if perr != nil { err = perr return } c.Participants = push(c.Participants, p) default: c.Extra = appendTag(c.Extra, tg) } } return } func (c *TrackCredits) Validate() (err error) { if c == nil { err = errorf.E("nil track credits") return } if c.D == "" { err = errorf.E("track credits: empty d tag") return } if c.Track == nil { err = errorf.E("track credits: no track address") return } if c.Track.Kind != KindTrack { err = errorf.E("track credits: target is kind %d, want %d", c.Track.Kind, KindTrack) return } if c.Release != nil && c.Release.Kind != KindReleaseCredits { err = errorf.E("track credits: release reference is kind %d, want %d", c.Release.Kind, KindReleaseCredits) return } for i := range c.Participants { if !IsHex64(c.Participants[i].Pubkey) { err = errorf.E("track credits: participant %d pubkey is not 64 hex characters", i) return } if c.Participants[i].Role == "" { err = errorf.E("track credits: participant %d has no role", i) return } } return } // Edition is the payload of a kind 32216 event: one format of a release. Each // pressing, replication or duplication is its own event linked to the album, // and edition credits override the release's where they differ. type Edition struct { D string Album *Address Title string Format string // format family: vinyl, CD, cassette, ... FormatDetail []string // packed into one tag: 180g, black, gatefold Catalog string Barcode string External []ExternalID Quantity string ReleaseDate string Territory string Participants []Participant Hashtags []string Content string Extra [][]string } func (e *Edition) Tags() (tags [][]string) { tags = appendKV(nil, TagD, e.D) tags = appendTag(tags, e.Album.Tag()) tags = appendKV(tags, TagTitle, e.Title) tags = appendKV(tags, TagFormat, e.Format) if len(e.FormatDetail) > 0 { t := []string{:len(e.FormatDetail) + 1} t[0] = TagFormatDetail for i := range e.FormatDetail { t[i+1] = e.FormatDetail[i] } tags = appendTag(tags, t) } tags = appendKV(tags, TagCatalog, e.Catalog) tags = appendKV(tags, TagBarcode, e.Barcode) for i := range e.External { tags = appendKV(tags, TagI, e.External[i].I) tags = appendKV(tags, TagK, e.External[i].K) } tags = appendKV(tags, TagQuantity, e.Quantity) tags = appendKV(tags, TagReleaseDate, e.ReleaseDate) tags = appendKV(tags, TagTerritory, e.Territory) for i := range e.Participants { tags = appendTag(tags, e.Participants[i].Tag()) } for i := range e.Hashtags { tags = appendKV(tags, TagT, e.Hashtags[i]) } tags = appendTags(tags, e.Extra) return } func ParseEdition(tags [][]string, content string) (e *Edition, err error) { e = &Edition{Content: content} for i := range tags { tg := tags[i] if len(tg) < 1 { continue } switch tg[0] { case TagD: e.D = elem(tg, 1) case TagA: a, aerr := AddressFromTag(tg) if aerr != nil { err = aerr return } e.Album = a case TagTitle: e.Title = elem(tg, 1) case TagFormat: e.Format = elem(tg, 1) case TagFormatDetail: for j := int32(1); j < int32(len(tg)); j++ { e.FormatDetail = push(e.FormatDetail, tg[j]) } case TagCatalog: e.Catalog = elem(tg, 1) case TagBarcode: e.Barcode = elem(tg, 1) case TagI: e.External = pushExternalI(e.External, elem(tg, 1)) case TagK: e.External = pushExternalK(e.External, elem(tg, 1)) case TagQuantity: e.Quantity = elem(tg, 1) case TagReleaseDate: e.ReleaseDate = elem(tg, 1) case TagTerritory: e.Territory = elem(tg, 1) case TagP: p, perr := parseParticipant(tg) if perr != nil { err = perr return } e.Participants = push(e.Participants, p) case TagT: e.Hashtags = push(e.Hashtags, elem(tg, 1)) default: e.Extra = appendTag(e.Extra, tg) } } return } func (e *Edition) Validate() (err error) { if e == nil { err = errorf.E("nil edition") return } if e.D == "" { err = errorf.E("edition: empty d tag") return } if e.Album != nil && e.Album.Kind != KindAlbum { err = errorf.E("edition: target is kind %d, want %d", e.Album.Kind, KindAlbum) return } return } func (e *Edition) Warnings() (w []string) { if e.Album == nil { w = push(w, "no album address") } if e.Format == "" { w = push(w, "no format tag") } return } // Formats is the edition format vocabulary. The spec calls it a convention // rather than a closed registry, so an unknown format is carried, not // rejected; this list exists so clients can group by family. Declared var Formats []string var formatsReady bool // initFormats fills the format table on first use, in the root arena, for the // same two reasons as ensureRoles: moxie test does not run a package init, and // a global must not point into a frame arena that is released on return. func initFormats() { if formatsReady { return } formatsReady = true runtime.SovereignSetArena(runtime.RootArena()) Formats = []string{ FormatVinyl, FormatCD, FormatCassette, "7\"", "10\"", "12\"", "box set", "digital", "minidisc", "reel-to-reel", "8-track", "floppy", } } // KnownFormat reports whether format appears in the format vocabulary. func KnownFormat(format string) (ok bool) { initFormats() for i := range Formats { if Formats[i] == format { return true } } return false } // Song is one entry of a setlist: an ordered song with an optional link to the // published track and an optional feature or cover annotation. // // The `song` tag is multi-letter and therefore not indexed, so a setlist also // carries one single-letter `a` tag per published song, in set order, which is // what makes "every live performance of this track" a plain #a query. type Song struct { Index string Title string Track string // track address, empty for an unreleased song Marker string // feat or cover Marker2 string // guest pubkey for feat, original artist for cover } // Setlist is the payload of a kind 32213 event: one artist's set at one event. type Setlist struct { D string Event *Address // the calendar/roster event, kind 31923 Artist *Participant Title string Songs []Song Encores []string TrackLinks []*Address // single-letter a tags, published songs in set order Hashtags []string Content string Extra [][]string } func (s *Song) Tag() (t []string) { if s == nil { return nil } n := int32(3) if s.Track != "" || s.Marker != "" { n = 4 } if s.Marker != "" { n = 6 } t = []string{:n} t[0] = TagSong t[1] = s.Index t[2] = s.Title if n > 3 { t[3] = s.Track } if n > 4 { t[4] = s.Marker t[5] = s.Marker2 } return } func (sl *Setlist) Tags() (tags [][]string) { tags = appendKV(nil, TagD, sl.D) tags = appendTag(tags, sl.Event.Tag()) tags = appendTag(tags, sl.Artist.Tag()) tags = appendKV(tags, TagTitle, sl.Title) for i := range sl.Songs { tags = appendTag(tags, sl.Songs[i].Tag()) } for i := range sl.Encores { tags = appendKV(tags, TagEncore, sl.Encores[i]) } for i := range sl.TrackLinks { tags = appendTag(tags, sl.TrackLinks[i].Tag()) } for i := range sl.Hashtags { tags = appendKV(tags, TagT, sl.Hashtags[i]) } tags = appendTags(tags, sl.Extra) return } func ParseSetlist(tags [][]string, content string) (sl *Setlist, err error) { sl = &Setlist{Content: content} for i := range tags { tg := tags[i] if len(tg) < 1 { continue } switch tg[0] { case TagD: sl.D = elem(tg, 1) case TagA: a, aerr := AddressFromTag(tg) if aerr != nil { err = aerr return } // The first a tag is the event; the rest are track links. if sl.Event == nil && a.Kind == KindCalendar { sl.Event = a } else { sl.TrackLinks = push(sl.TrackLinks, a) } case TagP: p, perr := parseParticipant(tg) if perr != nil { err = perr return } sl.Artist = &p case TagTitle: sl.Title = elem(tg, 1) case TagSong: song := Song{Index: elem(tg, 1), Title: elem(tg, 2), Track: elem(tg, 3), Marker: elem(tg, 4), Marker2: elem(tg, 5)} sl.Songs = push(sl.Songs, song) case TagEncore: sl.Encores = push(sl.Encores, elem(tg, 1)) case TagT: sl.Hashtags = push(sl.Hashtags, elem(tg, 1)) default: sl.Extra = appendTag(sl.Extra, tg) } } return } func (sl *Setlist) Validate() (err error) { if sl == nil { err = errorf.E("nil setlist") return } if sl.Event == nil { err = errorf.E("setlist: no event address") return } if sl.Event.Kind != KindCalendar { err = errorf.E("setlist: event is kind %d, want %d", sl.Event.Kind, KindCalendar) return } for i := range sl.Songs { if sl.Songs[i].Index == "" { err = errorf.E("setlist: song %d has no index", i) return } if sl.Songs[i].Title == "" { err = errorf.E("setlist: song %d has no title", i) return } } for i := range sl.TrackLinks { if sl.TrackLinks[i].Kind != KindTrack { err = errorf.E("setlist: track link %d is kind %d, want %d", i, sl.TrackLinks[i].Kind, KindTrack) return } } return } // Warnings reports the setlist gap: a published song with no single-letter a // tag cannot be found by "performed live" queries. func (sl *Setlist) Warnings() (w []string) { published := int32(0) for i := range sl.Songs { if sl.Songs[i].Track != "" { published++ } } if published > int32(len(sl.TrackLinks)) { w = push(w, "a published song has no indexed track link") } if sl.D == "" { w = push(w, "no d tag") } return }