Replace --list-chapters and --list-formats with --info, include additional infos, update README, and some minor fixes and improvements

This commit is contained in:
ChaoticByte 2025-03-09 19:46:44 +01:00
parent 6bbc319d1b
commit 5d490de7f1
No known key found for this signature in database
4 changed files with 114 additions and 106 deletions

View file

@ -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>

View file

@ -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)

View file

@ -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 {

View file

@ -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) {