Structured for download project and processing project. Responsibility split
This commit is contained in:
parent
f5da1941f6
commit
93b62618a1
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
data/
|
||||
scratch.txt
|
||||
4
.idea/m3u8-downloader.iml
generated
4
.idea/m3u8-downloader.iml
generated
@ -2,7 +2,9 @@
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/data" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
51
CLAUDE.md
Normal file
51
CLAUDE.md
Normal file
@ -0,0 +1,51 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Go-based M3U8 downloader that parses HLS (HTTP Live Streaming) playlists to extract video and audio stream metadata. The end goal of this project is to have a listening REST API take in m3u8 urls, parse them, and eventually send to a conversion service.
|
||||
|
||||
## Architecture
|
||||
|
||||
The project follows a clean separation of concerns:
|
||||
|
||||
- **main.go**: Entry point that demonstrates usage of the media package
|
||||
- **media/**: Core package containing M3U8 parsing logic
|
||||
- **types.go**: Contains the main parsing logic and data structures (`StreamSet`, `VideoURL`, `AudioURL`)
|
||||
- **utils.go**: Utility functions for parsing attributes and resolution calculations
|
||||
|
||||
The `GetStreamMetadata()` function is the main entry point that:
|
||||
1. Fetches the M3U8 master playlist via HTTP
|
||||
2. Parses the content line by line
|
||||
3. Extracts video streams (`#EXT-X-STREAM-INF`) and audio streams (`#EXT-X-MEDIA`)
|
||||
4. Returns a `StreamSet` containing all parsed metadata
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
```bash
|
||||
# Build the project
|
||||
go build -o m3u8-downloader
|
||||
|
||||
# Run the project
|
||||
go run main.go
|
||||
|
||||
# Run with module support
|
||||
go mod tidy
|
||||
|
||||
# Test the project (when tests are added)
|
||||
go test ./...
|
||||
|
||||
# Format code
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
- `StreamSet`: Root structure containing playlist URL and all streams
|
||||
- `VideoURL`: Represents video stream with bandwidth, codecs, resolution, frame rate
|
||||
- `AudioURL`: Represents audio stream with media type, group ID, name, and selection flags
|
||||
|
||||
## Error Handling
|
||||
|
||||
The current implementation uses `panic()` for error handling. When extending functionality, consider implementing proper error handling with returned error values following Go conventions.
|
||||
35
cmd/downloader/main.go
Normal file
35
cmd/downloader/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
downloader2 "m3u8-downloader/pkg/downloader"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234945/playlist_1752291107574_1752292056713.m3u8"
|
||||
|
||||
stream, err := downloader2.ParseMasterPlaylist(masterUrl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
audio, video, err := stream.FetchSegmentPlaylists()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
videoPlaylist := downloader2.ParseMediaPlaylist(video)
|
||||
audioPlaylist := downloader2.ParseMediaPlaylist(audio)
|
||||
|
||||
for _, segment := range videoPlaylist.Segments {
|
||||
fmt.Println(segment.URL)
|
||||
}
|
||||
|
||||
for _, segment := range audioPlaylist.Segments {
|
||||
err := downloader2.DownloadTSFile(stream.BuildSegmentURL(segment.URL), downloader2.OutputDirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
1
cmd/proc/main.go
Normal file
1
cmd/proc/main.go
Normal file
@ -0,0 +1 @@
|
||||
package proc
|
||||
27
main.go
27
main.go
@ -1,27 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"m3u8-downloader/media"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234945/playlist_1752291107574_1752292056713.m3u8"
|
||||
|
||||
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)
|
||||
|
||||
fmt.Println(videoPlaylist)
|
||||
fmt.Println(audioPlaylist)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -6,6 +6,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -86,6 +89,31 @@ func FetchPlaylistContent(url string) (string, error) {
|
||||
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 {
|
||||
@ -1,4 +1,4 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import "time"
|
||||
|
||||
@ -14,4 +14,5 @@ const (
|
||||
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"
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
@ -1,4 +1,4 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -1,7 +1,8 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -44,6 +45,10 @@ func (s *StreamSet) FetchSegmentPlaylists() (videoPlaylist, audioPlaylist string
|
||||
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)
|
||||
}
|
||||
|
||||
func ParseMediaPlaylist(content string) *MediaPlaylist {
|
||||
lines := strings.Split(content, "\n")
|
||||
var segments []Segment
|
||||
@ -1,4 +1,4 @@
|
||||
package media
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -54,7 +54,7 @@ type AudioStream struct {
|
||||
|
||||
func NewAudioStream(mediaInfo string) (*AudioStream, error) {
|
||||
if !strings.HasPrefix(mediaInfo, ExtXMedia) {
|
||||
return nil, errors.New("invalid media info line")
|
||||
return nil, errors.New("invalid downloader info line")
|
||||
}
|
||||
|
||||
attributes := parseMediaAttributes(mediaInfo)
|
||||
Loading…
x
Reference in New Issue
Block a user