Abstracted to service.go

This commit is contained in:
kacarmichael 2025-07-20 23:44:49 -05:00
parent 93b62618a1
commit b37a312b18
16 changed files with 466 additions and 391 deletions

View File

@ -2,34 +2,81 @@ package main
import (
"fmt"
downloader2 "m3u8-downloader/pkg/downloader"
"m3u8-downloader/pkg/downloader"
)
func main() {
//Stream URL
masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234945/playlist_1752291107574_1752292056713.m3u8"
stream, err := downloader2.ParseMasterPlaylist(masterUrl)
//Download Service
service := downloader.GetDownloadService()
//Parse Master Playlist -> Stream Set
stream, err := service.ParseMasterPlaylist(masterUrl)
if err != nil {
panic(err)
}
audio, video, err := stream.FetchSegmentPlaylists()
//Select best quality streams
audio, video := stream.Master.SelectBestQualityStreams()
//Populate Segment Lists
audio_segments, err := service.ParseSegmentPlaylist(stream.BuildSegmentURL(audio.URL))
if err != nil {
panic(err)
}
audio.Segments = *audio_segments
videoPlaylist := downloader2.ParseMediaPlaylist(video)
audioPlaylist := downloader2.ParseMediaPlaylist(audio)
video_segments, err := service.ParseSegmentPlaylist(stream.BuildSegmentURL(video.URL))
for _, segment := range videoPlaylist.Segments {
fmt.Println(segment.URL)
if err != nil {
panic(err)
}
video.Segments = *video_segments
for _, segment := range audioPlaylist.Segments {
err := downloader2.DownloadTSFile(stream.BuildSegmentURL(segment.URL), downloader2.OutputDirPath)
//Download Segment Playlists
for _, segment := range video.Segments.SegmentList {
err := service.DownloadFile(stream.BuildSegmentURL(segment.URL))
if err != nil {
fmt.Println(err)
return
}
}
for _, segment := range audio.Segments.SegmentList {
err := service.DownloadFile(stream.BuildSegmentURL(segment.URL))
if err != nil {
fmt.Println(err)
return
}
}
fmt.Println(stream)
//stream, err := media.ParseMasterPlaylist(masterUrl)
//if err != nil {
// panic(err)
//}
//
//audio, video, err := stream.FetchSegmentPlaylists()
//if err != nil {
// panic(err)
//}
//
//videoPlaylist := media.ParseMediaPlaylist(video)
//audioPlaylist := media.ParseMediaPlaylist(audio)
//
//for _, segment := range videoPlaylist.SegmentList {
// fmt.Println(segment.URL)
//}
//
//for _, segment := range audioPlaylist.SegmentList {
// err := http.DownloadFile(stream.BuildSegmentURL(segment.URL), media.OutputDirPath)
// if err != nil {
// return
// }
//}
}

View File

@ -0,0 +1,5 @@
package constants
const (
OutputDirPath = "./data"
)

7
pkg/constants/http.go Normal file
View File

@ -0,0 +1,7 @@
package constants
const (
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0"
HTTPPrefix = "http://"
HTTPSPrefix = "https://"
)

11
pkg/constants/media.go Normal file
View File

@ -0,0 +1,11 @@
package constants
const (
ExtXStreamInf = "#EXT-X-STREAM-INF"
ExtXMedia = "#EXT-X-MEDIA"
ExtXTargetDuration = "#EXT-X-TARGETDURATION"
ExtInf = "#EXTINF"
ExtXEndList = "#EXT-X-ENDLIST"
ExtXPlaylistType = "#EXT-X-PLAYLIST-TYPE"
PlaylistTypeVOD = "VOD"
)

View File

@ -1,131 +0,0 @@
package downloader
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"time"
)
const (
DefaultTimeout = 30 * time.Second
UserAgent = "M3U8-Downloader/1.0"
)
type HTTPClient struct {
client *http.Client
headers map[string]string
}
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
client: &http.Client{
Timeout: DefaultTimeout,
},
headers: map[string]string{
"User-Agent": UserAgent,
},
}
}
func (c *HTTPClient) SetTimeout(timeout time.Duration) {
c.client.Timeout = timeout
}
func (c *HTTPClient) SetHeader(key, value string) {
c.headers[key] = value
}
func (c *HTTPClient) Get(url string) ([]byte, error) {
return c.GetWithContext(context.Background(), url)
}
func (c *HTTPClient) GetWithContext(ctx context.Context, url string) ([]byte, error) {
if !ValidateURL(url) {
return nil, errors.New("invalid URL format")
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
for key, value := range c.headers {
req.Header.Set(key, value)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return data, nil
}
// Global client instance for convenience functions
var defaultClient = NewHTTPClient()
func FetchPlaylistContent(url string) (string, error) {
data, err := defaultClient.Get(url)
if err != nil {
return "", err
}
return string(data), nil
}
func DownloadTSFile(url string, outputDir string) error {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
fileName := path.Base(url)
filePath := filepath.Join(outputDir, fileName)
data, err := defaultClient.Get(url)
if err != nil {
return err
}
out, err := os.Create(filePath)
if err != nil {
return err
}
defer out.Close()
_, err = out.Write(data)
if err != nil {
return err
}
return nil
}
func FetchPlaylistContentWithContext(ctx context.Context, url string) (string, error) {
data, err := defaultClient.GetWithContext(ctx, url)
if err != nil {
return "", err
}
return string(data), nil
}
func SetDefaultTimeout(timeout time.Duration) {
defaultClient.SetTimeout(timeout)
}
func SetDefaultUserAgent(userAgent string) {
defaultClient.SetHeader("User-Agent", userAgent)
}

View File

@ -1,18 +0,0 @@
package downloader
import "time"
const (
ExtXStreamInf = "#EXT-X-STREAM-INF"
ExtXMedia = "#EXT-X-MEDIA"
ExtXTargetDuration = "#EXT-X-TARGETDURATION"
ExtInf = "#EXTINF"
ExtXEndList = "#EXT-X-ENDLIST"
ExtXPlaylistType = "#EXT-X-PLAYLIST-TYPE"
HTTPPrefix = "http://"
HTTPSPrefix = "https://"
ClientDefaultTimeout = 30 * time.Second
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0"
PlaylistTypeVOD = "VOD"
OutputDirPath = "./data"
)

View File

@ -1,71 +0,0 @@
package downloader
import (
"fmt"
"strings"
)
type PlaylistMetadata struct {
URL string
Domain string
StreamID string
}
func NewPlaylistMetadata(masterURL string) *PlaylistMetadata {
strippedURL := strings.Replace(masterURL, HTTPSPrefix, "", 1)
urlPrefix := strings.Split(strippedURL, "/")[0]
return &PlaylistMetadata{
URL: masterURL,
Domain: urlPrefix,
StreamID: strings.Split(strippedURL, "/")[2],
}
}
type StreamSet struct {
Metadata *PlaylistMetadata
VideoStreams []VideoStream
AudioStreams []AudioStream
}
func (s *StreamSet) BuildStreamURL(endpoint string) string {
return fmt.Sprintf("%s%s/streams/%s/%s",
HTTPSPrefix, s.Metadata.Domain, s.Metadata.StreamID, endpoint)
}
func ParseMasterPlaylist(masterURL string) (*StreamSet, error) {
metadata := NewPlaylistMetadata(masterURL)
content, err := FetchPlaylistContent(masterURL)
if err != nil {
return nil, err
}
videoStreams, audioStreams := parsePlaylistLines(content)
return &StreamSet{
Metadata: metadata,
VideoStreams: videoStreams,
AudioStreams: audioStreams,
}, nil
}
func parsePlaylistLines(content string) ([]VideoStream, []AudioStream) {
lines := strings.Split(content, "\n")
var videoStreams []VideoStream
var audioStreams []AudioStream
for i, line := range lines {
if strings.HasPrefix(line, ExtXStreamInf) && i+1 < len(lines) {
if video, err := NewVideoStream(line, lines[i+1]); err == nil {
videoStreams = append(videoStreams, *video)
}
} else if strings.HasPrefix(line, ExtXMedia) {
if audio, err := NewAudioStream(line); err == nil {
audioStreams = append(audioStreams, *audio)
}
}
}
return videoStreams, audioStreams
}

70
pkg/downloader/service.go Normal file
View File

@ -0,0 +1,70 @@
package downloader
import (
"fmt"
"m3u8-downloader/pkg/constants"
"m3u8-downloader/pkg/http"
"m3u8-downloader/pkg/media"
"os"
"path"
"path/filepath"
)
type DownloadService struct {
client *http.HTTPWrapper
}
func GetDownloadService() *DownloadService {
return &DownloadService{
client: http.DefaultClient,
}
}
func (s *DownloadService) GetStreamMetadata(url string) (*media.PlaylistMetadata, error) {
return media.GetPlaylistMetadata(url), nil
}
func (s *DownloadService) ParseSegmentPlaylist(url string) (*media.SegmentPlaylist, error) {
content, err := s.client.Get(url)
if err != nil {
return nil, err
}
return media.ParseMediaPlaylist(string(content)), nil
}
func (s *DownloadService) ParseMasterPlaylist(url string) (*media.StreamSet, error) {
metadata := media.GetPlaylistMetadata(url)
content, err := s.client.Get(url)
if err != nil {
return nil, err
}
video, audio := media.ParsePlaylistLines(string(content))
master := media.NewMasterPlaylist(video, audio)
return media.NewStreamSet(metadata, master), nil
}
func (s *DownloadService) DownloadFile(url string) error {
fmt.Println("Downloading: " + url)
if err := os.MkdirAll(constants.OutputDirPath, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
fileName := path.Base(url)
filePath := filepath.Join(constants.OutputDirPath, fileName)
data, err := s.client.Get(url)
if err != nil {
return err
}
out, err := os.Create(filePath)
if err != nil {
return err
}
defer out.Close()
_, err = out.Write(data)
if err != nil {
return err
}
return nil
}

View File

@ -1,98 +0,0 @@
package downloader
import (
"errors"
"regexp"
"strconv"
"strings"
)
type VideoStream struct {
URL string
Bandwidth int
Codecs string
Resolution string
FrameRate string
AudioGroup string
}
func NewVideoStream(streamInfo, url string) (*VideoStream, error) {
if !strings.HasPrefix(streamInfo, ExtXStreamInf) {
return nil, errors.New("invalid stream info line")
}
reg := regexp.MustCompile(`([A-Z0-9-]+)=(".*?"|[^,]*)`)
matches := reg.FindAllStringSubmatch(streamInfo, -1)
if len(matches) < 5 {
return nil, errors.New("insufficient stream attributes")
}
bandwidth, err := strconv.Atoi(matches[0][2])
if err != nil {
return nil, err
}
return &VideoStream{
URL: url,
Bandwidth: bandwidth,
Codecs: StripQuotes(matches[1][2]),
Resolution: StripQuotes(matches[2][2]),
FrameRate: StripQuotes(matches[3][2]),
AudioGroup: StripQuotes(matches[4][2]),
}, nil
}
type AudioStream struct {
URL string
MediaType string
GroupID string
Name string
IsDefault bool
AutoSelect bool
}
func NewAudioStream(mediaInfo string) (*AudioStream, error) {
if !strings.HasPrefix(mediaInfo, ExtXMedia) {
return nil, errors.New("invalid downloader info line")
}
attributes := parseMediaAttributes(mediaInfo)
return &AudioStream{
URL: StripQuotes(attributes["URI"]),
MediaType: StripQuotes(attributes["TYPE"]),
GroupID: StripQuotes(attributes["GROUP-ID"]),
Name: StripQuotes(attributes["NAME"]),
IsDefault: attributes["DEFAULT"] == "YES",
AutoSelect: attributes["AUTOSELECT"] == "YES",
}, nil
}
func (s *StreamSet) SelectBestQualityStreams() (*VideoStream, *AudioStream) {
if len(s.VideoStreams) == 0 {
return nil, nil
}
bestVideo := s.VideoStreams[0]
maxPixels := ResolutionToPixels(bestVideo.Resolution)
for _, video := range s.VideoStreams {
pixels := ResolutionToPixels(video.Resolution)
if video.Bandwidth > bestVideo.Bandwidth || pixels > maxPixels {
bestVideo = video
maxPixels = pixels
}
}
return &bestVideo, s.FindAudioStreamByGroup(bestVideo.AudioGroup)
}
func (s *StreamSet) FindAudioStreamByGroup(groupID string) *AudioStream {
for _, audio := range s.AudioStreams {
if audio.GroupID == groupID {
return &audio
}
}
return nil
}

64
pkg/http/client.go Normal file
View File

@ -0,0 +1,64 @@
package http
import (
"context"
"errors"
"fmt"
"io"
"m3u8-downloader/pkg/constants"
"net/http"
)
type Client interface {
Get(url string) ([]byte, error)
GetWithContext(ctx context.Context, url string) ([]byte, error)
}
type HTTPWrapper struct {
client *http.Client
headers map[string]string
}
func (c *HTTPWrapper) Get(url string) ([]byte, error) {
return c.GetWithContext(context.Background(), url)
}
func (c *HTTPWrapper) GetWithContext(ctx context.Context, url string) ([]byte, error) {
if !ValidateURL(url) {
return nil, errors.New("invalid URL format")
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
for key, value := range c.headers {
req.Header.Set(key, value)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return data, nil
}
var DefaultClient = &HTTPWrapper{
client: &http.Client{},
headers: map[string]string{
"User-Agent": constants.HTTPUserAgent,
},
}

10
pkg/http/utils.go Normal file
View File

@ -0,0 +1,10 @@
package http
import (
"m3u8-downloader/pkg/constants"
"strings"
)
func ValidateURL(url string) bool {
return strings.HasPrefix(url, constants.HTTPPrefix) || strings.HasPrefix(url, constants.HTTPSPrefix)
}

View File

@ -0,0 +1,41 @@
package media
type MasterPlaylist struct {
VideoFeeds []VideoPlaylist
AudioFeeds []AudioPlaylist
}
func NewMasterPlaylist(videoFeeds []VideoPlaylist, audioFeeds []AudioPlaylist) *MasterPlaylist {
return &MasterPlaylist{
VideoFeeds: videoFeeds,
AudioFeeds: audioFeeds,
}
}
func (m *MasterPlaylist) SelectBestQualityStreams() (*VideoPlaylist, *AudioPlaylist) {
if len(m.VideoFeeds) == 0 {
return nil, nil
}
bestVideo := m.VideoFeeds[0]
maxPixels := ResolutionToPixels(bestVideo.Resolution)
for _, video := range m.VideoFeeds {
pixels := ResolutionToPixels(video.Resolution)
if video.Bandwidth > bestVideo.Bandwidth || pixels > maxPixels {
bestVideo = video
maxPixels = pixels
}
}
return &bestVideo, m.FindAudioFeedByGroup(bestVideo.AudioGroup)
}
func (m *MasterPlaylist) FindAudioFeedByGroup(groupID string) *AudioPlaylist {
for _, audio := range m.AudioFeeds {
if audio.GroupID == groupID {
return &audio
}
}
return nil
}

139
pkg/media/media_playlist.go Normal file
View File

@ -0,0 +1,139 @@
package media
import (
"errors"
"fmt"
"m3u8-downloader/pkg/constants"
"regexp"
"strconv"
"strings"
)
type VideoPlaylist struct {
URL string
Bandwidth int
Codecs string
Resolution string
FrameRate string
AudioGroup string
Segments SegmentPlaylist
}
func NewVideoStream(streamInfo, url string) (*VideoPlaylist, error) {
if !strings.HasPrefix(streamInfo, constants.ExtXStreamInf) {
return nil, errors.New("invalid stream info line")
}
reg := regexp.MustCompile(`([A-Z0-9-]+)=(".*?"|[^,]*)`)
matches := reg.FindAllStringSubmatch(streamInfo, -1)
if len(matches) < 5 {
return nil, errors.New("insufficient stream attributes")
}
bandwidth, err := strconv.Atoi(matches[0][2])
if err != nil {
return nil, err
}
return &VideoPlaylist{
URL: url,
Bandwidth: bandwidth,
Codecs: StripQuotes(matches[1][2]),
Resolution: StripQuotes(matches[2][2]),
FrameRate: StripQuotes(matches[3][2]),
AudioGroup: StripQuotes(matches[4][2]),
}, nil
}
func (v *VideoPlaylist) BuildPlaylistURL(filename string) string {
return fmt.Sprintf("%s%s", constants.HTTPSPrefix, v.URL+"/a/5000/"+filename)
}
type AudioPlaylist struct {
URL string
MediaType string
GroupID string
Name string
IsDefault bool
AutoSelect bool
Segments SegmentPlaylist
}
func (a *AudioPlaylist) BuildPlaylistURL(filename string) string {
return fmt.Sprintf("%s%s", constants.HTTPSPrefix, a.URL+"/a/5000/"+filename)
}
func NewAudioStream(mediaInfo string) (*AudioPlaylist, error) {
if !strings.HasPrefix(mediaInfo, constants.ExtXMedia) {
return nil, errors.New("invalid downloader info line")
}
attributes := ParseMediaAttributes(mediaInfo)
return &AudioPlaylist{
URL: StripQuotes(attributes["URI"]),
MediaType: StripQuotes(attributes["TYPE"]),
GroupID: StripQuotes(attributes["GROUP-ID"]),
Name: StripQuotes(attributes["NAME"]),
IsDefault: attributes["DEFAULT"] == "YES",
AutoSelect: attributes["AUTOSELECT"] == "YES",
}, nil
}
type PlaylistMetadata struct {
URL string
Domain string
StreamID string
}
func GetPlaylistMetadata(masterURL string) *PlaylistMetadata {
strippedURL := strings.Replace(masterURL, constants.HTTPSPrefix, "", 1)
strippedURL = strings.Replace(strippedURL, constants.HTTPPrefix, "", 1)
urlPrefix := strings.Split(strippedURL, "/")[0]
return &PlaylistMetadata{
URL: masterURL,
Domain: urlPrefix,
StreamID: strings.Split(strippedURL, "/")[2],
}
}
func ParsePlaylistLines(content string) ([]VideoPlaylist, []AudioPlaylist) {
lines := strings.Split(content, "\n")
var videoStreams []VideoPlaylist
var audioStreams []AudioPlaylist
for i, line := range lines {
if strings.HasPrefix(line, constants.ExtXStreamInf) && i+1 < len(lines) {
if video, err := NewVideoStream(line, lines[i+1]); err == nil {
videoStreams = append(videoStreams, *video)
}
} else if strings.HasPrefix(line, constants.ExtXMedia) {
if audio, err := NewAudioStream(line); err == nil {
audioStreams = append(audioStreams, *audio)
}
}
}
return videoStreams, audioStreams
}
func ResolutionToPixels(resolution string) int {
parts := strings.Split(resolution, "x")
if len(parts) != 2 {
return 0
}
width, err := strconv.Atoi(parts[0])
if err != nil {
return 0
}
height, err := strconv.Atoi(parts[1])
if err != nil {
return 0
}
return width * height
}

View File

@ -1,7 +1,7 @@
package downloader
package media
import (
"strconv"
"m3u8-downloader/pkg/constants"
"strings"
)
@ -17,30 +17,11 @@ func StripQuotes(input string) string {
return strings.Trim(input, "\"")
}
func ResolutionToPixels(resolution string) int {
parts := strings.Split(resolution, "x")
if len(parts) != 2 {
return 0
}
width, err := strconv.Atoi(parts[0])
if err != nil {
return 0
}
height, err := strconv.Atoi(parts[1])
if err != nil {
return 0
}
return width * height
}
func parseMediaAttributes(mediaInfo string) map[string]string {
func ParseMediaAttributes(mediaInfo string) map[string]string {
attributes := make(map[string]string)
// Remove the #EXT-X-MEDIA: prefix
content := strings.TrimPrefix(mediaInfo, ExtXMedia+":")
content := strings.TrimPrefix(mediaInfo, constants.ExtXMedia+":")
// Split by comma, but respect quoted values
parts := splitRespectingQuotes(content, ',')
@ -82,7 +63,3 @@ func splitRespectingQuotes(input string, delimiter rune) []string {
return result
}
func ValidateURL(url string) bool {
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
}

View File

@ -1,8 +1,8 @@
package downloader
package media
import (
"errors"
"fmt"
"m3u8-downloader/pkg/constants"
"strconv"
"strings"
)
@ -13,47 +13,47 @@ type Segment struct {
Sequence int
}
type MediaPlaylist struct {
Segments []Segment
type SegmentPlaylist struct {
SegmentList []Segment
TargetDuration float64
IsLive bool
HasEndList bool
}
func (s *StreamSet) FetchSegmentPlaylists() (videoPlaylist, audioPlaylist string, err error) {
video, audio := s.SelectBestQualityStreams()
if video == nil {
return "", "", errors.New("no video streams available")
}
videoURL := s.BuildStreamURL(video.URL)
videoContent, err := FetchPlaylistContent(StripQuotes(videoURL))
if err != nil {
return "", "", err
}
var audioContent string
if audio != nil {
audioURL := s.BuildStreamURL(StripQuotes(audio.URL))
audioContent, err = FetchPlaylistContent(StripQuotes(audioURL))
if err != nil {
return "", "", err
}
}
return videoContent, audioContent, nil
}
//func (s *StreamSet) FetchSegmentPlaylists() (videoPlaylist, audioPlaylist string, err error) {
// video, audio := s.SelectBestQualityStreams()
// if video == nil {
// return "", "", errors.New("no VideoFeeds streams available")
// }
//
// videoURL := s.BuildStreamURL(video.URL)
//
// videoContent, err := http.FetchPlaylistContent(StripQuotes(videoURL))
// if err != nil {
// return "", "", err
// }
//
// var audioContent string
// if audio != nil {
// audioURL := s.BuildStreamURL(StripQuotes(audio.URL))
// audioContent, err = http.FetchPlaylistContent(StripQuotes(audioURL))
// if err != nil {
// return "", "", err
// }
// }
//
// return videoContent, audioContent, nil
//}
func (s *StreamSet) BuildSegmentURL(filename string) string {
return fmt.Sprintf("%s%s", HTTPSPrefix, s.Metadata.Domain+"/streams/"+s.Metadata.StreamID+"/a/5000/"+filename)
return fmt.Sprintf("%s%s", constants.HTTPSPrefix, s.Metadata.Domain+"/streams/"+s.Metadata.StreamID+"/a/5000/"+filename)
}
func ParseMediaPlaylist(content string) *MediaPlaylist {
func ParseMediaPlaylist(content string) *SegmentPlaylist {
lines := strings.Split(content, "\n")
var segments []Segment
playlist := &MediaPlaylist{
playlist := &SegmentPlaylist{
IsLive: true,
HasEndList: false,
}
@ -62,10 +62,10 @@ func ParseMediaPlaylist(content string) *MediaPlaylist {
for i, line := range lines {
switch {
case strings.HasPrefix(line, ExtXTargetDuration):
case strings.HasPrefix(line, constants.ExtXTargetDuration):
playlist.TargetDuration = parseTargetDuration(line)
case strings.HasPrefix(line, ExtInf):
case strings.HasPrefix(line, constants.ExtInf):
if i+1 < len(lines) {
duration := parseSegmentDuration(line)
segments = append(segments, Segment{
@ -76,17 +76,17 @@ func ParseMediaPlaylist(content string) *MediaPlaylist {
mediaSequence++
}
case strings.HasPrefix(line, ExtXPlaylistType):
if strings.Contains(line, PlaylistTypeVOD) {
case strings.HasPrefix(line, constants.ExtXPlaylistType):
if strings.Contains(line, constants.PlaylistTypeVOD) {
playlist.IsLive = false
}
case strings.HasPrefix(line, ExtXEndList):
case strings.HasPrefix(line, constants.ExtXEndList):
playlist.HasEndList = true
}
}
playlist.Segments = segments
playlist.SegmentList = segments
return playlist
}

22
pkg/media/stream.go Normal file
View File

@ -0,0 +1,22 @@
package media
import (
"fmt"
"m3u8-downloader/pkg/constants"
)
type StreamSet struct {
Metadata *PlaylistMetadata
Master *MasterPlaylist
}
func NewStreamSet(metadata *PlaylistMetadata, master *MasterPlaylist) *StreamSet {
return &StreamSet{
Metadata: metadata,
Master: master,
}
}
func (s *StreamSet) BuildPlaylistURL(url string) string {
return fmt.Sprintf("%s%s", constants.HTTPSPrefix, s.Metadata.Domain+"/streams/"+s.Metadata.StreamID+"/"+url)
}