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