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">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="Go" enabled="true" />
|
<component name="Go" enabled="true" />
|
||||||
<component name="NewModuleRootManager">
|
<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="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -6,6 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,6 +89,31 @@ func FetchPlaylistContent(url string) (string, error) {
|
|||||||
return string(data), nil
|
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) {
|
func FetchPlaylistContentWithContext(ctx context.Context, url string) (string, error) {
|
||||||
data, err := defaultClient.GetWithContext(ctx, url)
|
data, err := defaultClient.GetWithContext(ctx, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package media
|
package downloader
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@ -14,4 +14,5 @@ const (
|
|||||||
ClientDefaultTimeout = 30 * time.Second
|
ClientDefaultTimeout = 30 * time.Second
|
||||||
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0"
|
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||||
PlaylistTypeVOD = "VOD"
|
PlaylistTypeVOD = "VOD"
|
||||||
|
OutputDirPath = "./data"
|
||||||
)
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package media
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package media
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,7 +1,8 @@
|
|||||||
package media
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -44,6 +45,10 @@ func (s *StreamSet) FetchSegmentPlaylists() (videoPlaylist, audioPlaylist string
|
|||||||
return videoContent, audioContent, nil
|
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 {
|
func ParseMediaPlaylist(content string) *MediaPlaylist {
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
var segments []Segment
|
var segments []Segment
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package media
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -54,7 +54,7 @@ type AudioStream struct {
|
|||||||
|
|
||||||
func NewAudioStream(mediaInfo string) (*AudioStream, error) {
|
func NewAudioStream(mediaInfo string) (*AudioStream, error) {
|
||||||
if !strings.HasPrefix(mediaInfo, ExtXMedia) {
|
if !strings.HasPrefix(mediaInfo, ExtXMedia) {
|
||||||
return nil, errors.New("invalid media info line")
|
return nil, errors.New("invalid downloader info line")
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes := parseMediaAttributes(mediaInfo)
|
attributes := parseMediaAttributes(mediaInfo)
|
||||||
Loading…
x
Reference in New Issue
Block a user