diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-10-14 08:00:03 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-10-14 08:00:03 +0000 |
| commit | 0e274396faff9b359c5eb0edbb8b018d2456aa29 (patch) | |
| tree | 5d0385095a986e138d923ba58b902949340d8bc9 /server | |
| parent | 87507fabd7d7ceaaf7897d5b5fc0d9d93f6ec6d1 (diff) | |
Publish what kind each break is
The phone filters on it: advertising is always skipped, the show's own
audio by preference. Without the kind on the wire every break arrives
looking like an advertisement.
Diffstat (limited to 'server')
| -rw-r--r-- | server/cmd/listend/main.go | 1 | ||||
| -rw-r--r-- | server/internal/api/api.go | 7 | ||||
| -rw-r--r-- | server/internal/api/api_test.go | 24 | ||||
| -rw-r--r-- | server/internal/skips/ads.go | 2 | ||||
| -rw-r--r-- | server/internal/skips/ads_test.go | 58 | ||||
| -rw-r--r-- | server/internal/store/store.go | 3 |
6 files changed, 91 insertions, 4 deletions
diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go index 4be69e2..524f438 100644 --- a/server/cmd/listend/main.go +++ b/server/cmd/listend/main.go @@ -399,6 +399,7 @@ func (w *worker) detectAds( StartMs: br.StartMs, EndMs: br.EndMs, Partners: br.Partners, + Kind: string(br.Kind), }) } if len(e.Breaks) > 0 { diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 992f622..e50c94e 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -71,8 +71,9 @@ type episodeResponse struct { } type breakRange struct { - StartMs int64 `json:"startMs"` - EndMs int64 `json:"endMs"` + StartMs int64 `json:"startMs"` + EndMs int64 `json:"endMs"` + Kind string `json:"kind,omitempty"` } // Handler returns the routed, authenticated handler. @@ -163,7 +164,7 @@ func (s *Server) episode(w http.ResponseWriter, r *http.Request) { breaks := make([]breakRange, 0, len(found.Breaks)) for _, br := range found.Breaks { - breaks = append(breaks, breakRange{StartMs: br.StartMs, EndMs: br.EndMs}) + breaks = append(breaks, breakRange{StartMs: br.StartMs, EndMs: br.EndMs, Kind: br.Kind}) } writeJSON(w, http.StatusOK, episodeResponse{ Status: "ready", diff --git a/server/internal/api/api_test.go b/server/internal/api/api_test.go index 21ef1de..028bbd8 100644 --- a/server/internal/api/api_test.go +++ b/server/internal/api/api_test.go @@ -232,6 +232,30 @@ func TestAMirroredEpisodeReportsItsDetectedOffsets(t *testing.T) { } } +// The phone filters on the kind, so it has to cross the wire. Without it every break arrives +// looking like advertising and the setting has nothing to act on. +func TestEpisodeResponseCarriesTheKindOfEachBreak(t *testing.T) { + s, st, _, audioDir := newServer(t) + seedMirrored(t, st, audioDir, "audio-bytes") + e := st.Match("https://f", "g1", "", "") + e.Breaks = []store.Break{{StartMs: 0, EndMs: 98_000, Kind: "intro"}} + + rec := get(t, s, "/api/episode?feed=https://f&guid=g1") + var got episodeResponse + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if len(got.Breaks) == 0 { + t.Fatal("no breaks found") + } + if got.Breaks[0].Kind != "intro" { + t.Errorf("kind %q, want %q", got.Breaks[0].Kind, "intro") + } + if !strings.Contains(rec.Body.String(), `"kind":"intro"`) { + t.Errorf("kind missing from the response: %s", rec.Body.String()) + } +} + // The audio URL has to carry the token, or the phone gets a link it cannot use. func TestTheAudioUrlIsUsableAsGiven(t *testing.T) { s, st, _, audioDir := newServer(t) diff --git a/server/internal/skips/ads.go b/server/internal/skips/ads.go index 2ef08c6..25cf572 100644 --- a/server/internal/skips/ads.go +++ b/server/internal/skips/ads.go @@ -210,7 +210,7 @@ func DetectAds(ctx context.Context, src AudioSource, episodes []Episode, cfg AdC } if len(matched) > 0 { - result.Breaks[clip.episode.ID] = matched + result.Breaks[clip.episode.ID] = Classify(matched, clip.servedMs, clip.episode.DeclaredDurationMs) } } return result, nil diff --git a/server/internal/skips/ads_test.go b/server/internal/skips/ads_test.go index baaec43..3a4ab5f 100644 --- a/server/internal/skips/ads_test.go +++ b/server/internal/skips/ads_test.go @@ -426,3 +426,61 @@ func TestAPodBridgedPastTheSingleMatchCapIsStillPublished(t *testing.T) { t.Errorf("bridged pod is %dms, want the whole ~262000 of it", d) } } + +// The scan is where the length gap is already known, so it is where the kind is decided. Leaving +// it to the caller would mean every caller repeating the rule. +func TestDetectAdsNamesTheKindOfEachBreak(t *testing.T) { + ad := adNoise(940, 20*adRate) + stub := &adStub{ + whole: map[string][]float64{ + "a": put(adNoise(71, 600*adRate), ad, 30*adRate), + "b": put(adNoise(72, 600*adRate), ad, 300*adRate), + }, + durations: map[string]int64{"a": 600_000, "b": 600_000}, + } + // Declared equal to served: nothing was inserted, so what recurs is the show's own audio. + eps := []Episode{ + {ID: "e1", Path: "a", DeclaredDurationMs: 600_000}, + {ID: "e2", Path: "b", DeclaredDurationMs: 600_000}, + } + + got, err := DetectAds(context.Background(), stub, eps, adConfig()) + if err != nil { + t.Fatal(err) + } + + if len(got.Breaks["e1"]) == 0 { + t.Fatal("no breaks found") + } + if k := got.Breaks["e1"][0].Kind; k != KindIntro { + t.Errorf("kind %q, want %q for a break 30s into an episode with nothing inserted", k, KindIntro) + } +} + +func TestDetectAdsCallsAnInsertedBreakAdvertising(t *testing.T) { + ad := adNoise(941, 20*adRate) + stub := &adStub{ + whole: map[string][]float64{ + "a": put(adNoise(73, 600*adRate), ad, 30*adRate), + "b": put(adNoise(74, 600*adRate), ad, 300*adRate), + }, + durations: map[string]int64{"a": 600_000, "b": 600_000}, + } + // Served 600s against 540s declared: a minute of advertising is in there. + eps := []Episode{ + {ID: "e1", Path: "a", DeclaredDurationMs: 540_000}, + {ID: "e2", Path: "b", DeclaredDurationMs: 540_000}, + } + + got, err := DetectAds(context.Background(), stub, eps, adConfig()) + if err != nil { + t.Fatal(err) + } + + if len(got.Breaks["e1"]) == 0 { + t.Fatal("no breaks found") + } + if k := got.Breaks["e1"][0].Kind; k != KindAd { + t.Errorf("kind %q, want %q", k, KindAd) + } +} diff --git a/server/internal/store/store.go b/server/internal/store/store.go index 51e4ed7..4734009 100644 --- a/server/internal/store/store.go +++ b/server/internal/store/store.go @@ -60,6 +60,9 @@ type Break struct { EndMs int64 `json:"endMs"` // Partners is how many other episodes were found to carry this audio. Partners int `json:"partners,omitempty"` + // Kind is "ad" for inserted advertising, or "intro", "cue" or "outro" for the show's own + // audio. The phone skips advertising always and furniture by preference. + Kind string `json:"kind,omitempty"` } // Mirrored reports whether this episode's audio is on disk. |
