Screw it, we're vibe coding now
This commit is contained in:
parent
354bbbf7df
commit
9a9ca12cd9
@ -1,78 +1,282 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"m3u8-downloader/pkg/downloader"
|
"context"
|
||||||
"m3u8-downloader/pkg/media"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"sync"
|
"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() {
|
func main() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
//Stream URL
|
// Handle Ctrl+C
|
||||||
masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234951/playlist_vo_1752978025523_1752978954944.m3u8"
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
log.Println("Shutting down...")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
//Download Service
|
jobs := make(chan SegmentJob, 10) // smaller buffer to avoid stale tokens
|
||||||
service := downloader.GetDownloadService()
|
var seen sync.Map
|
||||||
|
|
||||||
//Parse Master Playlist -> Stream Set
|
// Start playlist refresher
|
||||||
stream, err := service.ParseMasterPlaylist(masterUrl)
|
go playlistRefresher(ctx, MasterURL, jobs, &seen, RefreshDelay)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
// Start download workers
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < WorkerCount; i++ {
|
||||||
if !(audio == nil) {
|
|
||||||
for i := 1; i <= 2; i++ {
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go service.DownloadWorker(i, audioChan, &wg)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for _, segment := range video.Segments.SegmentList {
|
|
||||||
videoChan <- segment
|
|
||||||
}
|
|
||||||
close(videoChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module m3u8-downloader
|
module m3u8-downloader
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
|
require github.com/grafov/m3u8 v0.12.1
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OutputDirPath = "./data"
|
OutputDirPath = "./data"
|
||||||
|
PlaylistRefreshInterval = 3
|
||||||
|
NumberOfWorkers = 4
|
||||||
|
RefreshDelay = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,21 +1,70 @@
|
|||||||
package downloader
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"m3u8-downloader/pkg/media"
|
"m3u8-downloader/pkg/media"
|
||||||
|
"net/url"
|
||||||
"sync"
|
"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()
|
defer wg.Done()
|
||||||
|
|
||||||
|
numErrors := 0
|
||||||
|
numDownloads := 0
|
||||||
|
|
||||||
for segment := range segmentChan {
|
for segment := range segmentChan {
|
||||||
|
|
||||||
fmt.Printf("[Worker %d] Downloading: %s\n", id, segment.URL)
|
cleanedURL, err := resolveURL(baseURL, segment.URL)
|
||||||
err := s.DownloadFile(segment.URL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[Worker %d] Error: %s\n", id, err)
|
fmt.Printf("[Worker %d] Error: %s\n", id, err)
|
||||||
|
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
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
67
pkg/dvr/downloader.go
Normal file
67
pkg/dvr/downloader.go
Normal file
@ -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"
|
||||||
|
}
|
||||||
73
pkg/dvr/playlist.go
Normal file
73
pkg/dvr/playlist.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -31,6 +31,25 @@ func (m *MasterPlaylist) SelectBestQualityStreams() (*VideoPlaylist, *AudioPlayl
|
|||||||
return &bestVideo, m.FindAudioFeedByGroup(bestVideo.AudioGroup)
|
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 {
|
func (m *MasterPlaylist) FindAudioFeedByGroup(groupID string) *AudioPlaylist {
|
||||||
for _, audio := range m.AudioFeeds {
|
for _, audio := range m.AudioFeeds {
|
||||||
if audio.GroupID == groupID {
|
if audio.GroupID == groupID {
|
||||||
|
|||||||
348
scratch.txt
348
scratch.txt
@ -1,327 +1,21 @@
|
|||||||
#EXTM3U
|
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]
|
||||||
#FLOSPORTS:56b3742
|
Worker 2: downloaded media_vo_5911_1752943488681.ts
|
||||||
#EXT-X-VERSION:7
|
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]
|
||||||
#EXT-X-TARGETDURATION:7
|
Worker 1: downloaded media_vo_5913_1752943488681.ts
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
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]
|
||||||
#EXT-X-PROGRAM-DATE-TIME:2025-07-12T03:31:41.699Z
|
Worker 3: downloaded media_vo_5912_1752943488681.ts
|
||||||
#EXTINF:6.006,
|
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]
|
||||||
media_vo_3225_1752271771877.ts
|
|
||||||
#EXTINF:6.006,
|
5854,5892,5790,5812,5817,5873,5882,5795,5821,5908,5814,5818,5855
|
||||||
media_vo_3226_1752271771877.ts
|
|
||||||
#EXTINF:5.989,
|
|
||||||
media_vo_3227_1752271771877.ts
|
Worker 2: downloaded media_vo_5911_1752943488681.ts
|
||||||
#EXTINF:6.006,
|
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]
|
||||||
media_vo_3228_1752271771877.ts
|
Worker 3: downloaded media_vo_5912_1752943488681.ts
|
||||||
#EXTINF:6.006,
|
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]
|
||||||
media_vo_3229_1752271771877.ts
|
Worker 1: 403 retry failed (media_vo_5908_1752943488681.ts): http 403
|
||||||
#EXTINF:5.99,
|
Worker 0: downloaded media_vo_5913_1752943488681.ts
|
||||||
media_vo_3230_1752271771877.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]
|
||||||
#EXTINF:6.006,
|
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]
|
||||||
media_vo_3231_1752271771877.ts
|
|
||||||
#EXTINF:6.006,
|
5790,5818,5854,5873,5795,5817,5848,5812,5855,5882,5908,5814,5821,5892
|
||||||
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
|
|
||||||
Loading…
x
Reference in New Issue
Block a user