From 8a605887ffd62cb0467eec6bac28f6854d9a40db Mon Sep 17 00:00:00 2001 From: Vincent Yang Date: Sun, 13 Jul 2025 23:06:47 +0800 Subject: [PATCH] refactor: update translation request handling to use new LMT_handle_texts method and improve response processing - Renamed makeRequest to makeRequestWithBody for clarity. - Introduced new TextItem and TextResponse types for better structure in translation requests and responses. - Updated translation logic to handle multiple texts and alternatives. - Enhanced error handling for blocked requests and translation failures. - Adjusted timestamp and random number generation for improved request uniqueness. --- translate/translate.go | 259 +++++++++++++---------------------------- translate/types.go | 34 +++++- translate/utils.go | 20 ++-- 3 files changed, 122 insertions(+), 191 deletions(-) diff --git a/translate/translate.go b/translate/translate.go index abdde05..38a0c7b 100644 --- a/translate/translate.go +++ b/translate/translate.go @@ -2,7 +2,7 @@ * @Author: Vincent Young * @Date: 2024-09-16 11:59:24 * @LastEditors: Vincent Yang - * @LastEditTime: 2025-04-08 14:26:33 + * @LastEditTime: 2025-07-13 23:06:01 * @FilePath: /DeepLX/translate/translate.go * @Telegram: https://t.me/missuo * @GitHub: https://github.com/missuo @@ -29,28 +29,16 @@ import ( "github.com/tidwall/gjson" ) -// makeRequest makes an HTTP request to DeepL API -func makeRequest(postData *PostData, proxyURL string, dlSession string) (gjson.Result, error) { +// makeRequestWithBody makes an HTTP request with pre-formatted body using minimal headers +func makeRequestWithBody(postStr string, proxyURL string, dlSession string) (gjson.Result, error) { urlFull := "https://www2.deepl.com/jsonrpc" - postStr := formatPostString(postData) // Create a new req client client := req.C().SetTLSFingerprintRandomized() - // Set headers + // Set minimal headers like TypeScript version headers := http.Header{ - "Content-Type": []string{"application/json"}, - "User-Agent": []string{"DeepL/1627620 CFNetwork/3826.500.62.2.1 Darwin/24.4.0"}, - "Accept": []string{"*/*"}, - "X-App-Os-Name": []string{"iOS"}, - "X-App-Os-Version": []string{"18.4.0"}, - "Accept-Language": []string{"en-US,en;q=0.9"}, - "Accept-Encoding": []string{"gzip, deflate, br"}, // Keep this! - "X-App-Device": []string{"iPhone16,2"}, - "Referer": []string{"https://www.deepl.com/"}, - "X-Product": []string{"translator"}, - "X-App-Build": []string{"1627620"}, - "X-App-Version": []string{"25.1"}, + "Content-Type": []string{"application/json"}, } if dlSession != "" { @@ -77,17 +65,22 @@ func makeRequest(postData *PostData, proxyURL string, dlSession string) (gjson.R return gjson.Result{}, err } + // Check for blocked status like TypeScript version + if resp.StatusCode == 429 { + return gjson.Result{}, fmt.Errorf("too many requests, your IP has been blocked by DeepL temporarily, please don't request it frequently in a short time") + } + var bodyReader io.Reader contentEncoding := resp.Header.Get("Content-Encoding") switch contentEncoding { case "br": bodyReader = brotli.NewReader(resp.Body) case "gzip": - bodyReader, err = gzip.NewReader(resp.Body) // Use gzip.NewReader + bodyReader, err = gzip.NewReader(resp.Body) if err != nil { return gjson.Result{}, fmt.Errorf("failed to create gzip reader: %w", err) } - case "deflate": // Less common, but good to handle + case "deflate": bodyReader = flate.NewReader(resp.Body) default: bodyReader = resp.Body @@ -109,182 +102,86 @@ func TranslateByDeepLX(sourceLang, targetLang, text string, tagHandling string, }, nil } - if tagHandling == "" { - tagHandling = "plaintext" + // Get detected language if source language is auto + if sourceLang == "auto" || sourceLang == "" { + sourceLang = strings.ToUpper(whatlanggo.DetectLang(text).Iso6391()) } - // Split text by newlines and store them for later reconstruction - textParts := strings.Split(text, "\n") - var translatedParts []string - var allAlternatives [][]string // Store alternatives for each part + // Prepare translation request using new LMT_handle_texts method + id := getRandomNumber() + iCount := getICount(text) + timestamp := getTimeStamp(iCount) - for _, part := range textParts { - if strings.TrimSpace(part) == "" { - translatedParts = append(translatedParts, "") - allAlternatives = append(allAlternatives, []string{""}) - continue - } - - // Get detected language if source language is auto - if sourceLang == "auto" || sourceLang == "" { - sourceLang = strings.ToUpper(whatlanggo.DetectLang(part).Iso6391()) - } - - // Prepare jobs from split result - var jobs []Job - - jobs = append(jobs, Job{ - Kind: "default", - PreferredNumBeams: 4, - RawEnContextBefore: []string{}, - RawEnContextAfter: []string{}, - Sentences: []Sentence{{ - Prefix: "", - Text: text, - ID: 0, - }}, - }) - - hasRegionalVariant := false - targetLangCode := targetLang - targetLangParts := strings.Split(targetLang, "-") - if len(targetLangParts) > 1 { - targetLangCode = targetLangParts[0] - hasRegionalVariant = true - } - - // Prepare translation request - id := getRandomNumber() - - postData := &PostData{ - Jsonrpc: "2.0", - Method: "LMT_handle_jobs", - ID: id, - Params: Params{ - CommonJobParams: CommonJobParams{ - Mode: "translate", - Formality: "undefined", - TranscribeAs: "romanize", - AdvancedMode: false, - TextType: tagHandling, - WasSpoken: false, - }, - Lang: Lang{ - SourceLangUserSelected: "auto", - TargetLang: strings.ToUpper(targetLangCode), - SourceLangComputed: strings.ToUpper(sourceLang), - }, - Jobs: jobs, - Timestamp: getTimeStamp(getICount(part)), + postData := &PostData{ + Jsonrpc: "2.0", + Method: "LMT_handle_texts", + ID: id, + Params: Params{ + Splitting: "newlines", + Lang: Lang{ + SourceLangUserSelected: sourceLang, + TargetLang: targetLang, }, - } - - if hasRegionalVariant { - postData = &PostData{ - Jsonrpc: "2.0", - Method: "LMT_handle_jobs", - ID: id, - Params: Params{ - CommonJobParams: CommonJobParams{ - Mode: "translate", - Formality: "undefined", - TranscribeAs: "romanize", - AdvancedMode: false, - TextType: tagHandling, - WasSpoken: false, - RegionalVariant: targetLang, - }, - Lang: Lang{ - SourceLangUserSelected: "auto", - TargetLang: strings.ToUpper(targetLangCode), - SourceLangComputed: strings.ToUpper(sourceLang), - }, - Jobs: jobs, - Timestamp: getTimeStamp(getICount(part)), - }, - } - } - - // Make translation request - result, err := makeRequest(postData, proxyURL, dlSession) - if err != nil { - return DeepLXTranslationResult{ - Code: http.StatusServiceUnavailable, - Message: err.Error(), - }, nil - } - - // Process translation results - var partTranslation string - var partAlternatives []string - - translations := result.Get("result.translations").Array() - if len(translations) > 0 { - // Process main translation - for _, translation := range translations { - partTranslation += translation.Get("beams.0.sentences.0.text").String() + " " - } - partTranslation = strings.TrimSpace(partTranslation) - - // Process alternatives - numBeams := len(translations[0].Get("beams").Array()) - for i := 1; i < numBeams; i++ { // Start from 1 since 0 is the main translation - var altText string - for _, translation := range translations { - beams := translation.Get("beams").Array() - if i < len(beams) { - altText += beams[i].Get("sentences.0.text").String() + " " - } - } - if altText != "" { - partAlternatives = append(partAlternatives, strings.TrimSpace(altText)) - } - } - } - - if partTranslation == "" { - return DeepLXTranslationResult{ - Code: http.StatusServiceUnavailable, - Message: "Translation failed", - }, nil - } - - translatedParts = append(translatedParts, partTranslation) - allAlternatives = append(allAlternatives, partAlternatives) + Texts: []TextItem{{ + Text: text, + RequestAlternatives: 3, + }}, + Timestamp: timestamp, + }, } - // Join all translated parts with newlines - translatedText := strings.Join(translatedParts, "\n") + // Format and apply body manipulation method like TypeScript + postStr := formatPostString(postData) + postStr = handlerBodyMethod(id, postStr) - // Combine alternatives with proper newline handling - var combinedAlternatives []string - maxAlts := 0 - for _, alts := range allAlternatives { - if len(alts) > maxAlts { - maxAlts = len(alts) + // Make translation request + result, err := makeRequestWithBody(postStr, proxyURL, dlSession) + if err != nil { + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: err.Error(), + }, nil + } + + // Process translation results using new format + textsArray := result.Get("result.texts").Array() + if len(textsArray) == 0 { + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: "Translation failed", + }, nil + } + + // Get main translation + mainText := textsArray[0].Get("text").String() + if mainText == "" { + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: "Translation failed", + }, nil + } + + // Get alternatives + var alternatives []string + alternativesArray := textsArray[0].Get("alternatives").Array() + for _, alt := range alternativesArray { + altText := alt.Get("text").String() + if altText != "" { + alternatives = append(alternatives, altText) } } - // Create combined alternatives preserving line structure - for i := 0; i < maxAlts; i++ { - var altParts []string - for j, alts := range allAlternatives { - if i < len(alts) { - altParts = append(altParts, alts[i]) - } else if len(translatedParts[j]) == 0 { - altParts = append(altParts, "") // Keep empty lines - } else { - altParts = append(altParts, translatedParts[j]) // Use main translation if no alternative - } - } - combinedAlternatives = append(combinedAlternatives, strings.Join(altParts, "\n")) + // Get detected source language from response + detectedLang := result.Get("result.lang").String() + if detectedLang != "" { + sourceLang = detectedLang } return DeepLXTranslationResult{ Code: http.StatusOK, - ID: getRandomNumber(), // Using new ID for the complete translation - Data: translatedText, - Alternatives: combinedAlternatives, + ID: id, + Data: mainText, + Alternatives: alternatives, SourceLang: sourceLang, TargetLang: targetLang, Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""], diff --git a/translate/types.go b/translate/types.go index f370d2a..b653933 100644 --- a/translate/types.go +++ b/translate/types.go @@ -46,8 +46,22 @@ type Job struct { Sentences []Sentence `json:"sentences"` } +// TextItem represents a text item for translation +type TextItem struct { + Text string `json:"text"` + RequestAlternatives int `json:"requestAlternatives"` +} + // Params represents parameters for translation requests type Params struct { + Splitting string `json:"splitting"` + Lang Lang `json:"lang"` + Texts []TextItem `json:"texts"` + Timestamp int64 `json:"timestamp"` +} + +// LegacyParams represents the old parameters structure for jobs (kept for compatibility) +type LegacyParams struct { CommonJobParams CommonJobParams `json:"commonJobParams"` Lang Lang `json:"lang"` Jobs []Job `json:"jobs"` @@ -62,8 +76,26 @@ type PostData struct { Params Params `json:"params"` } -// TranslationResponse represents the response from translation +// TextResponse represents a single text response +type TextResponse struct { + Text string `json:"text"` + Alternatives []struct { + Text string `json:"text"` + } `json:"alternatives"` +} + +// TranslationResponse represents the response from LMT_handle_texts type TranslationResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID int64 `json:"id"` + Result struct { + Lang string `json:"lang"` + Texts []TextResponse `json:"texts"` + } `json:"result"` +} + +// LegacyTranslationResponse represents the old response format (kept for compatibility) +type LegacyTranslationResponse struct { Jsonrpc string `json:"jsonrpc"` ID int64 `json:"id"` Result struct { diff --git a/translate/utils.go b/translate/utils.go index ae5fb2e..69b6143 100644 --- a/translate/utils.go +++ b/translate/utils.go @@ -28,7 +28,7 @@ func getICount(translateText string) int64 { func getRandomNumber() int64 { src := rand.NewSource(time.Now().UnixNano()) rng := rand.New(src) - num := rng.Int63n(99999) + 8300000 + num := rng.Int63n(99999) + 100000 return num * 1000 } @@ -37,7 +37,7 @@ func getTimeStamp(iCount int64) int64 { ts := time.Now().UnixMilli() if iCount != 0 { iCount = iCount + 1 - return ts - ts%iCount + iCount + return ts - (ts % iCount) + iCount } return ts } @@ -46,12 +46,14 @@ func getTimeStamp(iCount int64) int64 { func formatPostString(postData *PostData) string { postBytes, _ := json.Marshal(postData) postStr := string(postBytes) - - if (postData.ID+5)%29 == 0 || (postData.ID+3)%13 == 0 { - postStr = strings.Replace(postStr, `"method":"`, `"method" : "`, 1) - } else { - postStr = strings.Replace(postStr, `"method":"`, `"method": "`, 1) - } - return postStr } + +// handlerBodyMethod manipulates the request body based on random number calculation +func handlerBodyMethod(random int64, body string) string { + calc := (random+5)%29 == 0 || (random+3)%13 == 0 + if calc { + return strings.Replace(body, `"method":"`, `"method" : "`, 1) + } + return strings.Replace(body, `"method":"`, `"method": "`, 1) +}