music.mx raw

   1  package protocol
   2  
   3  import (
   4  	"git.smesh.lol/nostr/pkg/lol/errorf"
   5  )
   6  
   7  // Participant is a `p` tag carrying a role: an artist on a track, a credited
   8  // engineer, a roster member. The role is index 3, after the optional relay
   9  // hint. Roles come from the roster and credit vocabularies; the vocabularies
  10  // are conventions, so an unknown role is carried, not rejected.
  11  type Participant struct {
  12  	Pubkey string
  13  	Relay  string
  14  	Role   string
  15  }
  16  
  17  func (p *Participant) Tag() (t []string) {
  18  	if p == nil {
  19  		return nil
  20  	}
  21  	// The role sits at index 3, so a participant with a role but no relay hint
  22  	// still gets an empty relay slot: a bare third element would be read as a
  23  	// relay by NIP-52 consumers.
  24  	n := int32(2)
  25  	switch {
  26  	case p.Role != "":
  27  		n = 4
  28  	case p.Relay != "":
  29  		n = 3
  30  	}
  31  	t = []string{:n}
  32  	t[0] = TagP
  33  	t[1] = p.Pubkey
  34  	if n > 2 {
  35  		t[2] = p.Relay
  36  	}
  37  	if n > 3 {
  38  		t[3] = p.Role
  39  	}
  40  	return
  41  }
  42  
  43  // Price is a price tag: ["price", "<amount>", "<unit>"] and, for a service,
  44  // ["price", "<amount>", "<unit>", "<period>"]. The amount stays a string: the
  45  // wire type is a string and a fiat amount is not an integer.
  46  type Price struct {
  47  	Amount string
  48  	Unit   string
  49  	Period string
  50  }
  51  
  52  func (p *Price) Tag() (t []string) {
  53  	if p == nil {
  54  		return nil
  55  	}
  56  	if p.Period != "" {
  57  		t = []string{:4}
  58  		t[0] = TagPrice
  59  		t[1] = p.Amount
  60  		t[2] = p.Unit
  61  		t[3] = p.Period
  62  		return
  63  	}
  64  	t = []string{:3}
  65  	t[0] = TagPrice
  66  	t[1] = p.Amount
  67  	t[2] = p.Unit
  68  	return
  69  }
  70  
  71  // BlobRef is cover art or an event image held on Blossom: a sha256 plus the
  72  // server that has it. The spec writes it ["image", "<sha256>", "<server-url>"].
  73  type BlobRef struct {
  74  	Sha256 string
  75  	Server string
  76  }
  77  
  78  func (b *BlobRef) Tag() (t []string) {
  79  	if b == nil {
  80  		return nil
  81  	}
  82  	if b.Server == "" {
  83  		t = []string{:2}
  84  		t[0] = TagImage
  85  		t[1] = b.Sha256
  86  		return
  87  	}
  88  	t = []string{:3}
  89  	t[0] = TagImage
  90  	t[1] = b.Sha256
  91  	t[2] = b.Server
  92  	return
  93  }
  94  
  95  // ExternalID is a NIP-73 `i`/`k` pair: the identifier and its kind token, e.g.
  96  // musicbrainz:recording:<mbid> with musicbrainz:recording.
  97  type ExternalID struct {
  98  	I string
  99  	K string
 100  }
 101  
 102  // Encoding is one published rendering of a recording: an `audio` tag and its
 103  // mirrored single-letter `x` tag. The x tag is what makes "which asset is this
 104  // blob?" a plain {"#x": [...]} query; a host needs that answer to gate a
 105  // download.
 106  type Encoding struct {
 107  	Sha256 string // blob hash, 64 lowercase hex
 108  	Spec   string // codec-spec, e.g. flac-24bit-96kHz
 109  	Server string // Blossom server that has the blob; "" falls back to the publisher's server list
 110  	MIME   string // media type, lowercase
 111  	Size   int64  // blob size in bytes
 112  	HasSize bool  // false when the optional size field is absent
 113  }
 114  
 115  func (e *Encoding) Tag() (t []string) {
 116  	if e == nil {
 117  		return nil
 118  	}
 119  	// The tag is positional: a media type or a size cannot be written without
 120  	// the slot before it, so an absent middle field is written empty rather
 121  	// than shifting the rest left.
 122  	n := int32(3)
 123  	switch {
 124  	case e.HasSize:
 125  		n = 6
 126  	case e.MIME != "":
 127  		n = 5
 128  	case e.Server != "":
 129  		n = 4
 130  	}
 131  	t = []string{:n}
 132  	t[0] = TagAudio
 133  	t[1] = e.Sha256
 134  	t[2] = e.Spec
 135  	if n > 3 {
 136  		t[3] = e.Server
 137  	}
 138  	if n > 4 {
 139  		t[4] = e.MIME
 140  	}
 141  	if n > 5 {
 142  		t[5] = intStr(e.Size)
 143  	}
 144  	return
 145  }
 146  
 147  func (e *Encoding) XTag() (t []string) {
 148  	if e == nil {
 149  		return nil
 150  	}
 151  	t = []string{:2}
 152  	t[0] = TagX
 153  	t[1] = e.Sha256
 154  	return
 155  }
 156  
 157  // Track is the payload of a kind 32210 event: the recording, with every
 158  // published encoding. The d tag is the recording identity and should be the
 159  // ISRC when the recording has one; the event id changes on every update, so
 160  // references use the address.
 161  type Track struct {
 162  	D            string
 163  	Title        string
 164  	Artists      []string // free-text artist names, repeatable
 165  	Participants []Participant
 166  	Album        *Address
 167  	AlbumTitle   string
 168  	TrackNo      string
 169  	DiscNo       string
 170  	Duration     string // seconds, floating point (NIP-71 convention)
 171  	Genres       []string
 172  	Hashtags     []string
 173  	ReleaseDate  string
 174  	ISRC         string
 175  	Language     string
 176  	BPM          string
 177  	Key          string
 178  	Copyright    string
 179  	Price        *Price
 180  	Cover        *BlobRef
 181  	External     []ExternalID
 182  	Encodings    []Encoding
 183  	Master       string // sha256 of the lossless master encoding, exactly one
 184  	Content      string // liner notes
 185  	Extra        [][]string
 186  }
 187  
 188  // Tags renders the payload as the tag list of the event.
 189  func (t *Track) Tags() (tags [][]string) {
 190  	tags = appendKV(nil, TagD, t.D)
 191  	tags = appendKV(tags, TagTitle, t.Title)
 192  	for _, a := range t.Artists {
 193  		tags = appendKV(tags, TagArtist, a)
 194  	}
 195  	for i := range t.Participants {
 196  		tags = appendTag(tags, t.Participants[i].Tag())
 197  	}
 198  	if t.Album != nil {
 199  		tags = appendTag(tags, t.Album.Tag())
 200  	}
 201  	tags = appendKV(tags, TagAlbum, t.AlbumTitle)
 202  	tags = appendKV(tags, TagTrack, t.TrackNo)
 203  	tags = appendKV(tags, TagDisc, t.DiscNo)
 204  	tags = appendKV(tags, TagDuration, t.Duration)
 205  	for _, g := range t.Genres {
 206  		tags = appendKV(tags, TagGenre, g)
 207  	}
 208  	for _, h := range t.Hashtags {
 209  		tags = appendKV(tags, TagT, h)
 210  	}
 211  	tags = appendKV(tags, TagReleaseDate, t.ReleaseDate)
 212  	tags = appendKV(tags, TagISRC, t.ISRC)
 213  	tags = appendKV(tags, TagLanguage, t.Language)
 214  	tags = appendKV(tags, TagBPM, t.BPM)
 215  	tags = appendKV(tags, TagKey, t.Key)
 216  	tags = appendKV(tags, TagCopyright, t.Copyright)
 217  	tags = appendTag(tags, t.Price.Tag())
 218  	tags = appendTag(tags, t.Cover.Tag())
 219  	for i := range t.External {
 220  		tags = appendKV(tags, TagI, t.External[i].I)
 221  		tags = appendKV(tags, TagK, t.External[i].K)
 222  	}
 223  	for i := range t.Encodings {
 224  		tags = appendTag(tags, t.Encodings[i].Tag())
 225  	}
 226  	for i := range t.Encodings {
 227  		tags = appendTag(tags, t.Encodings[i].XTag())
 228  	}
 229  	tags = appendKV(tags, TagMaster, t.Master)
 230  	tags = appendTags(tags, t.Extra)
 231  	return
 232  }
 233  
 234  // ParseTrack reads a kind 32210 tag list. Tags this version does not know are
 235  // kept in Extra and re-emitted, so an older build cannot strip a newer field.
 236  func ParseTrack(tags [][]string, content string) (t *Track, err error) {
 237  	t = &Track{Content: content}
 238  	for i := range tags {
 239  		tg := tags[i]
 240  		if len(tg) < 1 {
 241  			continue
 242  		}
 243  		switch tg[0] {
 244  		case TagD:
 245  			t.D = elem(tg, 1)
 246  		case TagTitle:
 247  			t.Title = elem(tg, 1)
 248  		case TagArtist:
 249  			t.Artists = push(t.Artists, elem(tg, 1))
 250  		case TagP:
 251  			p, perr := parseParticipant(tg)
 252  			if perr != nil {
 253  				err = perr
 254  				return
 255  			}
 256  			t.Participants = push(t.Participants, p)
 257  		case TagA:
 258  			a, aerr := AddressFromTag(tg)
 259  			if aerr != nil {
 260  				err = aerr
 261  				return
 262  			}
 263  			t.Album = a
 264  		case TagAlbum:
 265  			t.AlbumTitle = elem(tg, 1)
 266  		case TagTrack:
 267  			t.TrackNo = elem(tg, 1)
 268  		case TagDisc:
 269  			t.DiscNo = elem(tg, 1)
 270  		case TagDuration:
 271  			t.Duration = elem(tg, 1)
 272  		case TagGenre:
 273  			t.Genres = push(t.Genres, elem(tg, 1))
 274  		case TagT:
 275  			t.Hashtags = push(t.Hashtags, elem(tg, 1))
 276  		case TagReleaseDate:
 277  			t.ReleaseDate = elem(tg, 1)
 278  		case TagISRC:
 279  			t.ISRC = elem(tg, 1)
 280  		case TagLanguage:
 281  			t.Language = elem(tg, 1)
 282  		case TagBPM:
 283  			t.BPM = elem(tg, 1)
 284  		case TagKey:
 285  			t.Key = elem(tg, 1)
 286  		case TagCopyright:
 287  			t.Copyright = elem(tg, 1)
 288  		case TagPrice:
 289  			t.Price = parsePrice(tg)
 290  		case TagImage:
 291  			t.Cover = parseBlobRef(tg)
 292  		case TagI:
 293  			t.External = pushExternalI(t.External, elem(tg, 1))
 294  		case TagK:
 295  			t.External = pushExternalK(t.External, elem(tg, 1))
 296  		case TagAudio:
 297  			enc := parseEncoding(tg)
 298  			// An x tag may have arrived first and left a bare entry; fill it
 299  			// rather than adding a second entry for the same blob.
 300  			merged := false
 301  			for j := range t.Encodings {
 302  				if t.Encodings[j].Sha256 == enc.Sha256 && t.Encodings[j].Spec == "" {
 303  					t.Encodings[j] = enc
 304  					merged = true
 305  					break
 306  				}
 307  			}
 308  			if !merged {
 309  				t.Encodings = push(t.Encodings, enc)
 310  			}
 311  		case TagX:
 312  			// The x tags mirror the audio tags; fold each into its encoding
 313  			// so the pair survives even when the order is interleaved.
 314  			hash := elem(tg, 1)
 315  			if hash != "" && !encodingHas(t.Encodings, hash) {
 316  				t.Encodings = push(t.Encodings, Encoding{Sha256: hash})
 317  			}
 318  		case TagMaster:
 319  			t.Master = elem(tg, 1)
 320  		default:
 321  			t.Extra = appendTag(t.Extra, tg)
 322  		}
 323  	}
 324  	return
 325  }
 326  
 327  // Validate checks the structural rules the spec states. It does not enforce a
 328  // required-tag list: the documents define no such list, so a track without a
 329  // title is carried and reported by Warnings instead.
 330  func (t *Track) Validate() (err error) {
 331  	if t == nil {
 332  		err = errorf.E("nil track")
 333  		return
 334  	}
 335  	if t.D == "" {
 336  		err = errorf.E("track: empty d tag")
 337  		return
 338  	}
 339  	if t.Album != nil && t.Album.Kind != KindAlbum {
 340  		err = errorf.E("track: album reference is kind %d, want %d", t.Album.Kind, KindAlbum)
 341  		return
 342  	}
 343  	if t.Price != nil && t.Price.Unit == "" {
 344  		err = errorf.E("track: price without a unit")
 345  		return
 346  	}
 347  	if t.Cover != nil && !IsHex64(t.Cover.Sha256) {
 348  		err = errorf.E("track: cover art hash %q is not 64 hex characters", t.Cover.Sha256)
 349  		return
 350  	}
 351  	for i := range t.Encodings {
 352  		e := &t.Encodings[i]
 353  		if !IsHex64(e.Sha256) {
 354  			err = errorf.E("track: encoding %d hash %q is not 64 hex characters", i, e.Sha256)
 355  			return
 356  		}
 357  		if e.Spec == "" {
 358  			err = errorf.E("track: encoding %d has no codec spec", i)
 359  			return
 360  		}
 361  		if _, cerr := ParseCodecSpec(e.Spec); cerr != nil {
 362  			err = errorf.E("track: encoding %d: %s", i, cerr.Error())
 363  			return
 364  		}
 365  	}
 366  	if t.Master != "" {
 367  		if !IsHex64(t.Master) {
 368  			err = errorf.E("track: master hash %q is not 64 hex characters", t.Master)
 369  			return
 370  		}
 371  		if !encodingHas(t.Encodings, t.Master) {
 372  			err = errorf.E("track: master %s is not one of the encodings", t.Master)
 373  			return
 374  		}
 375  	}
 376  	for i := range t.Participants {
 377  		if !IsHex64(t.Participants[i].Pubkey) {
 378  			err = errorf.E("track: participant %d pubkey is not 64 hex characters", i)
 379  			return
 380  		}
 381  	}
 382  	return
 383  }
 384  
 385  // Warnings lists what the spec calls for but does not require. A relay can
 386  // store such a track; a client should surface the gap rather than invent data.
 387  func (t *Track) Warnings() (w []string) {
 388  	if t.Title == "" {
 389  		w = push(w, "no title tag")
 390  	}
 391  	if len(t.Artists) == 0 && len(t.Participants) == 0 {
 392  		w = push(w, "no artist or participant")
 393  	}
 394  	if len(t.Encodings) == 0 {
 395  		w = push(w, "no audio encoding")
 396  	}
 397  	if len(t.Encodings) > 0 && t.Master == "" {
 398  		w = push(w, "encodings but no master tag")
 399  	}
 400  	if t.ISRC != "" && t.D != t.ISRC {
 401  		w = push(w, "isrc set but d tag is not the isrc")
 402  	}
 403  	for i := range t.Participants {
 404  		if !KnownRole(t.Participants[i].Role) && t.Participants[i].Role != "" {
 405  			w = push(w, "unknown role: " | t.Participants[i].Role)
 406  		}
 407  	}
 408  	return
 409  }
 410  
 411  // Album is the payload of a kind 32211 event: the release identity. The
 412  // ordered track addresses are the tracklist - tag order is the order.
 413  type Album struct {
 414  	D            string
 415  	Title        string
 416  	Artists      []string
 417  	Participants []Participant
 418  	ReleaseDate  string
 419  	Genres       []string
 420  	Hashtags     []string
 421  	Language     string
 422  	Copyright    string
 423  	Price        *Price
 424  	Cover        *BlobRef
 425  	External     []ExternalID
 426  	Tracks       []*Address
 427  	Content      string
 428  	Extra        [][]string
 429  }
 430  
 431  func (a *Album) Tags() (tags [][]string) {
 432  	tags = appendKV(nil, TagD, a.D)
 433  	tags = appendKV(tags, TagTitle, a.Title)
 434  	for _, s := range a.Artists {
 435  		tags = appendKV(tags, TagArtist, s)
 436  	}
 437  	for i := range a.Participants {
 438  		tags = appendTag(tags, a.Participants[i].Tag())
 439  	}
 440  	tags = appendKV(tags, TagReleaseDate, a.ReleaseDate)
 441  	for _, g := range a.Genres {
 442  		tags = appendKV(tags, TagGenre, g)
 443  	}
 444  	for _, h := range a.Hashtags {
 445  		tags = appendKV(tags, TagT, h)
 446  	}
 447  	tags = appendKV(tags, TagLanguage, a.Language)
 448  	tags = appendKV(tags, TagCopyright, a.Copyright)
 449  	tags = appendTag(tags, a.Price.Tag())
 450  	tags = appendTag(tags, a.Cover.Tag())
 451  	for i := range a.External {
 452  		tags = appendKV(tags, TagI, a.External[i].I)
 453  		tags = appendKV(tags, TagK, a.External[i].K)
 454  	}
 455  	for i := range a.Tracks {
 456  		tags = appendTag(tags, a.Tracks[i].Tag())
 457  	}
 458  	tags = appendTags(tags, a.Extra)
 459  	return
 460  }
 461  
 462  func ParseAlbum(tags [][]string, content string) (a *Album, err error) {
 463  	a = &Album{Content: content}
 464  	for i := range tags {
 465  		tg := tags[i]
 466  		if len(tg) < 1 {
 467  			continue
 468  		}
 469  		switch tg[0] {
 470  		case TagD:
 471  			a.D = elem(tg, 1)
 472  		case TagTitle:
 473  			a.Title = elem(tg, 1)
 474  		case TagArtist:
 475  			a.Artists = push(a.Artists, elem(tg, 1))
 476  		case TagP:
 477  			p, perr := parseParticipant(tg)
 478  			if perr != nil {
 479  				err = perr
 480  				return
 481  			}
 482  			a.Participants = push(a.Participants, p)
 483  		case TagReleaseDate:
 484  			a.ReleaseDate = elem(tg, 1)
 485  		case TagGenre:
 486  			a.Genres = push(a.Genres, elem(tg, 1))
 487  		case TagT:
 488  			a.Hashtags = push(a.Hashtags, elem(tg, 1))
 489  		case TagLanguage:
 490  			a.Language = elem(tg, 1)
 491  		case TagCopyright:
 492  			a.Copyright = elem(tg, 1)
 493  		case TagPrice:
 494  			a.Price = parsePrice(tg)
 495  		case TagImage:
 496  			a.Cover = parseBlobRef(tg)
 497  		case TagI:
 498  			a.External = pushExternalI(a.External, elem(tg, 1))
 499  		case TagK:
 500  			a.External = pushExternalK(a.External, elem(tg, 1))
 501  		case TagA:
 502  			addr, aerr := AddressFromTag(tg)
 503  			if aerr != nil {
 504  				err = aerr
 505  				return
 506  			}
 507  			a.Tracks = push(a.Tracks, addr)
 508  		default:
 509  			a.Extra = appendTag(a.Extra, tg)
 510  		}
 511  	}
 512  	return
 513  }
 514  
 515  func (a *Album) Validate() (err error) {
 516  	if a == nil {
 517  		err = errorf.E("nil album")
 518  		return
 519  	}
 520  	if a.D == "" {
 521  		err = errorf.E("album: empty d tag")
 522  		return
 523  	}
 524  	for i := range a.Tracks {
 525  		if a.Tracks[i].Kind != KindTrack {
 526  			err = errorf.E("album: track %d is kind %d, want %d", i, a.Tracks[i].Kind, KindTrack)
 527  			return
 528  		}
 529  	}
 530  	if a.Price != nil && a.Price.Unit == "" {
 531  		err = errorf.E("album: price without a unit")
 532  		return
 533  	}
 534  	return
 535  }
 536  
 537  func (a *Album) Warnings() (w []string) {
 538  	if a.Title == "" {
 539  		w = push(w, "no title tag")
 540  	}
 541  	if len(a.Tracks) == 0 {
 542  		w = push(w, "no track addresses")
 543  	}
 544  	return
 545  }
 546  
 547  // Playlist is the payload of a kind 32212 event: a named, ordered set of
 548  // tracks following NIP-51 conventions. A private playlist encrypts its members
 549  // into Content (NIP-44, self-key) and carries no track tags at all.
 550  type Playlist struct {
 551  	D           string
 552  	Title       string
 553  	Description string
 554  	Cover       *BlobRef
 555  	Tracks      []*Address
 556  	Content     string // private lists carry the encrypted member list here
 557  	Extra       [][]string
 558  }
 559  
 560  func (p *Playlist) Tags() (tags [][]string) {
 561  	tags = appendKV(nil, TagD, p.D)
 562  	tags = appendKV(tags, TagTitle, p.Title)
 563  	tags = appendKV(tags, TagDescription, p.Description)
 564  	tags = appendTag(tags, p.Cover.Tag())
 565  	for i := range p.Tracks {
 566  		tags = appendTag(tags, p.Tracks[i].Tag())
 567  	}
 568  	tags = appendTags(tags, p.Extra)
 569  	return
 570  }
 571  
 572  func ParsePlaylist(tags [][]string, content string) (p *Playlist, err error) {
 573  	p = &Playlist{Content: content}
 574  	for i := range tags {
 575  		tg := tags[i]
 576  		if len(tg) < 1 {
 577  			continue
 578  		}
 579  		switch tg[0] {
 580  		case TagD:
 581  			p.D = elem(tg, 1)
 582  		case TagTitle:
 583  			p.Title = elem(tg, 1)
 584  		case TagDescription:
 585  			p.Description = elem(tg, 1)
 586  		case TagImage:
 587  			p.Cover = parseBlobRef(tg)
 588  		case TagA:
 589  			addr, aerr := AddressFromTag(tg)
 590  			if aerr != nil {
 591  				err = aerr
 592  				return
 593  			}
 594  			p.Tracks = push(p.Tracks, addr)
 595  		default:
 596  			p.Extra = appendTag(p.Extra, tg)
 597  		}
 598  	}
 599  	return
 600  }
 601  
 602  func (p *Playlist) Validate() (err error) {
 603  	if p == nil {
 604  		err = errorf.E("nil playlist")
 605  		return
 606  	}
 607  	if p.D == "" {
 608  		err = errorf.E("playlist: empty d tag")
 609  		return
 610  	}
 611  	for i := range p.Tracks {
 612  		if p.Tracks[i].Kind != KindTrack {
 613  			err = errorf.E("playlist: member %d is kind %d, want %d", i, p.Tracks[i].Kind, KindTrack)
 614  			return
 615  		}
 616  	}
 617  	return
 618  }
 619  
 620  func (p *Playlist) IsPrivate() (ok bool) {
 621  	return len(p.Tracks) == 0 && p.Content != ""
 622  }
 623  
 624  // Dleq is the NUT-12 proof that makes a blind signature verifiable by a third
 625  // party. On secp256k1 the check C == K*H(m) is a pairing equation and cannot be
 626  // computed at all, so without this proof a paid play report is verifiable only
 627  // by the host that signed it.
 628  type Dleq struct {
 629  	E string
 630  	S string
 631  }
 632  
 633  // PlayReport is the payload of a kind 3221 event: one play, one immutable
 634  // record, counted by relays (NIP-45). A free play is self-attested and carries
 635  // no host proof; a paid play carries the host's blind signature and the DLEQ
 636  // proof.
 637  type PlayReport struct {
 638  	Asset     *Address
 639  	Artist    string
 640  	Blob      string
 641  	Server    string
 642  	Codec     string
 643  	T0        string
 644  	T1        string
 645  	Blind     string
 646  	Host      string
 647  	Statement string
 648  	Dleq      *Dleq
 649  	Content   string
 650  	Extra     [][]string
 651  }
 652  
 653  func (r *PlayReport) Tags() (tags [][]string) {
 654  	tags = appendTag(nil, r.Asset.Tag())
 655  	tags = appendKV(tags, TagP, r.Artist)
 656  	tags = appendKV(tags, TagX, r.Blob)
 657  	tags = appendKV(tags, TagServer, r.Server)
 658  	tags = appendKV(tags, TagCodec, r.Codec)
 659  	tags = appendKV(tags, TagT0, r.T0)
 660  	tags = appendKV(tags, TagT1, r.T1)
 661  	tags = appendKV(tags, TagBlind, r.Blind)
 662  	tags = appendKV(tags, TagHost, r.Host)
 663  	tags = appendKV(tags, TagStatement, r.Statement)
 664  	if r.Dleq != nil {
 665  		d := []string{:3}
 666  		d[0] = TagDleq
 667  		d[1] = r.Dleq.E
 668  		d[2] = r.Dleq.S
 669  		tags = appendTag(tags, d)
 670  	}
 671  	tags = appendTags(tags, r.Extra)
 672  	return
 673  }
 674  
 675  func ParsePlayReport(tags [][]string, content string) (r *PlayReport, err error) {
 676  	r = &PlayReport{Content: content}
 677  	for i := range tags {
 678  		tg := tags[i]
 679  		if len(tg) < 1 {
 680  			continue
 681  		}
 682  		switch tg[0] {
 683  		case TagA:
 684  			addr, aerr := AddressFromTag(tg)
 685  			if aerr != nil {
 686  				err = aerr
 687  				return
 688  			}
 689  			r.Asset = addr
 690  		case TagP:
 691  			r.Artist = elem(tg, 1)
 692  		case TagX:
 693  			r.Blob = elem(tg, 1)
 694  		case TagServer:
 695  			r.Server = elem(tg, 1)
 696  		case TagCodec:
 697  			r.Codec = elem(tg, 1)
 698  		case TagT0:
 699  			r.T0 = elem(tg, 1)
 700  		case TagT1:
 701  			r.T1 = elem(tg, 1)
 702  		case TagBlind:
 703  			r.Blind = elem(tg, 1)
 704  		case TagHost:
 705  			r.Host = elem(tg, 1)
 706  		case TagStatement:
 707  			r.Statement = elem(tg, 1)
 708  		case TagDleq:
 709  			r.Dleq = &Dleq{E: elem(tg, 1), S: elem(tg, 2)}
 710  		default:
 711  			r.Extra = appendTag(r.Extra, tg)
 712  		}
 713  	}
 714  	return
 715  }
 716  
 717  // IsPaid reports whether the report carries a host proof at all.
 718  func (r *PlayReport) IsPaid() (ok bool) {
 719  	return r.Blind != "" || r.Host != "" || r.Statement != "" || r.Dleq != nil
 720  }
 721  
 722  func (r *PlayReport) Validate() (err error) {
 723  	if r == nil {
 724  		err = errorf.E("nil play report")
 725  		return
 726  	}
 727  	if r.Asset == nil {
 728  		err = errorf.E("play report: no asset address")
 729  		return
 730  	}
 731  	if r.Asset.Kind != KindTrack {
 732  		err = errorf.E("play report: asset is kind %d, want %d", r.Asset.Kind, KindTrack)
 733  		return
 734  	}
 735  	if r.Blob != "" && !IsHex64(r.Blob) {
 736  		err = errorf.E("play report: blob %q is not 64 hex characters", r.Blob)
 737  		return
 738  	}
 739  	if r.Artist != "" && !IsHex64(r.Artist) {
 740  		err = errorf.E("play report: artist pubkey is not 64 hex characters")
 741  		return
 742  	}
 743  	if r.Host != "" && !IsHex64(r.Host) {
 744  		err = errorf.E("play report: host pubkey is not 64 hex characters")
 745  		return
 746  	}
 747  	// A paid play is all of the four fields or none: a blind signature without
 748  	// its DLEQ proof cannot be verified by anyone but the host.
 749  	if r.IsPaid() {
 750  		if r.Blind == "" || r.Host == "" || r.Statement == "" || r.Dleq == nil {
 751  			err = errorf.E("play report: paid play needs blind, host, statement and dleq")
 752  			return
 753  		}
 754  	}
 755  	if r.Codec != "" && !IsCodecSpec(r.Codec) {
 756  		err = errorf.E("play report: codec %q is not a codec spec", r.Codec)
 757  		return
 758  	}
 759  	return
 760  }
 761  
 762  // PlayStatement is the canonical string a host blind-signs, per PROTOCOL_FLOWS
 763  // R2 and the applied decision G-3: the fields are joined by '|' and no field
 764  // may contain one. It covers only what the host itself observed - asset, blob
 765  // and window - because a host that blind-signs an arbitrary statement is a
 766  // signing oracle, and money is accounted by receipts and aggregates instead.
 767  func PlayStatement(assetAddress, blob, start, end string) (s string) {
 768  	return "musiquay.play:v1|" | assetAddress | "|" | blob | "|" | start | "|" | end
 769  }
 770  
 771  // --- shared parse helpers ---
 772  
 773  func elem(tag []string, i int32) (s string) {
 774  	if i < 0 || i >= int32(len(tag)) {
 775  		return ""
 776  	}
 777  	return tag[i]
 778  }
 779  
 780  func parseParticipant(tag []string) (p Participant, err error) {
 781  	if len(tag) < 2 {
 782  		err = errorf.E("p tag needs a pubkey")
 783  		return
 784  	}
 785  	p.Pubkey = tag[1]
 786  	if len(tag) > 2 {
 787  		p.Relay = tag[2]
 788  	}
 789  	if len(tag) > 3 {
 790  		p.Role = tag[3]
 791  	}
 792  	return
 793  }
 794  
 795  func parsePrice(tag []string) (p *Price) {
 796  	if len(tag) < 3 {
 797  		return nil
 798  	}
 799  	p = &Price{Amount: tag[1], Unit: tag[2]}
 800  	if len(tag) > 3 {
 801  		p.Period = tag[3]
 802  	}
 803  	return
 804  }
 805  
 806  func parseBlobRef(tag []string) (b *BlobRef) {
 807  	if len(tag) < 2 {
 808  		return nil
 809  	}
 810  	b = &BlobRef{Sha256: tag[1]}
 811  	if len(tag) > 2 {
 812  		b.Server = tag[2]
 813  	}
 814  	return
 815  }
 816  
 817  func parseEncoding(tag []string) (e Encoding) {
 818  	e.Sha256 = elem(tag, 1)
 819  	e.Spec = elem(tag, 2)
 820  	e.Server = elem(tag, 3)
 821  	e.MIME = elem(tag, 4)
 822  	if len(tag) > 5 {
 823  		if n, err := ParseInt64(tag[5]); err == nil {
 824  			e.Size = n
 825  			e.HasSize = true
 826  		}
 827  	}
 828  	return
 829  }
 830  
 831  func encodingHas(encs []Encoding, hash string) (ok bool) {
 832  	for i := range encs {
 833  		if encs[i].Sha256 == hash {
 834  			return true
 835  		}
 836  	}
 837  	return false
 838  }
 839  
 840  // pushExternalI records an `i` value against the last external id, so an i/k
 841  // pair is one entry however the tags are ordered.
 842  func pushExternalI(xs []ExternalID, i string) (out []ExternalID) {
 843  	out = xs
 844  	out = push(out, ExternalID{I: i})
 845  	return
 846  }
 847  
 848  func pushExternalK(xs []ExternalID, k string) (out []ExternalID) {
 849  	out = xs
 850  	if len(out) > 0 && out[len(out)-1].K == "" {
 851  		out[len(out)-1].K = k
 852  		return
 853  	}
 854  	out = push(out, ExternalID{K: k})
 855  	return
 856  }
 857  
 858  func appendKV(dst [][]string, k, v string) (out [][]string) {
 859  	out = dst
 860  	if v == "" {
 861  		return
 862  	}
 863  	t := []string{:2}
 864  	t[0] = k
 865  	t[1] = v
 866  	out = push(out, t)
 867  	return
 868  }
 869  
 870  func appendTag(dst [][]string, t []string) (out [][]string) {
 871  	out = dst
 872  	if len(t) == 0 {
 873  		return
 874  	}
 875  	out = push(out, t)
 876  	return
 877  }
 878  
 879  func appendTags(dst, src [][]string) (out [][]string) {
 880  	out = dst
 881  	for i := range src {
 882  		out = appendTag(out, src[i])
 883  	}
 884  	return
 885  }
 886