credits.mx raw
1 package protocol
2
3 import (
4 "runtime"
5
6 "git.smesh.lol/nostr/pkg/lol/errorf"
7 )
8
9 // Studio credits a company or room by activity: ["studio", "recording", "Name,
10 // City"]. The activity is the same lowercase word the role vocabulary uses
11 // without the -engineer suffix.
12 const (
13 StudioRecording = "recording"
14 StudioMix = "mix"
15 StudioMastering = "mastering"
16 )
17
18 // Studio is a studio tag: which room, for which activity.
19 type Studio struct {
20 Activity string // recording, mix, mastering
21 Name string // free text: "Studio Name, City"
22 }
23
24 func (s *Studio) Tag() (t []string) {
25 if s == nil {
26 return nil
27 }
28 t = []string{:3}
29 t[0] = TagStudio
30 t[1] = s.Activity
31 t[2] = s.Name
32 return
33 }
34
35 // ReleaseCredits is the payload of a kind 32214 event: the full participant
36 // list for a release, each with a role, plus the studios it was made in.
37 //
38 // Credits are wiki-style and validated socially (confirmations, attestations,
39 // contributions), so nothing here is authoritative on its own; this is the
40 // authored claim.
41 type ReleaseCredits struct {
42 D string
43 Album *Address // 32211 album address
44 Title string
45 Participants []Participant
46 Studios []Studio
47 Content string
48 Extra [][]string
49 }
50
51 func (c *ReleaseCredits) Tags() (tags [][]string) {
52 tags = appendKV(nil, TagD, c.D)
53 tags = appendTag(tags, c.Album.Tag())
54 tags = appendKV(tags, TagTitle, c.Title)
55 for i := range c.Participants {
56 tags = appendTag(tags, c.Participants[i].Tag())
57 }
58 for i := range c.Studios {
59 tags = appendTag(tags, c.Studios[i].Tag())
60 }
61 tags = appendTags(tags, c.Extra)
62 return
63 }
64
65 func ParseReleaseCredits(tags [][]string, content string) (c *ReleaseCredits, err error) {
66 c = &ReleaseCredits{Content: content}
67 for i := range tags {
68 tg := tags[i]
69 if len(tg) < 1 {
70 continue
71 }
72 switch tg[0] {
73 case TagD:
74 c.D = elem(tg, 1)
75 case TagA:
76 a, aerr := AddressFromTag(tg)
77 if aerr != nil {
78 err = aerr
79 return
80 }
81 c.Album = a
82 case TagTitle:
83 c.Title = elem(tg, 1)
84 case TagP:
85 p, perr := parseParticipant(tg)
86 if perr != nil {
87 err = perr
88 return
89 }
90 c.Participants = push(c.Participants, p)
91 case TagStudio:
92 c.Studios = push(c.Studios, Studio{Activity: elem(tg, 1), Name: elem(tg, 2)})
93 default:
94 c.Extra = appendTag(c.Extra, tg)
95 }
96 }
97 return
98 }
99
100 func (c *ReleaseCredits) Validate() (err error) {
101 if c == nil {
102 err = errorf.E("nil release credits")
103 return
104 }
105 if c.D == "" {
106 err = errorf.E("release credits: empty d tag")
107 return
108 }
109 if c.Album != nil && c.Album.Kind != KindAlbum {
110 err = errorf.E("release credits: target is kind %d, want %d", c.Album.Kind, KindAlbum)
111 return
112 }
113 for i := range c.Participants {
114 if !IsHex64(c.Participants[i].Pubkey) {
115 err = errorf.E("release credits: participant %d pubkey is not 64 hex characters", i)
116 return
117 }
118 if c.Participants[i].Role == "" {
119 err = errorf.E("release credits: participant %d has no role", i)
120 return
121 }
122 }
123 for i := range c.Studios {
124 if c.Studios[i].Activity == "" || c.Studios[i].Name == "" {
125 err = errorf.E("release credits: studio %d is incomplete", i)
126 return
127 }
128 }
129 return
130 }
131
132 func (c *ReleaseCredits) Warnings() (w []string) {
133 if c.Album == nil {
134 w = push(w, "no album address")
135 }
136 if len(c.Participants) == 0 {
137 w = push(w, "no credited participants")
138 }
139 return
140 }
141
142 // TrackCredits is the payload of a kind 32215 event: per-track credits that
143 // inherit from the release and override only the roles they state.
144 type TrackCredits struct {
145 D string
146 Track *Address // 32210 track address
147 Release *Address // 32214 release credits address, the inheritance link
148 Participants []Participant
149 Content string
150 Extra [][]string
151 }
152
153 func (c *TrackCredits) Tags() (tags [][]string) {
154 tags = appendKV(nil, TagD, c.D)
155 tags = appendTag(tags, c.Track.Tag())
156 tags = appendTag(tags, c.Release.Tag())
157 for i := range c.Participants {
158 tags = appendTag(tags, c.Participants[i].Tag())
159 }
160 tags = appendTags(tags, c.Extra)
161 return
162 }
163
164 func ParseTrackCredits(tags [][]string, content string) (c *TrackCredits, err error) {
165 c = &TrackCredits{Content: content}
166 for i := range tags {
167 tg := tags[i]
168 if len(tg) < 1 {
169 continue
170 }
171 switch tg[0] {
172 case TagD:
173 c.D = elem(tg, 1)
174 case TagA:
175 a, aerr := AddressFromTag(tg)
176 if aerr != nil {
177 err = aerr
178 return
179 }
180 // The two a tags are told apart by kind: track first, release
181 // credits second, as the spec writes them.
182 if a.Kind == KindTrack && c.Track == nil {
183 c.Track = a
184 } else if a.Kind == KindReleaseCredits && c.Release == nil {
185 c.Release = a
186 } else if c.Track == nil {
187 c.Track = a
188 } else {
189 c.Release = a
190 }
191 case TagP:
192 p, perr := parseParticipant(tg)
193 if perr != nil {
194 err = perr
195 return
196 }
197 c.Participants = push(c.Participants, p)
198 default:
199 c.Extra = appendTag(c.Extra, tg)
200 }
201 }
202 return
203 }
204
205 func (c *TrackCredits) Validate() (err error) {
206 if c == nil {
207 err = errorf.E("nil track credits")
208 return
209 }
210 if c.D == "" {
211 err = errorf.E("track credits: empty d tag")
212 return
213 }
214 if c.Track == nil {
215 err = errorf.E("track credits: no track address")
216 return
217 }
218 if c.Track.Kind != KindTrack {
219 err = errorf.E("track credits: target is kind %d, want %d", c.Track.Kind, KindTrack)
220 return
221 }
222 if c.Release != nil && c.Release.Kind != KindReleaseCredits {
223 err = errorf.E("track credits: release reference is kind %d, want %d", c.Release.Kind, KindReleaseCredits)
224 return
225 }
226 for i := range c.Participants {
227 if !IsHex64(c.Participants[i].Pubkey) {
228 err = errorf.E("track credits: participant %d pubkey is not 64 hex characters", i)
229 return
230 }
231 if c.Participants[i].Role == "" {
232 err = errorf.E("track credits: participant %d has no role", i)
233 return
234 }
235 }
236 return
237 }
238
239 // Edition is the payload of a kind 32216 event: one format of a release. Each
240 // pressing, replication or duplication is its own event linked to the album,
241 // and edition credits override the release's where they differ.
242 type Edition struct {
243 D string
244 Album *Address
245 Title string
246 Format string // format family: vinyl, CD, cassette, ...
247 FormatDetail []string // packed into one tag: 180g, black, gatefold
248 Catalog string
249 Barcode string
250 External []ExternalID
251 Quantity string
252 ReleaseDate string
253 Territory string
254 Participants []Participant
255 Hashtags []string
256 Content string
257 Extra [][]string
258 }
259
260 func (e *Edition) Tags() (tags [][]string) {
261 tags = appendKV(nil, TagD, e.D)
262 tags = appendTag(tags, e.Album.Tag())
263 tags = appendKV(tags, TagTitle, e.Title)
264 tags = appendKV(tags, TagFormat, e.Format)
265 if len(e.FormatDetail) > 0 {
266 t := []string{:len(e.FormatDetail) + 1}
267 t[0] = TagFormatDetail
268 for i := range e.FormatDetail {
269 t[i+1] = e.FormatDetail[i]
270 }
271 tags = appendTag(tags, t)
272 }
273 tags = appendKV(tags, TagCatalog, e.Catalog)
274 tags = appendKV(tags, TagBarcode, e.Barcode)
275 for i := range e.External {
276 tags = appendKV(tags, TagI, e.External[i].I)
277 tags = appendKV(tags, TagK, e.External[i].K)
278 }
279 tags = appendKV(tags, TagQuantity, e.Quantity)
280 tags = appendKV(tags, TagReleaseDate, e.ReleaseDate)
281 tags = appendKV(tags, TagTerritory, e.Territory)
282 for i := range e.Participants {
283 tags = appendTag(tags, e.Participants[i].Tag())
284 }
285 for i := range e.Hashtags {
286 tags = appendKV(tags, TagT, e.Hashtags[i])
287 }
288 tags = appendTags(tags, e.Extra)
289 return
290 }
291
292 func ParseEdition(tags [][]string, content string) (e *Edition, err error) {
293 e = &Edition{Content: content}
294 for i := range tags {
295 tg := tags[i]
296 if len(tg) < 1 {
297 continue
298 }
299 switch tg[0] {
300 case TagD:
301 e.D = elem(tg, 1)
302 case TagA:
303 a, aerr := AddressFromTag(tg)
304 if aerr != nil {
305 err = aerr
306 return
307 }
308 e.Album = a
309 case TagTitle:
310 e.Title = elem(tg, 1)
311 case TagFormat:
312 e.Format = elem(tg, 1)
313 case TagFormatDetail:
314 for j := int32(1); j < int32(len(tg)); j++ {
315 e.FormatDetail = push(e.FormatDetail, tg[j])
316 }
317 case TagCatalog:
318 e.Catalog = elem(tg, 1)
319 case TagBarcode:
320 e.Barcode = elem(tg, 1)
321 case TagI:
322 e.External = pushExternalI(e.External, elem(tg, 1))
323 case TagK:
324 e.External = pushExternalK(e.External, elem(tg, 1))
325 case TagQuantity:
326 e.Quantity = elem(tg, 1)
327 case TagReleaseDate:
328 e.ReleaseDate = elem(tg, 1)
329 case TagTerritory:
330 e.Territory = elem(tg, 1)
331 case TagP:
332 p, perr := parseParticipant(tg)
333 if perr != nil {
334 err = perr
335 return
336 }
337 e.Participants = push(e.Participants, p)
338 case TagT:
339 e.Hashtags = push(e.Hashtags, elem(tg, 1))
340 default:
341 e.Extra = appendTag(e.Extra, tg)
342 }
343 }
344 return
345 }
346
347 func (e *Edition) Validate() (err error) {
348 if e == nil {
349 err = errorf.E("nil edition")
350 return
351 }
352 if e.D == "" {
353 err = errorf.E("edition: empty d tag")
354 return
355 }
356 if e.Album != nil && e.Album.Kind != KindAlbum {
357 err = errorf.E("edition: target is kind %d, want %d", e.Album.Kind, KindAlbum)
358 return
359 }
360 return
361 }
362
363 func (e *Edition) Warnings() (w []string) {
364 if e.Album == nil {
365 w = push(w, "no album address")
366 }
367 if e.Format == "" {
368 w = push(w, "no format tag")
369 }
370 return
371 }
372
373 // Formats is the edition format vocabulary. The spec calls it a convention
374 // rather than a closed registry, so an unknown format is carried, not
375 // rejected; this list exists so clients can group by family. Declared
376 var Formats []string
377
378 var formatsReady bool
379
380 // initFormats fills the format table on first use, in the root arena, for the
381 // same two reasons as ensureRoles: moxie test does not run a package init, and
382 // a global must not point into a frame arena that is released on return.
383 func initFormats() {
384 if formatsReady {
385 return
386 }
387 formatsReady = true
388 runtime.SovereignSetArena(runtime.RootArena())
389 Formats = []string{
390 FormatVinyl, FormatCD, FormatCassette,
391 "7\"", "10\"", "12\"", "box set", "digital",
392 "minidisc", "reel-to-reel", "8-track", "floppy",
393 }
394 }
395
396 // KnownFormat reports whether format appears in the format vocabulary.
397 func KnownFormat(format string) (ok bool) {
398 initFormats()
399 for i := range Formats {
400 if Formats[i] == format {
401 return true
402 }
403 }
404 return false
405 }
406
407 // Song is one entry of a setlist: an ordered song with an optional link to the
408 // published track and an optional feature or cover annotation.
409 //
410 // The `song` tag is multi-letter and therefore not indexed, so a setlist also
411 // carries one single-letter `a` tag per published song, in set order, which is
412 // what makes "every live performance of this track" a plain #a query.
413 type Song struct {
414 Index string
415 Title string
416 Track string // track address, empty for an unreleased song
417 Marker string // feat or cover
418 Marker2 string // guest pubkey for feat, original artist for cover
419 }
420
421 // Setlist is the payload of a kind 32213 event: one artist's set at one event.
422 type Setlist struct {
423 D string
424 Event *Address // the calendar/roster event, kind 31923
425 Artist *Participant
426 Title string
427 Songs []Song
428 Encores []string
429 TrackLinks []*Address // single-letter a tags, published songs in set order
430 Hashtags []string
431 Content string
432 Extra [][]string
433 }
434
435 func (s *Song) Tag() (t []string) {
436 if s == nil {
437 return nil
438 }
439 n := int32(3)
440 if s.Track != "" || s.Marker != "" {
441 n = 4
442 }
443 if s.Marker != "" {
444 n = 6
445 }
446 t = []string{:n}
447 t[0] = TagSong
448 t[1] = s.Index
449 t[2] = s.Title
450 if n > 3 {
451 t[3] = s.Track
452 }
453 if n > 4 {
454 t[4] = s.Marker
455 t[5] = s.Marker2
456 }
457 return
458 }
459
460 func (sl *Setlist) Tags() (tags [][]string) {
461 tags = appendKV(nil, TagD, sl.D)
462 tags = appendTag(tags, sl.Event.Tag())
463 tags = appendTag(tags, sl.Artist.Tag())
464 tags = appendKV(tags, TagTitle, sl.Title)
465 for i := range sl.Songs {
466 tags = appendTag(tags, sl.Songs[i].Tag())
467 }
468 for i := range sl.Encores {
469 tags = appendKV(tags, TagEncore, sl.Encores[i])
470 }
471 for i := range sl.TrackLinks {
472 tags = appendTag(tags, sl.TrackLinks[i].Tag())
473 }
474 for i := range sl.Hashtags {
475 tags = appendKV(tags, TagT, sl.Hashtags[i])
476 }
477 tags = appendTags(tags, sl.Extra)
478 return
479 }
480
481 func ParseSetlist(tags [][]string, content string) (sl *Setlist, err error) {
482 sl = &Setlist{Content: content}
483 for i := range tags {
484 tg := tags[i]
485 if len(tg) < 1 {
486 continue
487 }
488 switch tg[0] {
489 case TagD:
490 sl.D = elem(tg, 1)
491 case TagA:
492 a, aerr := AddressFromTag(tg)
493 if aerr != nil {
494 err = aerr
495 return
496 }
497 // The first a tag is the event; the rest are track links.
498 if sl.Event == nil && a.Kind == KindCalendar {
499 sl.Event = a
500 } else {
501 sl.TrackLinks = push(sl.TrackLinks, a)
502 }
503 case TagP:
504 p, perr := parseParticipant(tg)
505 if perr != nil {
506 err = perr
507 return
508 }
509 sl.Artist = &p
510 case TagTitle:
511 sl.Title = elem(tg, 1)
512 case TagSong:
513 song := Song{Index: elem(tg, 1), Title: elem(tg, 2), Track: elem(tg, 3), Marker: elem(tg, 4), Marker2: elem(tg, 5)}
514 sl.Songs = push(sl.Songs, song)
515 case TagEncore:
516 sl.Encores = push(sl.Encores, elem(tg, 1))
517 case TagT:
518 sl.Hashtags = push(sl.Hashtags, elem(tg, 1))
519 default:
520 sl.Extra = appendTag(sl.Extra, tg)
521 }
522 }
523 return
524 }
525
526 func (sl *Setlist) Validate() (err error) {
527 if sl == nil {
528 err = errorf.E("nil setlist")
529 return
530 }
531 if sl.Event == nil {
532 err = errorf.E("setlist: no event address")
533 return
534 }
535 if sl.Event.Kind != KindCalendar {
536 err = errorf.E("setlist: event is kind %d, want %d", sl.Event.Kind, KindCalendar)
537 return
538 }
539 for i := range sl.Songs {
540 if sl.Songs[i].Index == "" {
541 err = errorf.E("setlist: song %d has no index", i)
542 return
543 }
544 if sl.Songs[i].Title == "" {
545 err = errorf.E("setlist: song %d has no title", i)
546 return
547 }
548 }
549 for i := range sl.TrackLinks {
550 if sl.TrackLinks[i].Kind != KindTrack {
551 err = errorf.E("setlist: track link %d is kind %d, want %d", i, sl.TrackLinks[i].Kind, KindTrack)
552 return
553 }
554 }
555 return
556 }
557
558 // Warnings reports the setlist gap: a published song with no single-letter a
559 // tag cannot be found by "performed live" queries.
560 func (sl *Setlist) Warnings() (w []string) {
561 published := int32(0)
562 for i := range sl.Songs {
563 if sl.Songs[i].Track != "" {
564 published++
565 }
566 }
567 if published > int32(len(sl.TrackLinks)) {
568 w = push(w, "a published song has no indexed track link")
569 }
570 if sl.D == "" {
571 w = push(w, "no d tag")
572 }
573 return
574 }
575