1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
package skips
import "github.com/terst/listen/server/internal/advertising"
// BreakKind says what a published break actually is, so advertising and a show's own furniture
// can be told apart -- by the phone, which skips them on different terms, and by anyone reading
// the accounting.
type BreakKind string
const (
// KindAd is advertising inserted into this copy of the episode.
KindAd BreakKind = "ad"
// KindIntro, KindCue and KindOutro are the show's own recurring audio: its opening, the
// music it plays at an ad break, and its closing credits.
KindIntro BreakKind = "intro"
KindCue BreakKind = "cue"
KindOutro BreakKind = "outro"
)
const (
// adUnitMs is how far served may exceed declared before the excess is read as an insertion.
// One advertising slot: a gap smaller than that cannot be a real insertion, it is the feed
// being approximate, the same way encoders disagree about a file's length by a fraction of
// a second.
adUnitMs = advertising.UnitMs
// edgeWindowMs is how close to either end a break must sit to be the opening or the closing
// rather than a break cue. Two minutes: Jon Stewart's opening runs 98 seconds and Search
// Engine's begins at 2:00.
edgeWindowMs = 120_000
)
// Classify decides what each of one episode's breaks is.
//
// The length gap is the evidence, and the point of using it is that the detector does not
// produce it: the feed declares the ad-free length and the file is whatever was served. An
// episode served no more than one ad unit longer than declared had nothing inserted -- the excess
// is the feed being approximate, not an advertisement -- so whatever recurs in it is the show's
// own audio however strongly it matched.
//
// A served length *shorter* than declared is also not evidence of insertion, by any amount: you
// cannot insert advertising into a file and have it come out shorter, so a negative gap is
// evidence that nothing was inserted, not evidence that the feed can't be trusted. (An earlier
// version of this function disagreed and called every materially-shorter episode advertising,
// worried that a feed wrong about its own length in the short direction couldn't be trusted to
// judge the other direction either. Measured against the owner's library of 334 mirrored
// episodes, that caution cost far more than it protected: 31% of episodes are served shorter than
// declared -- feeds are routinely sloppy by tens of seconds -- and every break in every one of
// them fell to the unjudgeable default and got labelled advertising, including closing credits
// three seconds from the end of a file. Reversed here: a negative gap is classified by position
// like any other furniture.)
//
// Everything else -- served more than one ad unit longer than declared, or either length unknown
// -- is called advertising. That is still deliberately the unrefined answer for the case that is
// genuinely judgeable -- an ad-carrying episode also contains its own intro and cue -- but it
// matches what the app does today, and separating the two inside such an episode needs the
// furniture library, which cannot confirm anything until a second fetch occasion has passed.
func Classify(breaks []AdBreak, servedMs, declaredMs int64) []AdBreak {
out := make([]AdBreak, 0, len(breaks))
// If we can't judge (missing served or declared length), treat conservatively as inserted ads.
// A served length more than one ad unit longer than declared is evidence of an insertion. A
// served length shorter than declared -- by any amount -- is not: a file cannot come out
// shorter than it started by having something added to it, so there is no insertion to find.
inserted := servedMs <= 0 || declaredMs <= 0 || (servedMs-declaredMs) > adUnitMs
for _, b := range breaks {
b.Kind = KindAd
if !inserted {
b.Kind = furnitureKindAt(b, servedMs)
}
out = append(out, b)
}
return out
}
// furnitureKindAt names a break by where it sits.
//
// For normal-length episodes, the opening and closing windows don't overlap. For very short
// episodes, they do: a break may be close to both the start and the end. In that case, classify
// by the nearest edge.
func furnitureKindAt(b AdBreak, servedMs int64) BreakKind {
if servedMs <= 0 {
// Unreachable today -- Classify sets inserted = true whenever servedMs <= 0, so this is
// never called with one. Fail-safe anyway: if that precondition ever moves, an
// unjudgeable length must still read as advertising, not as the show's own furniture.
return KindAd
}
// Use a window size proportional to the episode length for very short episodes.
// For long episodes (>= 240s), use the fixed edgeWindowMs (120s).
// For short episodes, use a quarter of the episode length as the window.
windowMs := int64(edgeWindowMs)
if servedMs < 2*edgeWindowMs {
// For short episodes (< 240s), use a quarter of the episode as the window
windowMs = servedMs / 4
}
distToStart := b.StartMs
distToEnd := servedMs - b.EndMs
inOpeningWindow := distToStart <= windowMs
inClosingWindow := distToEnd <= windowMs
if inOpeningWindow && inClosingWindow {
// In the overlap - use nearest edge
if distToStart < distToEnd {
return KindIntro
} else if distToEnd < distToStart {
return KindOutro
} else {
// Exactly in the middle - treat as cue (interior break)
return KindCue
}
}
if inOpeningWindow {
return KindIntro
}
if inClosingWindow {
return KindOutro
}
return KindCue
}
|