summaryrefslogtreecommitdiff
path: root/server/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'server/cmd')
-rw-r--r--server/cmd/listend/main.go143
1 files changed, 120 insertions, 23 deletions
diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go
index 588af0d..04c933f 100644
--- a/server/cmd/listend/main.go
+++ b/server/cmd/listend/main.go
@@ -72,9 +72,17 @@ func main() {
server := api.New(state, worker, api.Options{
Token: token,
AudioDir: audioDir,
- // Act on a new subscription list straight away: the phone sends it on refresh, and
- // waiting for the next interval would leave a new show with nothing for hours.
- OnSubscriptions: func([]string) { worker.wake() },
+ // Persist the list the moment it arrives, then poll.
+ //
+ // Both halves were learned the hard way. The list is the one thing the server cannot
+ // rediscover, and it used to live only in memory until a whole poll cycle completed --
+ // which for 61 feeds is hours -- so a restart in the meantime lost every subscription
+ // the phone had sent. And acting on it at once matters because waiting for the next
+ // interval leaves a newly-subscribed show with nothing for hours.
+ OnSubscriptions: func([]string) {
+ worker.save()
+ worker.wake()
+ },
})
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
@@ -192,16 +200,31 @@ func (w *worker) pollAll(ctx context.Context) {
return
}
w.pollOne(ctx, feedURL)
- }
- // A feed no longer subscribed stops being polled; reclaim its audio too.
- for _, url := range w.state.SetFeeds(feeds) {
- if show := w.state.Forget(url); show != nil {
- for _, e := range show.Episodes {
+ // After every feed rather than at the end of the cycle: a cycle over 61 feeds takes
+ // hours, and a restart partway through used to discard all of it.
+ w.save()
+ }
+ // Reclaim the audio of shows no longer subscribed to.
+ //
+ // Against the list as it stands *now*, not the snapshot this cycle began with. A cycle over
+ // 61 feeds takes hours; re-applying a stale snapshot would silently undo a subscription the
+ // phone sent during it, and then delete the audio of every show the stale list omitted.
+ current := make(map[string]bool)
+ for _, url := range w.state.Feeds() {
+ current[url] = true
+ }
+ for _, show := range w.state.Shows() {
+ if current[show.FeedURL] {
+ continue
+ }
+ if dropped := w.state.Forget(show.FeedURL); dropped != nil {
+ for _, e := range dropped.Episodes {
_ = w.mirror.Prune(e)
}
- log.Printf("%s: unsubscribed, reclaimed %d episodes", url, len(show.Episodes))
+ log.Printf("%s: unsubscribed, reclaimed %d episodes", show.FeedURL, len(dropped.Episodes))
}
}
+ w.save()
}
func (w *worker) pollOne(ctx context.Context, feedURL string) {
@@ -261,12 +284,13 @@ func (w *worker) detect(ctx context.Context, show *store.Show, candidates []*sto
continue
}
episodes = append(episodes, skips.Episode{
- ID: e.ID,
- Path: filepath.Join(w.mirror.AudioDir, e.Path),
+ ID: e.ID,
+ Path: filepath.Join(w.mirror.AudioDir, e.Path),
+ DeclaredDurationMs: e.DeclaredDurationMs,
})
byID[e.ID] = e
}
- if len(episodes) < skips.DefaultConfirmThreshold {
+ if len(episodes) < skips.DefaultConfirmThreshold || ctx.Err() != nil {
return
}
@@ -278,24 +302,97 @@ func (w *worker) detect(ctx context.Context, show *store.Show, candidates []*sto
}
now := time.Now().Unix()
- applied := 0
for _, o := range result.Offsets {
- e := byID[o.EpisodeID]
- if e == nil {
- continue
- }
- e.DetectedAtUnix = now
- e.IntroMs, e.OutroMs = o.IntroMs, o.OutroMs
- if o.IntroMs != nil || o.OutroMs != nil {
- applied++
+ if e := byID[o.EpisodeID]; e != nil {
+ e.DetectedAtUnix = now
}
}
- log.Printf("%s: intro on %d/%d%s, outro on %d/%d%s, %d episode(s) carry offsets, took %s",
+ // Reported, deliberately not published.
+ //
+ // Intro detection assumes the agreeing episodes were fetched independently. Mirroring eight
+ // of them within one minute means they were not, and it showed: the segment confirmed on 8
+ // of 8 Radiolab episodes was searched for in an ad-free copy of the same episode fetched
+ // hours earlier and scored r = 0.0621. It was an advertisement. Until the agreeing episodes
+ // are required to span more than one fetch occasion these numbers cannot be trusted, and
+ // the advertising they keep finding is better served by the scan that measures itself
+ // against the declared-versus-served length gap.
+ log.Printf("%s: intro on %d/%d%s, outro on %d/%d%s (not published -- see detectAds), took %s",
show.Title,
result.IntroConfirmedOn, len(episodes), checkNote(result.IntroCheck),
result.OutroConfirmedOn, len(episodes), checkNote(result.OutroCheck),
- applied, time.Since(started).Round(time.Second))
+ time.Since(started).Round(time.Second))
+
+ w.detectAds(ctx, show, episodes, byID)
+}
+
+// detectAds finds the advertising a batch of episodes shares, which is what simultaneous
+// mirroring actually makes detectable.
+//
+// Separate from intro detection, and trusted differently. Intro detection assumes the episodes
+// were fetched independently; mirroring eight of them in one minute means they were not, which
+// is how it came to confirm an advertisement as an intro. This uses that dependence rather than
+// being defeated by it -- and reports what it found against the declared-versus-served length
+// gap, a figure it did not produce and which can therefore contradict it.
+func (w *worker) detectAds(
+ ctx context.Context,
+ show *store.Show,
+ episodes []skips.Episode,
+ byID map[string]*store.Episode,
+) {
+ if ctx.Err() != nil {
+ return
+ }
+ started := time.Now()
+ ads, err := skips.DetectAds(ctx, w.decoder, episodes, skips.DefaultAdConfig())
+ if err != nil {
+ log.Printf("%s: ad scan failed: %v", show.Title, err)
+ return
+ }
+
+ var withBreaks, refused int
+ var foundTotal, insertedTotal int64
+ for _, ep := range episodes {
+ e := byID[ep.ID]
+ if e == nil {
+ continue
+ }
+ acc := ads.Accounting[ep.ID]
+ e.FoundAdMs, e.InsertedAdMs = acc.FoundMs, acc.InsertedMs
+ e.AdsRefused = ads.Refused[ep.ID]
+ e.Breaks = nil
+ for _, br := range ads.Breaks[ep.ID] {
+ e.Breaks = append(e.Breaks, store.Break{
+ StartMs: br.StartMs,
+ EndMs: br.EndMs,
+ Partners: br.Partners,
+ })
+ }
+ if len(e.Breaks) > 0 {
+ withBreaks++
+ }
+ if e.AdsRefused {
+ refused++
+ }
+ foundTotal += acc.FoundMs
+ insertedTotal += acc.InsertedMs
+ }
+
+ log.Printf("%s: ads on %d/%d episode(s)%s, accounted %s of %s inserted, took %s",
+ show.Title, withBreaks, len(episodes), refusedNote(refused),
+ clock(foundTotal), clock(insertedTotal), time.Since(started).Round(time.Second))
+}
+
+func refusedNote(refused int) string {
+ if refused == 0 {
+ return ""
+ }
+ return fmt.Sprintf(" (%d refused as too extensive to be advertising)", refused)
+}
+
+func clock(ms int64) string {
+ total := ms / 1000
+ return fmt.Sprintf("%d:%02d", total/60, total%60)
}
// checkNote reports a withheld result loudly. A confirmation that could not verify itself is