From 9a9ca12cd9a451bd4c6b22d2ea94ea80f4bfca7f Mon Sep 17 00:00:00 2001 From: townandgown Date: Tue, 22 Jul 2025 00:34:04 -0500 Subject: [PATCH] Screw it, we're vibe coding now --- cmd/downloader/main.go | 334 ++++++++++++++++++++++++++------- go.mod | 2 + pkg/constants/downloader.go | 7 +- pkg/downloader/worker.go | 57 +++++- pkg/dvr/downloader.go | 67 +++++++ pkg/dvr/playlist.go | 73 ++++++++ pkg/media/master_playlist.go | 19 ++ scratch.txt | 348 +++-------------------------------- 8 files changed, 510 insertions(+), 397 deletions(-) create mode 100644 pkg/dvr/downloader.go create mode 100644 pkg/dvr/playlist.go diff --git a/cmd/downloader/main.go b/cmd/downloader/main.go index f135530..5934fca 100644 --- a/cmd/downloader/main.go +++ b/cmd/downloader/main.go @@ -1,78 +1,282 @@ package main import ( - "m3u8-downloader/pkg/downloader" - "m3u8-downloader/pkg/media" + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "strings" "sync" + "syscall" + "time" + + "github.com/grafov/m3u8" ) +// ==== CONFIG ==== +const ( + MasterURL = "https://d17cyqyz9yhmep.cloudfront.net/streams/234951/playlist_vo_1752978025523_1752978954944.m3u8" // Replace with your .m3u8 + WorkerCount = 4 + RefreshDelay = 3 * time.Second + + HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" + REFERRER = "https://www.flomarching.com" + OutputDirPath = "./data" +) + +// ==== TYPES ==== +type SegmentJob struct { + URI string + BaseURL *url.URL + Seq uint64 +} + +func (j SegmentJob) AbsoluteURL() string { + rel, _ := url.Parse(j.URI) + return j.BaseURL.ResolveReference(rel).String() +} + +type httpError struct { + code int +} + +func (e *httpError) Error() string { return fmt.Sprintf("http %d", e.code) } + +// ==== MAIN ==== func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - //Stream URL - masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234951/playlist_vo_1752978025523_1752978954944.m3u8" - - //Download Service - service := downloader.GetDownloadService() - - //Parse Master Playlist -> Stream Set - stream, err := service.ParseMasterPlaylist(masterUrl) - - if err != nil { - panic(err) - } - - //Select best quality streams - video, audio := stream.Master.SelectBestQualityStreams() - - if !(audio == nil) { - audio_segments, err := service.ParseSegmentPlaylist(stream.BuildSegmentURL(audio.URL)) - - if err != nil { - panic(err) - } - audio.Segments = *audio_segments - } - //Populate Segment Lists - - video_segments, err := service.ParseSegmentPlaylist(stream.BuildSegmentURL(video.URL)) - - if err != nil { - panic(err) - } - video.Segments = *video_segments - - audioChan := make(chan media.Segment, 10) - videoChan := make(chan media.Segment, 10) - - var wg sync.WaitGroup - - if !(audio == nil) { - for i := 1; i <= 2; i++ { - wg.Add(1) - go service.DownloadWorker(i, audioChan, &wg) - } - } - - for i := 1; i <= 4; i++ { - wg.Add(1) - go service.DownloadWorker(i, videoChan, &wg) - } - - if !(audio == nil) { - go func() { - for _, segment := range audio.Segments.SegmentList { - audioChan <- segment - } - close(audioChan) - }() - } - + // Handle Ctrl+C + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { - for _, segment := range video.Segments.SegmentList { - videoChan <- segment - } - close(videoChan) + <-sigChan + log.Println("Shutting down...") + cancel() }() + jobs := make(chan SegmentJob, 10) // smaller buffer to avoid stale tokens + var seen sync.Map + + // Start playlist refresher + go playlistRefresher(ctx, MasterURL, jobs, &seen, RefreshDelay) + + // Start download workers + var wg sync.WaitGroup + for i := 0; i < WorkerCount; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + numErrors, errorIDs := segmentDownloader(ctx, id, jobs) + if numErrors > 0 { + log.Printf("Worker %d: %d errors: %v", id, numErrors, errorIDs) + } + }(i) + } + wg.Wait() + log.Println("All workers finished.") +} + +// ==== PLAYLIST LOGIC ==== +func playlistRefresher(ctx context.Context, masterURL string, jobs chan<- SegmentJob, seen *sync.Map, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + variantURL, err := chooseVariant(masterURL) + if err != nil { + log.Printf("Variant selection failed, using master as media: %v", err) + variantURL = masterURL + } + + baseVariant, _ := url.Parse(variantURL) + + for { + select { + case <-ctx.Done(): + close(jobs) + return + default: + } + + media, err := loadMediaPlaylist(variantURL) + seq := media.SeqNo + if err != nil { + log.Printf("Error loading media playlist: %v", err) + goto waitTick + } + + for _, seg := range media.Segments { + if seg == nil { + continue + } + key := fmt.Sprintf("%d:%s", seq, seg.URI) + if _, loaded := seen.LoadOrStore(key, struct{}{}); !loaded { + jobs <- SegmentJob{URI: seg.URI, BaseURL: baseVariant, Seq: seq} + } + seq++ + } + + if media.Closed { + log.Println("Playlist closed (#EXT-X-ENDLIST); closing jobs.") + close(jobs) + return + } + + waitTick: + select { + case <-ctx.Done(): + close(jobs) + return + case <-ticker.C: + } + } +} + +func chooseVariant(masterURL string) (string, error) { + client := &http.Client{} + req, _ := http.NewRequest("GET", masterURL, nil) + req.Header.Set("User-Agent", HTTPUserAgent) + req.Header.Set("Referer", REFERRER) + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + playlist, listType, err := m3u8.DecodeFrom(resp.Body, true) + if err != nil { + return "", err + } + + base, _ := url.Parse(masterURL) + if listType == m3u8.MEDIA { + return masterURL, nil + } + + master := playlist.(*m3u8.MasterPlaylist) + if len(master.Variants) == 0 { + return "", fmt.Errorf("no variants found in master playlist") + } + + best := master.Variants[0] + for _, v := range master.Variants { + if v.Bandwidth > best.Bandwidth { + best = v + } + } + + vURL, _ := url.Parse(best.URI) + return base.ResolveReference(vURL).String(), nil +} + +func loadMediaPlaylist(mediaURL string) (*m3u8.MediaPlaylist, error) { + client := &http.Client{} + req, _ := http.NewRequest("GET", mediaURL, nil) + req.Header.Set("User-Agent", HTTPUserAgent) + req.Header.Set("Referer", REFERRER) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + pl, listType, err := m3u8.DecodeFrom(resp.Body, true) + if err != nil { + return nil, err + } + if listType == m3u8.MASTER { + return nil, fmt.Errorf("expected media playlist but got master") + } + return pl.(*m3u8.MediaPlaylist), nil +} + +// ==== WORKERS ==== +func segmentDownloader(ctx context.Context, id int, jobs <-chan SegmentJob) (int, []string) { + client := &http.Client{} + numErrors := 0 + errorIDs := make([]string, 0) + for { + select { + case <-ctx.Done(): + return numErrors, errorIDs + case job, ok := <-jobs: + if !ok { + return numErrors, errorIDs + } + abs := job.AbsoluteURL() + if err := downloadSegment(client, abs); err != nil { + if isHTTPStatus(err, 403) { + time.Sleep(300 * time.Millisecond) + if err2 := downloadSegment(client, abs); err2 != nil { + fmt.Printf("Worker %d: 403 retry failed (%s): %v\n", id, path.Base(abs), err2) + numErrors++ + errorIDs = append(errorIDs, path.Base(abs)) + } else { + fmt.Printf("Worker %d: recovered 403 (%s)\n", id, path.Base(abs)) + } + } else { + fmt.Printf("Worker %d: failed %s: %v\n", id, path.Base(abs), err) + } + } else { + fmt.Printf("Worker %d: downloaded %s\n", id, path.Base(abs)) + } + } + } +} + +func downloadSegment(client *http.Client, segmentURL string) error { + req, err := http.NewRequest("GET", segmentURL, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", HTTPUserAgent) + req.Header.Set("Referer", REFERRER) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + io.Copy(io.Discard, resp.Body) + return &httpError{code: resp.StatusCode} + } + + fileName := safeFileName(path.Join(OutputDirPath, path.Base(segmentURL))) + out, err := os.Create(fileName) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +// ==== HELPERS ==== +func safeFileName(base string) string { + if i := strings.IndexAny(base, "?&#"); i >= 0 { + base = base[:i] + } + if base == "" { + base = fmt.Sprintf("seg-%d.ts", time.Now().UnixNano()) + } + return base +} + +func isHTTPStatus(err error, code int) bool { + var he *httpError + if errors.As(err, &he) { + return he.code == code + } + return false } diff --git a/go.mod b/go.mod index f232466..7d6be6a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module m3u8-downloader go 1.23.0 + +require github.com/grafov/m3u8 v0.12.1 diff --git a/pkg/constants/downloader.go b/pkg/constants/downloader.go index 0fa6f5b..c68581b 100644 --- a/pkg/constants/downloader.go +++ b/pkg/constants/downloader.go @@ -1,5 +1,10 @@ package constants +import "time" + const ( - OutputDirPath = "./data" + OutputDirPath = "./data" + PlaylistRefreshInterval = 3 + NumberOfWorkers = 4 + RefreshDelay = 3 * time.Second ) diff --git a/pkg/downloader/worker.go b/pkg/downloader/worker.go index aebd6c4..6136fae 100644 --- a/pkg/downloader/worker.go +++ b/pkg/downloader/worker.go @@ -1,21 +1,70 @@ package downloader import ( + "context" "fmt" "m3u8-downloader/pkg/media" + "net/url" "sync" + "time" ) -func (s *DownloadService) DownloadWorker(id int, segmentChan <-chan media.Segment, wg *sync.WaitGroup) { +func resolveURL(baseURL string, segmentURL string) (string, error) { + base, err := url.Parse(baseURL) + if err != nil { + return "", err + } + segment, err := url.Parse(segmentURL) + if err != nil { + return "", err + } + return base.ResolveReference(segment).String(), nil +} + +func (s *DownloadService) DownloadWorker(id int, segmentChan <-chan media.Segment, wg *sync.WaitGroup, baseURL string) (int, int) { defer wg.Done() + numErrors := 0 + numDownloads := 0 + for segment := range segmentChan { - fmt.Printf("[Worker %d] Downloading: %s\n", id, segment.URL) - err := s.DownloadFile(segment.URL) + cleanedURL, err := resolveURL(baseURL, segment.URL) if err != nil { fmt.Printf("[Worker %d] Error: %s\n", id, err) - return + return -1, -1 } + fmt.Printf("[Worker %d] Downloading: %s\n", id, cleanedURL) + downloadErr := s.DownloadFile(cleanedURL) + if downloadErr != nil { + if downloadErr.Error() == "HTTP 403: 403 Forbidden" { + fmt.Printf("[Worker %d] URL Forbidden: %s\n", id, cleanedURL) + numErrors++ + continue + } + fmt.Printf("[Worker %d] Error: %s\n", id, downloadErr) + numErrors++ + return -1, -1 + } + numDownloads++ } + return numErrors, numDownloads +} + +func PlaylistRefreshWorker(ctx context.Context, playlistURL string, segmentsChan chan<- string, seen *sync.Map) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + segments, err := GetSegmentURLs(playlistURL) + if err != nil { + fmt.Println(err) + continue + } + for _, segment := range segments { + if _, ok := seen.Load(segment); !ok { + seen.Store(segment, true) } diff --git a/pkg/dvr/downloader.go b/pkg/dvr/downloader.go new file mode 100644 index 0000000..fcaa91b --- /dev/null +++ b/pkg/dvr/downloader.go @@ -0,0 +1,67 @@ +package dvr + +import ( + "context" + "fmt" + "io" + "m3u8-downloader/pkg/constants" + "net/http" + "os" +) + +// segmentDownloader processes segment URLs and downloads them +func SegmentDownloader(ctx context.Context, id int, segmentsChan <-chan string) { + client := &http.Client{} + + for { + select { + case <-ctx.Done(): + return + case seg, ok := <-segmentsChan: + if !ok { + return + } + if err := downloadSegment(client, seg); err != nil { + fmt.Printf("Worker %d: failed to download %s: %v\n", id, seg, err) + } else { + fmt.Printf("Worker %d: downloaded %s\n", id, seg) + } + } + } +} + +func downloadSegment(client *http.Client, segmentURL string) error { + req, err := http.NewRequest("GET", segmentURL, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", constants.HTTPUserAgent) + req.Header.Set("Referer", constants.REFERRER) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status %d", resp.StatusCode) + } + + // Save the segment (or buffer it, depending on your DVR strategy) + fileName := extractSegmentName(segmentURL) + out, err := os.Create(fileName) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +// extractSegmentName is a placeholder that turns the URL into a local filename +func extractSegmentName(segmentURL string) string { + // TODO: Implement robust name parsing + return "segment.ts" +} diff --git a/pkg/dvr/playlist.go b/pkg/dvr/playlist.go new file mode 100644 index 0000000..fc5a622 --- /dev/null +++ b/pkg/dvr/playlist.go @@ -0,0 +1,73 @@ +package dvr + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "sync" + "time" + + "github.com/grafov/m3u8" +) + +// playlistRefresher periodically fetches the playlist and enqueues new segments +func PlaylistRefresher(ctx context.Context, playlistURL string, segmentsChan chan<- string, seen *sync.Map, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + close(segmentsChan) + return + case <-ticker.C: + segments, err := fetchPlaylistSegments(playlistURL) + if err != nil { + log.Printf("Error fetching playlist: %v", err) + continue + } + for _, seg := range segments { + if _, exists := seen.LoadOrStore(seg, true); !exists { + segmentsChan <- seg + } + } + } + } +} + +// fetchPlaylistSegments is a placeholder that parses the M3U8 and returns full URLs +func fetchPlaylistSegments(playlistURL string) ([]string, error) { + resp, err := http.Get(playlistURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + playlist, listType, err := m3u8.DecodeFrom(resp.Body, true) + if err != nil { + return nil, err + } + + if listType == m3u8.MASTER { + return nil, fmt.Errorf("playlist is a master playlist") + } + + media := playlist.(*m3u8.MediaPlaylist) + base, err := url.Parse(playlistURL) + if err != nil { + return nil, err + } + var segments []string + for _, segment := range media.Segments { + if segment == nil { + continue + } + rel, err := url.Parse(segment.URI) + if err != nil { + return nil, err + } + segments = append(segments, base.ResolveReference(rel).String()) + } + return segments, nil +} diff --git a/pkg/media/master_playlist.go b/pkg/media/master_playlist.go index 0c6dc57..adf2767 100644 --- a/pkg/media/master_playlist.go +++ b/pkg/media/master_playlist.go @@ -31,6 +31,25 @@ func (m *MasterPlaylist) SelectBestQualityStreams() (*VideoPlaylist, *AudioPlayl return &bestVideo, m.FindAudioFeedByGroup(bestVideo.AudioGroup) } +func (s *StreamSet) SelectBestQualityStreams() (*VideoPlaylist, *AudioPlaylist) { + if len(s.Master.VideoFeeds) == 0 { + return nil, nil + } + + bestVideo := s.Master.VideoFeeds[0] + maxPixels := ResolutionToPixels(bestVideo.Resolution) + + for _, video := range s.Master.VideoFeeds { + pixels := ResolutionToPixels(video.Resolution) + if video.Bandwidth > bestVideo.Bandwidth || pixels > maxPixels { + bestVideo = video + maxPixels = pixels + } + } + + return &bestVideo, s.Master.FindAudioFeedByGroup(bestVideo.AudioGroup) +} + func (m *MasterPlaylist) FindAudioFeedByGroup(groupID string) *AudioPlaylist { for _, audio := range m.AudioFeeds { if audio.GroupID == groupID { diff --git a/scratch.txt b/scratch.txt index 00d8545..216e2d8 100644 --- a/scratch.txt +++ b/scratch.txt @@ -1,327 +1,21 @@ -#EXTM3U -#FLOSPORTS:56b3742 -#EXT-X-VERSION:7 -#EXT-X-TARGETDURATION:7 -#EXT-X-PLAYLIST-TYPE:VOD -#EXT-X-PROGRAM-DATE-TIME:2025-07-12T03:31:41.699Z -#EXTINF:6.006, -media_vo_3225_1752271771877.ts -#EXTINF:6.006, -media_vo_3226_1752271771877.ts -#EXTINF:5.989, -media_vo_3227_1752271771877.ts -#EXTINF:6.006, -media_vo_3228_1752271771877.ts -#EXTINF:6.006, -media_vo_3229_1752271771877.ts -#EXTINF:5.99, -media_vo_3230_1752271771877.ts -#EXTINF:6.006, -media_vo_3231_1752271771877.ts -#EXTINF:6.006, -media_vo_3232_1752271771877.ts -#EXTINF:5.989, -media_vo_3233_1752271771877.ts -#EXTINF:6.006, -media_vo_3234_1752271771877.ts -#EXTINF:6.006, -media_vo_3235_1752271771877.ts -#EXTINF:5.989, -media_vo_3236_1752271771877.ts -#EXTINF:6.006, -media_vo_3237_1752271771877.ts -#EXTINF:5.99, -media_vo_3238_1752271771877.ts -#EXTINF:6.006, -media_vo_3239_1752271771877.ts -#EXTINF:6.006, -media_vo_3240_1752271771877.ts -#EXTINF:5.989, -media_vo_3241_1752271771877.ts -#EXTINF:6.006, -media_vo_3242_1752271771877.ts -#EXTINF:6.006, -media_vo_3243_1752271771877.ts -#EXTINF:5.989, -media_vo_3244_1752271771877.ts -#EXTINF:6.006, -media_vo_3245_1752271771877.ts -#EXTINF:6.006, -media_vo_3246_1752271771877.ts -#EXTINF:5.989, -media_vo_3247_1752271771877.ts -#EXTINF:6.006, -media_vo_3248_1752271771877.ts -#EXTINF:5.99, -media_vo_3249_1752271771877.ts -#EXTINF:6.006, -media_vo_3250_1752271771877.ts -#EXTINF:6.006, -media_vo_3251_1752271771877.ts -#EXTINF:5.989, -media_vo_3252_1752271771877.ts -#EXTINF:6.006, -media_vo_3253_1752271771877.ts -#EXTINF:6.006, -media_vo_3254_1752271771877.ts -#EXTINF:5.989, -media_vo_3255_1752271771877.ts -#EXTINF:6.006, -media_vo_3256_1752271771877.ts -#EXTINF:6.006, -media_vo_3257_1752271771877.ts -#EXTINF:5.99, -media_vo_3258_1752271771877.ts -#EXTINF:6.006, -media_vo_3259_1752271771877.ts -#EXTINF:6.006, -media_vo_3260_1752271771877.ts -#EXTINF:5.989, -media_vo_3261_1752271771877.ts -#EXTINF:6.006, -media_vo_3262_1752271771877.ts -#EXTINF:5.989, -media_vo_3263_1752271771877.ts -#EXTINF:6.006, -media_vo_3264_1752271771877.ts -#EXTINF:6.006, -media_vo_3265_1752271771877.ts -#EXTINF:5.989, -media_vo_3266_1752271771877.ts -#EXTINF:6.006, -media_vo_3267_1752271771877.ts -#EXTINF:6.006, -media_vo_3268_1752271771877.ts -#EXTINF:5.99, -media_vo_3269_1752271771877.ts -#EXTINF:6.006, -media_vo_3270_1752271771877.ts -#EXTINF:6.006, -media_vo_3271_1752271771877.ts -#EXTINF:5.989, -media_vo_3272_1752271771877.ts -#EXTINF:6.006, -media_vo_3273_1752271771877.ts -#EXTINF:6.006, -media_vo_3274_1752271771877.ts -#EXTINF:5.989, -media_vo_3275_1752271771877.ts -#EXTINF:6.006, -media_vo_3276_1752271771877.ts -#EXTINF:5.99, -media_vo_3277_1752271771877.ts -#EXTINF:6.006, -media_vo_3278_1752271771877.ts -#EXTINF:6.006, -media_vo_3279_1752271771877.ts -#EXTINF:5.989, -media_vo_3280_1752271771877.ts -#EXTINF:6.006, -media_vo_3281_1752271771877.ts -#EXTINF:6.006, -media_vo_3282_1752271771877.ts -#EXTINF:5.989, -media_vo_3283_1752271771877.ts -#EXTINF:6.006, -media_vo_3284_1752271771877.ts -#EXTINF:6.006, -media_vo_3285_1752271771877.ts -#EXTINF:5.989, -media_vo_3286_1752271771877.ts -#EXTINF:6.006, -media_vo_3287_1752271771877.ts -#EXTINF:5.99, -media_vo_3288_1752271771877.ts -#EXTINF:6.006, -media_vo_3289_1752271771877.ts -#EXTINF:6.006, -media_vo_3290_1752271771877.ts -#EXTINF:5.989, -media_vo_3291_1752271771877.ts -#EXTINF:6.006, -media_vo_3292_1752271771877.ts -#EXTINF:6.006, -media_vo_3293_1752271771877.ts -#EXTINF:5.989, -media_vo_3294_1752271771877.ts -#EXTINF:6.006, -media_vo_3295_1752271771877.ts -#EXTINF:6.006, -media_vo_3296_1752271771877.ts -#EXTINF:5.99, -media_vo_3297_1752271771877.ts -#EXTINF:6.006, -media_vo_3298_1752271771877.ts -#EXTINF:6.006, -media_vo_3299_1752271771877.ts -#EXTINF:5.989, -media_vo_3300_1752271771877.ts -#EXTINF:6.006, -media_vo_3301_1752271771877.ts -#EXTINF:5.989, -media_vo_3302_1752271771877.ts -#EXTINF:6.006, -media_vo_3303_1752271771877.ts -#EXTINF:6.006, -media_vo_3304_1752271771877.ts -#EXTINF:5.989, -media_vo_3305_1752271771877.ts -#EXTINF:6.006, -media_vo_3306_1752271771877.ts -#EXTINF:6.006, -media_vo_3307_1752271771877.ts -#EXTINF:5.99, -media_vo_3308_1752271771877.ts -#EXTINF:6.006, -media_vo_3309_1752271771877.ts -#EXTINF:6.006, -media_vo_3310_1752271771877.ts -#EXTINF:5.989, -media_vo_3311_1752271771877.ts -#EXTINF:6.006, -media_vo_3312_1752271771877.ts -#EXTINF:6.006, -media_vo_3313_1752271771877.ts -#EXTINF:5.989, -media_vo_3314_1752271771877.ts -#EXTINF:6.006, -media_vo_3315_1752271771877.ts -#EXTINF:5.99, -media_vo_3316_1752271771877.ts -#EXTINF:6.006, -media_vo_3317_1752271771877.ts -#EXTINF:6.006, -media_vo_3318_1752271771877.ts -#EXTINF:5.989, -media_vo_3319_1752271771877.ts -#EXTINF:6.006, -media_vo_3320_1752271771877.ts -#EXTINF:6.006, -media_vo_3321_1752271771877.ts -#EXTINF:5.989, -media_vo_3322_1752271771877.ts -#EXTINF:6.006, -media_vo_3323_1752271771877.ts -#EXTINF:6.006, -media_vo_3324_1752271771877.ts -#EXTINF:5.989, -media_vo_3325_1752271771877.ts -#EXTINF:6.006, -media_vo_3326_1752271771877.ts -#EXTINF:5.99, -media_vo_3327_1752271771877.ts -#EXTINF:6.006, -media_vo_3328_1752271771877.ts -#EXTINF:6.006, -media_vo_3329_1752271771877.ts -#EXTINF:5.989, -media_vo_3330_1752271771877.ts -#EXTINF:6.006, -media_vo_3331_1752271771877.ts -#EXTINF:6.006, -media_vo_3332_1752271771877.ts -#EXTINF:5.989, -media_vo_3333_1752271771877.ts -#EXTINF:6.006, -media_vo_3334_1752271771877.ts -#EXTINF:6.006, -media_vo_3335_1752271771877.ts -#EXTINF:5.99, -media_vo_3336_1752271771877.ts -#EXTINF:6.006, -media_vo_3337_1752271771877.ts -#EXTINF:6.005, -media_vo_3338_1752271771877.ts -#EXTINF:5.99, -media_vo_3339_1752271771877.ts -#EXTINF:6.006, -media_vo_3340_1752271771877.ts -#EXTINF:5.989, -media_vo_3341_1752271771877.ts -#EXTINF:6.006, -media_vo_3342_1752271771877.ts -#EXTINF:6.006, -media_vo_3343_1752271771877.ts -#EXTINF:5.989, -media_vo_3344_1752271771877.ts -#EXTINF:6.006, -media_vo_3345_1752271771877.ts -#EXTINF:6.006, -media_vo_3346_1752271771877.ts -#EXTINF:5.99, -media_vo_3347_1752271771877.ts -#EXTINF:6.006, -media_vo_3348_1752271771877.ts -#EXTINF:6.006, -media_vo_3349_1752271771877.ts -#EXTINF:5.989, -media_vo_3350_1752271771877.ts -#EXTINF:6.006, -media_vo_3351_1752271771877.ts -#EXTINF:6.006, -media_vo_3352_1752271771877.ts -#EXTINF:5.989, -media_vo_3353_1752271771877.ts -#EXTINF:6.006, -media_vo_3354_1752271771877.ts -#EXTINF:5.99, -media_vo_3355_1752271771877.ts -#EXTINF:6.005, -media_vo_3356_1752271771877.ts -#EXTINF:6.006, -media_vo_3357_1752271771877.ts -#EXTINF:5.99, -media_vo_3358_1752271771877.ts -#EXTINF:6.006, -media_vo_3359_1752271771877.ts -#EXTINF:6.006, -media_vo_3360_1752271771877.ts -#EXTINF:5.989, -media_vo_3361_1752271771877.ts -#EXTINF:6.006, -media_vo_3362_1752271771877.ts -#EXTINF:6.006, -media_vo_3363_1752271771877.ts -#EXTINF:5.989, -media_vo_3364_1752271771877.ts -#EXTINF:6.006, -media_vo_3365_1752271771877.ts -#EXTINF:5.99, -media_vo_3366_1752271771877.ts -#EXTINF:6.006, -media_vo_3367_1752271771877.ts -#EXTINF:6.006, -media_vo_3368_1752271771877.ts -#EXTINF:5.989, -media_vo_3369_1752271771877.ts -#EXTINF:6.006, -media_vo_3370_1752271771877.ts -#EXTINF:6.006, -media_vo_3371_1752271771877.ts -#EXTINF:5.989, -media_vo_3372_1752271771877.ts -#EXTINF:6.006, -media_vo_3373_1752271771877.ts -#EXTINF:6.006, -media_vo_3374_1752271771877.ts -#EXTINF:5.989, -media_vo_3375_1752271771877.ts -#EXTINF:6.006, -media_vo_3376_1752271771877.ts -#EXTINF:6.006, -media_vo_3377_1752271771877.ts -#EXTINF:5.99, -media_vo_3378_1752271771877.ts -#EXTINF:6.006, -media_vo_3379_1752271771877.ts -#EXTINF:5.989, -media_vo_3380_1752271771877.ts -#EXTINF:6.006, -media_vo_3381_1752271771877.ts -#EXTINF:6.006, -media_vo_3382_1752271771877.ts -#EXTINF:5.989, -media_vo_3383_1752271771877.ts -#EXTINF:6.006, -media_vo_3384_1752271771877.ts -#EXT-X-ENDLIST \ No newline at end of file +2025/07/21 23:51:15 Worker 0: 3 errors: [media_vo_5848_1752943488681.ts media_vo_5854_1752943488681.ts media_vo_5892_1752943488681.ts] +Worker 2: downloaded media_vo_5911_1752943488681.ts +2025/07/21 23:51:15 Worker 2: 5 errors: [media_vo_5790_1752943488681.ts media_vo_5812_1752943488681.ts media_vo_5817_1752943488681.ts media_vo_5873_1752943488681.ts media_vo_5882_1752943488681.ts] +Worker 1: downloaded media_vo_5913_1752943488681.ts +2025/07/21 23:51:15 Worker 1: 3 errors: [media_vo_5795_1752943488681.ts media_vo_5821_1752943488681.ts media_vo_5908_1752943488681.ts] +Worker 3: downloaded media_vo_5912_1752943488681.ts +2025/07/21 23:51:15 Worker 3: 3 errors: [media_vo_5814_1752943488681.ts media_vo_5818_1752943488681.ts media_vo_5855_1752943488681.ts] + +5854,5892,5790,5812,5817,5873,5882,5795,5821,5908,5814,5818,5855 + + +Worker 2: downloaded media_vo_5911_1752943488681.ts +2025/07/21 23:52:08 Worker 2: 4 errors: [media_vo_5790_1752943488681.ts media_vo_5818_1752943488681.ts media_vo_5854_1752943488681.ts media_vo_5873_1752943488681.ts] +Worker 3: downloaded media_vo_5912_1752943488681.ts +2025/07/21 23:52:08 Worker 3: 3 errors: [media_vo_5795_1752943488681.ts media_vo_5817_1752943488681.ts media_vo_5848_1752943488681.ts] +Worker 1: 403 retry failed (media_vo_5908_1752943488681.ts): http 403 +Worker 0: downloaded media_vo_5913_1752943488681.ts +2025/07/21 23:52:08 Worker 1: 4 errors: [media_vo_5812_1752943488681.ts media_vo_5855_1752943488681.ts media_vo_5882_1752943488681.ts media_vo_5908_1752943488681.ts] +2025/07/21 23:52:08 Worker 0: 3 errors: [media_vo_5814_1752943488681.ts media_vo_5821_1752943488681.ts media_vo_5892_1752943488681.ts] + +5790,5818,5854,5873,5795,5817,5848,5812,5855,5882,5908,5814,5821,5892 \ No newline at end of file