Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
965c30c501 | |||
727575ff2e | |||
ee3518ab5c | |||
424e912f6c |
8 changed files with 156 additions and 124 deletions
32
README.md
32
README.md
|
@ -1,9 +1,10 @@
|
||||||
|
|
||||||
<details><summary>what could it be?</summary>
|
Definitely not an unofficial commandline downloader for https://gronkh.tv
|
||||||
|
|
||||||
<br>
|
## Compatibility
|
||||||
|
|
||||||
Definetly not a commandline downloader for https://gronkh.tv risen from the dead.
|
This tool is only compatible with recent Linux-based operating systems.
|
||||||
|
To run it on Windows, make use of WSL.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -13,26 +14,27 @@ Definetly not a commandline downloader for https://gronkh.tv risen from the dead
|
||||||
- Continuable Downloads
|
- Continuable Downloads
|
||||||
- Show infos about that Episode
|
- Show infos about that Episode
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues / Limitations
|
||||||
|
|
||||||
- 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 by default** and buffering is simulated to pre-empt IP blocking due to API rate-limiting
|
||||||
- Start- and stop-timestamps are not very accurate (± 8 seconds)
|
- Because of the length of video chunks, **start- and stop-timestamps are inaccurate** (± 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 downloaded video 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`
|
||||||
## Supported Platforms
|
|
||||||
|
|
||||||
Only compatible with Linux.
|
|
||||||
|
|
||||||
## Download / Installation
|
## Download / Installation
|
||||||
|
|
||||||
New versions will appear under [Releases](https://github.com/ChaoticByte/lurch-dl/releases). Just download the application and run it via the terminal/cmd/powershell/...
|
New versions will appear under [Releases](https://github.com/ChaoticByte/lurch-dl/releases).
|
||||||
|
Just download the application and run it via your favourite terminal emulator.
|
||||||
|
|
||||||
On Linux, you may have to mark the file as executable before being able to run it.
|
> Note: **You may have to mark the file as executable before being able to run it.**
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
## Cli Usage
|
|
||||||
|
|
||||||
Run `lurch-dl --help` to see available options.
|
Run `lurch-dl --help` to see available options.
|
||||||
|
|
||||||
|
> Note: This tool runs entirely on the command line.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
Download a video in its best available format:
|
Download a video in its best available format:
|
||||||
|
@ -105,5 +107,3 @@ Specify a filename:
|
||||||
```
|
```
|
||||||
./lurch-dl --url https://gronkh.tv/streams/777 --output Stream777.ts
|
./lurch-dl --url https://gronkh.tv/streams/777 --output Stream777.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
2.1.1
|
2.1.2
|
139
cli/cli.go
139
cli/cli.go
|
@ -3,7 +3,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
@ -38,7 +37,7 @@ func XtermSetTitle(title string) {
|
||||||
|
|
||||||
// Commandline
|
// Commandline
|
||||||
|
|
||||||
type Arguments struct {
|
var Arguments struct {
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
FormatName string `json:"format_name"`
|
FormatName string `json:"format_name"`
|
||||||
OutputFile string `json:"output_file"`
|
OutputFile string `json:"output_file"`
|
||||||
|
@ -50,12 +49,11 @@ type Arguments struct {
|
||||||
Help bool `json:"-"`
|
Help bool `json:"-"`
|
||||||
VideoInfo bool `json:"-"`
|
VideoInfo bool `json:"-"`
|
||||||
ListFormats bool `json:"-"`
|
ListFormats bool `json:"-"`
|
||||||
UnparsedChapterNum int `json:"chapter_num"`
|
ChapterNum int `json:"chapter_num"`
|
||||||
// Parsed
|
// Parsed
|
||||||
Video core.GtvVideo `json:"-"`
|
Video core.GtvVideo `json:"-"`
|
||||||
StartDuration time.Duration `json:"-"`
|
StartDuration time.Duration `json:"-"`
|
||||||
StopDuration time.Duration `json:"-"`
|
StopDuration time.Duration `json:"-"`
|
||||||
ChapterIdx int `json:"-"`
|
|
||||||
Ratelimit float64 `json:"-"`
|
Ratelimit float64 `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,96 +82,91 @@ lurch-dl --url string The url to the video
|
||||||
Version: ` + core.Version)
|
Version: ` + core.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CliParseArguments() (Arguments, error) {
|
func CliParseArguments() error {
|
||||||
var err error
|
var err error
|
||||||
var ratelimitMbs float64
|
var ratelimitMbs float64
|
||||||
a := Arguments{}
|
flag.BoolVar(&Arguments.Help, "h", false, "")
|
||||||
flag.BoolVar(&a.Help, "h", false, "")
|
flag.BoolVar(&Arguments.Help, "help", false, "")
|
||||||
flag.BoolVar(&a.Help, "help", false, "")
|
flag.BoolVar(&Arguments.VideoInfo, "info", false, "")
|
||||||
flag.BoolVar(&a.VideoInfo, "info", false, "")
|
flag.StringVar(&Arguments.Url, "url", "", "")
|
||||||
flag.StringVar(&a.Url, "url", "", "")
|
flag.IntVar(&Arguments.ChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream
|
||||||
flag.IntVar(&a.UnparsedChapterNum, "chapter", 0, "") // 0 -> chapter idx -1 -> complete stream
|
flag.StringVar(&Arguments.FormatName, "format", "auto", "")
|
||||||
flag.StringVar(&a.FormatName, "format", "auto", "")
|
flag.StringVar(&Arguments.OutputFile, "output", "", "")
|
||||||
flag.StringVar(&a.OutputFile, "output", "", "")
|
flag.StringVar(&Arguments.TimestampStart, "start", "", "")
|
||||||
flag.StringVar(&a.TimestampStart, "start", "", "")
|
flag.StringVar(&Arguments.TimestampStop, "stop", "", "")
|
||||||
flag.StringVar(&a.TimestampStop, "stop", "", "")
|
flag.BoolVar(&Arguments.Overwrite, "overwrite", false, "")
|
||||||
flag.BoolVar(&a.Overwrite, "overwrite", false, "")
|
flag.BoolVar(&Arguments.ContinueDl, "continue", false, "")
|
||||||
flag.BoolVar(&a.ContinueDl, "continue", false, "")
|
|
||||||
flag.Float64Var(&ratelimitMbs, "max-rate", 10.0, "")
|
flag.Float64Var(&ratelimitMbs, "max-rate", 10.0, "")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
a.Video, err = core.ParseGtvVideoUrl(a.Url)
|
Arguments.Video, err = core.ParseGtvVideoUrl(Arguments.Url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, err
|
return err
|
||||||
}
|
}
|
||||||
if a.Video.Category != "streams" {
|
if Arguments.TimestampStart == "" {
|
||||||
return a, errors.New("video category '" + a.Video.Category + "' not supported")
|
Arguments.StartDuration = -1
|
||||||
}
|
|
||||||
if a.TimestampStart == "" {
|
|
||||||
a.StartDuration = -1
|
|
||||||
} else {
|
} else {
|
||||||
a.StartDuration, err = time.ParseDuration(a.TimestampStart)
|
Arguments.StartDuration, err = time.ParseDuration(Arguments.TimestampStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.TimestampStop == "" {
|
if Arguments.TimestampStop == "" {
|
||||||
a.StopDuration = -1
|
Arguments.StopDuration = -1
|
||||||
} else {
|
} else {
|
||||||
a.StopDuration, err = time.ParseDuration(a.TimestampStop)
|
Arguments.StopDuration, err = time.ParseDuration(Arguments.TimestampStop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.ChapterIdx = a.UnparsedChapterNum - 1
|
Arguments.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s
|
||||||
a.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s
|
if Arguments.Ratelimit <= 0 {
|
||||||
if a.Ratelimit <= 0 {
|
return &GenericCliAgumentError{Msg: "the value of --max-rate must be greater than 0"}
|
||||||
return a, errors.New("the value of --max-rate must be greater than 0")
|
|
||||||
}
|
}
|
||||||
return a, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main
|
// Main
|
||||||
|
|
||||||
func CliRun() int {
|
func CliRun() int {
|
||||||
cli := Cli{}
|
|
||||||
defer fmt.Print("\n")
|
defer fmt.Print("\n")
|
||||||
// cli arguments & help text
|
// cli arguments & help text
|
||||||
flag.Usage = CliShowHelp
|
flag.Usage = CliShowHelp
|
||||||
args, err := CliParseArguments()
|
err := CliParseArguments()
|
||||||
if args.Help {
|
if Arguments.Help {
|
||||||
CliShowHelp()
|
CliShowHelp()
|
||||||
return 0
|
return 0
|
||||||
} else if args.Url == "" || err != nil {
|
} else if Arguments.Url == "" || err != nil {
|
||||||
CliShowHelp()
|
CliShowHelp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cli.ErrorMessage(err)
|
CliErrorMessage(err)
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
// detect terminal features
|
// detect terminal features
|
||||||
XtermDetectFeatures()
|
XtermDetectFeatures()
|
||||||
//
|
|
||||||
api := core.GtvApi{};
|
|
||||||
// Get video metadata
|
// Get video metadata
|
||||||
if CliXtermTitle {
|
if CliXtermTitle {
|
||||||
XtermSetTitle("lurch-dl - Fetching video metadata ...")
|
XtermSetTitle("lurch-dl - Fetching video metadata ...")
|
||||||
}
|
}
|
||||||
streamEp, err := api.GetStreamEpisode(args.Video.Id)
|
streamEp, err := core.GetStreamEpisode(Arguments.Video.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cli.ErrorMessage(err)
|
CliErrorMessage(err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
fmt.Printf("Title: %s\n", 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 {
|
targetChapter, err := streamEp.GetChapterByNumber(Arguments.ChapterNum)
|
||||||
if args.ChapterIdx >= len(streamEp.Chapters) {
|
if err != nil {
|
||||||
cli.ErrorMessage(&core.ChapterNotFoundError{ChapterNum: args.UnparsedChapterNum})
|
CliErrorMessage(err)
|
||||||
CliAvailableChapters(streamEp.Chapters)
|
CliAvailableChapters(streamEp.Chapters)
|
||||||
return 1
|
return 1
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if args.VideoInfo {
|
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("Episode: %s\n", streamEp.Episode)
|
||||||
fmt.Printf("Length: %s\n", streamEp.Length)
|
fmt.Printf("Length: %s\n", streamEp.Length)
|
||||||
fmt.Printf("Views: %d\n", streamEp.Views)
|
fmt.Printf("Views: %d\n", streamEp.Views)
|
||||||
|
@ -195,42 +188,36 @@ func CliRun() int {
|
||||||
CliAvailableChapters(streamEp.Chapters)
|
CliAvailableChapters(streamEp.Chapters)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
format, err := streamEp.GetFormatByName(args.FormatName)
|
format, err := streamEp.GetFormatByName(Arguments.FormatName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cli.ErrorMessage(err)
|
CliErrorMessage(err)
|
||||||
CliAvailableFormats(streamEp.Formats)
|
CliAvailableFormats(streamEp.Formats)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Printf("Format: %v\n", format.Name)
|
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
|
// We already set the output file correctly so we can output it
|
||||||
if args.OutputFile == "" {
|
if Arguments.OutputFile == "" {
|
||||||
args.OutputFile = streamEp.GetProposedFilename(args.ChapterIdx)
|
Arguments.OutputFile = streamEp.GetProposedFilename(targetChapter)
|
||||||
}
|
}
|
||||||
// Start Download
|
// Start Download
|
||||||
fmt.Printf("Output: %v\n", args.OutputFile)
|
fmt.Printf("Output: %v\n", Arguments.OutputFile)
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
successful := false
|
successful := false
|
||||||
aborted := false
|
aborted := false
|
||||||
for p := range api.DownloadEpisode(
|
for p := range core.DownloadStreamEpisode(
|
||||||
streamEp,
|
streamEp,
|
||||||
targetChapter,
|
targetChapter,
|
||||||
args.FormatName,
|
Arguments.FormatName,
|
||||||
args.OutputFile,
|
Arguments.OutputFile,
|
||||||
args.Overwrite,
|
Arguments.Overwrite,
|
||||||
args.ContinueDl,
|
Arguments.ContinueDl,
|
||||||
args.StartDuration,
|
Arguments.StartDuration,
|
||||||
args.StopDuration,
|
Arguments.StopDuration,
|
||||||
args.Ratelimit,
|
Arguments.Ratelimit,
|
||||||
make(chan os.Signal, 1),
|
make(chan os.Signal, 1),
|
||||||
) { // Iterate over download progress
|
) { // Iterate over download progress
|
||||||
if p.Error != nil {
|
if p.Error != nil {
|
||||||
cli.ErrorMessage(p.Error)
|
CliErrorMessage(p.Error)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if p.Success {
|
if p.Success {
|
||||||
|
@ -238,7 +225,7 @@ func CliRun() int {
|
||||||
} else if p.Aborted {
|
} else if p.Aborted {
|
||||||
aborted = true
|
aborted = true
|
||||||
} else {
|
} 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")
|
fmt.Print("\n")
|
||||||
|
@ -246,7 +233,7 @@ func CliRun() int {
|
||||||
fmt.Print("\nAborted. ")
|
fmt.Print("\nAborted. ")
|
||||||
return 130
|
return 130
|
||||||
} else if !successful {
|
} else if !successful {
|
||||||
cli.ErrorMessage(errors.New("download failed"))
|
CliErrorMessage(&GenericDownloadError{})
|
||||||
return 1
|
return 1
|
||||||
} else { return 0 }
|
} else { return 0 }
|
||||||
}
|
}
|
||||||
|
@ -270,9 +257,7 @@ func CliAvailableFormats(formats []core.VideoFormat) {
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cli struct{}
|
func CliDownloadProgress(progress float32, rate float64, delaying bool, waiting bool, retries int, title string) {
|
||||||
|
|
||||||
func (cli *Cli) DownloadProgress(progress float32, rate float64, delaying bool, waiting bool, retries int, title string) {
|
|
||||||
if retries > 0 {
|
if retries > 0 {
|
||||||
if retries == 1 {
|
if retries == 1 {
|
||||||
fmt.Print("\n")
|
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.Print("\n")
|
||||||
fmt.Println("An error occured:", err)
|
fmt.Println("An error occured:", err)
|
||||||
}
|
}
|
||||||
|
|
15
cli/errors.go
Normal file
15
cli/errors.go
Normal 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"
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ type FileExistsError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *FileExistsError) Error() string {
|
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 {
|
type FormatNotFoundError struct {
|
||||||
|
@ -43,7 +43,7 @@ type FormatNotFoundError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *FormatNotFoundError) Error() string {
|
func (err *FormatNotFoundError) Error() string {
|
||||||
return "Format " + err.FormatName + " is not available."
|
return "format " + err.FormatName + " is not available"
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapterNotFoundError struct {
|
type ChapterNotFoundError struct {
|
||||||
|
@ -51,5 +51,27 @@ type ChapterNotFoundError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *ChapterNotFoundError) Error() string {
|
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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
"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 ApiBaseurlStreamEpisodeInfo = "https://api.gronkh.tv/v1/video/info?episode=%s"
|
||||||
const ApiBaseurlStreamEpisodePlInfo = "https://api.gronkh.tv/v1/video/playlist?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{
|
var ApiHeadersBase = http.Header{
|
||||||
"User-Agent": {"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"},
|
"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"},
|
"Accept-Language": {"de,en-US;q=0.7,en;q=0.3"},
|
||||||
|
@ -48,9 +59,7 @@ var ApiHeadersVideoAdditional = http.Header{
|
||||||
"Accept": {"*/*"},
|
"Accept": {"*/*"},
|
||||||
}
|
}
|
||||||
|
|
||||||
type GtvApi struct{}
|
func GetStreamEpisode(episode string) (StreamEpisode, error) {
|
||||||
|
|
||||||
func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) {
|
|
||||||
ep := StreamEpisode{}
|
ep := StreamEpisode{}
|
||||||
ep.Episode = episode
|
ep.Episode = episode
|
||||||
info_data, err := httpGet(
|
info_data, err := httpGet(
|
||||||
|
@ -93,7 +102,7 @@ func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) {
|
||||||
return ep, err
|
return ep, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *GtvApi) GetStreamChunkList(video VideoFormat) (ChunkList, error) {
|
func GetStreamChunkList(video VideoFormat) (ChunkList, error) {
|
||||||
baseUrl := video.Url[:strings.LastIndex(video.Url, "/")]
|
baseUrl := video.Url[:strings.LastIndex(video.Url, "/")]
|
||||||
data, err := httpGet(video.Url, []http.Header{ApiHeadersBase, ApiHeadersMetaAdditional}, time.Second*10)
|
data, err := httpGet(video.Url, []http.Header{ApiHeadersBase, ApiHeadersMetaAdditional}, time.Second*10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -103,7 +112,7 @@ func (api *GtvApi) GetStreamChunkList(video VideoFormat) (ChunkList, error) {
|
||||||
return chunklist, err
|
return chunklist, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *GtvApi) DownloadEpisode(
|
func DownloadStreamEpisode(
|
||||||
ep StreamEpisode,
|
ep StreamEpisode,
|
||||||
chapter Chapter,
|
chapter Chapter,
|
||||||
formatName string,
|
formatName string,
|
||||||
|
@ -118,7 +127,7 @@ func (api *GtvApi) DownloadEpisode(
|
||||||
return func (yield func(DownloadProgress) bool) {
|
return func (yield func(DownloadProgress) bool) {
|
||||||
// Set automatic values
|
// Set automatic values
|
||||||
if outputFile == "" {
|
if outputFile == "" {
|
||||||
outputFile = ep.GetProposedFilename(chapter.Index)
|
outputFile = ep.GetProposedFilename(chapter)
|
||||||
}
|
}
|
||||||
if chapter.Index >= 0 {
|
if chapter.Index >= 0 {
|
||||||
if startDuration < 0 {
|
if startDuration < 0 {
|
||||||
|
@ -157,7 +166,7 @@ func (api *GtvApi) DownloadEpisode(
|
||||||
if continueDl {
|
if continueDl {
|
||||||
infoFileData, err := os.ReadFile(infoFilename)
|
infoFileData, err := os.ReadFile(infoFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
yield(DownloadProgress{Error: errors.New("could not access download info file, can't continue download")})
|
yield(DownloadProgress{Error: &DownloadInfoFileReadError{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
i, err := strconv.ParseInt(string(infoFileData), 10, 32)
|
i, err := strconv.ParseInt(string(infoFileData), 10, 32)
|
||||||
|
@ -181,7 +190,7 @@ func (api *GtvApi) DownloadEpisode(
|
||||||
}
|
}
|
||||||
// download
|
// download
|
||||||
format, _ := ep.GetFormatByName(formatName) // we don't have to check the error, as it was already checked by CliRun()
|
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)
|
chunklist = chunklist.Cut(startDuration, stopDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
yield(DownloadProgress{Error: err})
|
yield(DownloadProgress{Error: err})
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
@ -40,10 +39,13 @@ func ParseGtvVideoUrl(url string) (GtvVideo, error) {
|
||||||
video := GtvVideo{}
|
video := GtvVideo{}
|
||||||
match := videoUrlRegex.FindStringSubmatch(url)
|
match := videoUrlRegex.FindStringSubmatch(url)
|
||||||
if len(match) < 2 {
|
if len(match) < 2 {
|
||||||
return video, errors.New("Could not parse URL " + url)
|
return video, &GtvVideoUrlParseError{Url: url}
|
||||||
}
|
}
|
||||||
video.Category = match[1]
|
video.Category = match[1]
|
||||||
video.Id = match[2]
|
video.Id = match[2]
|
||||||
|
if video.Category != "streams" {
|
||||||
|
return video, &VideoCategoryUnsupportedError{Category: video.Category}
|
||||||
|
}
|
||||||
return video, nil
|
return video, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,9 +111,21 @@ func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *StreamEpisode) GetProposedFilename(chapterIdx int) string {
|
func (ep *StreamEpisode) GetChapterByNumber(number int) (Chapter, error) {
|
||||||
if chapterIdx >= 0 && chapterIdx < len(ep.Chapters) {
|
chapter := Chapter{Index: -1} // set Index to -1 for noop
|
||||||
return fmt.Sprintf("GTV%04s - %v. %s.ts", ep.Episode, chapterIdx+1, sanitizeUnicodeFilename(ep.Chapters[chapterIdx].Title))
|
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 {
|
} else {
|
||||||
return sanitizeUnicodeFilename(ep.Title) + ".ts"
|
return sanitizeUnicodeFilename(ep.Title) + ".ts"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue