@@ -39,6 +39,11 @@ var indexHTML embed.FS
3939
4040var mediaCache = cache .New (4 * time .Hour , 10 * time .Minute )
4141var authKey string
42+ var enableContentTypeGuess bool
43+
44+ const (
45+ AppVersion = "V1.0.1 20260322"
46+ )
4247
4348type 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+
390489func 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
10881148func 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