package web import ( "encoding/json" "fmt" "net/http" "strings" "time" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/locale" "gitgud.io/mike/mpv-manager/pkg/log" ) func (s *Server) handleLanguagesLoadAPI(w http.ResponseWriter, r *http.Request) { audioLanguages := config.GetConfigValue(constants.ConfigKeyAudioLanguage) subtitleLanguages := config.GetConfigValue(constants.ConfigKeySubtitleLanguage) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "audio_languages": audioLanguages, "subtitle_languages": subtitleLanguages, }) } // saveLanguagePreferencesWithRetry saves audio and subtitle language preferences to mpv.conf // with retry logic and exponential backoff (3 attempts: 1s, 2s, 3s delays) // Returns accumulated errors if all retries fail func saveLanguagePreferencesWithRetry(audioLanguages, subtitleLanguages []string) error { maxRetries := 3 delays := []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second} for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { log.Info(constants.LogPrefixAPI + fmt.Sprintf("Retrying language preferences save (attempt %d/%d)...", attempt+1, maxRetries)) time.Sleep(delays[attempt-1]) } err := config.SetConfigValues([]config.ConfigValueUpdate{ {Key: constants.ConfigKeyAudioLanguage, Values: audioLanguages, PreserveComments: true}, {Key: constants.ConfigKeySubtitleLanguage, Values: subtitleLanguages, PreserveComments: true}, }) if err == nil { if len(audioLanguages) > 0 { log.Info(constants.LogPrefixAPI + "Audio language written to mpv.conf: " + strings.Join(audioLanguages, ",")) } else { log.Info(constants.LogPrefixAPI + "Audio language cleared from mpv.conf") } if len(subtitleLanguages) > 0 { log.Info(constants.LogPrefixAPI + "Subtitle language written to mpv.conf: " + strings.Join(subtitleLanguages, ",")) } else { log.Info(constants.LogPrefixAPI + "Subtitle language cleared from mpv.conf") } log.Info(constants.LogPrefixAPI + "Language preferences saved successfully") return nil } log.Warn(constants.LogPrefixAPI + fmt.Sprintf("Failed to save language preferences atomically (attempt %d): %v", attempt+1, err)) // If this is the last attempt, break and return accumulated errors if attempt == maxRetries-1 { break } } // Return accumulated errors return fmt.Errorf("failed to atomically save language preferences after %d attempts", maxRetries) } func (s *Server) handleLanguagesSaveAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") return } var req LanguagesSaveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Error(constants.LogPrefixAPI + "Failed to decode languages save request: " + err.Error()) sendJSONError(w, http.StatusBadRequest, err.Error()) return } log.Info(constants.LogPrefixAPI + "Languages save request received: audio_languages=" + fmt.Sprintf("%v", req.AudioLanguages) + ", subtitle_languages=" + fmt.Sprintf("%v", req.SubtitleLanguages)) // Validate every language code before saving (mpv.conf injection protection) for _, code := range req.AudioLanguages { if err := ValidateLanguageCode(code); err != nil { log.Error(constants.LogPrefixAPI + "Invalid audio language code: " + err.Error()) sendJSONError(w, http.StatusBadRequest, "Invalid audio language code: "+err.Error()) return } } for _, code := range req.SubtitleLanguages { if err := ValidateLanguageCode(code); err != nil { log.Error(constants.LogPrefixAPI + "Invalid subtitle language code: " + err.Error()) sendJSONError(w, http.StatusBadRequest, "Invalid subtitle language code: "+err.Error()) return } } // Save language preferences with retry logic if err := saveLanguagePreferencesWithRetry(req.AudioLanguages, req.SubtitleLanguages); err != nil { log.Error(constants.LogPrefixAPI + "Failed to save language preferences after retries: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, err.Error()) return } log.Info(constants.LogPrefixAPI + "Language preferences saved successfully") w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "success", }) } type LanguagesSaveRequest struct { AudioLanguages []string `json:"audio_languages"` SubtitleLanguages []string `json:"subtitle_languages"` } // handleAllRegionsAPI returns all regions for regional variant search func (s *Server) handleAllRegionsAPI(w http.ResponseWriter, r *http.Request) { // Load locale data from assets locales, err := assets.ReadLocales() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, err.Error()) return } // Flatten all regions from all locales, adding parent language code var allRegions []ExtendedRegionalData for _, entry := range locales { for _, region := range entry.Regions { extendedRegion := ExtendedRegionalData{ Locale: region.Locale, CountryName: region.CountryName, CountryLocal: region.CountryLocal, CountryCode: region.CountryCode, Flag: region.Flag, MajorRegionalVariant: region.MajorRegionalVariant, Population: region.Population, LanguageCode: entry.LanguageCode, LanguageName: entry.LanguageName, LanguageLocal: entry.LanguageLocal, } allRegions = append(allRegions, extendedRegion) } } w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "locales": allRegions, }) } // handleMajorLanguagesAPI returns list of major languages for step 1 selection func (s *Server) handleMajorLanguagesAPI(w http.ResponseWriter, r *http.Request) { // Load locale data from assets locales, err := assets.ReadLocales() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, err.Error()) return } // Get major languages (one entry per language code, no regional variants) majorEntries := locale.GetMajorLanguages(locales) // Convert to MajorLanguageData format majorLanguages := make([]MajorLanguageData, len(majorEntries)) for i := range majorEntries { majorLanguages[i] = MajorLanguageData{ LanguageCode: majorEntries[i].LanguageCode, LanguageName: majorEntries[i].LanguageName, LanguageLocal: majorEntries[i].LanguageLocal, Flag: locale.GetFlagForLanguageCode(majorEntries[i].LanguageCode), } } w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "major_languages": majorLanguages, }) } // handleRegionalVariantsAPI returns regional variants for a specific major language func (s *Server) handleRegionalVariantsAPI(w http.ResponseWriter, r *http.Request) { majorCode := r.URL.Query().Get(constants.QueryParamMethodID) if majorCode == "" { sendJSONError(w, http.StatusBadRequest, "Missing major language code parameter") return } // Load locale data from assets locales, err := assets.ReadLocales() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, err.Error()) return } // Find and merge all entries with matching language code // (handles cases like "Spanish" and "Spanish (Latin America)" both having code "es") majorEntry := locale.FindAndMergeByLanguageCode(majorCode, locales) if majorEntry == nil { sendJSONError(w, http.StatusNotFound, "Major language not found: "+majorCode) return } // Get regional variants with prioritization regionalVariants := locale.GetMajorRegionalVariants(majorEntry) // Convert to RegionalVariantData format variantData := make([]RegionalVariantData, len(regionalVariants)) for i := range regionalVariants { variantData[i] = RegionalVariantData{ Locale: regionalVariants[i].Locale, CountryName: regionalVariants[i].CountryName, CountryLocal: regionalVariants[i].CountryLocal, CountryCode: regionalVariants[i].CountryCode, Flag: regionalVariants[i].Flag, MajorRegionalVariant: regionalVariants[i].MajorRegionalVariant, Population: regionalVariants[i].Population, } } // Get flag for major language majorLanguageFlag := locale.GetFlagForLanguageCode(majorCode) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "regional_variants": variantData, "major_language": map[string]interface{}{ "language_code": majorEntry.LanguageCode, "language_name": majorEntry.LanguageName, "language_local": majorEntry.LanguageLocal, "flag": majorLanguageFlag, }, }) }