196 lines
8.2 KiB
Go
196 lines
8.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/store"
|
|
)
|
|
|
|
// The console's half of subtitle downloads.
|
|
//
|
|
// The operator has two providers to choose between and they need different things said
|
|
// about them. Bazarr is a service the household already runs, so the console can only turn
|
|
// it on or off — its address is deployment configuration and stays an environment
|
|
// variable. OpenSubtitles is an account, so its credentials live here and can be entered,
|
|
// replaced or removed without a redeployment.
|
|
//
|
|
// Nothing on this page ever returns a credential. The console is told whether a key is
|
|
// saved and whether an account is attached, which is what an operator needs to answer
|
|
// "why is this not working", and never the values themselves — the stance the MDBList page
|
|
// already takes.
|
|
|
|
// subtitleAdminSettings is the page's whole view of the policy.
|
|
type subtitleAdminSettings struct {
|
|
// BazarrConfigured is whether this deployment has a Bazarr at all. It is separate from
|
|
// BazarrEnabled so the page can say "no address configured" rather than drawing a
|
|
// switch that would do nothing.
|
|
BazarrConfigured bool `json:"bazarrConfigured"`
|
|
BazarrEnabled bool `json:"bazarrEnabled"`
|
|
BazarrURL string `json:"bazarrUrl,omitempty"`
|
|
|
|
OpenSubtitlesEnabled bool `json:"openSubtitlesEnabled"`
|
|
OpenSubtitlesKeyConfigured bool `json:"openSubtitlesKeyConfigured"`
|
|
// OpenSubtitlesAccount is whether a username and password are saved. It matters more
|
|
// than it looks: without one, downloads go against the anonymous allowance, which is a
|
|
// handful of files a day and fails in front of a television rather than in a log.
|
|
OpenSubtitlesAccount bool `json:"openSubtitlesAccount"`
|
|
OpenSubtitlesUsername string `json:"openSubtitlesUsername,omitempty"`
|
|
|
|
// FeatureEnabled is the `subtitle_download` flag. It is reported here because it
|
|
// overrides both providers, and an operator who has turned it off on the features page
|
|
// should not have to guess why these switches do nothing.
|
|
FeatureEnabled bool `json:"featureEnabled"`
|
|
// Available is the answer a television gets: the feature is on and at least one
|
|
// provider can be asked.
|
|
Available bool `json:"available"`
|
|
|
|
Stored store.DownloadedSubtitleStats `json:"stored"`
|
|
}
|
|
|
|
func (s *Server) subtitleAdminSettings(ctx context.Context) subtitleAdminSettings {
|
|
policy := s.subtitlePolicy(ctx)
|
|
sources := s.subtitleSources(ctx)
|
|
settings := subtitleAdminSettings{
|
|
BazarrConfigured: s.bazarr != nil,
|
|
BazarrEnabled: policy.BazarrEnabled,
|
|
BazarrURL: s.cfg.BazarrURL,
|
|
OpenSubtitlesEnabled: policy.OpenSubtitlesEnabled,
|
|
OpenSubtitlesKeyConfigured: policy.OpenSubtitlesAPIKey != "",
|
|
OpenSubtitlesAccount: policy.OpenSubtitlesUsername != "" && policy.OpenSubtitlesPassword != "",
|
|
OpenSubtitlesUsername: policy.OpenSubtitlesUsername,
|
|
FeatureEnabled: s.featureEnabled(ctx, featureSubtitleDownload),
|
|
Available: sources.any(),
|
|
}
|
|
if stats, err := s.store.DownloadedSubtitleStats(ctx); err == nil {
|
|
settings.Stored = stats
|
|
} else {
|
|
s.loggerFor(ctx).Warn("downloaded subtitle stats failed", "error", err)
|
|
}
|
|
return settings
|
|
}
|
|
|
|
type subtitleSettingsRequest struct {
|
|
Action string `json:"action"`
|
|
|
|
BazarrEnabled bool `json:"bazarrEnabled"`
|
|
OpenSubtitlesEnabled bool `json:"openSubtitlesEnabled"`
|
|
|
|
// A blank key keeps whatever is saved, so an operator changing one switch does not
|
|
// have to paste a credential back in to do it. Clearing is its own flag, because
|
|
// "leave it alone" and "remove it" cannot both be the empty string.
|
|
OpenSubtitlesAPIKey string `json:"openSubtitlesApiKey"`
|
|
ClearOpenSubtitlesAPIKey bool `json:"clearOpenSubtitlesApiKey"`
|
|
OpenSubtitlesUsername string `json:"openSubtitlesUsername"`
|
|
OpenSubtitlesPassword string `json:"openSubtitlesPassword"`
|
|
ClearOpenSubtitlesLogin bool `json:"clearOpenSubtitlesLogin"`
|
|
}
|
|
|
|
func (s *Server) handleAdminSubtitleSettings(w http.ResponseWriter, r *http.Request) {
|
|
var req subtitleSettingsRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "malformed request body")
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
|
|
// Emptying the store is the page's one destructive control, and it is safe in the way
|
|
// a cache purge is: every file can be fetched again, at the cost of the provider
|
|
// allowance that fetched it. It is a separate action rather than a checkbox on the
|
|
// save, so it cannot happen as a side effect of changing a switch.
|
|
if strings.TrimSpace(req.Action) == "clear-stored" {
|
|
removed, err := s.store.ClearDownloadedSubtitles(ctx)
|
|
if err != nil {
|
|
s.loggerFor(ctx).Error("clearing stored subtitles failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "could not clear stored subtitles")
|
|
return
|
|
}
|
|
s.loggerFor(ctx).Info("stored subtitles cleared", "removed", removed)
|
|
writeJSON(w, http.StatusOK, s.subtitleAdminSettings(ctx))
|
|
return
|
|
}
|
|
|
|
current, err := s.store.SubtitlePolicy(ctx)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "could not read subtitle settings")
|
|
return
|
|
}
|
|
next := store.SubtitlePolicy{
|
|
BazarrEnabled: req.BazarrEnabled,
|
|
OpenSubtitlesEnabled: req.OpenSubtitlesEnabled,
|
|
OpenSubtitlesAPIKey: current.OpenSubtitlesAPIKey,
|
|
OpenSubtitlesUsername: current.OpenSubtitlesUsername,
|
|
OpenSubtitlesPassword: current.OpenSubtitlesPassword,
|
|
}
|
|
if req.ClearOpenSubtitlesAPIKey {
|
|
next.OpenSubtitlesAPIKey = ""
|
|
} else if replacement := strings.TrimSpace(req.OpenSubtitlesAPIKey); replacement != "" {
|
|
next.OpenSubtitlesAPIKey = replacement
|
|
}
|
|
if req.ClearOpenSubtitlesLogin {
|
|
next.OpenSubtitlesUsername, next.OpenSubtitlesPassword = "", ""
|
|
} else if username := strings.TrimSpace(req.OpenSubtitlesUsername); username != "" {
|
|
next.OpenSubtitlesUsername = username
|
|
// The password only moves when one was typed. Changing a username without
|
|
// retyping the password is an ordinary edit, and the field is blank on every load.
|
|
if password := req.OpenSubtitlesPassword; password != "" {
|
|
next.OpenSubtitlesPassword = password
|
|
}
|
|
}
|
|
|
|
if err := s.store.SetSubtitlePolicy(ctx, next); err != nil {
|
|
s.loggerFor(ctx).Error("subtitle policy write failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "could not save subtitle settings")
|
|
return
|
|
}
|
|
s.loggerFor(ctx).Info("subtitle providers changed",
|
|
"bazarr", next.BazarrEnabled,
|
|
"opensubtitles", next.OpenSubtitlesEnabled,
|
|
"opensubtitles_account", next.OpenSubtitlesUsername != "",
|
|
)
|
|
writeJSON(w, http.StatusOK, s.subtitleAdminSettings(ctx))
|
|
}
|
|
|
|
// handleAdminSubtitleTest asks each enabled provider whether it is actually reachable.
|
|
//
|
|
// It exists because every other symptom of a wrong key looks identical from a television:
|
|
// the search comes back empty. One button that says "the key is rejected" is the whole
|
|
// difference between a five-minute fix and an evening of guessing.
|
|
func (s *Server) handleAdminSubtitleTest(w http.ResponseWriter, r *http.Request) {
|
|
if s.rejectWorkDuringQuietTime(w) {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
type probe struct {
|
|
Provider string `json:"provider"`
|
|
OK bool `json:"ok"`
|
|
Message string `json:"message"`
|
|
}
|
|
results := []probe{}
|
|
|
|
if s.bazarr != nil {
|
|
result := probe{Provider: "Bazarr", OK: true, Message: "Reachable."}
|
|
if err := s.bazarr.Ping(ctx); err != nil {
|
|
result.OK, result.Message = false, "Did not answer: "+err.Error()
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
if client := s.openSubtitlesClient(ctx); client != nil {
|
|
result := probe{Provider: "OpenSubtitles", OK: true}
|
|
if err := client.Ping(ctx); err != nil {
|
|
result.OK, result.Message = false, "Did not answer: "+err.Error()
|
|
} else if client.HasAccount() {
|
|
result.Message = "Reachable, signed in."
|
|
} else {
|
|
// Worth saying rather than reporting a plain success: an anonymous key works
|
|
// perfectly for searching and runs out after a few downloads, which is the
|
|
// failure this page exists to make findable.
|
|
result.Message = "Reachable, but with no account — downloads use the small anonymous allowance."
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"results": results})
|
|
}
|