mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 18:10:29 +00:00
feat(hls): add native HLS (m3u8) download protocol
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).
This commit is contained in:
1 parent
e5ac13cfdc
commit
4f74101b3a
44 files changed
+2824
-6
No files matched your search
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user