Compare commits

..

No commits in common. "72beefbebe921a887f995a658495dc25c01cb75b" and "56637adc3c82c83bf26bcaf0cceb286ffc32c109" have entirely different histories.

4 changed files with 192 additions and 123 deletions

2
go.mod
View File

@ -2,7 +2,7 @@ module github.com/OwO-Network/DeepLX
go 1.24.0 go 1.24.0
toolchain go1.24.5 toolchain go1.24.2
require ( require (
github.com/abadojack/whatlanggo v1.0.1 github.com/abadojack/whatlanggo v1.0.1

View File

@ -2,7 +2,7 @@
* @Author: Vincent Young * @Author: Vincent Young
* @Date: 2024-09-16 11:59:24 * @Date: 2024-09-16 11:59:24
* @LastEditors: Vincent Yang * @LastEditors: Vincent Yang
* @LastEditTime: 2025-07-13 23:09:49 * @LastEditTime: 2025-04-08 14:26:33
* @FilePath: /DeepLX/translate/translate.go * @FilePath: /DeepLX/translate/translate.go
* @Telegram: https://t.me/missuo * @Telegram: https://t.me/missuo
* @GitHub: https://github.com/missuo * @GitHub: https://github.com/missuo
@ -29,16 +29,28 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
// makeRequestWithBody makes an HTTP request with pre-formatted body using minimal headers // makeRequest makes an HTTP request to DeepL API
func makeRequestWithBody(postStr string, proxyURL string, dlSession string) (gjson.Result, error) { func makeRequest(postData *PostData, proxyURL string, dlSession string) (gjson.Result, error) {
urlFull := "https://www2.deepl.com/jsonrpc" urlFull := "https://www2.deepl.com/jsonrpc"
postStr := formatPostString(postData)
// Create a new req client // Create a new req client
client := req.C().SetTLSFingerprintRandomized() client := req.C().SetTLSFingerprintRandomized()
// Set minimal headers like TypeScript version // Set headers
headers := http.Header{ headers := http.Header{
"Content-Type": []string{"application/json"}, "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"},
} }
if dlSession != "" { if dlSession != "" {
@ -65,22 +77,17 @@ func makeRequestWithBody(postStr string, proxyURL string, dlSession string) (gjs
return gjson.Result{}, err 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 var bodyReader io.Reader
contentEncoding := resp.Header.Get("Content-Encoding") contentEncoding := resp.Header.Get("Content-Encoding")
switch contentEncoding { switch contentEncoding {
case "br": case "br":
bodyReader = brotli.NewReader(resp.Body) bodyReader = brotli.NewReader(resp.Body)
case "gzip": case "gzip":
bodyReader, err = gzip.NewReader(resp.Body) bodyReader, err = gzip.NewReader(resp.Body) // Use gzip.NewReader
if err != nil { if err != nil {
return gjson.Result{}, fmt.Errorf("failed to create gzip reader: %w", err) return gjson.Result{}, fmt.Errorf("failed to create gzip reader: %w", err)
} }
case "deflate": case "deflate": // Less common, but good to handle
bodyReader = flate.NewReader(resp.Body) bodyReader = flate.NewReader(resp.Body)
default: default:
bodyReader = resp.Body bodyReader = resp.Body
@ -102,40 +109,104 @@ func TranslateByDeepLX(sourceLang, targetLang, text string, tagHandling string,
}, nil }, nil
} }
// Get detected language if source language is auto if tagHandling == "" {
if sourceLang == "auto" || sourceLang == "" { tagHandling = "plaintext"
sourceLang = strings.ToUpper(whatlanggo.DetectLang(text).Iso6391())
} }
// Prepare translation request using new LMT_handle_texts method // 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
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() id := getRandomNumber()
iCount := getICount(text)
timestamp := getTimeStamp(iCount)
postData := &PostData{ postData := &PostData{
Jsonrpc: "2.0", Jsonrpc: "2.0",
Method: "LMT_handle_texts", Method: "LMT_handle_jobs",
ID: id, ID: id,
Params: Params{ Params: Params{
Splitting: "newlines", CommonJobParams: CommonJobParams{
Lang: Lang{ Mode: "translate",
SourceLangUserSelected: sourceLang, Formality: "undefined",
TargetLang: targetLang, TranscribeAs: "romanize",
AdvancedMode: false,
TextType: tagHandling,
WasSpoken: false,
}, },
Texts: []TextItem{{ Lang: Lang{
Text: text, SourceLangUserSelected: "auto",
RequestAlternatives: 3, TargetLang: strings.ToUpper(targetLangCode),
}}, SourceLangComputed: strings.ToUpper(sourceLang),
Timestamp: timestamp, },
Jobs: jobs,
Timestamp: getTimeStamp(getICount(part)),
}, },
} }
// Format and apply body manipulation method like TypeScript if hasRegionalVariant {
postStr := formatPostString(postData) postData = &PostData{
postStr = handlerBodyMethod(id, postStr) 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 // Make translation request
result, err := makeRequestWithBody(postStr, proxyURL, dlSession) result, err := makeRequest(postData, proxyURL, dlSession)
if err != nil { if err != nil {
return DeepLXTranslationResult{ return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable, Code: http.StatusServiceUnavailable,
@ -143,45 +214,77 @@ func TranslateByDeepLX(sourceLang, targetLang, text string, tagHandling string,
}, nil }, nil
} }
// Process translation results using new format // Process translation results
textsArray := result.Get("result.texts").Array() var partTranslation string
if len(textsArray) == 0 { var partAlternatives []string
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: "Translation failed",
}, nil
}
// Get main translation translations := result.Get("result.translations").Array()
mainText := textsArray[0].Get("text").String() if len(translations) > 0 {
if mainText == "" { // Process main translation
return DeepLXTranslationResult{ for _, translation := range translations {
Code: http.StatusServiceUnavailable, partTranslation += translation.Get("beams.0.sentences.0.text").String() + " "
Message: "Translation failed",
}, nil
} }
partTranslation = strings.TrimSpace(partTranslation)
// Get alternatives // Process alternatives
var alternatives []string numBeams := len(translations[0].Get("beams").Array())
alternativesArray := textsArray[0].Get("alternatives").Array() for i := 1; i < numBeams; i++ { // Start from 1 since 0 is the main translation
for _, alt := range alternativesArray { var altText string
altText := alt.Get("text").String() for _, translation := range translations {
beams := translation.Get("beams").Array()
if i < len(beams) {
altText += beams[i].Get("sentences.0.text").String() + " "
}
}
if altText != "" { if altText != "" {
alternatives = append(alternatives, altText) partAlternatives = append(partAlternatives, strings.TrimSpace(altText))
}
} }
} }
// Get detected source language from response if partTranslation == "" {
detectedLang := result.Get("result.lang").String() return DeepLXTranslationResult{
if detectedLang != "" { Code: http.StatusServiceUnavailable,
sourceLang = detectedLang Message: "Translation failed",
}, nil
}
translatedParts = append(translatedParts, partTranslation)
allAlternatives = append(allAlternatives, partAlternatives)
}
// Join all translated parts with newlines
translatedText := strings.Join(translatedParts, "\n")
// Combine alternatives with proper newline handling
var combinedAlternatives []string
maxAlts := 0
for _, alts := range allAlternatives {
if len(alts) > maxAlts {
maxAlts = len(alts)
}
}
// 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"))
} }
return DeepLXTranslationResult{ return DeepLXTranslationResult{
Code: http.StatusOK, Code: http.StatusOK,
ID: id, ID: getRandomNumber(), // Using new ID for the complete translation
Data: mainText, Data: translatedText,
Alternatives: alternatives, Alternatives: combinedAlternatives,
SourceLang: sourceLang, SourceLang: sourceLang,
TargetLang: targetLang, TargetLang: targetLang,
Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""], Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""],

View File

@ -46,22 +46,8 @@ type Job struct {
Sentences []Sentence `json:"sentences"` 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 // Params represents parameters for translation requests
type Params struct { 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"` CommonJobParams CommonJobParams `json:"commonJobParams"`
Lang Lang `json:"lang"` Lang Lang `json:"lang"`
Jobs []Job `json:"jobs"` Jobs []Job `json:"jobs"`
@ -76,26 +62,8 @@ type PostData struct {
Params Params `json:"params"` Params Params `json:"params"`
} }
// TextResponse represents a single text response // TranslationResponse represents the response from translation
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 { 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"` Jsonrpc string `json:"jsonrpc"`
ID int64 `json:"id"` ID int64 `json:"id"`
Result struct { Result struct {

View File

@ -28,7 +28,7 @@ func getICount(translateText string) int64 {
func getRandomNumber() int64 { func getRandomNumber() int64 {
src := rand.NewSource(time.Now().UnixNano()) src := rand.NewSource(time.Now().UnixNano())
rng := rand.New(src) rng := rand.New(src)
num := rng.Int63n(99999) + 100000 num := rng.Int63n(99999) + 8300000
return num * 1000 return num * 1000
} }
@ -37,7 +37,7 @@ func getTimeStamp(iCount int64) int64 {
ts := time.Now().UnixMilli() ts := time.Now().UnixMilli()
if iCount != 0 { if iCount != 0 {
iCount = iCount + 1 iCount = iCount + 1
return ts - (ts % iCount) + iCount return ts - ts%iCount + iCount
} }
return ts return ts
} }
@ -46,14 +46,12 @@ func getTimeStamp(iCount int64) int64 {
func formatPostString(postData *PostData) string { func formatPostString(postData *PostData) string {
postBytes, _ := json.Marshal(postData) postBytes, _ := json.Marshal(postData)
postStr := string(postBytes) postStr := string(postBytes)
return postStr
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)
} }
// handlerBodyMethod manipulates the request body based on random number calculation return postStr
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)
} }