summaryrefslogtreecommitdiff
path: root/server/internal
diff options
context:
space:
mode:
Diffstat (limited to 'server/internal')
-rw-r--r--server/internal/store/store.go18
-rw-r--r--server/internal/store/store_test.go50
-rw-r--r--server/internal/transcript/fits.go10
-rw-r--r--server/internal/transcript/store.go15
4 files changed, 92 insertions, 1 deletions
diff --git a/server/internal/store/store.go b/server/internal/store/store.go
index 7f81bd5..2a6992b 100644
--- a/server/internal/store/store.go
+++ b/server/internal/store/store.go
@@ -73,6 +73,14 @@ type Episode struct {
// states as everywhere else: words, a reason, or nobody has looked.
MasterCues int `json:"masterCues,omitempty"`
MasterNote string `json:"masterNote,omitempty"`
+ // MasterSource says what kind of master it is: "published" for the publisher's own ad-free
+ // transcript, "refetch" for a recognised second fetch of the same episode. They are not
+ // equally good and the readout should not pretend otherwise -- a refetch carries its own
+ // advertising, so anything both copies happen to share is invisible to it.
+ MasterSource string `json:"masterSource,omitempty"`
+ // RefetchAttemptedAtUnix is when a second copy was last fetched for this episode, so a
+ // failure is not retried every cycle. Recorded only when an attempt finished.
+ RefetchAttemptedAtUnix int64 `json:"refetchAttemptedAtUnix,omitempty"`
// TranscribeAttemptedAtUnix is when speech recognition last ran over this episode, whether
// it worked or not, so a failure is not retried on every poll for ever. Recorded only when
@@ -110,6 +118,16 @@ func (e *Episode) NeedsMaster() bool {
e.MasterCues == 0 && e.MasterNote == ""
}
+// NeedsRefetch reports whether a second copy of this episode is worth fetching.
+//
+// Only for an episode that has its own words but no master to subtract them from, and only
+// where the lengths say advertising was inserted: without a gap the two fetches would be the
+// same recording and there would be nothing to find.
+func (e *Episode) NeedsRefetch() bool {
+ return e.Mirrored() && e.HasTranscript() && e.EnclosureURL != "" &&
+ e.MasterCues == 0 && e.MasterNote == "" && e.RefetchAttemptedAtUnix == 0
+}
+
// NeedsRecognising reports whether speech recognition should be run over this episode.
//
// Deliberately not the negation of [Episode.TranscriptExamined]. An episode whose publisher's
diff --git a/server/internal/store/store_test.go b/server/internal/store/store_test.go
index c04dc29..9020b17 100644
--- a/server/internal/store/store_test.go
+++ b/server/internal/store/store_test.go
@@ -435,3 +435,53 @@ func TestAFinishedAttemptIsNotRepeated(t *testing.T) {
t.Error("an episode already attempted would be attempted again")
}
}
+
+/*
+Which episodes are worth fetching a second time.
+
+Advertising is inserted per request, so two fetches of the same episode carry different
+campaigns and agree on the programme. Subtracting one from the other therefore says where the
+advertising is -- the same arithmetic as a publisher's master, with a second recording standing
+in for one.
+
+Only worth doing where there is advertising to find. An episode whose served length matches what
+the feed declares has none inserted, and the second fetch would be the same recording.
+*/
+
+func TestAnEpisodeWithItsOwnWordsAndNoMasterIsWorthRefetching(t *testing.T) {
+ e := Episode{
+ Path: "abc.mp3", EnclosureURL: "https://cdn/1.mp3", TranscriptCues: 400,
+ }
+ if !e.NeedsRefetch() {
+ t.Error("an episode with words and nothing to subtract them from was passed over")
+ }
+}
+
+func TestAnEpisodeThatAlreadyHasAMasterIsNotRefetched(t *testing.T) {
+ e := Episode{
+ Path: "abc.mp3", EnclosureURL: "https://cdn/1.mp3", TranscriptCues: 400, MasterCues: 300,
+ }
+ if e.NeedsRefetch() {
+ t.Error("would fetch a second copy for an episode that already has a master")
+ }
+}
+
+// Nothing to subtract from until its own words exist.
+func TestAnEpisodeWithNoWordsYetIsNotRefetched(t *testing.T) {
+ e := Episode{Path: "abc.mp3", EnclosureURL: "https://cdn/1.mp3"}
+ if e.NeedsRefetch() {
+ t.Error("would fetch a second copy before there was a first to compare it against")
+ }
+}
+
+// A finished attempt is not repeated: a publisher that will not serve it again will not serve
+// it again next cycle either.
+func TestAFinishedRefetchIsNotRepeated(t *testing.T) {
+ e := Episode{
+ Path: "abc.mp3", EnclosureURL: "https://cdn/1.mp3", TranscriptCues: 400,
+ RefetchAttemptedAtUnix: 1_700_000_000,
+ }
+ if e.NeedsRefetch() {
+ t.Error("would fetch a second copy again")
+ }
+}
diff --git a/server/internal/transcript/fits.go b/server/internal/transcript/fits.go
index b0495d0..c22958b 100644
--- a/server/internal/transcript/fits.go
+++ b/server/internal/transcript/fits.go
@@ -76,6 +76,16 @@ func Fits(lastCueEndMs, servedMs, declaredMs int64) Verdict {
return Verdict{OK: true}
}
+// HasInsertedAdvertising reports whether this copy is longer than the feed says the programme
+// is, by more than the feed could be approximate about.
+//
+// The one fact about an episode that comes from neither a detector nor a transcript. An episode
+// without it has nothing inserted to find, so there is no point fetching a second copy to
+// compare against -- the two would be the same recording.
+func HasInsertedAdvertising(servedMs, declaredMs int64) bool {
+ return declaredMs > 0 && servedMs-declaredMs > insertionMs
+}
+
// LastCueEndMs is where a transcript stops describing the audio.
func LastCueEndMs(cues []Cue) int64 {
if len(cues) == 0 {
diff --git a/server/internal/transcript/store.go b/server/internal/transcript/store.go
index 9450b3a..50feae4 100644
--- a/server/internal/transcript/store.go
+++ b/server/internal/transcript/store.go
@@ -19,6 +19,15 @@ const (
// SourceASR is speech recognition over the mirrored bytes: approximate words, of exactly
// the audio the phone will play.
SourceASR Source = "asr"
+ // SourceRefetch is speech recognition over a *second* fetch of the same episode, kept as a
+ // master for subtraction. It is not ad-free -- it carries whatever campaign was running
+ // when it was fetched -- but advertising is inserted per request, so the two copies differ
+ // in exactly the places the advertising sits. What they share is the programme.
+ //
+ // Subtracting it therefore reports the mirrored copy's advertising minus anything both
+ // copies happened to carry, which is the one thing it cannot see. That is the opposite
+ // blind spot to the audio scan, which only finds what a batch of episodes shares.
+ SourceRefetch Source = "refetch"
)
// Transcript is one episode's words and when they were said.
@@ -110,9 +119,13 @@ func (s Store) Put(t Transcript) error {
// inserted into, and subtracting it from a recognised reading of the served file is what says
// where. See Inserted.
func (s Store) PutMaster(t Transcript) error {
+ source := t.Source
+ if source == "" {
+ source = SourcePublished
+ }
return s.Put(Transcript{
EpisodeID: t.EpisodeID + masterSuffix,
- Source: SourcePublished,
+ Source: source,
LanguageCode: t.LanguageCode,
Cues: t.Cues,
})