Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

records.gno

9.68 Kb · 350 lines
  1package catalog
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  7	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
  8)
  9
 10// Records are store.Rec values: fields behind a length header, so reading a
 11// field never scans the record. Hot fields come first. Id lists use store's
 12// JSON-ready width and go straight into the JSON exports. The fields are
 13// catalog v0's, ordered so the frequent writes touch the first ones.
 14
 15type Split struct {
 16	To  address
 17	Pct int64 // percent of each tip
 18}
 19
 20type Artist struct {
 21	ID        int
 22	Owner     address // "" while unclaimed (curated / Audius / Jamendo)
 23	Kind      string  // artist | curated | audius | jamendo
 24	Name      string
 25	Bio       string
 26	Source    string // curated: source page; audius / jamendo: platform id (audius-user:<id>, jamendo-artist:<id>)
 27	Joined    int64
 28	Tips      int64
 29	Followers int
 30	Tracks    []int
 31	Albums    []int
 32	Verified  bool
 33	Hidden    bool
 34}
 35
 36type Track struct {
 37	ID          int
 38	Artist      int
 39	Origin      string
 40	Title       string
 41	Genre       int
 42	Duration    int64 // seconds
 43	License     string
 44	CMO         string // none | sacem-nc
 45	Credits     string
 46	Audio       string // ipfs://… ar://… audius:… jamendo:… https://…
 47	AudioSHA    string // sha256 hex, required for https
 48	Cover       string
 49	CoverSHA    string
 50	SourceURL   string
 51	Attribution string
 52	Splits      []Split
 53	Album       int
 54	Created     int64
 55	Likes       int
 56	Tips        int64
 57	Supporters  int
 58	Hidden      bool
 59	Early       []address // first listeners to like it
 60}
 61
 62type Album struct {
 63	ID       int
 64	Artist   int
 65	Title    string
 66	Cover    string
 67	CoverSHA string
 68	Year     int
 69	Tracks   []int
 70	Created  int64
 71	Hidden   bool
 72}
 73
 74type Playlist struct {
 75	ID      int
 76	Owner   address
 77	Title   string
 78	Tracks  []int
 79	Updated int64
 80	Hidden  bool
 81}
 82
 83// User is what a listener writes, so a follow only touches that listener's
 84// own record. Follows are a sorted packed list (9 bytes each); likes and the
 85// tipped tracks live apart, per range of 512 track ids.
 86type User struct {
 87	Follows   string // sorted packed artist ids
 88	Tipped    int64
 89	Playlists []int
 90	Reports   int // open reports filed by this user
 91}
 92
 93type ReportInfo struct {
 94	ID     int
 95	Kind   string // track | album | artist | playlist
 96	Target string
 97	Reason string
 98	By     address
 99	At     int64
100}
101
102// ---- tracks: what the radio reads first, then what likes and tips change ----
103
104// Track record fields. Reading or replacing field i walks the i fields
105// before it, so the fields the frequent actions read and change come first.
106const (
107	fArtist = iota
108	fGenre
109	fDuration
110	fHidden
111	fLikes
112	fEarly // first listeners to like it, comma-joined
113	fTips
114	fSupporters
115	fAlbum
116	fCreated
117	fOrigin
118	fTitle
119	fLicense
120	fCMO
121	fCredits
122	fAudio
123	fAudioSHA
124	fCover
125	fCoverSHA
126	fSource
127	fAttribution
128	fSplits // "addr:pct,…"
129)
130
131func getTrack(t *tx, id int) *Track {
132	rec := t.rec(cTracks, id)
133	if rec == "" {
134		return nil
135	}
136	f := store.Fields(rec)
137	tr := &Track{
138		ID: id, Artist: text.Atoi(f[fArtist]), Genre: text.Atoi(f[fGenre]), Duration: text.Atoi64(f[fDuration]), Hidden: f[fHidden] != "",
139		Likes: text.Atoi(f[fLikes]), Tips: text.Atoi64(f[fTips]), Supporters: text.Atoi(f[fSupporters]), Album: text.Atoi(f[fAlbum]),
140		Created: text.Atoi64(f[fCreated]), Origin: f[fOrigin], Title: f[fTitle], License: f[fLicense], CMO: f[fCMO],
141		Credits: f[fCredits], Audio: f[fAudio], AudioSHA: f[fAudioSHA], Cover: f[fCover], CoverSHA: f[fCoverSHA],
142		SourceURL: f[fSource], Attribution: f[fAttribution],
143	}
144	if f[fSplits] != "" {
145		for _, p := range strings.Split(f[fSplits], ",") {
146			to, pct, _ := strings.Cut(p, ":")
147			tr.Splits = append(tr.Splits, Split{To: address(to), Pct: text.Atoi64(pct)})
148		}
149	}
150	if f[fEarly] != "" {
151		for _, a := range strings.Split(f[fEarly], ",") {
152			tr.Early = append(tr.Early, address(a))
153		}
154	}
155	return tr
156}
157
158func trackRecord(tr *Track) string {
159	var splits, early []string
160	for _, s := range tr.Splits {
161		splits = append(splits, s.To.String()+":"+text.Itoa64(s.Pct))
162	}
163	for _, a := range tr.Early {
164		early = append(early, a.String())
165	}
166	return store.Rec(text.Itoa(tr.Artist), text.Itoa(tr.Genre), text.Itoa64(tr.Duration), text.Flag(tr.Hidden), text.Itoa(tr.Likes),
167		strings.Join(early, ","), text.Itoa64(tr.Tips), text.Itoa(tr.Supporters), text.Itoa(tr.Album), text.Itoa64(tr.Created), tr.Origin,
168		tr.Title, tr.License, tr.CMO, tr.Credits, tr.Audio, tr.AudioSHA, tr.Cover, tr.CoverSHA, tr.SourceURL,
169		tr.Attribution, strings.Join(splits, ","))
170}
171
172func putTrack(t *tx, tr *Track) { t.setRec(cTracks, tr.ID, trackRecord(tr)) }
173
174// brief reads only what playback needs, without decoding the whole track.
175func brief(t *tx, id int) (artist, genre int, dur int64, hidden, ok bool) {
176	rec := t.rec(cTracks, id)
177	if rec == "" {
178		return 0, 0, 0, false, false
179	}
180	return text.Atoi(store.Field(rec, fArtist)), text.Atoi(store.Field(rec, fGenre)), text.Atoi64(store.Field(rec, fDuration)), store.Field(rec, fHidden) != "", true
181}
182
183// ---- artists: a small head record, the id lists apart ----
184
185// Artist head fields, in the same spirit: what tips and follows read and
186// change first. The permanent tickets/nft realm reads aHidden and aName (and
187// chunkSize) straight from the data realm: no release may move them.
188const (
189	aHidden = iota
190	aOwner
191	aVerified
192	aTips
193	aFollowers
194	aKind
195	aName
196	aBio
197	aSource
198	aJoined
199)
200
201// getArtist returns an artist's head; Tracks and Albums stay empty (see withLists).
202func getArtist(t *tx, id int) *Artist {
203	rec := t.rec(cArtists, id)
204	if rec == "" {
205		return nil
206	}
207	f := store.Fields(rec)
208	return &Artist{
209		ID: id, Hidden: f[aHidden] != "", Owner: address(f[aOwner]), Verified: f[aVerified] != "", Tips: text.Atoi64(f[aTips]),
210		Followers: text.Atoi(f[aFollowers]), Kind: f[aKind], Name: f[aName], Bio: f[aBio], Source: f[aSource], Joined: text.Atoi64(f[aJoined]),
211	}
212}
213
214func artistRecord(a *Artist) string {
215	return store.Rec(text.Flag(a.Hidden), a.Owner.String(), text.Flag(a.Verified), text.Itoa64(a.Tips), text.Itoa(a.Followers), a.Kind,
216		a.Name, a.Bio, a.Source, text.Itoa64(a.Joined))
217}
218
219func putArtist(t *tx, a *Artist) { t.setRec(cArtists, a.ID, artistRecord(a)) }
220
221// artistField reads one field of the head ("" for an unknown artist).
222func artistField(t *tx, id, i int) string {
223	if rec := t.rec(cArtists, id); rec != "" {
224		return store.Field(rec, i)
225	}
226	return ""
227}
228
229func artistVisible(t *tx, id int) bool {
230	rec := t.rec(cArtists, id)
231	return rec != "" && store.Field(rec, aHidden) == ""
232}
233
234// lists returns an artist's track and album id lists (made at its first publish).
235func lists(t *tx, artistID int) (trackIDs, albumIDs string) {
236	rec := t.val(cAlists, store.Pad(artistID))
237	if rec == "" {
238		return "", ""
239	}
240	f := store.Fields(rec)
241	return f[0], f[1]
242}
243
244func setLists(t *tx, artistID int, trackIDs, albumIDs string) {
245	t.set(cAlists, store.Pad(artistID), store.Rec(trackIDs, albumIDs))
246}
247
248// withLists fills an artist head's Tracks and Albums.
249func withLists(t *tx, a *Artist) *Artist {
250	tl, al := lists(t, a.ID)
251	a.Tracks, a.Albums = store.Ints(tl), store.Ints(al)
252	return a
253}
254
255func artistOf(t *tx, owner address) int { return text.Atoi(t.val(cOwners, owner.String())) }
256
257// ---- albums and playlists ----
258
259func getAlbum(t *tx, id int) *Album {
260	rec := t.rec(cAlbums, id)
261	if rec == "" {
262		return nil
263	}
264	f := store.Fields(rec)
265	return &Album{ID: id, Artist: text.Atoi(f[0]), Title: f[1], Cover: f[2], CoverSHA: f[3], Year: text.Atoi(f[4]),
266		Created: text.Atoi64(f[5]), Hidden: f[6] != "", Tracks: store.Ints(f[7])}
267}
268
269func albumRecord(al *Album) string {
270	return store.Rec(text.Itoa(al.Artist), al.Title, al.Cover, al.CoverSHA, text.Itoa(al.Year), text.Itoa64(al.Created),
271		text.Flag(al.Hidden), store.Pack(al.Tracks))
272}
273
274func putAlbum(t *tx, al *Album) { t.setRec(cAlbums, al.ID, albumRecord(al)) }
275
276func getPlaylist(t *tx, id int) *Playlist {
277	if id < 1 {
278		return nil
279	}
280	rec := t.val(cPlaylists, store.Pad(id))
281	if rec == "" {
282		return nil
283	}
284	f := store.Fields(rec)
285	return &Playlist{ID: id, Owner: address(f[0]), Title: f[1], Updated: text.Atoi64(f[2]), Hidden: f[3] != "",
286		Tracks: store.Ints(f[4])}
287}
288
289func putPlaylist(t *tx, p *Playlist) {
290	t.set(cPlaylists, store.Pad(p.ID), store.Rec(p.Owner.String(), p.Title, text.Itoa64(p.Updated), text.Flag(p.Hidden), store.Pack(p.Tracks)))
291}
292
293// ---- listeners ----
294
295// user returns a listener's record (an empty one if none); save it with putUser.
296func user(t *tx, addr address) *User {
297	rec := t.val(cUsers, addr.String())
298	if rec == "" {
299		return &User{}
300	}
301	f := store.Fields(rec)
302	return &User{Follows: f[0], Tipped: text.Atoi64(f[1]), Playlists: store.Ints(f[2]), Reports: text.Atoi(f[3])}
303}
304
305func putUser(t *tx, addr address, u *User) {
306	t.set(cUsers, addr.String(), store.Rec(u.Follows, text.Itoa64(u.Tipped), store.Pack(u.Playlists), text.Itoa(u.Reports)))
307}
308
309// ---- lookups shared by every file ----
310
311func visibleArtist(a *Artist) bool { return a != nil && !a.Hidden }
312
313func playable(t *tx, tr *Track) bool {
314	return tr != nil && !tr.Hidden && artistVisible(t, tr.Artist)
315}
316
317func mustArtist(t *tx, id int) *Artist {
318	a := getArtist(t, id)
319	if !visibleArtist(a) {
320		panic("catalog: unknown artist")
321	}
322	return a
323}
324
325func mustTrack(t *tx, id int) *Track {
326	tr := getTrack(t, id)
327	if !playable(t, tr) {
328		panic("catalog: unknown track")
329	}
330	return tr
331}
332
333// ownArtist returns the caller's visible artist profile: its id and head.
334func ownArtist(t *tx, caller address) (int, string) {
335	id := artistOf(t, caller)
336	arec := t.rec(cArtists, id)
337	if arec == "" || store.Field(arec, aHidden) != "" {
338		panic("catalog: create your artist profile first")
339	}
340	return id, arec
341}
342
343// mayEdit reports whether caller manages artist a: its owner, or the admin
344// while the artist is unclaimed.
345func mayEdit(caller address, a *Artist) bool {
346	if a.Owner != "" {
347		return caller == a.Owner
348	}
349	return caller != "" && caller == adminAddr()
350}