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 }