summaryrefslogtreecommitdiff
path: root/server/internal
diff options
context:
space:
mode:
Diffstat (limited to 'server/internal')
-rw-r--r--server/internal/advertising/advertising.go25
-rw-r--r--server/internal/advertising/audit_test.go71
-rw-r--r--server/internal/signature/shadow.go13
-rw-r--r--server/internal/skips/kinds.go11
-rw-r--r--server/internal/transcript/ads.go9
-rw-r--r--server/internal/transcript/fits.go13
6 files changed, 124 insertions, 18 deletions
diff --git a/server/internal/advertising/advertising.go b/server/internal/advertising/advertising.go
new file mode 100644
index 0000000..d56f500
--- /dev/null
+++ b/server/internal/advertising/advertising.go
@@ -0,0 +1,25 @@
+// Package advertising holds what is known about the shape of inserted advertising, for the
+// packages that have to recognise it.
+//
+// It exists because one measured figure -- the length of an ad slot -- had been written out four
+// times, in skips, transcript (twice) and signature, each copy carrying a comment pointing at the
+// others: "the same unit skips.Classify uses", "the same figure the length-gap rule uses", "one
+// ad unit from the start". The agreement was real and deliberate, and recorded only in prose, so
+// a corpus that revised the figure would have moved one copy and left three.
+package advertising
+
+// UnitMs is the length of one advertising slot.
+//
+// Advertising is sold in units of roughly fifteen seconds -- the owner's own observation, borne
+// out by the corpus -- and three separate rules depend on that same fact:
+//
+// - a served file exceeding its declared length by less than one unit cannot carry a real
+// insertion; it is the feed being approximate, the way encoders disagree about a file's
+// length by a fraction of a second;
+// - anything briefer than one unit is a mention rather than a break, and skipping it would
+// clip the programme;
+// - one unit from the start of a guessed break is far more audio than a landmark fingerprint
+// needs, and is the part most certainly advertising rather than programme.
+//
+// Changing this changes all three, which is the point of it being one constant.
+const UnitMs int64 = 15_000
diff --git a/server/internal/advertising/audit_test.go b/server/internal/advertising/audit_test.go
new file mode 100644
index 0000000..f8d8e73
--- /dev/null
+++ b/server/internal/advertising/audit_test.go
@@ -0,0 +1,71 @@
+package advertising
+
+import (
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+/*
+The ad slot's length is written once.
+
+This package exists because the figure had been written out four times -- in skips, in transcript
+twice, and in signature -- each copy carrying a comment saying it agreed with the others. The
+agreement held only as long as somebody read all four comments before changing one.
+
+Consolidating them fixes the four copies that existed. This refuses the fifth: a production file
+that writes the number instead of naming the constant. The same arrangement as
+safefetch/audit_test.go, and for the same reason -- the instance that was noticed was never the
+whole of it.
+*/
+
+// literal matches the slot length written as a number, in either of Go's spellings.
+var literal = regexp.MustCompile(`\b15_?000\b`)
+
+// allowed names the files that may write it, and why.
+var allowed = map[string]string{
+ // This package is where the figure lives.
+ filepath.Join("internal", "advertising", "advertising.go"): "defines it",
+}
+
+func TestTheAdSlotLengthIsNotWrittenTwice(t *testing.T) {
+ root, err := filepath.Abs("../..")
+ if err != nil {
+ t.Fatalf("root: %v", err)
+ }
+
+ err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
+ return err
+ }
+ // A test states its own fixtures: an episode 15 seconds long is the case being
+ // described, not a second definition of the unit.
+ if strings.HasSuffix(path, "_test.go") {
+ return nil
+ }
+ relative, err := filepath.Rel(root, path)
+ if err != nil {
+ return err
+ }
+ if _, ok := allowed[relative]; ok {
+ return nil
+ }
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ for _, line := range strings.Split(string(raw), "\n") {
+ if literal.MatchString(line) {
+ t.Errorf("%s writes the ad slot length as a literal (%s) -- use "+
+ "advertising.UnitMs, so a revised figure moves every rule that depends on it",
+ relative, strings.TrimSpace(line))
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("walk: %v", err)
+ }
+}
diff --git a/server/internal/signature/shadow.go b/server/internal/signature/shadow.go
index 928743a..fe856a7 100644
--- a/server/internal/signature/shadow.go
+++ b/server/internal/signature/shadow.go
@@ -1,6 +1,10 @@
package signature
-import "context"
+import (
+ "context"
+
+ "github.com/terst/listen/server/internal/advertising"
+)
// Audio supplies decoded audio at [SampleRate].
type Audio interface {
@@ -70,9 +74,10 @@ type Proposal struct {
//
// An uncorrected end is wherever the 25-second jump happened to land, not where the advertising
// stopped, so the tail of that range may be programme -- and a signature carrying programme is
-// the one thing this must never build. One ad unit from the start is far more audio than a
-// landmark fingerprint needs to be distinctive, and it is the part most certainly advertising.
-const GuessedEndSeedMs = int64(15_000)
+// the one thing this must never build. One advertising slot from the start is far more audio
+// than a landmark fingerprint needs to be distinctive, and it is the part most certainly
+// advertising.
+const GuessedEndSeedMs = advertising.UnitMs
// Report is what one shadow run saw.
type Report struct {
diff --git a/server/internal/skips/kinds.go b/server/internal/skips/kinds.go
index 66d4b21..f38f040 100644
--- a/server/internal/skips/kinds.go
+++ b/server/internal/skips/kinds.go
@@ -1,5 +1,7 @@
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.
@@ -17,11 +19,10 @@ const (
const (
// adUnitMs is how far served may exceed declared before the excess is read as an insertion.
- // Advertising is sold in units of roughly fifteen seconds -- the owner's own observation, and
- // borne out by the corpus -- so a served length exceeding declared by less than one such unit
- // cannot be a real inserted advertisement: it is the feed being approximate, the same way
- // encoders disagree about a file's length by a fraction of a second.
- adUnitMs = 15_000
+ // 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.
diff --git a/server/internal/transcript/ads.go b/server/internal/transcript/ads.go
index f9f9a12..069909b 100644
--- a/server/internal/transcript/ads.go
+++ b/server/internal/transcript/ads.go
@@ -3,6 +3,8 @@ package transcript
import (
"regexp"
"strings"
+
+ "github.com/terst/listen/server/internal/advertising"
)
/*
@@ -39,10 +41,9 @@ const (
// with the product description between them; 25 gives that room without reaching the
// programme on either side.
bridgeMs = 25_000
- // minCandidateMs is the shortest thing worth calling an advertisement. Advertising is sold
- // in units of roughly fifteen seconds, the same figure the length-gap rule uses, and
- // anything briefer is a mention rather than a break -- skipping one would clip the show.
- minCandidateMs = 15_000
+ // minCandidateMs is the shortest thing worth calling an advertisement. One advertising slot:
+ // anything briefer is a mention rather than a break, and skipping it would clip the show.
+ minCandidateMs = advertising.UnitMs
// minKinds is how many different kinds of marker must appear. Two, because one kind
// repeated says one thing: a host saying "sponsor" four times while describing a
// sponsorship deal is the programme, which is the same trap the toc=false chapter rule
diff --git a/server/internal/transcript/fits.go b/server/internal/transcript/fits.go
index c22958b..de4d74c 100644
--- a/server/internal/transcript/fits.go
+++ b/server/internal/transcript/fits.go
@@ -1,13 +1,16 @@
package transcript
-import "fmt"
+import (
+ "fmt"
+
+ "github.com/terst/listen/server/internal/advertising"
+)
const (
// insertionMs is how far a served file may exceed its declared length before the excess is
- // read as inserted advertising rather than as the feed being approximate. Fifteen seconds,
- // the same unit skips.Classify uses and for the same reason: advertising is sold in units of
- // roughly that length, so a smaller gap cannot be a real insertion.
- insertionMs = 15_000
+ // read as inserted advertising rather than as the feed being approximate. One advertising
+ // slot, the same judgement skips.Classify makes and for the same reason.
+ insertionMs = advertising.UnitMs
// tailMs is how much of a file may follow the last spoken word before the silence has to be
// explained. Closing music runs to fifteen seconds in this corpus (LOW's "Part 5: Bane"
// ends 15.2s before its file does); the shortest advertisement does not fit in sixty.