初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const danmakuWindowMS = 30_000
|
||||
const danmakuMaxDurationMS = 24 * 60 * 60 * 1000
|
||||
|
||||
type hongguoDanmakuItem struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
TimeMS int64 `json:"timeMs"`
|
||||
}
|
||||
|
||||
type hongguoDanmakuPage struct {
|
||||
Items []hongguoDanmakuItem `json:"items"`
|
||||
StartMS int64 `json:"startMs"`
|
||||
NextMS int64 `json:"nextMs"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type hongguoDanmakuCacheEntry struct {
|
||||
page hongguoDanmakuPage
|
||||
err error
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type hongguoDanmakuCall struct {
|
||||
done chan struct{}
|
||||
entry hongguoDanmakuCacheEntry
|
||||
}
|
||||
|
||||
func hongguoPlaybackIDs(task Task) (string, string, bool) {
|
||||
source, seriesID, valid := splitProviderDramaID(task.DramaID)
|
||||
if !valid || source != sourceHongguo || task.Chapter.Source != sourceHongguo || !strings.HasPrefix(task.Chapter.VideoURL, "hongguo-cenc://") {
|
||||
return "", "", false
|
||||
}
|
||||
videoID := strings.TrimPrefix(task.Chapter.VideoURL, "hongguo-cenc://")
|
||||
return seriesID, videoID, hongguoNumericID.MatchString(seriesID) && hongguoNumericID.MatchString(videoID)
|
||||
}
|
||||
|
||||
func (app *UIApp) handlePlaybackDanmaku(writer http.ResponseWriter, request *http.Request) {
|
||||
if !playbackRequestAllowed(writer, request, http.MethodGet) {
|
||||
return
|
||||
}
|
||||
query := request.URL.Query()
|
||||
index, indexErr := strconv.Atoi(query.Get("episode"))
|
||||
start, startErr := strconv.ParseInt(query.Get("start"), 10, 64)
|
||||
duration, durationErr := strconv.ParseInt(query.Get("duration"), 10, 64)
|
||||
if indexErr != nil || index < 1 || startErr != nil || durationErr != nil || start < 0 || duration <= 0 || duration > danmakuMaxDurationMS || start >= duration {
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "弹幕分集或时间范围无效"})
|
||||
return
|
||||
}
|
||||
app.playbackMu.Lock()
|
||||
session := app.playbacks[query.Get("session")]
|
||||
if !viewerOwnsPlayback(request.Context(), session) {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusGone, map[string]string{"error": "播放会话已过期"})
|
||||
return
|
||||
}
|
||||
if index > len(session.tasks) {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "分集不存在"})
|
||||
return
|
||||
}
|
||||
seriesID, videoID, supported := hongguoPlaybackIDs(session.tasks[index-1])
|
||||
app.playbackMu.Unlock()
|
||||
if !supported {
|
||||
writeJSON(writer, http.StatusNotFound, map[string]string{"error": "本集暂不支持弹幕"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(request.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
page, err := app.downloader.hongguoDanmaku(ctx, seriesID, videoID, start, duration)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusBadGateway, map[string]string{"error": "弹幕暂不可用,可稍后重新开启弹幕重试"})
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, page)
|
||||
}
|
||||
|
||||
func (d *Downloader) hongguoDanmaku(ctx context.Context, seriesID, videoID string, start, duration int64) (hongguoDanmakuPage, error) {
|
||||
if !hongguoNumericID.MatchString(seriesID) || !hongguoNumericID.MatchString(videoID) || start < 0 || duration <= start || duration > danmakuMaxDurationMS {
|
||||
return hongguoDanmakuPage{}, errors.New("弹幕请求参数无效")
|
||||
}
|
||||
key := fmt.Sprintf("%s:%s:%d:%d", seriesID, videoID, start, duration)
|
||||
client := d.hongguoClient()
|
||||
client.mu.Lock()
|
||||
if entry, found := client.danmaku[key]; found && time.Now().Before(entry.expires) {
|
||||
client.mu.Unlock()
|
||||
return cloneDanmakuPage(entry.page), entry.err
|
||||
}
|
||||
if pending := client.danmakuPending[key]; pending != nil {
|
||||
client.mu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return hongguoDanmakuPage{}, ctx.Err()
|
||||
case <-pending.done:
|
||||
if ctx.Err() == nil && (errors.Is(pending.entry.err, context.Canceled) || errors.Is(pending.entry.err, context.DeadlineExceeded)) {
|
||||
return d.hongguoDanmaku(ctx, seriesID, videoID, start, duration)
|
||||
}
|
||||
return cloneDanmakuPage(pending.entry.page), pending.entry.err
|
||||
}
|
||||
}
|
||||
if client.danmakuPending == nil {
|
||||
client.danmakuPending = map[string]*hongguoDanmakuCall{}
|
||||
}
|
||||
if len(client.danmakuPending) >= 16 {
|
||||
client.mu.Unlock()
|
||||
return hongguoDanmakuPage{}, errors.New("弹幕请求繁忙")
|
||||
}
|
||||
pending := &hongguoDanmakuCall{done: make(chan struct{})}
|
||||
client.danmakuPending[key] = pending
|
||||
client.mu.Unlock()
|
||||
ctx = context.WithValue(ctx, backgroundCatalogKey{}, true)
|
||||
ctx = context.WithValue(ctx, hongguoCommentKey{}, true)
|
||||
body := map[string]any{
|
||||
"comment_source": 601, "server_channel": 1000, "group_id": videoID, "group_type": 30,
|
||||
"comment_type": 20, "sort": 1, "count": 90, "cursor": "", "aid": 8662, "compliance_status": 0,
|
||||
"business_param": map[string]any{"book_id": seriesID, "start_offset_time": start, "playlet_item_duration": duration, "need_danmaku_guide_type": []int{1, 3, 4, 2}},
|
||||
}
|
||||
result, err := d.hongguoAppRequest(ctx, http.MethodPost, "/novel/commentapi/comment/list/"+videoID+"/v1/", nil, body)
|
||||
var page hongguoDanmakuPage
|
||||
if err == nil {
|
||||
page, err = parseHongguoDanmaku(result, videoID, start, duration)
|
||||
}
|
||||
entry := hongguoDanmakuCacheEntry{page: page, err: err, expires: time.Now().Add(5 * time.Minute)}
|
||||
if err != nil {
|
||||
entry.expires = time.Now().Add(15 * time.Second)
|
||||
}
|
||||
client.mu.Lock()
|
||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||
if client.danmaku == nil || len(client.danmaku) >= 128 {
|
||||
client.danmaku = map[string]hongguoDanmakuCacheEntry{}
|
||||
}
|
||||
client.danmaku[key] = entry
|
||||
}
|
||||
pending.entry = entry
|
||||
delete(client.danmakuPending, key)
|
||||
close(pending.done)
|
||||
client.mu.Unlock()
|
||||
return cloneDanmakuPage(page), err
|
||||
}
|
||||
|
||||
func cloneDanmakuPage(page hongguoDanmakuPage) hongguoDanmakuPage {
|
||||
page.Items = append([]hongguoDanmakuItem{}, page.Items...)
|
||||
return page
|
||||
}
|
||||
|
||||
func parseHongguoDanmaku(result map[string]any, videoID string, start, duration int64) (hongguoDanmakuPage, error) {
|
||||
data := nestedMap(result, "data")
|
||||
rows, valid := data["data_list"].([]any)
|
||||
next, err := strconv.ParseInt(mapString(nestedMap(data, "extra"), "next_query_danmaku_list_time"), 10, 64)
|
||||
if !valid || err != nil || next <= start || next > danmakuMaxDurationMS {
|
||||
return hongguoDanmakuPage{}, errors.New("红果弹幕时间段格式无效")
|
||||
}
|
||||
page := hongguoDanmakuPage{Items: []hongguoDanmakuItem{}, StartMS: start, NextMS: min(next, duration)}
|
||||
var cursor struct {
|
||||
Total int64 `json:"danmaku_count"`
|
||||
}
|
||||
if raw := mapString(nestedMap(data, "common_list_info"), "cursor"); len(raw) <= 8192 && json.Unmarshal([]byte(raw), &cursor) == nil && cursor.Total >= 0 {
|
||||
page.Total = cursor.Total
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, raw := range rows {
|
||||
row, _ := raw.(map[string]any)
|
||||
comment := nestedMap(row, "comment")
|
||||
common := nestedMap(comment, "common")
|
||||
if mapString(common, "group_id") != videoID || mapString(common, "status") != "1" {
|
||||
continue
|
||||
}
|
||||
position, err := strconv.ParseInt(mapString(nestedMap(comment, "expand"), "offset_time"), 10, 64)
|
||||
text := strings.TrimSpace(strings.Map(func(r rune) rune {
|
||||
if unicode.IsControl(r) || r == '\u2028' || r == '\u2029' {
|
||||
return ' '
|
||||
}
|
||||
return r
|
||||
}, mapString(nestedMap(common, "content"), "text")))
|
||||
id := mapString(comment, "comment_id")
|
||||
if err != nil || position < start || position >= page.NextMS || text == "" || id == "" || len(id) > 120 || seen[id] {
|
||||
continue
|
||||
}
|
||||
if runes := []rune(text); len(runes) > 180 {
|
||||
text = string(runes[:180]) + "…"
|
||||
}
|
||||
seen[id] = true
|
||||
page.Items = append(page.Items, hongguoDanmakuItem{ID: id, Text: text, TimeMS: position})
|
||||
if len(page.Items) == 90 {
|
||||
break
|
||||
}
|
||||
}
|
||||
sort.SliceStable(page.Items, func(i, j int) bool { return page.Items[i].TimeMS < page.Items[j].TimeMS })
|
||||
return page, nil
|
||||
}
|
||||
Reference in New Issue
Block a user