Skip to content

Commit 3ab817b

Browse files
author
Taois
committed
feat:完美
1 parent 7056c17 commit 3ab817b

File tree

4 files changed

+485
-424
lines changed

4 files changed

+485
-424
lines changed

mediaProxy/custom_spider.jar

15.1 KB
Binary file not shown.

mediaProxy/custom_spider.jar.md5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
198c66eaf2db6ed7a38ab13f9a4e6c70
1+
7062c68c2f7aa7220bd17caa1ace479c

mediaProxy/proxy.go

Lines changed: 156 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ var indexHTML embed.FS
3939

4040
var mediaCache = cache.New(4*time.Hour, 10*time.Minute)
4141
var authKey string
42+
var enableContentTypeGuess bool
43+
44+
const (
45+
AppVersion = "V1.0.1 20260322"
46+
)
4247

4348
type Chunk struct {
4449
startOffset int64
@@ -301,6 +306,7 @@ func (p *ProxyDownloadStruct) ProxyWorker(req *http.Request) {
301306

302307
var resp *resty.Response
303308
var err error
309+
var finalBody []byte
304310
for retry := 0; retry < maxRetries; retry++ {
305311
resp, err = base.RestyClient.
306312
SetTimeout(30*time.Second).
@@ -351,22 +357,67 @@ func (p *ProxyDownloadStruct) ProxyWorker(req *http.Request) {
351357
resp = nil
352358
break // 跳出重试循环,标记此 chunk 失败
353359
}
360+
361+
// 检查数据长度
362+
body := resp.Body()
363+
expectedLen := int(chunk.endOffset - chunk.startOffset + 1)
364+
365+
if resp.StatusCode() == 200 && chunk.startOffset > 0 {
366+
logrus.Warnf("【警告】请求部分数据 range=%d-%d 但服务器返回 200 OK (全量数据), 丢弃并重试", chunk.startOffset, chunk.endOffset)
367+
err = fmt.Errorf("server returned 200 instead of 206")
368+
resp = nil
369+
select {
370+
case <-p.Ctx.Done():
371+
return
372+
case <-time.After(2 * time.Second):
373+
}
374+
continue
375+
}
376+
377+
// 严格校验 Content-Range 偏移量,防止 CDN 返回错误的分片数据导致播放器解码卡死
378+
respContentRange := resp.Header().Get("Content-Range")
379+
if respContentRange != "" && resp.StatusCode() == 206 {
380+
expectedPrefix := fmt.Sprintf("bytes %d-", chunk.startOffset)
381+
if !strings.HasPrefix(respContentRange, expectedPrefix) {
382+
logrus.Warnf("【致命警告】CDN返回的Range偏移量错误! 期望: %s, 实际: %s. 丢弃并重试以防止播放器画面卡死", expectedPrefix, respContentRange)
383+
err = fmt.Errorf("invalid content-range: %s", respContentRange)
384+
resp = nil
385+
select {
386+
case <-p.Ctx.Done():
387+
return
388+
case <-time.After(1 * time.Second):
389+
}
390+
continue
391+
}
392+
}
393+
394+
if len(body) < expectedLen {
395+
logrus.Warnf("【警告】收到数据长度不足! 请求 range=%d-%d (预期 %d), 实际收到 %d bytes, 丢弃并重试", chunk.startOffset, chunk.endOffset, expectedLen, len(body))
396+
err = fmt.Errorf("short read: %d < %d", len(body), expectedLen)
397+
resp = nil
398+
select {
399+
case <-p.Ctx.Done():
400+
return
401+
case <-time.After(1 * time.Second):
402+
}
403+
continue
404+
} else if len(body) > expectedLen {
405+
logrus.Debugf("收到数据长度超长 (预期 %d, 实际 %d), 进行截断", expectedLen, len(body))
406+
finalBody = body[:expectedLen]
407+
} else {
408+
finalBody = body
409+
}
410+
354411
break
355412
}
356413

357-
if err != nil {
414+
if err != nil && resp == nil && finalBody == nil {
358415
logrus.Errorf("处理链接 range=%d-%d 最终失败: %+v", chunk.startOffset, chunk.endOffset, err)
359-
resp = nil
360416
}
361417

362418
// 接收数据
363-
if resp != nil {
364-
body := resp.Body()
365-
expectedLen := int(chunk.endOffset - chunk.startOffset + 1)
366-
if len(body) != expectedLen {
367-
logrus.Warnf("【警告】收到数据长度不匹配! 请求 range=%d-%d (预期 %d), 实际收到 %d bytes, Content-Range: %s", chunk.startOffset, chunk.endOffset, expectedLen, len(body), resp.Header().Get("Content-Range"))
368-
}
369-
chunk.put(body)
419+
if finalBody != nil {
420+
chunk.put(finalBody)
370421
} else {
371422
logrus.Debugf("Chunk range=%d-%d 无法获取数据,写入 nil 并停止调度新任务", chunk.startOffset, chunk.endOffset)
372423
chunk.put(nil) // 放入 nil 标记此 chunk 失败或结束
@@ -387,16 +438,70 @@ func (p *ProxyDownloadStruct) ProxyWorker(req *http.Request) {
387438
}
388439
}
389440

441+
func guessContentType(url string, contentDisposition string) string {
442+
var fileName string
443+
contentDisposition = strings.ToLower(contentDisposition)
444+
if contentDisposition != "" {
445+
regCompile := regexp.MustCompile(`^.*filename=\"([^\"]+)\".*$`)
446+
if regCompile.MatchString(contentDisposition) {
447+
fileName = regCompile.ReplaceAllString(contentDisposition, "$1")
448+
}
449+
} else {
450+
// 找到最后一个 "/" 的索引
451+
lastSlashIndex := strings.LastIndex(url, "/")
452+
// 找到第一个 "?" 的索引
453+
queryIndex := strings.Index(url, "?")
454+
if queryIndex == -1 {
455+
// 如果没有 "?",则提取从最后一个 "/" 到结尾的字符串
456+
fileName = url[lastSlashIndex+1:]
457+
} else {
458+
// 如果存在 "?",则提取从最后一个 "/" 到 "?" 之间的字符串
459+
fileName = url[lastSlashIndex+1 : queryIndex]
460+
}
461+
}
462+
463+
contentType := ""
464+
urlLower := strings.ToLower(url)
465+
if strings.HasSuffix(fileName, ".webm") || strings.Contains(urlLower, "fext=webm") || strings.Contains(urlLower, ".webm") {
466+
contentType = "video/webm"
467+
} else if strings.HasSuffix(fileName, ".avi") || strings.Contains(urlLower, "fext=avi") || strings.Contains(urlLower, ".avi") {
468+
contentType = "video/x-msvideo"
469+
} else if strings.HasSuffix(fileName, ".wmv") || strings.Contains(urlLower, "fext=wmv") || strings.Contains(urlLower, ".wmv") {
470+
contentType = "video/x-ms-wmv"
471+
} else if strings.HasSuffix(fileName, ".flv") || strings.Contains(urlLower, "fext=flv") || strings.Contains(urlLower, ".flv") {
472+
contentType = "video/x-flv"
473+
} else if strings.HasSuffix(fileName, ".mov") || strings.Contains(urlLower, "fext=mov") || strings.Contains(urlLower, ".mov") {
474+
contentType = "video/quicktime"
475+
} else if strings.HasSuffix(fileName, ".mkv") || strings.Contains(urlLower, "fext=mkv") || strings.Contains(urlLower, ".mkv") {
476+
contentType = "video/x-matroska"
477+
} else if strings.HasSuffix(fileName, ".ts") || strings.Contains(urlLower, "fext=ts") || strings.Contains(urlLower, ".ts") {
478+
contentType = "video/mp2t"
479+
} else if strings.HasSuffix(fileName, ".mpeg") || strings.HasSuffix(fileName, ".mpg") {
480+
contentType = "video/mpeg"
481+
} else if strings.HasSuffix(fileName, ".3gpp") || strings.HasSuffix(fileName, ".3gp") {
482+
contentType = "video/3gpp"
483+
} else if strings.HasSuffix(fileName, ".mp4") || strings.HasSuffix(fileName, ".m4s") || strings.Contains(urlLower, "fext=mp4") || strings.Contains(urlLower, ".mp4") {
484+
contentType = "video/mp4"
485+
}
486+
return contentType
487+
}
488+
390489
func handleMethod(w http.ResponseWriter, req *http.Request) {
391490
switch req.Method {
392491
case http.MethodGet, http.MethodHead:
393492
// 处理 GET 和 HEAD 请求
394493
logrus.Info("正在 GET/HEAD 请求")
395494
// 检查查询参数是否为空
396495
if req.URL.RawQuery == "" {
397-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
398496
if req.Method == http.MethodGet {
399-
w.Write([]byte("欢迎使用drpyS专用多线程媒体代理服务,由道长于2026年开发"))
497+
indexContent, err := indexHTML.ReadFile("static/index.html")
498+
if err == nil {
499+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
500+
w.Write(indexContent)
501+
} else {
502+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
503+
w.Write([]byte(fmt.Sprintf("欢迎使用drpyS专用多线程媒体代理服务,由道长于2026年开发\n版本: %s", AppVersion)))
504+
}
400505
}
401506
} else {
402507
// 如果有查询参数,则返回自定义的内容
@@ -583,64 +688,19 @@ func handleGetMethod(w http.ResponseWriter, req *http.Request) {
583688

584689
logrus.Debugf("请求头: %+v", responseHeaders)
585690

586-
var fileName string
587-
contentDisposition := strings.ToLower(responseHeaders.Get("Content-Disposition"))
588-
if contentDisposition != "" {
589-
regCompile := regexp.MustCompile(`^.*filename=\"([^\"]+)\".*$`)
590-
if regCompile.MatchString(contentDisposition) {
591-
fileName = regCompile.ReplaceAllString(contentDisposition, "$1")
592-
}
593-
} else {
594-
// 找到最后一个 "/" 的索引
595-
lastSlashIndex := strings.LastIndex(url, "/")
596-
// 找到第一个 "?" 的索引
597-
queryIndex := strings.Index(url, "?")
598-
if queryIndex == -1 {
599-
// 如果没有 "?",则提取从最后一个 "/" 到结尾的字符串
600-
fileName = url[lastSlashIndex+1:]
601-
} else {
602-
// 如果存在 "?",则提取从最后一个 "/" 到 "?" 之间的字符串
603-
fileName = url[lastSlashIndex+1 : queryIndex]
604-
}
605-
}
606-
607691
contentType := responseHeaders.Get("Content-Type")
608692
if contentType == "" || contentType == "application/octet-stream" {
609-
urlLower := strings.ToLower(url)
610-
if strings.HasSuffix(fileName, ".webm") || strings.Contains(urlLower, "fext=webm") || strings.Contains(urlLower, ".webm") {
611-
contentType = "video/webm"
612-
} else if strings.HasSuffix(fileName, ".avi") || strings.Contains(urlLower, "fext=avi") || strings.Contains(urlLower, ".avi") {
613-
contentType = "video/x-msvideo"
614-
} else if strings.HasSuffix(fileName, ".wmv") || strings.Contains(urlLower, "fext=wmv") || strings.Contains(urlLower, ".wmv") {
615-
contentType = "video/x-ms-wmv"
616-
} else if strings.HasSuffix(fileName, ".flv") || strings.Contains(urlLower, "fext=flv") || strings.Contains(urlLower, ".flv") {
617-
contentType = "video/x-flv"
618-
} else if strings.HasSuffix(fileName, ".mov") || strings.Contains(urlLower, "fext=mov") || strings.Contains(urlLower, ".mov") {
619-
contentType = "video/quicktime"
620-
} else if strings.HasSuffix(fileName, ".mkv") || strings.Contains(urlLower, "fext=mkv") || strings.Contains(urlLower, ".mkv") {
621-
contentType = "video/x-matroska"
622-
} else if strings.HasSuffix(fileName, ".ts") || strings.Contains(urlLower, "fext=ts") || strings.Contains(urlLower, ".ts") {
623-
contentType = "video/mp2t"
624-
} else if strings.HasSuffix(fileName, ".mpeg") || strings.HasSuffix(fileName, ".mpg") {
625-
contentType = "video/mpeg"
626-
} else if strings.HasSuffix(fileName, ".3gpp") || strings.HasSuffix(fileName, ".3gp") {
627-
contentType = "video/3gpp"
628-
} else if strings.HasSuffix(fileName, ".mp4") || strings.HasSuffix(fileName, ".m4s") || strings.Contains(urlLower, "fext=mp4") || strings.Contains(urlLower, ".mp4") {
629-
contentType = "video/mp4"
630-
} else {
631-
// ExoPlayer 如果没有明确类型且为 octet-stream 会报错。
632-
// IjkPlayer 在碰到明确错误格式时可能会播放失败。
633-
// MPV 等严格的播放器如果遇到强制篡改的 video/mp4 但实际是 mkv 可能会解码失败。
634-
// 我们需要做的是:
635-
// 1. 尝试从网盘的响应头中原样透传。
636-
// 2. 如果实在没有,我们就不设置它,让播放器自己嗅探 (不要强制设为 video/mp4)
637-
}
638-
639-
if contentType != "" {
640-
responseHeaders.Set("Content-Type", contentType)
693+
if enableContentTypeGuess {
694+
guessedType := guessContentType(url, responseHeaders.Get("Content-Disposition"))
695+
if guessedType != "" {
696+
responseHeaders.Set("Content-Type", guessedType)
697+
} else {
698+
responseHeaders.Del("Content-Type")
699+
}
641700
} else {
642-
// 如果实在没有识别出类型,为了最大兼容性,最好删除该头,让 mpv/ijk 等播放器自己根据内容嗅探
643-
// 不要给默认的 application/octet-stream,这会让 mpv 困惑
701+
// 默认不启用:不要强行根据 URL 猜测 Content-Type,因为网盘的 fext=mp4 可能是假的(实际上是 mkv 等)。
702+
// 强行设置为 video/mp4 会导致 MPV/ffmpeg 强制使用 mp4 解码器,从而在拖拽时因为索引不匹配而卡死!
703+
// 删除 Content-Type 让所有播放器(Exo/Ijk/MPV)强制进行真实的二进制嗅探 (Sniffing)。
644704
responseHeaders.Del("Content-Type")
645705
}
646706
}
@@ -771,7 +831,7 @@ func handleGetMethod(w http.ResponseWriter, req *http.Request) {
771831
}
772832
rangeEnd = contentSize - 1
773833
} else {
774-
if !isExactRange && (rangeEnd == -1 || rangeEnd >= contentSize) {
834+
if rangeEnd == -1 || rangeEnd >= contentSize {
775835
rangeEnd = contentSize - 1
776836
}
777837
if rangeStart < 0 {
@@ -1086,13 +1146,40 @@ func shouldFilterHeaderName(key string) bool {
10861146
}
10871147

10881148
func main() {
1089-
// 定义 dns 和 debug 命令行参数
1149+
// 定义命令行参数
10901150
dns := flag.String("dns", "8.8.8.8", "DNS解析 IP:port")
10911151
port := flag.String("port", "5575", "服务器端口")
10921152
debug := flag.Bool("debug", false, "Debug模式")
10931153
auth := flag.String("auth", "", "认证密钥")
1154+
guessType := flag.Bool("guess-type", false, "是否根据URL强制猜测并设置 Content-Type (可能导致 MPV 等播放器拖拽失败,默认不启用)")
1155+
1156+
// 帮助和版本信息
1157+
showHelp := flag.Bool("h", false, "显示帮助信息")
1158+
showHelpLong := flag.Bool("help", false, "显示帮助信息")
1159+
showVersion := flag.Bool("v", false, "显示版本信息")
1160+
showVersionLong := flag.Bool("version", false, "显示版本信息")
1161+
1162+
// 自定义 Usage
1163+
flag.Usage = func() {
1164+
fmt.Fprintf(os.Stderr, "drpyS专用多线程媒体代理服务 %s\n\n", AppVersion)
1165+
fmt.Fprintf(os.Stderr, "用法:\n")
1166+
fmt.Fprintf(os.Stderr, " %s [参数]\n\n", os.Args[0])
1167+
fmt.Fprintf(os.Stderr, "参数列表:\n")
1168+
flag.PrintDefaults()
1169+
}
1170+
10941171
flag.Parse()
10951172

1173+
if *showHelp || *showHelpLong {
1174+
flag.Usage()
1175+
return
1176+
}
1177+
1178+
if *showVersion || *showVersionLong {
1179+
fmt.Printf("drpyS专用多线程媒体代理服务 %s\n", AppVersion)
1180+
return
1181+
}
1182+
10961183
// 忽略 SIGPIPE 信号
10971184
signal.Ignore(syscall.SIGPIPE)
10981185

@@ -1106,11 +1193,9 @@ func main() {
11061193
}
11071194
logrus.Infof("服务器运行在 %s 端口.", *port)
11081195

1109-
// 开启Debug
1110-
//logrus.SetLevel(logrus.DebugLevel)
1111-
11121196
// 设置全局变量
11131197
authKey = *auth
1198+
enableContentTypeGuess = *guessType
11141199
base.DnsResolverIP = *dns
11151200
base.InitClient()
11161201
var server = http.Server{

0 commit comments

Comments
 (0)