catalog.gno
7.77 Kb · 222 lines
1// Package catalog is GnoRadio's catalog rules, release 1: artists, tracks,
2// albums, playlists, likes, follows, tips, support, moderation and
3// verification. Other GnoRadio realms (radio, tickets, home) read it through
4// the value getters in read.gno; it imports none of them.
5//
6// It keeps no state but its admin role. Every record lives in the data realm
7// (gno.land/r/nym-alexiscolin000/gnoradio/data), in the "catalog/…" collections only the current
8// catalog writer may change (db.gno). Promo budgets sit in data's vault, so a
9// later release takes over the data and the coins without copying anything.
10//
11// Four track origins share one catalog:
12// - "artist": published by the artist's own wallet, tips enabled;
13// - "curated": imported by the admin from CC0 / CC BY / CC BY-SA / CC BY-NC / CC BY-NC-SA sources;
14// - "audius", "jamendo": pointers the admin imports. The record keeps only
15// what the radio needs (platform id, genre, duration, licence); the app
16// reads title, artist and cover live, since both platforms' API terms
17// allow session caching only (attribution required).
18//
19// Imported artists start unclaimed; once the admin assigns them to
20// a wallet (AssignArtist) or the robot verifies a claim, their tracks receive
21// tips.
22//
23// Tips and support are forwarded in the same transaction: this realm ends
24// every transaction holding no coins.
25package catalog
26
27import (
28 "chain"
29 "chain/runtime"
30 "chain/runtime/unsafe"
31 "strings"
32 "time"
33
34 "gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
35 "gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
36 "gno.land/r/nym-alexiscolin000/gnoradio/data"
37)
38
39const (
40 version = "1.0.0"
41 denom = "ugnot"
42 ugnot = 1_000_000
43
44 OriginArtist = "artist"
45 OriginCurated = "curated"
46 OriginAudius = "audius"
47 OriginJamendo = "jamendo"
48
49 minTip = 100_000 // 0.1 GNOT
50 maxTip = 1_000_000 * ugnot // keeps share math far from overflow
51 maxSplits = 4
52 maxSplitPct = 90 // the artist always keeps at least 10%
53 earlyBadges = 10
54 minDuration = 10
55 maxDuration = 1200
56 maxPerArtist = 500 // tracks per artist
57 maxAlbumTracks = 40
58 maxListTracks = 200
59 maxUserLists = 50
60 maxFollows = 1000
61 maxUserReports = 5 // open reports per reporter
62 maxTitle = 64 // track, album and playlist titles, in characters
63 maxBio = 280 // artist bio
64 maxCredits = 160 // a track's credits and a curated import's attribution
65 maxReason = 200 // report and moderation reasons
66 topSize = 10
67 maxSupportPct = 50
68)
69
70// NumGenres is the size of the fixed genre list; genre ids are 1..NumGenres.
71const NumGenres = 20
72
73var genres = [NumGenres + 1]string{"",
74 "Electronica", "Synthwave", "Ambient", "Techno", "House", "Drum & Bass", "Dubstep & Trap",
75 "Lo-fi Beats", "Hip-hop & Rap", "R&B & Soul", "Rock & Indie", "Metal & Punk", "Pop",
76 "Jazz & Blues", "Folk & Acoustic", "Cinematic & Classical", "World", "Latin", "Reggae & Dub", "Funk & Disco"}
77
78// licenses an artist may pick; curated imports are limited to CC0, BY, BY-SA, BY-NC and BY-NC-SA
79// (no ND, no reserved rights).
80var licenses = []string{
81 "CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0", "CC-BY-ND-4.0",
82 "CC-BY-NC-4.0", "CC-BY-NC-SA-4.0", "CC-BY-NC-ND-4.0", "ALL-RIGHTS-RESERVED",
83}
84
85// AudiusLicense is the license id of tracks streamed from Audius.
86const AudiusLicense = "Audius-OML"
87
88const rightsTerms = "I own or control the rights to this recording and its composition, or hold a licence that allows this, and I grant the operator of GnoRadio and its users a worldwide, non-exclusive, royalty-free licence to store, stream, broadcast and display it through GnoRadio and gno.land for as long as it is published."
89
90var (
91 self string // this realm's path
92 admin *role.Role // the moderator; nil when a release before renounced it
93)
94
95// init takes the admin over from the previous release (catalog/config
96// "admin"), or names the deployer. While this realm is the catalog writer
97// (release 1 at deploy) it also makes the catalog's collections.
98func init(cur realm) {
99 self = cur.PkgPath()
100 holder, mirrored := data.Get(cConfig, "admin")
101 if !mirrored {
102 deployer := unsafe.OriginCaller()
103 if deployer == "" && runtime.ChainID() == "dev" {
104 deployer = chain.PackageAddress(self + "/dev") // gno test: a key nobody holds
105 }
106 holder = deployer.String()
107 }
108 if holder != "" {
109 admin = role.New("admin", address(holder))
110 }
111 if data.Writer("catalog") == self {
112 setup(cur, address(holder))
113 }
114}
115
116// ---- helpers shared by every file ----
117
118func now() int64 { return time.Now().Unix() }
119
120func userCaller(cur realm) address {
121 if !cur.Previous().IsUserCall() {
122 panic("catalog: call this directly from your wallet")
123 }
124 return cur.Previous().Address()
125}
126
127// adminAddr is the admin: the data realm's mirror is the authority, so a
128// change the previous release made after this one was deployed counts.
129func adminAddr() address {
130 h, ok := data.Get(cConfig, "admin")
131 return role.Mirror(admin, h, ok)
132}
133
134// syncAdmin adopts the mirror when the previous release changed it after
135// this one's init (during the release's delay).
136func syncAdmin() {
137 h, ok := data.Get(cConfig, "admin")
138 admin = role.Adopt(admin, "admin", h, ok)
139}
140
141func onlyAdmin(cur realm) {
142 syncAdmin()
143 if admin == nil || !admin.Is(0, cur) {
144 panic("catalog: admin only")
145 }
146 noPayment()
147}
148
149func noPayment() {
150 if len(unsafe.OriginSend()) != 0 {
151 panic("catalog: this action does not take coins")
152 }
153}
154
155// paid returns the ugnot attached to the call; anything else is refused.
156func paid() int64 {
157 sent := unsafe.OriginSend()
158 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount <= 0 {
159 panic("catalog: attach an amount in ugnot")
160 }
161 return sent[0].Amount
162}
163
164func emitEvent(name string, kv ...string) { chain.Emit(name, kv...) }
165
166// ---- release and admin ----
167
168// Ready tells the data realm that this release is on chain (see
169// data.Propose). Anyone may call it.
170func Ready(cur realm) {
171 noPayment()
172 data.Ready(cross(cur))
173}
174
175// OfferAdmin offers the admin role to an address (offering it to the admin
176// cancels the offer); AcceptAdmin takes it. Each change is mirrored to the
177// data realm, so the next release keeps the admin.
178func OfferAdmin(cur realm, to address) {
179 onlyAdmin(cur)
180 admin.Offer(0, cur, to)
181}
182
183func AcceptAdmin(cur realm) {
184 noPayment()
185 syncAdmin()
186 if admin == nil {
187 panic("catalog: nothing offered to you")
188 }
189 admin.Accept(0, cur)
190 data.Set(cross(cur), cConfig, "admin", admin.Holder().String())
191}
192
193// RenounceAdmin gives the admin role up for good, for this release and the
194// next ones.
195func RenounceAdmin(cur realm) {
196 onlyAdmin(cur)
197 admin.Renounce(0, cur)
198 data.Set(cross(cur), cConfig, "admin", "")
199}
200
201// Render points gnoweb visitors to the GnoRadio site; a retired release says
202// which realm took over.
203func Render(_ string) string {
204 home := strings.TrimPrefix(strings.TrimSuffix(self, "catalog/v1"), "gno.land") + "home/v1"
205 d := strings.TrimPrefix(dataPath, "gno.land")
206 out := "# GnoRadio · catalog\n\nThis contract holds GnoRadio's catalog rules; its data lives in [" + d + "](" + d +
207 "). The site lives at [" + home + "](" + home + ").\n"
208 if w := data.Writer("catalog"); w != self {
209 out += "\nThis release is retired: the catalog is now `" + w + "`.\n"
210 }
211 return out
212}
213
214// Info is the catalog's summary for the app: counts, the admin and the
215// realm that writes the catalog now ("current", this one unless retired).
216func Info() string {
217 t := rd()
218 return `{"version":` + text.Str(version) + `,"artists":` + text.Itoa64(t.count(mArtists)) + `,"tracks":` + text.Itoa64(t.count(mTracks)) +
219 `,"albums":` + text.Itoa64(t.count(mAlbums)) + `,"playlists":` + text.Itoa64(t.count(mPlaylists)) + `,"listeners":` + text.Itoa(data.Size(cUsers)) +
220 `,"genres":` + text.Itoa(NumGenres) + `,"now":` + text.Itoa64(now()) + `,"current":` + text.Str(data.Writer("catalog")) +
221 `,"admin":` + text.Str(adminAddr().String()) + `}`
222}