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