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.
This commit is contained in:
Vincent Yang 2025-07-13 23:06:47 +08:00
parent 56637adc3c
commit 8a605887ff
No known key found for this signature in database
GPG Key ID: F9D66A5FEA75787E
3 changed files with 122 additions and 191 deletions

View File

@ -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 != ""],

View File

@ -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 {

View File

@ -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)
}