From 424e912f6c4e6579c27788e7beb37edcfc72ef36 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Fri, 14 Mar 2025 19:47:23 +0100 Subject: [PATCH 1/3] Remove the usage of custom types where it isn't required --- cli/cli.go | 128 ++++++++++++++++++++++------------------------ core/gtv_api.go | 22 +++++--- core/interface.go | 13 ----- 3 files changed, 77 insertions(+), 86 deletions(-) delete mode 100644 core/interface.go diff --git a/cli/cli.go b/cli/cli.go index eb0b1d8..ef1f7c5 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -38,7 +38,7 @@ func XtermSetTitle(title string) { // Commandline -type Arguments struct { +var Arguments struct { Url string `json:"url"` FormatName string `json:"format_name"` OutputFile string `json:"output_file"` @@ -84,96 +84,92 @@ lurch-dl --url string The url to the video Version: ` + core.Version) } -func CliParseArguments() (Arguments, error) { +func CliParseArguments() error { var err error var ratelimitMbs float64 - a := Arguments{} - flag.BoolVar(&a.Help, "h", false, "") - flag.BoolVar(&a.Help, "help", false, "") - flag.BoolVar(&a.VideoInfo, "info", false, "") - flag.StringVar(&a.Url, "url", "", "") - flag.IntVar(&a.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream - flag.StringVar(&a.FormatName, "format", "auto", "") - flag.StringVar(&a.OutputFile, "output", "", "") - flag.StringVar(&a.TimestampStart, "start", "", "") - flag.StringVar(&a.TimestampStop, "stop", "", "") - flag.BoolVar(&a.Overwrite, "overwrite", false, "") - flag.BoolVar(&a.ContinueDl, "continue", false, "") + flag.BoolVar(&Arguments.Help, "h", false, "") + flag.BoolVar(&Arguments.Help, "help", false, "") + flag.BoolVar(&Arguments.VideoInfo, "info", false, "") + flag.StringVar(&Arguments.Url, "url", "", "") + flag.IntVar(&Arguments.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream + flag.StringVar(&Arguments.FormatName, "format", "auto", "") + flag.StringVar(&Arguments.OutputFile, "output", "", "") + flag.StringVar(&Arguments.TimestampStart, "start", "", "") + flag.StringVar(&Arguments.TimestampStop, "stop", "", "") + flag.BoolVar(&Arguments.Overwrite, "overwrite", false, "") + flag.BoolVar(&Arguments.ContinueDl, "continue", false, "") flag.Float64Var(&ratelimitMbs, "max-rate", 10.0, "") flag.Parse() - a.Video, err = core.ParseGtvVideoUrl(a.Url) + Arguments.Video, err = core.ParseGtvVideoUrl(Arguments.Url) if err != nil { - return a, err + return err } - if a.Video.Category != "streams" { - return a, errors.New("video category '" + a.Video.Category + "' not supported") + if Arguments.Video.Category != "streams" { + return errors.New("video category '" + Arguments.Video.Category + "' not supported") } - if a.TimestampStart == "" { - a.StartDuration = -1 + if Arguments.TimestampStart == "" { + Arguments.StartDuration = -1 } else { - a.StartDuration, err = time.ParseDuration(a.TimestampStart) + Arguments.StartDuration, err = time.ParseDuration(Arguments.TimestampStart) if err != nil { - return a, err + return err } } - if a.TimestampStop == "" { - a.StopDuration = -1 + if Arguments.TimestampStop == "" { + Arguments.StopDuration = -1 } else { - a.StopDuration, err = time.ParseDuration(a.TimestampStop) + Arguments.StopDuration, err = time.ParseDuration(Arguments.TimestampStop) if err != nil { - return a, err + return err } } - a.ChapterIdx = a.UnparsedChapterNum - 1 - a.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s - if a.Ratelimit <= 0 { - return a, errors.New("the value of --max-rate must be greater than 0") + Arguments.ChapterIdx = Arguments.UnparsedChapterNum - 1 + Arguments.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s + if Arguments.Ratelimit <= 0 { + return errors.New("the value of --max-rate must be greater than 0") } - return a, err + return err } // Main func CliRun() int { - cli := Cli{} defer fmt.Print("\n") // cli arguments & help text flag.Usage = CliShowHelp - args, err := CliParseArguments() - if args.Help { + err := CliParseArguments() + if Arguments.Help { CliShowHelp() return 0 - } else if args.Url == "" || err != nil { + } else if Arguments.Url == "" || err != nil { CliShowHelp() if err != nil { - cli.ErrorMessage(err) + CliErrorMessage(err) } return 1 } // detect terminal features XtermDetectFeatures() - // - api := core.GtvApi{}; // Get video metadata if CliXtermTitle { XtermSetTitle("lurch-dl - Fetching video metadata ...") } - streamEp, err := api.GetStreamEpisode(args.Video.Id) + streamEp, err := core.GetStreamEpisode(Arguments.Video.Id) if err != nil { - cli.ErrorMessage(err) + CliErrorMessage(err) return 1 } fmt.Print("\n") fmt.Printf("Title: %s\n", streamEp.Title) // Check and list chapters/formats and exit - if args.ChapterIdx >= 0 { - if args.ChapterIdx >= len(streamEp.Chapters) { - cli.ErrorMessage(&core.ChapterNotFoundError{ChapterNum: args.UnparsedChapterNum}) + if Arguments.ChapterIdx >= 0 { + if Arguments.ChapterIdx >= len(streamEp.Chapters) { + CliErrorMessage(&core.ChapterNotFoundError{ChapterNum: Arguments.UnparsedChapterNum}) CliAvailableChapters(streamEp.Chapters) return 1 } } - if args.VideoInfo { + if Arguments.VideoInfo { fmt.Printf("Episode: %s\n", streamEp.Episode) fmt.Printf("Length: %s\n", streamEp.Length) fmt.Printf("Views: %d\n", streamEp.Views) @@ -195,42 +191,42 @@ func CliRun() int { CliAvailableChapters(streamEp.Chapters) return 0 } - format, err := streamEp.GetFormatByName(args.FormatName) + format, err := streamEp.GetFormatByName(Arguments.FormatName) if err != nil { - cli.ErrorMessage(err) + CliErrorMessage(err) CliAvailableFormats(streamEp.Formats) return 1 } fmt.Printf("Format: %v\n", format.Name) // chapter targetChapter := core.Chapter{Index: -1} // set Index to -1 for noop - if len(streamEp.Chapters) > 0 && args.ChapterIdx >= 0 { - targetChapter = streamEp.Chapters[args.ChapterIdx] - fmt.Printf("Chapter: %v. %v\n", args.UnparsedChapterNum, targetChapter.Title) + if len(streamEp.Chapters) > 0 && Arguments.ChapterIdx >= 0 { + targetChapter = streamEp.Chapters[Arguments.ChapterIdx] + fmt.Printf("Chapter: %v. %v\n", Arguments.UnparsedChapterNum, targetChapter.Title) } // We already set the output file correctly so we can output it - if args.OutputFile == "" { - args.OutputFile = streamEp.GetProposedFilename(args.ChapterIdx) + if Arguments.OutputFile == "" { + Arguments.OutputFile = streamEp.GetProposedFilename(Arguments.ChapterIdx) } // Start Download - fmt.Printf("Output: %v\n", args.OutputFile) + fmt.Printf("Output: %v\n", Arguments.OutputFile) fmt.Print("\n") successful := false aborted := false - for p := range api.DownloadEpisode( + for p := range core.DownloadEpisode( streamEp, targetChapter, - args.FormatName, - args.OutputFile, - args.Overwrite, - args.ContinueDl, - args.StartDuration, - args.StopDuration, - args.Ratelimit, + Arguments.FormatName, + Arguments.OutputFile, + Arguments.Overwrite, + Arguments.ContinueDl, + Arguments.StartDuration, + Arguments.StopDuration, + Arguments.Ratelimit, make(chan os.Signal, 1), ) { // Iterate over download progress if p.Error != nil { - cli.ErrorMessage(p.Error) + CliErrorMessage(p.Error) return 1 } if p.Success { @@ -238,7 +234,7 @@ func CliRun() int { } else if p.Aborted { aborted = true } else { - cli.DownloadProgress(p.Progress, p.Rate, p.Delaying, p.Waiting, p.Retries, p.Title) + CliDownloadProgress(p.Progress, p.Rate, p.Delaying, p.Waiting, p.Retries, p.Title) } } fmt.Print("\n") @@ -246,7 +242,7 @@ func CliRun() int { fmt.Print("\nAborted. ") return 130 } else if !successful { - cli.ErrorMessage(errors.New("download failed")) + CliErrorMessage(errors.New("download failed")) return 1 } else { return 0 } } @@ -270,9 +266,7 @@ func CliAvailableFormats(formats []core.VideoFormat) { fmt.Print("\n") } -type Cli struct{} - -func (cli *Cli) DownloadProgress(progress float32, rate float64, delaying bool, waiting bool, retries int, title string) { +func CliDownloadProgress(progress float32, rate float64, delaying bool, waiting bool, retries int, title string) { if retries > 0 { if retries == 1 { fmt.Print("\n") @@ -291,7 +285,7 @@ func (cli *Cli) DownloadProgress(progress float32, rate float64, delaying bool, } } -func (cli *Cli) ErrorMessage(err error) { +func CliErrorMessage(err error) { fmt.Print("\n") fmt.Println("An error occured:", err) } diff --git a/core/gtv_api.go b/core/gtv_api.go index b77a1d4..471a561 100644 --- a/core/gtv_api.go +++ b/core/gtv_api.go @@ -25,6 +25,18 @@ const RatelimitDelayAfter = 5.0 // in Seconds; Delay the next chunk download aft const ApiBaseurlStreamEpisodeInfo = "https://api.gronkh.tv/v1/video/info?episode=%s" const ApiBaseurlStreamEpisodePlInfo = "https://api.gronkh.tv/v1/video/playlist?episode=%s" +type DownloadProgress struct { + Aborted bool + Error error + Success bool + Delaying bool + Progress float32 + Rate float64 + Retries int + Title string + Waiting bool +} + var ApiHeadersBase = http.Header{ "User-Agent": {"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"}, "Accept-Language": {"de,en-US;q=0.7,en;q=0.3"}, @@ -48,9 +60,7 @@ var ApiHeadersVideoAdditional = http.Header{ "Accept": {"*/*"}, } -type GtvApi struct{} - -func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) { +func GetStreamEpisode(episode string) (StreamEpisode, error) { ep := StreamEpisode{} ep.Episode = episode info_data, err := httpGet( @@ -93,7 +103,7 @@ func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) { return ep, err } -func (api *GtvApi) GetStreamChunkList(video VideoFormat) (ChunkList, error) { +func GetStreamChunkList(video VideoFormat) (ChunkList, error) { baseUrl := video.Url[:strings.LastIndex(video.Url, "/")] data, err := httpGet(video.Url, []http.Header{ApiHeadersBase, ApiHeadersMetaAdditional}, time.Second*10) if err != nil { @@ -103,7 +113,7 @@ func (api *GtvApi) GetStreamChunkList(video VideoFormat) (ChunkList, error) { return chunklist, err } -func (api *GtvApi) DownloadEpisode( +func DownloadEpisode( ep StreamEpisode, chapter Chapter, formatName string, @@ -181,7 +191,7 @@ func (api *GtvApi) DownloadEpisode( } // download format, _ := ep.GetFormatByName(formatName) // we don't have to check the error, as it was already checked by CliRun() - chunklist, err := api.GetStreamChunkList(format) + chunklist, err := GetStreamChunkList(format) chunklist = chunklist.Cut(startDuration, stopDuration) if err != nil { yield(DownloadProgress{Error: err}) diff --git a/core/interface.go b/core/interface.go deleted file mode 100644 index 26bb1a2..0000000 --- a/core/interface.go +++ /dev/null @@ -1,13 +0,0 @@ -package core - -type DownloadProgress struct { - Aborted bool - Error error - Success bool - Delaying bool - Progress float32 - Rate float64 - Retries int - Title string - Waiting bool -} From ee3518ab5c268f32bad62911295f44f3cd7a207d Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Fri, 14 Mar 2025 23:20:47 +0100 Subject: [PATCH 2/3] Refactoring, added remaining custom errors --- cli/cli.go | 39 +++++++++++++++------------------------ cli/errors.go | 15 +++++++++++++++ core/errors.go | 28 +++++++++++++++++++++++++--- core/gtv_api.go | 7 +++---- core/gtv_common.go | 24 +++++++++++++++++++----- 5 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 cli/errors.go diff --git a/cli/cli.go b/cli/cli.go index ef1f7c5..5030f27 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -3,7 +3,6 @@ package main import ( - "errors" "flag" "fmt" "os" @@ -50,12 +49,11 @@ var Arguments struct { Help bool `json:"-"` VideoInfo bool `json:"-"` ListFormats bool `json:"-"` - UnparsedChapterNum int `json:"chapter_num"` + ChapterNum int `json:"chapter_num"` // Parsed Video core.GtvVideo `json:"-"` StartDuration time.Duration `json:"-"` StopDuration time.Duration `json:"-"` - ChapterIdx int `json:"-"` Ratelimit float64 `json:"-"` } @@ -91,7 +89,7 @@ func CliParseArguments() error { flag.BoolVar(&Arguments.Help, "help", false, "") flag.BoolVar(&Arguments.VideoInfo, "info", false, "") flag.StringVar(&Arguments.Url, "url", "", "") - flag.IntVar(&Arguments.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream + flag.IntVar(&Arguments.ChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream flag.StringVar(&Arguments.FormatName, "format", "auto", "") flag.StringVar(&Arguments.OutputFile, "output", "", "") flag.StringVar(&Arguments.TimestampStart, "start", "", "") @@ -104,9 +102,6 @@ func CliParseArguments() error { if err != nil { return err } - if Arguments.Video.Category != "streams" { - return errors.New("video category '" + Arguments.Video.Category + "' not supported") - } if Arguments.TimestampStart == "" { Arguments.StartDuration = -1 } else { @@ -123,10 +118,9 @@ func CliParseArguments() error { return err } } - Arguments.ChapterIdx = Arguments.UnparsedChapterNum - 1 Arguments.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s if Arguments.Ratelimit <= 0 { - return errors.New("the value of --max-rate must be greater than 0") + return &GenericCliAgumentError{Msg: "the value of --max-rate must be greater than 0"} } return err } @@ -162,13 +156,16 @@ func CliRun() int { fmt.Print("\n") fmt.Printf("Title: %s\n", streamEp.Title) // Check and list chapters/formats and exit - if Arguments.ChapterIdx >= 0 { - if Arguments.ChapterIdx >= len(streamEp.Chapters) { - CliErrorMessage(&core.ChapterNotFoundError{ChapterNum: Arguments.UnparsedChapterNum}) - CliAvailableChapters(streamEp.Chapters) - return 1 - } + targetChapter, err := streamEp.GetChapterByNumber(Arguments.ChapterNum) + if err != nil { + CliErrorMessage(err) + CliAvailableChapters(streamEp.Chapters) + return 1 } + if Arguments.ChapterNum > 0 && len(streamEp.Chapters) > 0 { + fmt.Printf("Chapter: %v. %v\n", Arguments.ChapterNum, targetChapter.Title) + } + // Video Info if Arguments.VideoInfo { fmt.Printf("Episode: %s\n", streamEp.Episode) fmt.Printf("Length: %s\n", streamEp.Length) @@ -198,22 +195,16 @@ func CliRun() int { return 1 } fmt.Printf("Format: %v\n", format.Name) - // chapter - targetChapter := core.Chapter{Index: -1} // set Index to -1 for noop - if len(streamEp.Chapters) > 0 && Arguments.ChapterIdx >= 0 { - targetChapter = streamEp.Chapters[Arguments.ChapterIdx] - fmt.Printf("Chapter: %v. %v\n", Arguments.UnparsedChapterNum, targetChapter.Title) - } // We already set the output file correctly so we can output it if Arguments.OutputFile == "" { - Arguments.OutputFile = streamEp.GetProposedFilename(Arguments.ChapterIdx) + Arguments.OutputFile = streamEp.GetProposedFilename(targetChapter) } // Start Download fmt.Printf("Output: %v\n", Arguments.OutputFile) fmt.Print("\n") successful := false aborted := false - for p := range core.DownloadEpisode( + for p := range core.DownloadStreamEpisode( streamEp, targetChapter, Arguments.FormatName, @@ -242,7 +233,7 @@ func CliRun() int { fmt.Print("\nAborted. ") return 130 } else if !successful { - CliErrorMessage(errors.New("download failed")) + CliErrorMessage(&GenericDownloadError{}) return 1 } else { return 0 } } diff --git a/cli/errors.go b/cli/errors.go new file mode 100644 index 0000000..941ac6f --- /dev/null +++ b/cli/errors.go @@ -0,0 +1,15 @@ +package main + +type GenericCliAgumentError struct { + Msg string +} + +func (err *GenericCliAgumentError) Error() string { + return err.Msg +} + +type GenericDownloadError struct {} + +func (err *GenericDownloadError) Error() string { + return "download failed" +} diff --git a/core/errors.go b/core/errors.go index b9e5240..38ce24d 100644 --- a/core/errors.go +++ b/core/errors.go @@ -35,7 +35,7 @@ type FileExistsError struct { } func (err *FileExistsError) Error() string { - return "File '" + err.Filename + "' already exists. See the available options on how to proceed." + return "file '" + err.Filename + "' already exists - see the available options on how to proceed" } type FormatNotFoundError struct { @@ -43,7 +43,7 @@ type FormatNotFoundError struct { } func (err *FormatNotFoundError) Error() string { - return "Format " + err.FormatName + " is not available." + return "format " + err.FormatName + " is not available" } type ChapterNotFoundError struct { @@ -51,5 +51,27 @@ type ChapterNotFoundError struct { } func (err *ChapterNotFoundError) Error() string { - return fmt.Sprintf("Chapter %v not found.", err.ChapterNum) + return fmt.Sprintf("chapter %v not found", err.ChapterNum) +} + +type VideoCategoryUnsupportedError struct { + Category string +} + +func (err *VideoCategoryUnsupportedError) Error() string { + return fmt.Sprintf("video category '%v' not supported", err.Category) +} + +type GtvVideoUrlParseError struct { + Url string +} + +func (err *GtvVideoUrlParseError) Error() string { + return fmt.Sprintf("Could not parse URL %v", err.Url) +} + +type DownloadInfoFileReadError struct {} + +func (err *DownloadInfoFileReadError) Error() string { + return "could not read download info file, can't continue download" } diff --git a/core/gtv_api.go b/core/gtv_api.go index 471a561..f41a9ac 100644 --- a/core/gtv_api.go +++ b/core/gtv_api.go @@ -4,7 +4,6 @@ package core import ( "encoding/json" - "errors" "fmt" "io" "iter" @@ -113,7 +112,7 @@ func GetStreamChunkList(video VideoFormat) (ChunkList, error) { return chunklist, err } -func DownloadEpisode( +func DownloadStreamEpisode( ep StreamEpisode, chapter Chapter, formatName string, @@ -128,7 +127,7 @@ func DownloadEpisode( return func (yield func(DownloadProgress) bool) { // Set automatic values if outputFile == "" { - outputFile = ep.GetProposedFilename(chapter.Index) + outputFile = ep.GetProposedFilename(chapter) } if chapter.Index >= 0 { if startDuration < 0 { @@ -167,7 +166,7 @@ func DownloadEpisode( if continueDl { infoFileData, err := os.ReadFile(infoFilename) if err != nil { - yield(DownloadProgress{Error: errors.New("could not access download info file, can't continue download")}) + yield(DownloadProgress{Error: &DownloadInfoFileReadError{}}) return } i, err := strconv.ParseInt(string(infoFileData), 10, 32) diff --git a/core/gtv_common.go b/core/gtv_common.go index 9ee0b5a..436e7d5 100644 --- a/core/gtv_common.go +++ b/core/gtv_common.go @@ -3,7 +3,6 @@ package core import ( - "errors" "fmt" "regexp" "time" @@ -40,10 +39,13 @@ func ParseGtvVideoUrl(url string) (GtvVideo, error) { video := GtvVideo{} match := videoUrlRegex.FindStringSubmatch(url) if len(match) < 2 { - return video, errors.New("Could not parse URL " + url) + return video, &GtvVideoUrlParseError{Url: url} } video.Category = match[1] video.Id = match[2] + if video.Category != "streams" { + return video, &VideoCategoryUnsupportedError{Category: video.Category} + } return video, nil } @@ -109,9 +111,21 @@ func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error) } } -func (ep *StreamEpisode) GetProposedFilename(chapterIdx int) string { - if chapterIdx >= 0 && chapterIdx < len(ep.Chapters) { - return fmt.Sprintf("GTV%04s - %v. %s.ts", ep.Episode, chapterIdx+1, sanitizeUnicodeFilename(ep.Chapters[chapterIdx].Title)) +func (ep *StreamEpisode) GetChapterByNumber(number int) (Chapter, error) { + chapter := Chapter{Index: -1} // set Index to -1 for noop + idx := number-1 + if idx >= 0 && idx >= len(ep.Chapters) { + return chapter, &ChapterNotFoundError{ChapterNum: number} + } + if len(ep.Chapters) > 0 && idx >= 0 { + chapter = ep.Chapters[idx] + } + return chapter, nil +} + +func (ep *StreamEpisode) GetProposedFilename(chapter Chapter) string { + if chapter.Index >= 0 && chapter.Index < len(ep.Chapters) { + return fmt.Sprintf("GTV%04s - %v. %s.ts", ep.Episode, chapter.Index, sanitizeUnicodeFilename(ep.Chapters[chapter.Index].Title)) } else { return sanitizeUnicodeFilename(ep.Title) + ".ts" } From 727575ff2eec5a9a67aa282a5142b40e39c440db Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Sat, 15 Mar 2025 17:13:38 +0100 Subject: [PATCH 3/3] Bump version to 2.1.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7c32728..8f9174b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.1 \ No newline at end of file +2.1.2 \ No newline at end of file