package media import ( "context" "fmt" "io" "m3u8-downloader/pkg/constants" "m3u8-downloader/pkg/httpClient" "net/http" "net/url" "os" "path" "strings" "time" ) type SegmentJob struct { URI string Seq uint64 VariantID int Variant *StreamVariant } func (j SegmentJob) AbsoluteURL() string { rel, _ := url.Parse(j.URI) return j.Variant.BaseURL.ResolveReference(rel).String() } func (j SegmentJob) Key() string { return fmt.Sprintf("%d:%s", j.Seq, j.URI) } func DownloadSegment(ctx context.Context, client *http.Client, segmentURL string, outputDir string) error { for attempt := 0; attempt < 2; attempt++ { if attempt > 0 { time.Sleep(300 * time.Millisecond) } req, err := http.NewRequestWithContext(ctx, "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 { if attempt == 1 { return err } continue } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { io.Copy(io.Discard, resp.Body) httpErr := &httpClient.HttpError{Code: resp.StatusCode} if resp.StatusCode == 403 && attempt == 0 { continue } return httpErr } if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } fileName := safeFileName(path.Join(outputDir, path.Base(segmentURL))) out, err := os.Create(fileName) if err != nil { return err } defer out.Close() n, err := io.Copy(out, resp.Body) if err != nil { return err } if n == 0 { return fmt.Errorf("zero-byte download for %s", segmentURL) } return nil } return fmt.Errorf("exhausted retries") } 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 }