Compare commits

..

3 commits

7 changed files with 140 additions and 108 deletions

View file

@ -1 +1 @@
2.1.1
2.1.2

View file

@ -3,7 +3,6 @@
package main
import (
"errors"
"flag"
"fmt"
"os"
@ -38,7 +37,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"`
@ -50,12 +49,11 @@ type 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:"-"`
}
@ -84,96 +82,91 @@ 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.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", "", "")
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 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.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s
if Arguments.Ratelimit <= 0 {
return &GenericCliAgumentError{Msg: "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})
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)
}
if args.VideoInfo {
// Video Info
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 +188,36 @@ 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)
}
// 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(targetChapter)
}
// 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.DownloadStreamEpisode(
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 +225,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 +233,7 @@ func CliRun() int {
fmt.Print("\nAborted. ")
return 130
} else if !successful {
cli.ErrorMessage(errors.New("download failed"))
CliErrorMessage(&GenericDownloadError{})
return 1
} else { return 0 }
}
@ -270,9 +257,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 +276,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)
}

15
cli/errors.go Normal file
View file

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

View file

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

View file

@ -4,7 +4,6 @@ package core
import (
"encoding/json"
"errors"
"fmt"
"io"
"iter"
@ -25,6 +24,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 +59,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 +102,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 +112,7 @@ func (api *GtvApi) GetStreamChunkList(video VideoFormat) (ChunkList, error) {
return chunklist, err
}
func (api *GtvApi) DownloadEpisode(
func DownloadStreamEpisode(
ep StreamEpisode,
chapter Chapter,
formatName string,
@ -118,7 +127,7 @@ func (api *GtvApi) 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 {
@ -157,7 +166,7 @@ func (api *GtvApi) 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)
@ -181,7 +190,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})

View file

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

View file

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