json.gno
7.40 Kb · 232 lines
1package catalog
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
8 "gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
9)
10
11// JSON exports for the GnoRadio app and indexers (vm/qeval). Their shapes
12// are the app's contract (app/src/lib): change them together.
13
14func ints(xs []int) string {
15 var b strings.Builder
16 b.WriteString("[")
17 for i, x := range xs {
18 if i > 0 {
19 b.WriteString(",")
20 }
21 b.WriteString(text.Itoa(x))
22 }
23 b.WriteString("]")
24 return b.String()
25}
26
27// The JSON is written from the records' fields as stored: numbers are
28// already decimal strings, and converting them costs gas.
29
30// trackJSON writes track id from its record and its artist's head.
31func trackJSON(id int, rec, arec string) string {
32 f := store.Fields(rec)
33 out := `{"id":` + text.Itoa(id) + `,"artist":` + f[fArtist] + `,"artistName":"` + store.Field(arec, aName) +
34 `","claimed":` + strconv.FormatBool(store.Field(arec, aOwner) != "") + `,"origin":"` + f[fOrigin] + `","title":"` + f[fTitle] +
35 `","genre":` + f[fGenre] + `,"duration":` + f[fDuration] + `,"license":"` + f[fLicense] +
36 `","cmo":"` + f[fCMO] + `","credits":"` + f[fCredits] + `","audio":"` + f[fAudio] + `","audioSha256":"` + f[fAudioSHA] +
37 `","cover":"` + f[fCover] + `","coverSha256":"` + f[fCoverSHA] + `","source":"` + f[fSource] +
38 `","attribution":"` + f[fAttribution] + `","album":` + f[fAlbum] + `,"created":` + f[fCreated] +
39 `,"likes":` + f[fLikes] + `,"tips":` + f[fTips] + `,"supporters":` + f[fSupporters] + `,"splits":[`
40 if f[fSplits] != "" {
41 for i, p := range strings.Split(f[fSplits], ",") {
42 to, pct, _ := strings.Cut(p, ":")
43 if i > 0 {
44 out += ","
45 }
46 out += `{"to":"` + to + `","pct":` + pct + `}`
47 }
48 }
49 return out + `]}`
50}
51
52// playableRec returns a track's record and its artist's head when both are
53// visible ("" otherwise).
54func playableRec(t *tx, id int) (rec, arec string) {
55 rec = t.rec(cTracks, id)
56 if rec == "" || store.Field(rec, fHidden) != "" {
57 return "", ""
58 }
59 arec = t.rec(cArtists, text.Atoi(store.Field(rec, fArtist)))
60 if arec == "" || store.Field(arec, aHidden) != "" {
61 return "", ""
62 }
63 return rec, arec
64}
65
66// TrackJSON returns one playable track.
67func TrackJSON(id int) string {
68 rec, arec := playableRec(rd(), id)
69 if rec == "" {
70 panic("catalog: unknown track")
71 }
72 return trackJSON(id, rec, arec)
73}
74
75// TracksJSON pages through playable tracks (newest first), at most 100.
76// Chunks and artist heads are read once per page.
77func TracksJSON(offset, limit int) string {
78 limit = store.Limit(limit, maxBatch)
79 t := rd()
80 total := int(t.count(mTracks))
81 var b strings.Builder
82 b.WriteString(`{"total":` + text.Itoa(total) + `,"tracks":[`)
83 n := 0
84 for _, id := range seqPage(total, offset, limit, true) {
85 if rec, arec := playableRec(t, id); rec != "" {
86 if n > 0 {
87 b.WriteString(",")
88 }
89 b.WriteString(trackJSON(id, rec, arec))
90 n++
91 }
92 }
93 b.WriteString(`]}`)
94 return b.String()
95}
96
97func ArtistJSON(id int) string {
98 t := rd()
99 arec := t.rec(cArtists, id)
100 if arec == "" || store.Field(arec, aHidden) != "" {
101 panic("catalog: unknown artist")
102 }
103 return artistJSON(t, id, arec)
104}
105
106func artistJSON(t *tx, id int, arec string) string {
107 f := store.Fields(arec)
108 trackIDs, albumIDs := lists(t, id)
109 promo, ok := t.get(cPromo, store.Pad(id))
110 if !ok {
111 promo = text.Itoa(defaultPromoPct)
112 }
113 a := &Artist{ID: id, Hidden: f[aHidden] != "", Owner: address(f[aOwner]), Verified: f[aVerified] != ""}
114 return `{"id":` + text.Itoa(id) + `,"name":` + text.Str(f[aName]) + `,"kind":` + text.Str(f[aKind]) + `,"owner":` + text.Str(f[aOwner]) +
115 `,"bio":` + text.Str(f[aBio]) + `,"source":` + text.Str(f[aSource]) + `,"joined":` + f[aJoined] + `,"tips":` + f[aTips] +
116 `,"followers":` + f[aFollowers] + `,"verified":` + strconv.FormatBool(a.Verified) +
117 `,"proofHost":` + text.Str(proofHost(t, id, f[aVerified])) + `,"promo":` + promo +
118 `,"sponsor":` + text.Itoa64(offer(t, a)) + `,"tracks":` + store.JSON(trackIDs) + `,"albums":` + store.JSON(albumIDs) + `}`
119}
120
121// albumJSON writes a visible album ("" when it or its artist is hidden).
122func albumJSON(t *tx, id int) string {
123 rec := t.rec(cAlbums, id)
124 if rec == "" {
125 return ""
126 }
127 f := store.Fields(rec)
128 if f[6] != "" || !artistVisible(t, text.Atoi(f[0])) {
129 return ""
130 }
131 return `{"id":` + text.Itoa(id) + `,"artist":` + f[0] + `,"title":` + text.Str(f[1]) + `,"cover":` + text.Str(f[2]) +
132 `,"coverSha256":` + text.Str(f[3]) + `,"year":` + f[4] + `,"tracks":` + store.JSON(f[7]) + `}`
133}
134
135func AlbumJSON(id int) string {
136 s := albumJSON(rd(), id)
137 if s == "" {
138 panic("catalog: unknown album")
139 }
140 return s
141}
142
143// playlistJSON writes a visible playlist ("" when hidden or unknown).
144func playlistJSON(t *tx, id int) string {
145 rec := ""
146 if id > 0 {
147 rec = t.val(cPlaylists, store.Pad(id))
148 }
149 if rec == "" || store.Field(rec, 3) != "" {
150 return ""
151 }
152 f := store.Fields(rec)
153 return `{"id":` + text.Itoa(id) + `,"owner":` + text.Str(f[0]) + `,"title":` + text.Str(f[1]) +
154 `,"updated":` + f[2] + `,"tracks":` + store.JSON(f[4]) + `}`
155}
156
157func PlaylistJSON(id int) string {
158 s := playlistJSON(rd(), id)
159 if s == "" {
160 panic("catalog: unknown playlist")
161 }
162 return s
163}
164
165// UserJSON returns a listener's public counters (likes are paged with LikesJSON).
166func UserJSON(addr address) string {
167 likes, follows, tipped, lists := UserStats(addr)
168 return `{"address":` + text.Str(addr.String()) + `,"likes":` + text.Itoa(likes) + `,"follows":` + text.Itoa(follows) +
169 `,"tipped":` + text.Itoa64(tipped) + `,"playlists":` + ints(lists) + `,"artist":` + text.Itoa(ArtistOf(addr)) + `}`
170}
171
172func LikesJSON(addr address, offset, limit int) string { return ints(LikedPage(addr, offset, limit)) }
173
174func GenresJSON() string {
175 var b strings.Builder
176 b.WriteString("[")
177 for g := 1; g <= NumGenres; g++ {
178 if g > 1 {
179 b.WriteString(",")
180 }
181 b.WriteString(`{"id":` + text.Itoa(g) + `,"name":` + text.Str(genres[g]) + `}`)
182 }
183 b.WriteString("]")
184 return b.String()
185}
186
187// maxBatch caps the batched readers, so one query stays far below the qeval budget.
188const maxBatch = 100
189
190// batch writes {"total":n,"<key>":[...]} for ids offset+1..offset+limit, skipping
191// hidden items; one query replaces up to 100 single reads.
192func batch(key string, total, offset, limit int, one func(id int) string) string {
193 limit = store.Limit(limit, maxBatch)
194 offset = max0(offset)
195 var b strings.Builder
196 b.WriteString(`{"total":` + text.Itoa(total) + `,"` + key + `":[`)
197 n := 0
198 for id := offset + 1; id <= total && id <= offset+limit; id++ {
199 if s := one(id); s != "" {
200 if n > 0 {
201 b.WriteString(",")
202 }
203 b.WriteString(s)
204 n++
205 }
206 }
207 b.WriteString(`]}`)
208 return b.String()
209}
210
211// ArtistsJSON returns visible artists with ids offset+1..offset+limit (at most 100).
212func ArtistsJSON(offset, limit int) string {
213 t := rd()
214 return batch("artists", int(t.count(mArtists)), offset, limit, func(id int) string {
215 if arec := t.rec(cArtists, id); arec != "" && store.Field(arec, aHidden) == "" {
216 return artistJSON(t, id, arec)
217 }
218 return ""
219 })
220}
221
222// AlbumsJSON returns visible albums with ids offset+1..offset+limit (at most 100).
223func AlbumsJSON(offset, limit int) string {
224 t := rd()
225 return batch("albums", int(t.count(mAlbums)), offset, limit, func(id int) string { return albumJSON(t, id) })
226}
227
228// PlaylistsJSON returns visible playlists with ids offset+1..offset+limit (at most 100).
229func PlaylistsJSON(offset, limit int) string {
230 t := rd()
231 return batch("playlists", int(t.count(mPlaylists)), offset, limit, func(id int) string { return playlistJSON(t, id) })
232}