summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-10-14 08:00:03 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-10-14 08:00:03 +0000
commit0e274396faff9b359c5eb0edbb8b018d2456aa29 (patch)
tree5d0385095a986e138d923ba58b902949340d8bc9 /server
parent87507fabd7d7ceaaf7897d5b5fc0d9d93f6ec6d1 (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.go1
-rw-r--r--server/internal/api/api.go7
-rw-r--r--server/internal/api/api_test.go24
-rw-r--r--server/internal/skips/ads.go2
-rw-r--r--server/internal/skips/ads_test.go58
-rw-r--r--server/internal/store/store.go3
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.