From b37a312b184c885f30070b9c1b5be8a78fe32328 Mon Sep 17 00:00:00 2001 From: kacarmichael Date: Sun, 20 Jul 2025 23:44:49 -0500 Subject: [PATCH] Abstracted to service.go --- cmd/downloader/main.go | 65 +++++++++++-- pkg/constants/downloader.go | 5 + pkg/constants/http.go | 7 ++ pkg/constants/media.go | 11 +++ pkg/downloader/client.go | 131 ------------------------- pkg/downloader/constants.go | 18 ---- pkg/downloader/playlist.go | 71 -------------- pkg/downloader/service.go | 70 ++++++++++++++ pkg/downloader/stream.go | 98 ------------------- pkg/http/client.go | 64 ++++++++++++ pkg/http/utils.go | 10 ++ pkg/media/master_playlist.go | 41 ++++++++ pkg/media/media_playlist.go | 139 +++++++++++++++++++++++++++ pkg/{downloader => media}/parser.go | 31 +----- pkg/{downloader => media}/segment.go | 74 +++++++------- pkg/media/stream.go | 22 +++++ 16 files changed, 466 insertions(+), 391 deletions(-) create mode 100644 pkg/constants/downloader.go create mode 100644 pkg/constants/http.go create mode 100644 pkg/constants/media.go delete mode 100644 pkg/downloader/client.go delete mode 100644 pkg/downloader/constants.go delete mode 100644 pkg/downloader/playlist.go create mode 100644 pkg/downloader/service.go delete mode 100644 pkg/downloader/stream.go create mode 100644 pkg/http/client.go create mode 100644 pkg/http/utils.go create mode 100644 pkg/media/master_playlist.go create mode 100644 pkg/media/media_playlist.go rename pkg/{downloader => media}/parser.go (66%) rename pkg/{downloader => media}/segment.go (50%) create mode 100644 pkg/media/stream.go diff --git a/cmd/downloader/main.go b/cmd/downloader/main.go index 1e0b7cf..ea1e000 100644 --- a/cmd/downloader/main.go +++ b/cmd/downloader/main.go @@ -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 + // } + //} } diff --git a/pkg/constants/downloader.go b/pkg/constants/downloader.go new file mode 100644 index 0000000..0fa6f5b --- /dev/null +++ b/pkg/constants/downloader.go @@ -0,0 +1,5 @@ +package constants + +const ( + OutputDirPath = "./data" +) diff --git a/pkg/constants/http.go b/pkg/constants/http.go new file mode 100644 index 0000000..e0ac345 --- /dev/null +++ b/pkg/constants/http.go @@ -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://" +) diff --git a/pkg/constants/media.go b/pkg/constants/media.go new file mode 100644 index 0000000..48b971d --- /dev/null +++ b/pkg/constants/media.go @@ -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" +) diff --git a/pkg/downloader/client.go b/pkg/downloader/client.go deleted file mode 100644 index 57b914d..0000000 --- a/pkg/downloader/client.go +++ /dev/null @@ -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) -} diff --git a/pkg/downloader/constants.go b/pkg/downloader/constants.go deleted file mode 100644 index ee5378d..0000000 --- a/pkg/downloader/constants.go +++ /dev/null @@ -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" -) diff --git a/pkg/downloader/playlist.go b/pkg/downloader/playlist.go deleted file mode 100644 index 3441f7e..0000000 --- a/pkg/downloader/playlist.go +++ /dev/null @@ -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 -} diff --git a/pkg/downloader/service.go b/pkg/downloader/service.go new file mode 100644 index 0000000..78e332f --- /dev/null +++ b/pkg/downloader/service.go @@ -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 +} diff --git a/pkg/downloader/stream.go b/pkg/downloader/stream.go deleted file mode 100644 index 56d97bd..0000000 --- a/pkg/downloader/stream.go +++ /dev/null @@ -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 -} diff --git a/pkg/http/client.go b/pkg/http/client.go new file mode 100644 index 0000000..d805762 --- /dev/null +++ b/pkg/http/client.go @@ -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, + }, +} diff --git a/pkg/http/utils.go b/pkg/http/utils.go new file mode 100644 index 0000000..0c63223 --- /dev/null +++ b/pkg/http/utils.go @@ -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) +} diff --git a/pkg/media/master_playlist.go b/pkg/media/master_playlist.go new file mode 100644 index 0000000..0c6dc57 --- /dev/null +++ b/pkg/media/master_playlist.go @@ -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 +} diff --git a/pkg/media/media_playlist.go b/pkg/media/media_playlist.go new file mode 100644 index 0000000..79e532e --- /dev/null +++ b/pkg/media/media_playlist.go @@ -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 +} diff --git a/pkg/downloader/parser.go b/pkg/media/parser.go similarity index 66% rename from pkg/downloader/parser.go rename to pkg/media/parser.go index 7a81c85..203a0c1 100644 --- a/pkg/downloader/parser.go +++ b/pkg/media/parser.go @@ -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://") -} diff --git a/pkg/downloader/segment.go b/pkg/media/segment.go similarity index 50% rename from pkg/downloader/segment.go rename to pkg/media/segment.go index e3197ca..4c6af2a 100644 --- a/pkg/downloader/segment.go +++ b/pkg/media/segment.go @@ -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 } diff --git a/pkg/media/stream.go b/pkg/media/stream.go new file mode 100644 index 0000000..b5a21ad --- /dev/null +++ b/pkg/media/stream.go @@ -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) +}