···72727373air should automatically build and run piper, and watch for changes on relevant files.
74747575+#### Lexicon changes
7676+1. Copy the new or changed json schema files to the [lexicon folders](./lexicons)
7777+2. run `make go-lexicons`
7878+7979+Go types should be updated and should have the changes to the schemas
8080+7581#### docker
7682We also provide a docker compose file to use to run piper locally. There are a few edits to the [.env](.env) to make it run smoother in a container
7783`SERVER_HOST`- `0.0.0.0`
7884`DB_PATH` = `/db/piper.db` to persist your piper db through container restarts
79858080-Make sure you have docker and docker compose installed, then you can run piper with `docker compose up`8686+Make sure you have docker and docker compose installed, then you can run piper with `docker compose up`
+346-177
api/teal/cbor_gen.go
···2727 }
28282929 cw := cbg.NewCborWriter(w)
3030- fieldCount := 14
3030+ fieldCount := 15
31313232 if t.ArtistMbIds == nil {
3333+ fieldCount--
3434+ }
3535+3636+ if t.ArtistNames == nil {
3737+ fieldCount--
3838+ }
3939+4040+ if t.Artists == nil {
3341 fieldCount--
3442 }
3543···126134 }
127135 if _, err := cw.WriteString(string("fm.teal.alpha.feed.play")); err != nil {
128136 return err
137137+ }
138138+139139+ // t.Artists ([]*teal.AlphaFeedDefs_Artist) (slice)
140140+ if t.Artists != nil {
141141+142142+ if len("artists") > 1000000 {
143143+ return xerrors.Errorf("Value in field \"artists\" was too long")
144144+ }
145145+146146+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artists"))); err != nil {
147147+ return err
148148+ }
149149+ if _, err := cw.WriteString(string("artists")); err != nil {
150150+ return err
151151+ }
152152+153153+ if len(t.Artists) > 8192 {
154154+ return xerrors.Errorf("Slice value in field t.Artists was too long")
155155+ }
156156+157157+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Artists))); err != nil {
158158+ return err
159159+ }
160160+ for _, v := range t.Artists {
161161+ if err := v.MarshalCBOR(cw); err != nil {
162162+ return err
163163+ }
164164+165165+ }
129166 }
130167131168 // t.Duration (int64) (int64)
···316353 }
317354318355 // t.ArtistNames ([]string) (slice)
319319- if len("artistNames") > 1000000 {
320320- return xerrors.Errorf("Value in field \"artistNames\" was too long")
321321- }
356356+ if t.ArtistNames != nil {
322357323323- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistNames"))); err != nil {
324324- return err
325325- }
326326- if _, err := cw.WriteString(string("artistNames")); err != nil {
327327- return err
328328- }
358358+ if len("artistNames") > 1000000 {
359359+ return xerrors.Errorf("Value in field \"artistNames\" was too long")
360360+ }
329361330330- if len(t.ArtistNames) > 8192 {
331331- return xerrors.Errorf("Slice value in field t.ArtistNames was too long")
332332- }
362362+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistNames"))); err != nil {
363363+ return err
364364+ }
365365+ if _, err := cw.WriteString(string("artistNames")); err != nil {
366366+ return err
367367+ }
333368334334- if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ArtistNames))); err != nil {
335335- return err
336336- }
337337- for _, v := range t.ArtistNames {
338338- if len(v) > 1000000 {
339339- return xerrors.Errorf("Value in field v was too long")
369369+ if len(t.ArtistNames) > 8192 {
370370+ return xerrors.Errorf("Slice value in field t.ArtistNames was too long")
340371 }
341372342342- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
373373+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ArtistNames))); err != nil {
343374 return err
344375 }
345345- if _, err := cw.WriteString(string(v)); err != nil {
346346- return err
347347- }
376376+ for _, v := range t.ArtistNames {
377377+ if len(v) > 1000000 {
378378+ return xerrors.Errorf("Value in field v was too long")
379379+ }
348380381381+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
382382+ return err
383383+ }
384384+ if _, err := cw.WriteString(string(v)); err != nil {
385385+ return err
386386+ }
387387+388388+ }
349389 }
350390351391 // t.ReleaseMbId (string) (string)
···582622 }
583623584624 t.LexiconTypeID = string(sval)
625625+ }
626626+ // t.Artists ([]*teal.AlphaFeedDefs_Artist) (slice)
627627+ case "artists":
628628+629629+ maj, extra, err = cr.ReadHeader()
630630+ if err != nil {
631631+ return err
632632+ }
633633+634634+ if extra > 8192 {
635635+ return fmt.Errorf("t.Artists: array too large (%d)", extra)
636636+ }
637637+638638+ if maj != cbg.MajArray {
639639+ return fmt.Errorf("expected cbor array")
640640+ }
641641+642642+ if extra > 0 {
643643+ t.Artists = make([]*AlphaFeedDefs_Artist, extra)
644644+ }
645645+646646+ for i := 0; i < int(extra); i++ {
647647+ {
648648+ var maj byte
649649+ var extra uint64
650650+ var err error
651651+ _ = maj
652652+ _ = extra
653653+ _ = err
654654+655655+ {
656656+657657+ b, err := cr.ReadByte()
658658+ if err != nil {
659659+ return err
660660+ }
661661+ if b != cbg.CborNull[0] {
662662+ if err := cr.UnreadByte(); err != nil {
663663+ return err
664664+ }
665665+ t.Artists[i] = new(AlphaFeedDefs_Artist)
666666+ if err := t.Artists[i].UnmarshalCBOR(cr); err != nil {
667667+ return xerrors.Errorf("unmarshaling t.Artists[i] pointer: %w", err)
668668+ }
669669+ }
670670+671671+ }
672672+673673+ }
585674 }
586675 // t.Duration (int64) (int64)
587676 case "duration":
···17331822 }
1734182317351824 cw := cbg.NewCborWriter(w)
17361736- fieldCount := 13
17371737-17381738- if t.ArtistMbIds == nil {
17391739- fieldCount--
17401740- }
18251825+ fieldCount := 12
1741182617421827 if t.Duration == nil {
17431828 fieldCount--
···18131898 return err
18141899 }
18151900 }
19011901+ }
19021902+19031903+ // t.Artists ([]*teal.AlphaFeedDefs_Artist) (slice)
19041904+ if len("artists") > 1000000 {
19051905+ return xerrors.Errorf("Value in field \"artists\" was too long")
19061906+ }
19071907+19081908+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artists"))); err != nil {
19091909+ return err
19101910+ }
19111911+ if _, err := cw.WriteString(string("artists")); err != nil {
19121912+ return err
19131913+ }
19141914+19151915+ if len(t.Artists) > 8192 {
19161916+ return xerrors.Errorf("Slice value in field t.Artists was too long")
19171917+ }
19181918+19191919+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Artists))); err != nil {
19201920+ return err
19211921+ }
19221922+ for _, v := range t.Artists {
19231923+ if err := v.MarshalCBOR(cw); err != nil {
19241924+ return err
19251925+ }
19261926+18161927 }
1817192818181929 // t.Duration (int64) (int64)
···19662077 }
19672078 }
1968207919691969- // t.ArtistMbIds ([]string) (slice)
19701970- if t.ArtistMbIds != nil {
19711971-19721972- if len("artistMbIds") > 1000000 {
19731973- return xerrors.Errorf("Value in field \"artistMbIds\" was too long")
19741974- }
19751975-19761976- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistMbIds"))); err != nil {
19771977- return err
19781978- }
19791979- if _, err := cw.WriteString(string("artistMbIds")); err != nil {
19801980- return err
19811981- }
19821982-19831983- if len(t.ArtistMbIds) > 8192 {
19841984- return xerrors.Errorf("Slice value in field t.ArtistMbIds was too long")
19851985- }
19861986-19871987- if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ArtistMbIds))); err != nil {
19881988- return err
19891989- }
19901990- for _, v := range t.ArtistMbIds {
19911991- if len(v) > 1000000 {
19921992- return xerrors.Errorf("Value in field v was too long")
19931993- }
19941994-19951995- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
19961996- return err
19971997- }
19981998- if _, err := cw.WriteString(string(v)); err != nil {
19991999- return err
20002000- }
20012001-20022002- }
20032003- }
20042004-20052005- // t.ArtistNames ([]string) (slice)
20062006- if len("artistNames") > 1000000 {
20072007- return xerrors.Errorf("Value in field \"artistNames\" was too long")
20082008- }
20092009-20102010- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistNames"))); err != nil {
20112011- return err
20122012- }
20132013- if _, err := cw.WriteString(string("artistNames")); err != nil {
20142014- return err
20152015- }
20162016-20172017- if len(t.ArtistNames) > 8192 {
20182018- return xerrors.Errorf("Slice value in field t.ArtistNames was too long")
20192019- }
20202020-20212021- if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.ArtistNames))); err != nil {
20222022- return err
20232023- }
20242024- for _, v := range t.ArtistNames {
20252025- if len(v) > 1000000 {
20262026- return xerrors.Errorf("Value in field v was too long")
20272027- }
20282028-20292029- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
20302030- return err
20312031- }
20322032- if _, err := cw.WriteString(string(v)); err != nil {
20332033- return err
20342034- }
20352035-20362036- }
20372037-20382080 // t.ReleaseMbId (string) (string)
20392081 if t.ReleaseMbId != nil {
20402082···22592301 t.Isrc = (*string)(&sval)
22602302 }
22612303 }
23042304+ // t.Artists ([]*teal.AlphaFeedDefs_Artist) (slice)
23052305+ case "artists":
23062306+23072307+ maj, extra, err = cr.ReadHeader()
23082308+ if err != nil {
23092309+ return err
23102310+ }
23112311+23122312+ if extra > 8192 {
23132313+ return fmt.Errorf("t.Artists: array too large (%d)", extra)
23142314+ }
23152315+23162316+ if maj != cbg.MajArray {
23172317+ return fmt.Errorf("expected cbor array")
23182318+ }
23192319+23202320+ if extra > 0 {
23212321+ t.Artists = make([]*AlphaFeedDefs_Artist, extra)
23222322+ }
23232323+23242324+ for i := 0; i < int(extra); i++ {
23252325+ {
23262326+ var maj byte
23272327+ var extra uint64
23282328+ var err error
23292329+ _ = maj
23302330+ _ = extra
23312331+ _ = err
23322332+23332333+ {
23342334+23352335+ b, err := cr.ReadByte()
23362336+ if err != nil {
23372337+ return err
23382338+ }
23392339+ if b != cbg.CborNull[0] {
23402340+ if err := cr.UnreadByte(); err != nil {
23412341+ return err
23422342+ }
23432343+ t.Artists[i] = new(AlphaFeedDefs_Artist)
23442344+ if err := t.Artists[i].UnmarshalCBOR(cr); err != nil {
23452345+ return xerrors.Errorf("unmarshaling t.Artists[i] pointer: %w", err)
23462346+ }
23472347+ }
23482348+23492349+ }
23502350+23512351+ }
23522352+ }
22622353 // t.Duration (int64) (int64)
22632354 case "duration":
22642355 {
···23692460 t.PlayedTime = (*string)(&sval)
23702461 }
23712462 }
23722372- // t.ArtistMbIds ([]string) (slice)
23732373- case "artistMbIds":
23742374-23752375- maj, extra, err = cr.ReadHeader()
23762376- if err != nil {
23772377- return err
23782378- }
23792379-23802380- if extra > 8192 {
23812381- return fmt.Errorf("t.ArtistMbIds: array too large (%d)", extra)
23822382- }
23832383-23842384- if maj != cbg.MajArray {
23852385- return fmt.Errorf("expected cbor array")
23862386- }
23872387-23882388- if extra > 0 {
23892389- t.ArtistMbIds = make([]string, extra)
23902390- }
23912391-23922392- for i := 0; i < int(extra); i++ {
23932393- {
23942394- var maj byte
23952395- var extra uint64
23962396- var err error
23972397- _ = maj
23982398- _ = extra
23992399- _ = err
24002400-24012401- {
24022402- sval, err := cbg.ReadStringWithMax(cr, 1000000)
24032403- if err != nil {
24042404- return err
24052405- }
24062406-24072407- t.ArtistMbIds[i] = string(sval)
24082408- }
24092409-24102410- }
24112411- }
24122412- // t.ArtistNames ([]string) (slice)
24132413- case "artistNames":
24142414-24152415- maj, extra, err = cr.ReadHeader()
24162416- if err != nil {
24172417- return err
24182418- }
24192419-24202420- if extra > 8192 {
24212421- return fmt.Errorf("t.ArtistNames: array too large (%d)", extra)
24222422- }
24232423-24242424- if maj != cbg.MajArray {
24252425- return fmt.Errorf("expected cbor array")
24262426- }
24272427-24282428- if extra > 0 {
24292429- t.ArtistNames = make([]string, extra)
24302430- }
24312431-24322432- for i := 0; i < int(extra); i++ {
24332433- {
24342434- var maj byte
24352435- var extra uint64
24362436- var err error
24372437- _ = maj
24382438- _ = extra
24392439- _ = err
24402440-24412441- {
24422442- sval, err := cbg.ReadStringWithMax(cr, 1000000)
24432443- if err != nil {
24442444- return err
24452445- }
24462446-24472447- t.ArtistNames[i] = string(sval)
24482448- }
24492449-24502450- }
24512451- }
24522463 // t.ReleaseMbId (string) (string)
24532464 case "releaseMbId":
24542465···2565257625662577 return nil
25672578}
25792579+func (t *AlphaFeedDefs_Artist) MarshalCBOR(w io.Writer) error {
25802580+ if t == nil {
25812581+ _, err := w.Write(cbg.CborNull)
25822582+ return err
25832583+ }
25842584+25852585+ cw := cbg.NewCborWriter(w)
25862586+ fieldCount := 2
25872587+25882588+ if t.ArtistMbId == nil {
25892589+ fieldCount--
25902590+ }
25912591+25922592+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
25932593+ return err
25942594+ }
25952595+25962596+ // t.ArtistMbId (string) (string)
25972597+ if t.ArtistMbId != nil {
25982598+25992599+ if len("artistMbId") > 1000000 {
26002600+ return xerrors.Errorf("Value in field \"artistMbId\" was too long")
26012601+ }
26022602+26032603+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistMbId"))); err != nil {
26042604+ return err
26052605+ }
26062606+ if _, err := cw.WriteString(string("artistMbId")); err != nil {
26072607+ return err
26082608+ }
26092609+26102610+ if t.ArtistMbId == nil {
26112611+ if _, err := cw.Write(cbg.CborNull); err != nil {
26122612+ return err
26132613+ }
26142614+ } else {
26152615+ if len(*t.ArtistMbId) > 1000000 {
26162616+ return xerrors.Errorf("Value in field t.ArtistMbId was too long")
26172617+ }
26182618+26192619+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ArtistMbId))); err != nil {
26202620+ return err
26212621+ }
26222622+ if _, err := cw.WriteString(string(*t.ArtistMbId)); err != nil {
26232623+ return err
26242624+ }
26252625+ }
26262626+ }
26272627+26282628+ // t.ArtistName (string) (string)
26292629+ if len("artistName") > 1000000 {
26302630+ return xerrors.Errorf("Value in field \"artistName\" was too long")
26312631+ }
26322632+26332633+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artistName"))); err != nil {
26342634+ return err
26352635+ }
26362636+ if _, err := cw.WriteString(string("artistName")); err != nil {
26372637+ return err
26382638+ }
26392639+26402640+ if len(t.ArtistName) > 1000000 {
26412641+ return xerrors.Errorf("Value in field t.ArtistName was too long")
26422642+ }
26432643+26442644+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ArtistName))); err != nil {
26452645+ return err
26462646+ }
26472647+ if _, err := cw.WriteString(string(t.ArtistName)); err != nil {
26482648+ return err
26492649+ }
26502650+ return nil
26512651+}
26522652+26532653+func (t *AlphaFeedDefs_Artist) UnmarshalCBOR(r io.Reader) (err error) {
26542654+ *t = AlphaFeedDefs_Artist{}
26552655+26562656+ cr := cbg.NewCborReader(r)
26572657+26582658+ maj, extra, err := cr.ReadHeader()
26592659+ if err != nil {
26602660+ return err
26612661+ }
26622662+ defer func() {
26632663+ if err == io.EOF {
26642664+ err = io.ErrUnexpectedEOF
26652665+ }
26662666+ }()
26672667+26682668+ if maj != cbg.MajMap {
26692669+ return fmt.Errorf("cbor input should be of type map")
26702670+ }
26712671+26722672+ if extra > cbg.MaxLength {
26732673+ return fmt.Errorf("AlphaFeedDefs_Artist: map struct too large (%d)", extra)
26742674+ }
26752675+26762676+ n := extra
26772677+26782678+ nameBuf := make([]byte, 10)
26792679+ for i := uint64(0); i < n; i++ {
26802680+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
26812681+ if err != nil {
26822682+ return err
26832683+ }
26842684+26852685+ if !ok {
26862686+ // Field doesn't exist on this type, so ignore it
26872687+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
26882688+ return err
26892689+ }
26902690+ continue
26912691+ }
26922692+26932693+ switch string(nameBuf[:nameLen]) {
26942694+ // t.ArtistMbId (string) (string)
26952695+ case "artistMbId":
26962696+26972697+ {
26982698+ b, err := cr.ReadByte()
26992699+ if err != nil {
27002700+ return err
27012701+ }
27022702+ if b != cbg.CborNull[0] {
27032703+ if err := cr.UnreadByte(); err != nil {
27042704+ return err
27052705+ }
27062706+27072707+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
27082708+ if err != nil {
27092709+ return err
27102710+ }
27112711+27122712+ t.ArtistMbId = (*string)(&sval)
27132713+ }
27142714+ }
27152715+ // t.ArtistName (string) (string)
27162716+ case "artistName":
27172717+27182718+ {
27192719+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
27202720+ if err != nil {
27212721+ return err
27222722+ }
27232723+27242724+ t.ArtistName = string(sval)
27252725+ }
27262726+27272727+ default:
27282728+ // Field doesn't exist on this type, so ignore it
27292729+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
27302730+ return err
27312731+ }
27322732+ }
27332733+ }
27342734+27352735+ return nil
27362736+}
+10-4
api/teal/feeddefs.go
···4455// schema: fm.teal.alpha.feed.defs
6677+// AlphaFeedDefs_Artist is a "artist" in the fm.teal.alpha.feed.defs schema.
88+type AlphaFeedDefs_Artist struct {
99+ // artistMbId: The Musicbrainz ID of the artist
1010+ ArtistMbId *string `json:"artistMbId,omitempty" cborgen:"artistMbId,omitempty"`
1111+ // artistName: The name of the artist
1212+ ArtistName string `json:"artistName" cborgen:"artistName"`
1313+}
1414+715// AlphaFeedDefs_PlayView is a "playView" in the fm.teal.alpha.feed.defs schema.
816type AlphaFeedDefs_PlayView struct {
99- // artistMbIds: Array of Musicbrainz artist IDs
1010- ArtistMbIds []string `json:"artistMbIds,omitempty" cborgen:"artistMbIds,omitempty"`
1111- // artistNames: Array of artist names in order of original appearance.
1212- ArtistNames []string `json:"artistNames" cborgen:"artistNames"`
1717+ // artists: Array of artists in order of original appearance.
1818+ Artists []*AlphaFeedDefs_Artist `json:"artists" cborgen:"artists"`
1319 // duration: The length of the track in seconds
1420 Duration *int64 `json:"duration,omitempty" cborgen:"duration,omitempty"`
1521 // isrc: The ISRC code associated with the recording
+5-3
api/teal/feedplay.go
···1414// RECORDTYPE: AlphaFeedPlay
1515type AlphaFeedPlay struct {
1616 LexiconTypeID string `json:"$type,const=fm.teal.alpha.feed.play" cborgen:"$type,const=fm.teal.alpha.feed.play"`
1717- // artistMbIds: Array of Musicbrainz artist IDs
1717+ // artistMbIds: Array of Musicbrainz artist IDs. Prefer using 'artists'.
1818 ArtistMbIds []string `json:"artistMbIds,omitempty" cborgen:"artistMbIds,omitempty"`
1919- // artistNames: Array of artist names in order of original appearance.
2020- ArtistNames []string `json:"artistNames" cborgen:"artistNames"`
1919+ // artistNames: Array of artist names in order of original appearance. Prefer using 'artists'.
2020+ ArtistNames []string `json:"artistNames,omitempty" cborgen:"artistNames,omitempty"`
2121+ // artists: Array of artists in order of original appearance.
2222+ Artists []*AlphaFeedDefs_Artist `json:"artists,omitempty" cborgen:"artists,omitempty"`
2123 // duration: The length of the track in seconds
2224 Duration *int64 `json:"duration,omitempty" cborgen:"duration,omitempty"`
2325 // isrc: The ISRC code associated with the recording
+22-14
lexicons/teal/feed/defs.json
···55 "defs": {
66 "playView": {
77 "type": "object",
88- "required": ["trackName", "artistNames"],
88+ "required": ["trackName", "artists"],
99 "properties": {
1010 "trackName": {
1111 "type": "string",
···2626 "type": "integer",
2727 "description": "The length of the track in seconds"
2828 },
2929- "artistNames": {
3030- "type": "array",
3131- "items": {
3232- "type": "string",
3333- "minLength": 1,
3434- "maxLength": 256,
3535- "maxGraphemes": 2560
3636- },
3737- "description": "Array of artist names in order of original appearance."
3838- },
3939- "artistMbIds": {
2929+ "artists": {
4030 "type": "array",
4131 "items": {
4242- "type": "string"
3232+ "type": "ref",
3333+ "ref": "#artist"
4334 },
4444- "description": "Array of Musicbrainz artist IDs"
3535+ "description": "Array of artists in order of original appearance."
4536 },
4637 "releaseName": {
4738 "type": "string",
···7566 "type": "string",
7667 "format": "datetime",
7768 "description": "The unix timestamp of when the track was played"
6969+ }
7070+ }
7171+ },
7272+ "artist": {
7373+ "type": "object",
7474+ "required": ["artistName"],
7575+ "properties": {
7676+ "artistName": {
7777+ "type": "string",
7878+ "minLength": 1,
7979+ "maxLength": 256,
8080+ "maxGraphemes": 2560,
8181+ "description": "The name of the artist"
8282+ },
8383+ "artistMbId": {
8484+ "type": "string",
8585+ "description": "The Musicbrainz ID of the artist"
7886 }
7987 }
8088 }
+11-3
lexicons/teal/feed/play.json
···88 "key": "tid",
99 "record": {
1010 "type": "object",
1111- "required": ["trackName", "artistNames"],
1111+ "required": ["trackName"],
1212 "properties": {
1313 "trackName": {
1414 "type": "string",
···3838 "maxLength": 256,
3939 "maxGraphemes": 2560
4040 },
4141- "description": "Array of artist names in order of original appearance."
4141+ "description": "Array of artist names in order of original appearance. Prefer using 'artists'."
4242 },
4343 "artistMbIds": {
4444 "type": "array",
4545 "items": {
4646 "type": "string"
4747 },
4848- "description": "Array of Musicbrainz artist IDs"
4848+ "description": "Array of Musicbrainz artist IDs. Prefer using 'artists'."
4949+ },
5050+ "artists": {
5151+ "type": "array",
5252+ "items": {
5353+ "type": "ref",
5454+ "ref": "fm.teal.alpha.feed.defs#artist"
5555+ },
5656+ "description": "Array of artists in order of original appearance."
4957 },
5058 "releaseName": {
5159 "type": "string",