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

station.gno

11.94 Kb · 447 lines
  1package radio
  2
  3import (
  4	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  5	"gno.land/r/nym-alexiscolin000/gnoradio/catalog/v1"
  6)
  7
  8// ---- Station internals: the rotation loop and the schedule over it ----
  9
 10// paused is the airtime taken by programmed slots before ts. It reads an
 11// undecoded schedule as it is, and keeps its result until the schedule
 12// moves (setSlots, move).
 13func (st *station) paused(ts int64) int64 {
 14	if st.pzOK && st.pzAt == ts {
 15		return st.pz
 16	}
 17	var p int64
 18	if st.decoded {
 19		for _, s := range st.sched {
 20			if ts <= s.Start {
 21				break
 22			}
 23			if ts >= s.Start+s.Dur {
 24				p += s.Dur
 25			} else {
 26				p += ts - s.Start
 27			}
 28		}
 29	} else {
 30		for c := (cursor{s: st.raw}); c.next() && ts > c.start; {
 31			if ts >= c.end {
 32				p += c.end - c.start
 33			} else {
 34				p += ts - c.start
 35			}
 36		}
 37	}
 38	st.pz, st.pzAt, st.pzOK = p, ts, true
 39	return p
 40}
 41
 42// pos is the position in the rotation loop at ts.
 43func (st *station) pos(ts int64) int64 {
 44	total := st.total()
 45	if total == 0 {
 46		return 0
 47	}
 48	p := (ts - st.epoch - st.paused(ts)) % total
 49	if p < 0 {
 50		p += total
 51	}
 52	return p
 53}
 54
 55// anchor sets the epoch so that the rotation is at position p at ts.
 56func (st *station) anchor(ts, p int64) {
 57	st.epoch, st.dirty = ts-st.paused(ts)-p, true
 58}
 59
 60func (st *station) rotationAt(ts int64) (slot, track int, offset, dur int64, ok bool) {
 61	if st.total() == 0 {
 62		return 0, 0, 0, 0, false
 63	}
 64	slot, track, offset, ok = st.find(st.pos(ts))
 65	if !ok {
 66		return 0, 0, 0, 0, false
 67	}
 68	_, dur = st.rotSlot(slot)
 69	return slot, track, offset, dur, true
 70}
 71
 72// rotating is how many tracks play on the station and its loop length: on
 73// Main, which simulcasts the genre stations, theirs added up.
 74func (st *station) rotating() (live int, loop int64) {
 75	if st.id != MainStation {
 76		return st.live, st.total()
 77	}
 78	for g := 1; g <= NumGenres; g++ {
 79		gs := st.t.station(g)
 80		live, loop = live+gs.live, loop+gs.total()
 81	}
 82	return
 83}
 84
 85// playing is the programmed slot on air at ts.
 86func (st *station) playing(ts int64) (Slot, bool) {
 87	for _, s := range st.slots() {
 88		if ts >= s.Start && ts < s.Start+s.Dur {
 89			return s, true
 90		}
 91	}
 92	return Slot{}, false
 93}
 94
 95// between is what a station plays at ts outside programmed slots: its
 96// rotation, or on Main a simulcast of the hour's genre station (flow.gno):
 97// what that station plays at ts, its own picks included, cut where it pauses
 98// for a pick and at the hour (the app's top-of-hour jingle covers the cut).
 99// The genre station never jumps (its edits re-anchor it), so neither does Main.
100func (st *station) between(ts int64) (track int, off, dur int64, ok bool) {
101	if st.id != MainStation {
102		_, track, off, dur, ok = st.rotationAt(ts)
103		return
104	}
105	g := hourGenre(st.t, ts)
106	if g == 0 {
107		return
108	}
109	gs := st.t.station(g)
110	if s, prog := gs.playing(ts); prog {
111		track, off, dur, ok = s.Track, ts-s.Start, s.Dur, true
112	} else if _, track, off, dur, ok = gs.rotationAt(ts); !ok {
113		return
114	} else {
115		for _, s := range gs.slots() { // the genre rotation pauses for its next pick
116			if s.Start > ts && s.Start < ts-off+dur {
117				dur = s.Start - (ts - off)
118			}
119		}
120	}
121	if end := ts - ts%3600 + 3600; ts-off+dur > end {
122		dur = end - (ts - off)
123	}
124	if st.t.dropped(MainStation, track) {
125		track = 0 // dropped from Main: silence (onAir and ScheduleJSON skip it)
126	}
127	return
128}
129
130// relay is the station Main simulcasts at ts (the hour's genre station), or st.
131func (st *station) relay(ts int64) *station {
132	if st.id == MainStation {
133		if g := hourGenre(st.t, ts); g != 0 {
134			return st.t.station(g)
135		}
136	}
137	return st
138}
139
140// pickAt is the listener pick on air at ts: the station's own or, on Main
141// between its own picks, the genre station's pick it simulcasts, so Main
142// shows, tips and reports it as that pick.
143func (st *station) pickAt(ts int64) (Slot, bool) {
144	if s, ok := st.playing(ts); ok || st.id != MainStation {
145		return s, ok
146	}
147	if r := st.relay(ts); r != st {
148		if s, ok := r.playing(ts); ok && !st.t.dropped(MainStation, s.Track) {
149			return s, true
150		}
151	}
152	return Slot{}, false
153}
154
155func (st *station) onAir(ts int64) (int, int64, bool) {
156	if s, ok := st.pickAt(ts); ok {
157		if _, _, _, ok := catalog.TrackBrief(s.Track); ok {
158			return s.Track, ts - s.Start, true
159		}
160		return 0, 0, false // a removed programmed slot plays silence until its end
161	}
162	track, off, _, ok := st.between(ts)
163	if !ok {
164		return 0, 0, false
165	}
166	if _, _, _, playable := catalog.TrackBrief(track); !playable {
167		return 0, 0, false // hidden but not refreshed yet: silence, never its title
168	}
169	return track, off, false
170}
171
172// queued counts the listener slots not ended yet.
173func (st *station) queued(ts int64) int {
174	n := 0
175	for _, s := range st.slots() {
176		if s.Start+s.Dur > ts {
177			n++
178		}
179	}
180	return n
181}
182
183// add appends a track at the end of the loop and returns its slot; listeners do not move.
184func (st *station) add(trackID int, dur int64, ts int64) int {
185	p := st.pos(ts)
186	i := st.appendRot(trackID, dur)
187	if dur > 0 {
188		st.addLive(1)
189	}
190	st.anchor(ts, p)
191	return i
192}
193
194// join puts a synced track in New or Choice, a ring of keep slots: once
195// full, the oldest slot is reused in place, so the rotation never outgrows
196// keep whatever the catalog size or the number of picks. The track it held
197// leaves the ring's index, so Refresh leaves it out. Listeners do not jump:
198// replace re-anchors around the change.
199func (st *station) join(trackID int, dur int64, ts int64, keep int) {
200	p, found := st.ringFind(trackID)
201	st.joinAt(p, found, trackID, dur, ts, keep)
202}
203
204// joinAt is join for a track at position p of the ring's index (ringFind).
205func (st *station) joinAt(p int, found bool, trackID int, dur int64, ts int64, keep int) {
206	if st.full() {
207		return // no room in the record now: the track misses this ring (Choice takes it again with a later pick or Sync)
208	}
209	i := st.ring % keep
210	st.ring++
211	st.dirty = true
212	if st.count() < keep {
213		st.ringIndex(p, found, trackID, st.add(trackID, dur, ts))
214		return
215	}
216	old, _ := st.rotSlot(i)
217	st.replace(i, trackID, dur, ts)
218	if q := st.ringUnindex(old, i); q >= 0 && q < p {
219		p--
220	} else if q == p && old == trackID {
221		found = false
222	}
223	st.ringIndex(p, found, trackID, i)
224}
225
226// choose adds a synced track to Listeners' choice unless it is already there.
227func choose(t *tx, trackID int, dur int64, ts int64) {
228	if trackID < 1 || trackID > t.synced() {
229		return // not ingested yet
230	}
231	cs := t.station(ChoiceStation)
232	p, in := cs.ringFind(trackID)
233	if in {
234		return
235	}
236	if t.dropped(ChoiceStation, trackID) {
237		dur = 0
238	}
239	cs.joinAt(p, false, trackID, dur, ts, choiceCap)
240}
241
242// slot returns a track's index in this station's rotation. Main has no
243// rotation (it simulcasts the genre stations); New and Choice keep an index;
244// genre stations look the slot up in homes.
245func (st *station) slot(trackID int) (int, bool) {
246	switch {
247	case st.id == MainStation:
248		return 0, false
249	case st.ringed():
250		return st.ringSlot(trackID)
251	}
252	return homeSlot(st.t.homes(trackID), st.id)
253}
254
255// setDuration changes slot i and re-anchors so the song on air keeps playing
256// (or, if it is the one removed, the next one starts now). O(n/Size + Size).
257func (st *station) setDuration(i int, d int64, ts int64) {
258	id, old := st.rotSlot(i)
259	if old == d {
260		return
261	}
262	p := st.pos(ts)
263	prefix := st.prefixBefore(i)
264	np := p
265	switch {
266	case st.total() == 0 || p < prefix:
267		// the change is ahead of the listeners
268	case p >= prefix+old:
269		np = p - old + d // the change is behind them
270	default:
271		// the slot on air changes
272		if p-prefix >= d {
273			np = prefix + d // cut short: the next track starts now
274		}
275	}
276	st.setRot(i, id, d)
277	if old == 0 && d > 0 {
278		st.addLive(1)
279	} else if old > 0 && d == 0 {
280		st.addLive(-1)
281	}
282	if total := st.total(); total > 0 {
283		st.anchor(ts, np%total)
284	}
285}
286
287// replace puts another track in slot i, as setDuration(i, 0), a new id,
288// then setDuration(i, d) would, in one rotation write: the old track stops
289// if on air (the next starts now), listeners do not jump.
290func (st *station) replace(i, id int, d int64, ts int64) {
291	_, old := st.rotSlot(i)
292	if old == 0 && d == 0 {
293		st.setRot(i, id, 0) // the old sequence left the epoch alone
294		return
295	}
296	p, prefix := st.pos(ts), st.prefixBefore(i)
297	np := p
298	switch {
299	case st.total() == 0 || p < prefix:
300	case p >= prefix+old:
301		np = p - old + d
302	default:
303		np = prefix + d // the slot on air ends now
304	}
305	st.setRot(i, id, d)
306	if old == 0 && d > 0 {
307		st.addLive(1)
308	} else if old > 0 && d == 0 {
309		st.addLive(-1)
310	}
311	if total := st.total(); total > 0 {
312		st.anchor(ts, np%total)
313	}
314}
315
316// fold moves programmed slots that ended long ago into the epoch.
317func (st *station) fold(ts int64) {
318	sl := st.slots()
319	i := 0
320	for i < len(sl) && sl[i].Start+sl[i].Dur <= ts-foldAfter {
321		s := sl[i]
322		st.epoch += s.Dur
323		if s.Note != "" { // only a dedication has reports
324			st.t.del(cNotes, noteKey(st.id, s.Start))
325		}
326		if s.At > 0 { // the replay gap counts from when a booked pick aired
327			st.setPicked(s.Track, s.Start)
328		}
329		i++
330	}
331	if i > 0 {
332		st.setSlots(sl[i:])
333	}
334}
335
336// dropSlots removes a track's programmed slots; one on air ends now. It
337// returns the sponsored ones, which no longer air in full: their payout goes
338// back to the artist (release).
339func (st *station) dropSlots(trackID int, ts int64) (cut []hold) {
340	var out []Slot
341	changed := false
342	for _, s := range st.slots() {
343		if s.Track == trackID && s.Pay > 0 && s.Start+s.Dur > ts {
344			if h, ok := unsponsor(st.t, st.id, s, ts); ok {
345				cut = append(cut, h)
346			}
347		}
348		if s.Track == trackID {
349			if s.Start >= ts {
350				// a later pick may reuse this start: its reports go too
351				if s.Note != "" {
352					st.t.del(cNotes, noteKey(st.id, s.Start))
353				}
354				changed = true
355				continue
356			}
357			if s.Start+s.Dur > ts {
358				s.Dur = ts - s.Start
359				changed = true
360			}
361		}
362		out = append(out, s)
363	}
364	if changed {
365		st.setSlots(out)
366	}
367	return cut
368}
369
370// The replay record of a station (in its record: a pick changes both) is
371// fixed-width entries, the track (4 digits) then the pick time (6), read by
372// slicing.
373const pickW = 10
374
375// lastPicked is when a track was last picked on the station, within replayGap.
376func (st *station) lastPicked(trackID int) (int64, bool) {
377	rec := st.picks
378	key := store.Fixed(int64(trackID), 4)
379	for i := 0; i+pickW <= len(rec); i += pickW {
380		if rec[i:i+4] == key {
381			return store.Num(rec, i+4, 6), true
382		}
383	}
384	return 0, false
385}
386
387// setPicked records a pick time (the later one wins) and drops the picks
388// older than replayGap, which no check reads: the record stays as short as
389// the picks of the last three hours.
390func (st *station) setPicked(trackID int, at int64) {
391	rec := st.picks
392	key := store.Fixed(int64(trackID), 4)
393	cut := now() - replayGap
394	out := ""
395	for i := 0; i+pickW <= len(rec); i += pickW {
396		when := store.Num(rec, i+4, 6)
397		if rec[i:i+4] == key {
398			if when > at {
399				at = when
400			}
401		} else if when > cut {
402			out += rec[i : i+pickW]
403		}
404	}
405	if at > cut {
406		out += key + store.Fixed(at, 6)
407	}
408	if out != rec {
409		st.picks, st.dirty = out, true
410	}
411}
412
413// shownNote is the dedication listeners see ("" once hidden).
414func (s Slot) shownNote() string {
415	if s.NoteHidden {
416		return ""
417	}
418	return s.Note
419}
420
421// picker is the listener whose pick of trackID plays at ts on the station,
422// or ended less than tipGrace ago, read from the stored schedule: "" when
423// none, a sponsored pick (its artist already refunds it) or the admin's (a
424// curator pick: GnoRadio takes no share), and the station the pick lives on
425// (on Main, the genre station whose pick it simulcast).
426func (st *station) picker(trackID int, ts int64, adm address) (address, int) {
427	var by address
428	for c := (cursor{s: st.raw}); c.next(); {
429		b := st.raw[c.b:c.i]
430		if c.start > ts || ts >= c.end+tipGrace || int(dec[b[3]]<<18|dec[b[4]]<<12|dec[b[5]]<<6|dec[b[6]]) != trackID || store.Num(b, 17, 4) != 0 {
431			continue
432		}
433		if p := address(b[23 : 23+int(dec[b[22]])]); p != adm {
434			by = p
435		}
436	}
437	if by == "" && st.id == MainStation && !st.t.dropped(MainStation, trackID) {
438		for _, at := range []int64{ts, ts - tipGrace} { // the pick may have ended in the hour before
439			if r := st.relay(at); r != st {
440				if by, _ = r.picker(trackID, ts, adm); by != "" {
441					return by, r.id
442				}
443			}
444		}
445	}
446	return by, st.id
447}