feat(subtitles): subtitle-fetch jobs vía sync + auto-fetch opcional en scan
El web empuja SubtitleFetchRequest por el sync (URL del proxy, ya charset-fixed a WebVTT); el daemon lo descarga y lo escribe como sidecar <base>.<lang>.vtt junto al medio (contención en scan paths con EvalSymlinks, cap 10 MiB) y reporta done/failed en el siguiente sync para que el web marque el job. Config nueva library.subtitles (auto_fetch + languages) para el auto-fetch en scan, off por defecto.
This commit is contained in:
parent
63be565227
commit
6a7a2e292e
10 changed files with 264 additions and 22 deletions
|
|
@ -66,6 +66,12 @@ type SyncClient struct {
|
|||
// It should delete the files and return the IDs of successfully deleted items.
|
||||
OnDeleteFiles func(items []LibraryDeleteRequest) []int
|
||||
|
||||
// OnSubtitleFetch is called when the server requests on-demand subtitle
|
||||
// downloads. It should download each (from req.URL, already VTT), write a
|
||||
// sidecar next to req.FilePath, and return the IDs successfully fetched plus
|
||||
// the ones that failed (so the web can mark them errored).
|
||||
OnSubtitleFetch func(reqs []SubtitleFetchRequest) ([]int, []SubtitleFetchError)
|
||||
|
||||
// OnRevoked is called when a sync is rejected because this agent's credential
|
||||
// was revoked (the user deleted the agent from the dashboard). The daemon
|
||||
// wires this to wipe the stored key + stop — it must NOT keep retrying or the
|
||||
|
|
@ -89,6 +95,11 @@ type SyncClient struct {
|
|||
// deleteInFlight tracks item IDs currently being processed or awaiting confirmation.
|
||||
// Prevents the same file from being passed to OnDeleteFiles multiple times.
|
||||
deleteInFlight map[int]struct{}
|
||||
|
||||
// Subtitle-fetch jobs awaiting confirmation + dedup (guarded by pendingDeleteMu).
|
||||
pendingSubtitlesFetched []int
|
||||
pendingSubtitlesFailed []SubtitleFetchError
|
||||
subtitleInFlight map[int]struct{}
|
||||
}
|
||||
|
||||
// NewSyncClient creates a sync client.
|
||||
|
|
@ -218,6 +229,20 @@ func (sc *SyncClient) buildRequest() SyncRequest {
|
|||
}
|
||||
sc.pendingDeleteConfirmed = nil
|
||||
}
|
||||
if len(sc.pendingSubtitlesFetched) > 0 {
|
||||
req.SubtitlesFetched = sc.pendingSubtitlesFetched
|
||||
for _, id := range sc.pendingSubtitlesFetched {
|
||||
delete(sc.subtitleInFlight, id)
|
||||
}
|
||||
sc.pendingSubtitlesFetched = nil
|
||||
}
|
||||
if len(sc.pendingSubtitlesFailed) > 0 {
|
||||
req.SubtitlesFailed = sc.pendingSubtitlesFailed
|
||||
for _, f := range sc.pendingSubtitlesFailed {
|
||||
delete(sc.subtitleInFlight, f.ID)
|
||||
}
|
||||
sc.pendingSubtitlesFailed = nil
|
||||
}
|
||||
sc.pendingDeleteMu.Unlock()
|
||||
return req
|
||||
}
|
||||
|
|
@ -289,6 +314,37 @@ func (sc *SyncClient) processResponse(resp *SyncResponse) {
|
|||
}(newItems)
|
||||
}
|
||||
}
|
||||
|
||||
// On-demand subtitle fetches — dedup against in-flight, run off the sync
|
||||
// goroutine (network + disk I/O), confirm on the next cycle.
|
||||
if len(resp.SubtitleFetches) > 0 && sc.OnSubtitleFetch != nil {
|
||||
sc.pendingDeleteMu.Lock()
|
||||
if sc.subtitleInFlight == nil {
|
||||
sc.subtitleInFlight = make(map[int]struct{})
|
||||
}
|
||||
var newReqs []SubtitleFetchRequest
|
||||
for _, r := range resp.SubtitleFetches {
|
||||
if _, inFlight := sc.subtitleInFlight[r.ID]; !inFlight {
|
||||
newReqs = append(newReqs, r)
|
||||
sc.subtitleInFlight[r.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
sc.pendingDeleteMu.Unlock()
|
||||
|
||||
if len(newReqs) > 0 {
|
||||
go func(reqs []SubtitleFetchRequest) {
|
||||
done, failed := sc.OnSubtitleFetch(reqs)
|
||||
// Both done and failed are reported on the next uplink; buildRequest
|
||||
// clears them from subtitleInFlight when it flushes them. A failure
|
||||
// becomes status='error' on the web (no silent infinite retry — the
|
||||
// user re-requests, which creates a fresh row).
|
||||
sc.pendingDeleteMu.Lock()
|
||||
sc.pendingSubtitlesFetched = append(sc.pendingSubtitlesFetched, done...)
|
||||
sc.pendingSubtitlesFailed = append(sc.pendingSubtitlesFailed, failed...)
|
||||
sc.pendingDeleteMu.Unlock()
|
||||
}(newReqs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runWakeListener holds a long-poll connection to /api/internal/agent/wake.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue