Replace --list-chapters and --list-formats with --info, include additional infos, update README, and some minor fixes and improvements
This commit is contained in:
parent
6bbc319d1b
commit
5d490de7f1
4 changed files with 114 additions and 106 deletions
71
README.md
71
README.md
|
@ -11,10 +11,10 @@ Definetly not a commandline downloader for https://gronkh.tv risen from the dead
|
||||||
- Specify a start- and stop-timestamp to download only a portion of the video
|
- Specify a start- and stop-timestamp to download only a portion of the video
|
||||||
- Download a specific chapter
|
- Download a specific chapter
|
||||||
- Continuable Downloads
|
- Continuable Downloads
|
||||||
|
- Show infos about that Episode
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- You may get a "Windows Defender SmartScreen prevented an unrecognized app from starting" warning when running a new version for the first time
|
|
||||||
- Downloads are capped to 10 Mbyte/s and buffering is simulated to pre-empt IP blocking due to API ratelimiting
|
- Downloads are capped to 10 Mbyte/s and buffering is simulated to pre-empt IP blocking due to API ratelimiting
|
||||||
- Start- and stop-timestamps are not very accurate (± 8 seconds)
|
- Start- and stop-timestamps are not very accurate (± 8 seconds)
|
||||||
- Some videoplayers may have problems with the resulting file. To fix this, you can use ffmpeg to rewrite the video into a MKV-File: `ffmpeg -i video.ts -acodec copy -vcodec copy video.mkv`
|
- Some videoplayers may have problems with the resulting file. To fix this, you can use ffmpeg to rewrite the video into a MKV-File: `ffmpeg -i video.ts -acodec copy -vcodec copy video.mkv`
|
||||||
|
@ -36,70 +36,59 @@ Run `lurch-dl --help` to see available options.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
Download a video in its best available format (Windows):
|
Download a video in its best available format:
|
||||||
|
|
||||||
```
|
```
|
||||||
.\lurch-dl.exe --url https://gronkh.tv/streams/777
|
./lurch-dl.exe --url https://gronkh.tv/streams/777
|
||||||
|
|
||||||
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND SOLLTE VERBOTEN WERDEN!! ⭐ ️ 247 auf @GronkhTV ⭐ ️ !comic !archiv !a
|
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND ...
|
||||||
Format: 1080p60
|
Format: 1080p60
|
||||||
Downloaded 0.43% at 10.00 MB/s
|
Output: GTV0777, 2023-11-09 - DIESER STREAM IST [...].ts
|
||||||
...
|
|
||||||
|
Downloaded 0.32% at 10.00 MB/s ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Continue a download (Windows):
|
Continue a download:
|
||||||
|
|
||||||
```
|
```
|
||||||
.\lurch-dl.exe --url https://gronkh.tv/streams/777 --continue
|
./lurch-dl.exe --url https://gronkh.tv/streams/777 --continue
|
||||||
|
|
||||||
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND SOLLTE VERBOTEN WERDEN!! ⭐ ️ 247 auf @GronkhTV ⭐ ️ !comic !archiv !a
|
|
||||||
Format: 1080p60
|
|
||||||
Downloaded 0.68% at 10.00 MB/s
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
List all chapters (Windows):
|
|
||||||
|
|
||||||
```
|
|
||||||
.\lurch-dl.exe --url https://gronkh.tv/streams/777 --list-chapters
|
|
||||||
|
|
||||||
GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND SOLLTE VERBOTEN WERDEN!! ⭐ ️ 247 auf @GronkhTV ⭐ ️ !comic !archiv !a
|
|
||||||
|
|
||||||
Chapters:
|
|
||||||
1 0s Just Chatting
|
|
||||||
2 2h53m7s Alan Wake II
|
|
||||||
3 9h35m0s Just Chatting
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Download a specific chapter (Windows):
|
Download a specific chapter (Windows):
|
||||||
|
|
||||||
```
|
```
|
||||||
.\lurch-dl.exe --url https://gronkh.tv/streams/777 --chapter 2
|
./lurch-dl.exe --url https://gronkh.tv/streams/777 --chapter 2
|
||||||
|
|
||||||
GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND SOLLTE VERBOTEN WERDEN!! ⭐ ️ 247 auf @GronkhTV ⭐ ️ !comic !archiv !a
|
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND ...
|
||||||
Format: 1080p60
|
Format: 1080p60
|
||||||
Chapter: 2. Alan Wake II
|
Chapter: 2. Alan Wake II
|
||||||
|
Output: GTV0777 - 2. Alan Wake II.ts
|
||||||
|
|
||||||
Downloaded 3.22% at 10.00 MB/s
|
Downloaded 0.33% at 4.28 MB/s ...
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify a start- and stop-timestamp (Linux):
|
Specify a start- and stop-timestamp:
|
||||||
|
|
||||||
```
|
```
|
||||||
./lurch-dl --url https://gronkh.tv/streams/777 --start 5h6m41s --stop 5h6m58s
|
./lurch-dl --url https://gronkh.tv/streams/777 --start 5h6m41s --stop 5h6m58s
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
List all available formats for a video (Linux):
|
List all available formats for a video (Linux):
|
||||||
|
|
||||||
```
|
```
|
||||||
./lurch-dl --url https://gronkh.tv/streams/777 --list-formats
|
./lurch-dl --url https://gronkh.tv/streams/777 --info
|
||||||
|
|
||||||
Available formats:
|
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND ...
|
||||||
- 1080p60
|
Episode: 777
|
||||||
- 720p
|
Length: 9h48m55s
|
||||||
- 360p
|
Views: 45424
|
||||||
|
Timestamp: 2023-11-09T18:23:01Z
|
||||||
|
Tags: -
|
||||||
|
Formats: 1080p60, 720p, 360p
|
||||||
|
Chapters:
|
||||||
|
1 0s Just Chatting
|
||||||
|
2 2h53m7s Alan Wake II
|
||||||
|
3 9h35m0s Just Chatting
|
||||||
```
|
```
|
||||||
|
|
||||||
Download the video in a specific format (Linux):
|
Download the video in a specific format (Linux):
|
||||||
|
@ -107,17 +96,15 @@ Download the video in a specific format (Linux):
|
||||||
```
|
```
|
||||||
./lurch-dl --url https://gronkh.tv/streams/777 --format 720p
|
./lurch-dl --url https://gronkh.tv/streams/777 --format 720p
|
||||||
|
|
||||||
Title: GTV0777, 2023-11-09 - DIESER STREAM IST ILLEGAL UND SOLLTE VERBOTEN WERDEN!! ⭐ ️ 247 auf @GronkhTV ⭐ ️ !comic !archiv !a
|
[...]
|
||||||
Format: 720p
|
Format: 720p
|
||||||
Downloaded 0.32% at 10.00 MB/s
|
[...]
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify a filename (Windows):
|
Specify a filename (Windows):
|
||||||
|
|
||||||
```
|
```
|
||||||
.\lurch-dl.exe --url https://gronkh.tv/streams/777 --output Stream777.ts
|
./lurch-dl.exe --url https://gronkh.tv/streams/777 --output Stream777.ts
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
77
cli/cli.go
77
cli/cli.go
|
@ -48,7 +48,7 @@ type Arguments struct {
|
||||||
ContinueDl bool `json:"continue"`
|
ContinueDl bool `json:"continue"`
|
||||||
//
|
//
|
||||||
Help bool `json:"-"`
|
Help bool `json:"-"`
|
||||||
ListChapters bool `json:"-"`
|
VideoInfo bool `json:"-"`
|
||||||
ListFormats bool `json:"-"`
|
ListFormats bool `json:"-"`
|
||||||
UnparsedChapterNum int `json:"chapter_num"`
|
UnparsedChapterNum int `json:"chapter_num"`
|
||||||
// Parsed
|
// Parsed
|
||||||
|
@ -63,8 +63,7 @@ func CliShowHelp() {
|
||||||
fmt.Println(`
|
fmt.Println(`
|
||||||
lurch-dl --url string The url to the video
|
lurch-dl --url string The url to the video
|
||||||
[-h --help] Show this help and exit
|
[-h --help] Show this help and exit
|
||||||
[--list-chapters] List chapters and exit
|
[--info] Show video info (chapters, formats, length, ...)
|
||||||
[--list-formats] List available formats and exit
|
|
||||||
[--chapter int] The chapter you want to download
|
[--chapter int] The chapter you want to download
|
||||||
The calculated start and stop timestamps can be
|
The calculated start and stop timestamps can be
|
||||||
overwritten by --start and --stop
|
overwritten by --start and --stop
|
||||||
|
@ -91,8 +90,7 @@ func CliParseArguments() (Arguments, error) {
|
||||||
a := Arguments{}
|
a := Arguments{}
|
||||||
flag.BoolVar(&a.Help, "h", false, "")
|
flag.BoolVar(&a.Help, "h", false, "")
|
||||||
flag.BoolVar(&a.Help, "help", false, "")
|
flag.BoolVar(&a.Help, "help", false, "")
|
||||||
flag.BoolVar(&a.ListChapters, "list-chapters", false, "")
|
flag.BoolVar(&a.VideoInfo, "info", false, "")
|
||||||
flag.BoolVar(&a.ListFormats, "list-formats", false, "")
|
|
||||||
flag.StringVar(&a.Url, "url", "", "")
|
flag.StringVar(&a.Url, "url", "", "")
|
||||||
flag.IntVar(&a.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream
|
flag.IntVar(&a.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream
|
||||||
flag.StringVar(&a.FormatName, "format", "auto", "")
|
flag.StringVar(&a.FormatName, "format", "auto", "")
|
||||||
|
@ -107,8 +105,8 @@ func CliParseArguments() (Arguments, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, err
|
return a, err
|
||||||
}
|
}
|
||||||
if a.Video.Class != "streams" {
|
if a.Video.Category != "streams" {
|
||||||
return a, errors.New("video category '" + a.Video.Class + "' not supported")
|
return a, errors.New("video category '" + a.Video.Category + "' not supported")
|
||||||
}
|
}
|
||||||
if a.TimestampStart == "" {
|
if a.TimestampStart == "" {
|
||||||
a.StartDuration = -1
|
a.StartDuration = -1
|
||||||
|
@ -166,7 +164,7 @@ func CliRun() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
fmt.Println(streamEp.Title)
|
fmt.Printf("Title: %s\n", streamEp.Title)
|
||||||
// Check and list chapters/formats and exit
|
// Check and list chapters/formats and exit
|
||||||
if args.ChapterIdx >= 0 {
|
if args.ChapterIdx >= 0 {
|
||||||
if args.ChapterIdx >= len(streamEp.Chapters) {
|
if args.ChapterIdx >= len(streamEp.Chapters) {
|
||||||
|
@ -175,15 +173,26 @@ func CliRun() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if args.ListChapters || args.ListFormats {
|
if args.VideoInfo {
|
||||||
if args.ListChapters {
|
fmt.Printf("Episode: %s\n", streamEp.Episode)
|
||||||
fmt.Print("\n")
|
fmt.Printf("Length: %s\n", streamEp.Length)
|
||||||
CliAvailableChapters(streamEp.Chapters)
|
fmt.Printf("Views: %d\n", streamEp.Views)
|
||||||
|
fmt.Printf("Timestamp: %s\n", streamEp.Timestamp)
|
||||||
|
if len(streamEp.Tags) > 0 {
|
||||||
|
fmt.Print("Tags: ")
|
||||||
|
for i, t := range streamEp.Tags {
|
||||||
|
if i == 0 {
|
||||||
|
fmt.Print(t.Title)
|
||||||
|
} else {
|
||||||
|
fmt.Print(", ", t.Title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if args.ListFormats {
|
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Tags: -")
|
||||||
|
}
|
||||||
CliAvailableFormats(streamEp.Formats)
|
CliAvailableFormats(streamEp.Formats)
|
||||||
}
|
CliAvailableChapters(streamEp.Chapters)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
format, err := streamEp.GetFormatByName(args.FormatName)
|
format, err := streamEp.GetFormatByName(args.FormatName)
|
||||||
|
@ -192,21 +201,22 @@ func CliRun() int {
|
||||||
CliAvailableFormats(streamEp.Formats)
|
CliAvailableFormats(streamEp.Formats)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
cli.InfoMessage(fmt.Sprintf("Format: %v", format.Name))
|
fmt.Printf("Format: %v\n", format.Name)
|
||||||
// chapter
|
// chapter
|
||||||
targetChapter := core.Chapter{Index: -1} // set Index to -1 for noop
|
targetChapter := core.Chapter{Index: -1} // set Index to -1 for noop
|
||||||
if len(streamEp.Chapters) > 0 && args.ChapterIdx >= 0 {
|
if len(streamEp.Chapters) > 0 && args.ChapterIdx >= 0 {
|
||||||
targetChapter = streamEp.Chapters[args.ChapterIdx]
|
targetChapter = streamEp.Chapters[args.ChapterIdx]
|
||||||
cli.InfoMessage(fmt.Sprintf("Chapter: %v. %v", args.UnparsedChapterNum, targetChapter.Title))
|
fmt.Printf("Chapter: %v. %v\n", args.UnparsedChapterNum, targetChapter.Title)
|
||||||
}
|
}
|
||||||
// We already set the output file correctly so we can output it
|
// We already set the output file correctly so we can output it
|
||||||
if args.OutputFile == "" {
|
if args.OutputFile == "" {
|
||||||
args.OutputFile = streamEp.GetProposedFilename(args.ChapterIdx)
|
args.OutputFile = streamEp.GetProposedFilename(args.ChapterIdx)
|
||||||
}
|
}
|
||||||
// Start Download
|
// Start Download
|
||||||
cli.InfoMessage(fmt.Sprintf("Output: %v", args.OutputFile))
|
fmt.Printf("Output: %v\n", args.OutputFile)
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
successful := false
|
successful := false
|
||||||
|
aborted := false
|
||||||
for p := range api.DownloadEpisode(
|
for p := range api.DownloadEpisode(
|
||||||
streamEp,
|
streamEp,
|
||||||
targetChapter,
|
targetChapter,
|
||||||
|
@ -218,18 +228,24 @@ func CliRun() int {
|
||||||
args.StopDuration,
|
args.StopDuration,
|
||||||
args.Ratelimit,
|
args.Ratelimit,
|
||||||
make(chan os.Signal, 1),
|
make(chan os.Signal, 1),
|
||||||
) {
|
) { // Iterate over download progress
|
||||||
if p.Error != nil {
|
if p.Error != nil {
|
||||||
cli.ErrorMessage(p.Error)
|
cli.ErrorMessage(p.Error)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if p.Success { successful = true }
|
if p.Success {
|
||||||
if p.Aborted { cli.Aborted() } else {
|
successful = true
|
||||||
|
} else if p.Aborted {
|
||||||
|
aborted = true
|
||||||
|
} else {
|
||||||
cli.DownloadProgress(p.Progress, p.Rate, p.Delaying, p.Waiting, p.Retries, p.Title)
|
cli.DownloadProgress(p.Progress, p.Rate, p.Delaying, p.Waiting, p.Retries, p.Title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
if !successful {
|
if aborted {
|
||||||
|
fmt.Print("\nAborted. ")
|
||||||
|
return 130
|
||||||
|
} else if !successful {
|
||||||
cli.ErrorMessage(errors.New("download failed"))
|
cli.ErrorMessage(errors.New("download failed"))
|
||||||
return 1
|
return 1
|
||||||
} else { return 0 }
|
} else { return 0 }
|
||||||
|
@ -243,11 +259,16 @@ func CliAvailableChapters(chapters []core.Chapter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CliAvailableFormats(formats []core.VideoFormat) {
|
func CliAvailableFormats(formats []core.VideoFormat) {
|
||||||
fmt.Println("Available formats:")
|
fmt.Print("Formats: ")
|
||||||
for _, f := range formats {
|
for i, f := range formats {
|
||||||
fmt.Println(" - " + f.Name)
|
if i == 0 {
|
||||||
|
fmt.Print(f.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Print(", ", f.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fmt.Print("\n")
|
||||||
|
}
|
||||||
|
|
||||||
type Cli struct{}
|
type Cli struct{}
|
||||||
|
|
||||||
|
@ -270,14 +291,6 @@ func (cli *Cli) DownloadProgress(progress float32, rate float64, delaying bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Cli) Aborted() {
|
|
||||||
fmt.Print("\nAborted. ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Cli) InfoMessage(msg string) {
|
|
||||||
fmt.Println(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Cli) ErrorMessage(err error) {
|
func (cli *Cli) ErrorMessage(err error) {
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
fmt.Println("An error occured:", err)
|
fmt.Println("An error occured:", err)
|
||||||
|
|
|
@ -64,6 +64,8 @@ func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) {
|
||||||
// Title
|
// Title
|
||||||
json.Unmarshal(info_data, &ep)
|
json.Unmarshal(info_data, &ep)
|
||||||
ep.Title = strings.ToValidUTF8(ep.Title, "")
|
ep.Title = strings.ToValidUTF8(ep.Title, "")
|
||||||
|
// Length
|
||||||
|
ep.Length = ep.Length * time.Second
|
||||||
// Sort Chapters, correct offset and set index
|
// Sort Chapters, correct offset and set index
|
||||||
sort.Slice(ep.Chapters, func(i int, j int) bool {
|
sort.Slice(ep.Chapters, func(i int, j int) bool {
|
||||||
return ep.Chapters[i].Offset < ep.Chapters[j].Offset
|
return ep.Chapters[i].Offset < ep.Chapters[j].Offset
|
||||||
|
@ -194,7 +196,7 @@ func (api *GtvApi) DownloadEpisode(
|
||||||
// Handle Interrupts
|
// Handle Interrupts
|
||||||
<-interruptChan
|
<-interruptChan
|
||||||
keyboardInterrupt = true
|
keyboardInterrupt = true
|
||||||
yield(DownloadProgress{Progress: progress, Rate: actualRate, Retries: 0, Title: ep.Title})
|
yield(DownloadProgress{Aborted: true, Progress: progress, Rate: actualRate, Retries: 0, Title: ep.Title})
|
||||||
}()
|
}()
|
||||||
for i, chunk := range chunklist.Chunks {
|
for i, chunk := range chunklist.Chunks {
|
||||||
if i < nextChunk {
|
if i < nextChunk {
|
||||||
|
|
|
@ -13,8 +13,26 @@ var videoUrlRegex = regexp.MustCompile(`gronkh\.tv\/([a-z]+)\/([0-9]+)`)
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
|
type VideoFormat struct {
|
||||||
|
Name string `json:"format"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chapter struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Offset time.Duration `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VideoTag struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
type GtvVideo struct {
|
type GtvVideo struct {
|
||||||
Class string `json:"class"`
|
Category string `json:"category"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,20 +42,13 @@ func ParseGtvVideoUrl(url string) (GtvVideo, error) {
|
||||||
if len(match) < 2 {
|
if len(match) < 2 {
|
||||||
return video, errors.New("Could not parse URL " + url)
|
return video, errors.New("Could not parse URL " + url)
|
||||||
}
|
}
|
||||||
video.Class = match[1]
|
video.Category = match[1]
|
||||||
video.Id = match[2]
|
video.Id = match[2]
|
||||||
return video, nil
|
return video, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
type VideoFormat struct {
|
|
||||||
Name string `json:"format"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
type ChunkList struct {
|
type ChunkList struct {
|
||||||
BaseUrl string
|
BaseUrl string
|
||||||
Chunks []string
|
Chunks []string
|
||||||
|
@ -65,21 +76,16 @@ func (cl *ChunkList) Cut(from time.Duration, to time.Duration) ChunkList {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
type Chapter struct {
|
|
||||||
Index int `json:"index"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Offset time.Duration `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
type StreamEpisode struct {
|
type StreamEpisode struct {
|
||||||
Episode string `json:"episode"`
|
Episode string `json:"episode"`
|
||||||
Formats []VideoFormat `json:"formats"`
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
// ProposedFilename string `json:"proposed_filename"`
|
Formats []VideoFormat `json:"formats"`
|
||||||
PlaylistUrl string `json:"playlist_url"`
|
|
||||||
Chapters []Chapter `json:"chapters"`
|
Chapters []Chapter `json:"chapters"`
|
||||||
|
PlaylistUrl string `json:"playlist_url"`
|
||||||
|
Length time.Duration `json:"source_length"`
|
||||||
|
Views int `json:"views"`
|
||||||
|
Timestamp string `json:"created_at"`
|
||||||
|
Tags []VideoTag `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error) {
|
func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue