summaryrefslogtreecommitdiff
path: root/server/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'server/cmd')
-rw-r--r--server/cmd/listend/main.go76
1 files changed, 72 insertions, 4 deletions
diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go
index a4400d2..3ca2727 100644
--- a/server/cmd/listend/main.go
+++ b/server/cmd/listend/main.go
@@ -34,6 +34,7 @@ import (
"github.com/terst/listen/server/internal/signature"
"github.com/terst/listen/server/internal/skips"
"github.com/terst/listen/server/internal/store"
+ "github.com/terst/listen/server/internal/transcript"
)
func main() {
@@ -69,6 +70,8 @@ func main() {
}
decoder := decode.Decoder{}
+ transcriptDir := filepath.Join(*dataDir, "transcripts")
+ transcripts := transcript.Store{Dir: transcriptDir}
worker := &worker{
state: state,
statePath: statePath,
@@ -77,13 +80,16 @@ func main() {
keep: *keep,
mirror: &mirror.Mirror{AudioDir: audioDir, Probe: decoder},
decoder: decoder,
+ transcripts: transcripts,
+ harvester: transcript.Harvester{Store: transcripts},
requests: make(chan request, 64),
wakeups: make(chan struct{}, 1),
}
server := api.New(state, worker, api.Options{
- Token: token,
- AudioDir: audioDir,
+ Token: token,
+ AudioDir: audioDir,
+ TranscriptDir: transcriptDir,
// Persist the list the moment it arrives, then poll.
//
// Both halves were learned the hard way. The list is the one thing the server cannot
@@ -156,8 +162,12 @@ type worker struct {
keep int
mirror *mirror.Mirror
decoder decode.Decoder
- requests chan request
- wakeups chan struct{}
+ // transcripts holds each episode's words, and harvester takes the publishers' own where
+ // they describe the copy that was mirrored. See transcript.Fits for when they do not.
+ transcripts transcript.Store
+ harvester transcript.Harvester
+ requests chan request
+ wakeups chan struct{}
}
// wake asks for a poll now. Coalescing, so a burst of subscription updates is one poll.
@@ -291,6 +301,14 @@ func (w *worker) pollOne(ctx context.Context, feedURL string) {
if e.Mirrored() {
_ = w.mirror.Prune(e)
}
+ // The words go with the audio. A transcript of a file nobody holds any more is of no
+ // use to the phone, which plays the server's bytes, and the directory would otherwise
+ // grow for ever.
+ if e.TranscriptExamined() {
+ _ = w.transcripts.Prune(e.ID)
+ e.TranscriptSource, e.TranscriptCues, e.TranscriptNote = "", 0, ""
+ e.TranscriptAtUnix = 0
+ }
}
fetched := 0
@@ -311,9 +329,59 @@ func (w *worker) pollOne(ctx context.Context, feedURL string) {
if fetched > 0 {
log.Printf("%s: mirrored %d new episode(s)", channel.Title, fetched)
}
+ w.harvestTranscripts(ctx, show, kept)
w.detect(ctx, show, kept)
}
+// harvestTranscripts takes the publishers' own transcripts for a show's mirrored episodes.
+//
+// Only where the publisher's master is what was mirrored, which transcript.Fits decides from the
+// declared-versus-served length gap rather than from the words. A show using dynamic ad
+// insertion publishes a transcript of the ad-free programme, and reading along with it would be
+// minutes out of step with the audio the phone plays.
+//
+// Each episode is examined once. A refusal is a judgement about this copy, so re-fetching it on
+// every poll would be work done for ever to reach the same answer -- and the refusal is itself
+// the record of which episodes speech recognition would have to do.
+func (w *worker) harvestTranscripts(ctx context.Context, show *store.Show, candidates []*store.Episode) {
+ taken, declined := 0, 0
+ for _, e := range candidates {
+ if ctx.Err() != nil {
+ return
+ }
+ if !e.Mirrored() || e.TranscriptExamined() || e.TranscriptURL == "" {
+ continue
+ }
+ result, err := w.harvester.Harvest(ctx, transcript.Offer{
+ EpisodeID: e.ID,
+ URL: e.TranscriptURL,
+ Type: e.TranscriptType,
+ ServedMs: e.DurationMs,
+ DeclaredMs: e.DeclaredDurationMs,
+ })
+ if err != nil {
+ // The download failed, so nothing has been decided and the next poll tries again.
+ log.Printf("%s: transcript for %q: %v", show.Title, e.Title, err)
+ continue
+ }
+ switch {
+ case result.Stored:
+ e.TranscriptSource = string(transcript.SourcePublished)
+ e.TranscriptCues = result.Cues
+ e.TranscriptAtUnix = time.Now().Unix()
+ e.TranscriptNote = ""
+ taken++
+ case result.Note != "":
+ e.TranscriptNote = result.Note
+ e.TranscriptAtUnix = time.Now().Unix()
+ declined++
+ }
+ }
+ if taken > 0 || declined > 0 {
+ log.Printf("%s: transcripts -- took %d, declined %d", show.Title, taken, declined)
+ }
+}
+
// detect runs the intro/outro search over a show's mirrored episodes and records the result.
//
// Every mirrored episode at once, which is the whole reason this is a server: the phone could