mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 01:50:30 +00:00
New hls protocol in internal/protocol/hls (registered ahead of http): - master playlist variant picking (highest bandwidth), RFC3986 segment URL resolution, fMP4 init segments, EXT-X-BYTERANGE, EXT-X-GAP skip - AES-128 decryption with mid-playlist key rotation and sequence-derived IVs; explicit errors for live streams, SAMPLE-AES and multi-period fMP4 - concurrent segment downloads with retry, journal-based resume across pause/app restarts, automatic in-order merge into a single .ts/.mp4 - best-effort Content-Length probing for determinate progress - anti-hotlink Referer/Cookie/UA via task extra headers (Go transport does not filter them) Core: fix SchemeFilter extension match for URLs with query strings (also benefits .torrent links); Flutter: Protocol.hls enum, hls protocol config model (prevents config round-trip wipe) and HLS settings block with localized labels in all 22 locales. Tests: parser/crypto unit tests, httptest e2e (Referer-enforced CDN, encrypted streams, byterange, pause/resume, store/restore) and an opt-in live-site acceptance gate (HLS_LIVE_URL).
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package hls
|
|
|
|
import (
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
)
|
|
|
|
type FetcherManager struct {
|
|
}
|
|
|
|
func (fm *FetcherManager) Name() string {
|
|
return "hls"
|
|
}
|
|
|
|
// Filters registers the .m3u8 extension. The manager must be registered BEFORE
|
|
// the http manager in the downloader config: protocol routing picks the first
|
|
// manager whose filter matches, and the http filter claims every http(s) URL.
|
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
|
return []*fetcher.SchemeFilter{
|
|
{
|
|
Type: fetcher.FilterTypeFile,
|
|
Pattern: "M3U8",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
|
return &Fetcher{}
|
|
}
|
|
|
|
func (fm *FetcherManager) ParseName(u string) string {
|
|
parsed, err := url.Parse(u)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
name := path.Base(parsed.Path)
|
|
name = strings.TrimSuffix(name, path.Ext(name))
|
|
if name == "" || name == "/" || name == "." {
|
|
name = parsed.Hostname()
|
|
}
|
|
return name
|
|
}
|
|
|
|
func (fm *FetcherManager) AutoRename() bool {
|
|
return true
|
|
}
|
|
|
|
func (fm *FetcherManager) DefaultConfig() any {
|
|
return &config{
|
|
SegmentConnections: defaultSegmentConnections,
|
|
MaxRetries: defaultMaxRetries,
|
|
TimeoutSeconds: defaultTimeoutSeconds,
|
|
PrefetchContentLength: true,
|
|
}
|
|
}
|
|
|
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
|
_f := f.(*Fetcher)
|
|
if _f.state == nil {
|
|
return nil, nil
|
|
}
|
|
// The segment plan is immutable after resolve; the completed-segment
|
|
// journal lives on disk next to the segments.
|
|
return _f.state, nil
|
|
}
|
|
|
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
|
return &fetcherState{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
|
fb := &FetcherManager{}
|
|
restored := fb.Build().(*Fetcher)
|
|
restored.meta = meta
|
|
if state, ok := v.(*fetcherState); ok && state.MediaURL != "" {
|
|
restored.state = state
|
|
}
|
|
// Setup is invoked by the engine (restoreFetcher) after this builder
|
|
// returns; ReqExtra parsing happens there as well.
|
|
return restored
|
|
}
|
|
}
|
|
|
|
func (fm *FetcherManager) Close() error {
|
|
return nil
|
|
}
|