Refactored core, improved core <-> cli interaction

This commit is contained in:
ChaoticByte 2025-03-09 09:08:07 +01:00
parent 5e6884af66
commit d2b0ca37be
No known key found for this signature in database
6 changed files with 363 additions and 311 deletions

View file

@ -38,11 +38,25 @@ func XtermSetTitle(title string) {
// Commandline
type CliOnlyArguments struct {
type Arguments struct {
Url string `json:"url"`
FormatName string `json:"format_name"`
OutputFile string `json:"output_file"`
TimestampStart string `json:"timestamp_start"`
TimestampStop string `json:"timestamp_stop"`
Overwrite bool `json:"overwrite"`
ContinueDl bool `json:"continue"`
//
Help bool `json:"-"`
ListChapters bool `json:"-"`
ListFormats bool `json:"-"`
ChapterNum int `json:"chapter_num"`
UnparsedChapterNum int `json:"chapter_num"`
// Parsed
Video core.GtvVideo `json:"-"`
StartDuration time.Duration `json:"-"`
StopDuration time.Duration `json:"-"`
ChapterIdx int `json:"-"`
Ratelimit float64 `json:"-"`
}
func CliShowHelp() {
@ -71,17 +85,16 @@ lurch-dl --url string The url to the video
Version: ` + core.Version)
}
func CliParseArguments() (core.Arguments, CliOnlyArguments, error) {
func CliParseArguments() (Arguments, error) {
var err error
var ratelimitMbs float64
a := core.Arguments{}
c := CliOnlyArguments{}
flag.BoolVar(&c.Help, "h", false, "")
flag.BoolVar(&c.Help, "help", false, "")
flag.BoolVar(&c.ListChapters, "list-chapters", false, "")
flag.BoolVar(&c.ListFormats, "list-formats", false, "")
a := Arguments{}
flag.BoolVar(&a.Help, "h", false, "")
flag.BoolVar(&a.Help, "help", false, "")
flag.BoolVar(&a.ListChapters, "list-chapters", false, "")
flag.BoolVar(&a.ListFormats, "list-formats", false, "")
flag.StringVar(&a.Url, "url", "", "")
flag.IntVar(&c.ChapterNum, "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.OutputFile, "output", "", "")
flag.StringVar(&a.TimestampStart, "start", "", "")
@ -92,17 +105,17 @@ func CliParseArguments() (core.Arguments, CliOnlyArguments, error) {
flag.Parse()
a.Video, err = core.ParseGtvVideoUrl(a.Url)
if err != nil {
return a, c, err
return a, err
}
if a.Video.Class != "streams" {
return a, c, errors.New("video category '" + a.Video.Class + "' not supported")
return a, errors.New("video category '" + a.Video.Class + "' not supported")
}
if a.TimestampStart == "" {
a.StartDuration = -1
} else {
a.StartDuration, err = time.ParseDuration(a.TimestampStart)
if err != nil {
return a, c, err
return a, err
}
}
if a.TimestampStop == "" {
@ -110,15 +123,15 @@ func CliParseArguments() (core.Arguments, CliOnlyArguments, error) {
} else {
a.StopDuration, err = time.ParseDuration(a.TimestampStop)
if err != nil {
return a, c, err
return a, err
}
}
a.ChapterIdx = c.ChapterNum - 1
a.ChapterIdx = a.UnparsedChapterNum - 1
a.Ratelimit = ratelimitMbs * 1_000_000.0 // MB/s -> B/s
if a.Ratelimit <= 0 {
return a, c, errors.New("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, c, err
return a, err
}
// Main
@ -128,8 +141,8 @@ func CliRun() int {
defer fmt.Print("\n")
// cli arguments & help text
flag.Usage = CliShowHelp
args, cliArgs, err := CliParseArguments()
if cliArgs.Help {
args, err := CliParseArguments()
if args.Help {
CliShowHelp()
return 0
} else if args.Url == "" || err != nil {
@ -141,11 +154,13 @@ func CliRun() int {
}
// detect terminal features
XtermDetectFeatures()
//
api := core.GtvApi{};
// Get video metadata
if CliXtermTitle {
XtermSetTitle("lurch-dl - Fetching video metadata ...")
}
streamEp, err := core.GetStreamEpisode(args.Video.Id)
streamEp, err := api.GetStreamEpisode(args.Video.Id)
if err != nil {
cli.ErrorMessage(err)
return 1
@ -155,17 +170,17 @@ func CliRun() int {
// Check and list chapters/formats and exit
if args.ChapterIdx >= 0 {
if args.ChapterIdx >= len(streamEp.Chapters) {
cli.ErrorMessage(&core.ChapterNotFoundError{ChapterNum: cliArgs.ChapterNum})
cli.ErrorMessage(&core.ChapterNotFoundError{ChapterNum: args.UnparsedChapterNum})
CliAvailableChapters(streamEp.Chapters)
return 1
}
}
if cliArgs.ListChapters || cliArgs.ListFormats {
if cliArgs.ListChapters {
if args.ListChapters || args.ListFormats {
if args.ListChapters {
fmt.Print("\n")
CliAvailableChapters(streamEp.Chapters)
}
if cliArgs.ListFormats {
if args.ListFormats {
fmt.Print("\n")
CliAvailableFormats(streamEp.Formats)
}
@ -178,8 +193,11 @@ func CliRun() int {
return 1
}
cli.InfoMessage(fmt.Sprintf("Format: %v", format.Name))
if args.ChapterIdx >= 0 {
cli.InfoMessage(fmt.Sprintf("Chapter: %v. %v", cliArgs.ChapterNum, streamEp.Chapters[args.ChapterIdx].Title))
// 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]
cli.InfoMessage(fmt.Sprintf("Chapter: %v. %v", args.UnparsedChapterNum, targetChapter.Title))
}
// We already set the output file correctly so we can output it
if args.OutputFile == "" {
@ -188,12 +206,33 @@ func CliRun() int {
// Start Download
cli.InfoMessage(fmt.Sprintf("Output: %v", args.OutputFile))
fmt.Print("\n")
if err = streamEp.Download(args, &cli, make(chan os.Signal, 1)); err != nil {
cli.ErrorMessage(err)
return 1
successful := false
for p := range api.DownloadEpisode(
streamEp,
targetChapter,
args.FormatName,
args.OutputFile,
args.Overwrite,
args.ContinueDl,
args.StartDuration,
args.StopDuration,
args.Ratelimit,
make(chan os.Signal, 1),
) {
if p.Error != nil {
cli.ErrorMessage(p.Error)
return 1
}
if p.Success { successful = true }
if p.Aborted { cli.Aborted() } else {
cli.DownloadProgress(p.Progress, p.Rate, p.Delaying, p.Waiting, p.Retries, p.Title)
}
}
fmt.Print("\n")
return 0
if !successful {
cli.ErrorMessage(errors.New("download failed"))
return 1
} else { return 0 }
}
func CliAvailableChapters(chapters []core.Chapter) {

View file

@ -1,19 +0,0 @@
package core
import "time"
type Arguments struct {
Url string `json:"url"`
FormatName string `json:"format_name"`
OutputFile string `json:"output_file"`
TimestampStart string `json:"timestamp_start"`
TimestampStop string `json:"timestamp_stop"`
Overwrite bool `json:"overwrite"`
ContinueDl bool `json:"continue"`
// Parsed
Video GtvVideo `json:"-"`
StartDuration time.Duration `json:"-"`
StopDuration time.Duration `json:"-"`
ChapterIdx int `json:"-"`
Ratelimit float64 `json:"-"`
}

View file

@ -3,9 +3,25 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"io"
"iter"
"net/http"
"os"
"os/signal"
"sort"
"strconv"
"strings"
"time"
)
const MaxRetries = 5
// The following two values are used to simulate buffering
const RatelimitDelay = 2.0 // in Seconds; How long to delay the next chunk download.
const RatelimitDelayAfter = 5.0 // in Seconds; Delay the next chunk download after this duration.
const ApiBaseurlStreamEpisodeInfo = "https://api.gronkh.tv/v1/video/info?episode=%s"
const ApiBaseurlStreamEpisodePlInfo = "https://api.gronkh.tv/v1/video/playlist?episode=%s"
@ -31,3 +47,215 @@ var ApiHeadersMetaAdditional = http.Header{
var ApiHeadersVideoAdditional = http.Header{
"Accept": {"*/*"},
}
type GtvApi struct{}
func (api *GtvApi) GetStreamEpisode(episode string) (StreamEpisode, error) {
ep := StreamEpisode{}
ep.Episode = episode
info_data, err := httpGet(
fmt.Sprintf(ApiBaseurlStreamEpisodeInfo, episode),
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
if err != nil {
return ep, err
}
// Title
json.Unmarshal(info_data, &ep)
ep.Title = strings.ToValidUTF8(ep.Title, "")
// Sort Chapters, correct offset and set index
sort.Slice(ep.Chapters, func(i int, j int) bool {
return ep.Chapters[i].Offset < ep.Chapters[j].Offset
})
for i := range ep.Chapters {
ep.Chapters[i].Offset = ep.Chapters[i].Offset * time.Second
ep.Chapters[i].Index = i
}
// Formats
playlist_url_data, err := httpGet(
fmt.Sprintf(ApiBaseurlStreamEpisodePlInfo, episode),
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
if err != nil {
return ep, err
}
json.Unmarshal(playlist_url_data, &ep)
playlist_data, err := httpGet(
ep.PlaylistUrl,
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
ep.Formats = parseAvailFormatsFromM3u8(string(playlist_data))
return ep, err
}
func (api *GtvApi) 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 {
return ChunkList{}, err
}
chunklist, err := parseChunkListFromM3u8(string(data), baseUrl)
return chunklist, err
}
func (api *GtvApi) DownloadEpisode(
ep StreamEpisode,
chapter Chapter,
formatName string,
outputFile string,
overwrite bool,
continueDl bool,
startDuration time.Duration,
stopDuration time.Duration,
ratelimit float64,
interruptChan chan os.Signal,
) iter.Seq[DownloadProgress] {
return func (yield func(DownloadProgress) bool) {
// Set automatic values
if outputFile == "" {
outputFile = ep.GetProposedFilename(chapter.Index)
}
if chapter.Index >= 0 {
if startDuration < 0 {
startDuration = time.Duration(ep.Chapters[chapter.Index].Offset)
}
if stopDuration < 0 && chapter.Index+1 < len(ep.Chapters) {
// next chapter is stop
stopDuration = time.Duration(ep.Chapters[chapter.Index+1].Offset)
}
}
//
var err error
var nextChunk int = 0
var videoFile *os.File
var infoFile *os.File
var infoFilename string
if !overwrite && !continueDl {
if _, err := os.Stat(outputFile); err == nil {
yield(DownloadProgress{Error: &FileExistsError{Filename: outputFile}})
return
}
}
videoFile, err = os.OpenFile(outputFile, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
yield(DownloadProgress{Error: err})
return
}
defer videoFile.Close()
if overwrite {
videoFile.Truncate(0)
}
// always seek to the end
videoFile.Seek(0, io.SeekEnd)
// info file
infoFilename = outputFile + ".dl-info"
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")})
return
}
i, err := strconv.ParseInt(string(infoFileData), 10, 32)
nextChunk = int(i)
if err != nil {
yield(DownloadProgress{Error: err})
return
}
}
infoFile, err = os.OpenFile(infoFilename, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
yield(DownloadProgress{Error: err})
return
}
infoFile.Truncate(0)
infoFile.Seek(0, io.SeekStart)
_, err = infoFile.Write([]byte(strconv.Itoa(nextChunk)))
if err != nil {
yield(DownloadProgress{Error: err})
return
}
// 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 = chunklist.Cut(startDuration, stopDuration)
if err != nil {
yield(DownloadProgress{Error: err})
return
}
var bufferDt float64
var progress float32
var actualRate float64
keyboardInterrupt := false
signal.Notify(interruptChan, os.Interrupt)
go func() {
// Handle Interrupts
<-interruptChan
keyboardInterrupt = true
yield(DownloadProgress{Progress: progress, Rate: actualRate, Retries: 0, Title: ep.Title})
}()
for i, chunk := range chunklist.Chunks {
if i < nextChunk {
continue
}
var time1 int64
var data []byte
retries := 0
for {
if keyboardInterrupt {
break
}
time1 = time.Now().UnixNano()
if !yield(DownloadProgress{Progress: progress, Rate: actualRate, Delaying: false, Waiting: true, Retries: retries, Title: ep.Title}) { return }
data, err = httpGet(chunklist.BaseUrl+"/"+chunk, []http.Header{ApiHeadersBase, ApiHeadersVideoAdditional}, time.Second*5)
if err != nil {
if retries == MaxRetries {
yield(DownloadProgress{Error: err})
return
}
retries++
continue
}
break
}
if keyboardInterrupt {
break
}
var dtDownload float64 = float64(time.Now().UnixNano()-time1) / 1000000000.0
rate := float64(len(data)) / dtDownload
actualRate = rate - max(rate-ratelimit, 0)
progress = float32(i+1) / float32(len(chunklist.Chunks))
delayNow := bufferDt > RatelimitDelayAfter
if !yield(DownloadProgress{Progress: progress, Rate: actualRate, Delaying: delayNow, Waiting: false, Retries: retries, Title: ep.Title}) { return }
if delayNow {
bufferDt = 0
// this simulates that the buffering is finished and the player is playing
time.Sleep(time.Duration(RatelimitDelay * float64(time.Second)))
} else if rate > ratelimit {
// slow down, we are too fast.
deferTime := (rate - ratelimit) / ratelimit * dtDownload
time.Sleep(time.Duration(deferTime * float64(time.Second)))
}
videoFile.Write(data)
nextChunk++
infoFile.Truncate(0)
infoFile.Seek(0, io.SeekStart)
infoFile.Write([]byte(strconv.Itoa(nextChunk)))
var dtIteration float64 = float64(time.Now().UnixNano()-time1) / 1000000000.0
if !delayNow {
bufferDt += dtIteration
}
}
infoFile.Close()
if !keyboardInterrupt {
err := os.Remove(infoFilename)
if err != nil {
yield(DownloadProgress{Progress: progress, Rate: actualRate, Error: err})
return
}
}
yield(DownloadProgress{Progress: progress, Rate: actualRate, Success: true})
}
}

View file

@ -4,16 +4,15 @@ package core
import (
"errors"
"fmt"
"regexp"
"time"
)
// The following two values are used to simulate buffering
const RatelimitDelay = 2.0 // in Seconds; How long to delay the next chunk download.
const RatelimitDelayAfter = 5.0 // in Seconds; Delay the next chunk download after this duration.
var videoUrlRegex = regexp.MustCompile(`gronkh\.tv\/([a-z]+)\/([0-9]+)`)
//
type GtvVideo struct {
Class string `json:"class"`
Id string `json:"id"`
@ -22,7 +21,7 @@ type GtvVideo struct {
func ParseGtvVideoUrl(url string) (GtvVideo, error) {
video := GtvVideo{}
match := videoUrlRegex.FindStringSubmatch(url)
if match == nil || len(match) < 2 {
if len(match) < 2 {
return video, errors.New("Could not parse URL " + url)
}
video.Class = match[1]
@ -30,11 +29,15 @@ func ParseGtvVideoUrl(url string) (GtvVideo, error) {
return video, nil
}
//
type VideoFormat struct {
Name string `json:"format"`
Url string `json:"url"`
}
//
type ChunkList struct {
BaseUrl string
Chunks []string
@ -59,3 +62,51 @@ func (cl *ChunkList) Cut(from time.Duration, to time.Duration) ChunkList {
ChunkDuration: cl.ChunkDuration,
}
}
//
type Chapter struct {
Index int `json:"index"`
Title string `json:"title"`
Offset time.Duration `json:"offset"`
}
//
type StreamEpisode struct {
Episode string `json:"episode"`
Formats []VideoFormat `json:"formats"`
Title string `json:"title"`
// ProposedFilename string `json:"proposed_filename"`
PlaylistUrl string `json:"playlist_url"`
Chapters []Chapter `json:"chapters"`
}
func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error) {
var idx int
var err error = nil
if formatName == "auto" {
// at the moment, the best format is always the first -> 0
return ep.Formats[idx], nil
} else {
formatFound := false
for i, f := range ep.Formats {
if f.Name == formatName {
idx = i
formatFound = true
}
}
if !formatFound {
err = &FormatNotFoundError{FormatName: formatName}
}
return ep.Formats[idx], err
}
}
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))
} else {
return sanitizeUnicodeFilename(ep.Title) + ".ts"
}
}

View file

@ -1,252 +0,0 @@
// Copyright (c) 2025, Julian Müller (ChaoticByte)
package core
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"sort"
"strconv"
"strings"
"time"
)
const MaxRetries = 5
type Chapter struct {
Index int `json:"index"`
Title string `json:"title"`
Offset time.Duration `json:"offset"`
}
type StreamEpisode struct {
Episode string `json:"episode"`
Formats []VideoFormat `json:"formats"`
Title string `json:"title"`
// ProposedFilename string `json:"proposed_filename"`
PlaylistUrl string `json:"playlist_url"`
Chapters []Chapter `json:"chapters"`
}
func (ep *StreamEpisode) GetFormatByName(formatName string) (VideoFormat, error) {
var idx int
var err error = nil
if formatName == "auto" {
// at the moment, the best format is always the first -> 0
return ep.Formats[idx], nil
} else {
formatFound := false
for i, f := range ep.Formats {
if f.Name == formatName {
idx = i
formatFound = true
}
}
if !formatFound {
err = &FormatNotFoundError{FormatName: formatName}
}
return ep.Formats[idx], err
}
}
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))
} else {
return sanitizeUnicodeFilename(ep.Title) + ".ts"
}
}
func (ep *StreamEpisode) Download(args Arguments, ui UserInterface, interruptChan chan os.Signal) error {
// Set automatic values
if args.OutputFile == "" {
args.OutputFile = ep.GetProposedFilename(args.ChapterIdx)
}
if args.ChapterIdx >= 0 {
if args.StartDuration < 0 {
args.StartDuration = time.Duration(ep.Chapters[args.ChapterIdx].Offset)
}
if args.StopDuration < 0 && args.ChapterIdx+1 < len(ep.Chapters) {
// next chapter is stop
args.StopDuration = time.Duration(ep.Chapters[args.ChapterIdx+1].Offset)
}
}
//
var err error
var nextChunk int = 0
var videoFile *os.File
var infoFile *os.File
var infoFilename string
if !args.Overwrite && !args.ContinueDl {
if _, err := os.Stat(args.OutputFile); err == nil {
return &FileExistsError{Filename: args.OutputFile}
}
}
videoFile, err = os.OpenFile(args.OutputFile, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
return err
}
defer videoFile.Close()
if args.Overwrite {
videoFile.Truncate(0)
}
// always seek to the end
videoFile.Seek(0, io.SeekEnd)
// info file
infoFilename = args.OutputFile + ".dl-info"
if args.ContinueDl {
infoFileData, err := os.ReadFile(infoFilename)
if err != nil {
return errors.New("could not access download info file, can't continue download")
}
i, err := strconv.ParseInt(string(infoFileData), 10, 32)
nextChunk = int(i)
if err != nil {
return err
}
}
infoFile, err = os.OpenFile(infoFilename, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
return err
}
infoFile.Truncate(0)
infoFile.Seek(0, io.SeekStart)
_, err = infoFile.Write([]byte(strconv.Itoa(nextChunk)))
if err != nil {
return err
}
// download
format, _ := ep.GetFormatByName(args.FormatName) // we don't have to check the error, as it was already checked by CliRun()
chunklist, err := GetStreamChunkList(format)
chunklist = chunklist.Cut(args.StartDuration, args.StopDuration)
if err != nil {
return err
}
var bufferDt float64
var progress float32
var actualRate float64
keyboardInterrupt := false
signal.Notify(interruptChan, os.Interrupt)
go func() {
// Handle Interrupts
<-interruptChan
keyboardInterrupt = true
ui.DownloadProgress(progress, actualRate, false, false, 0, ep.Title)
ui.Aborted()
}()
for i, chunk := range chunklist.Chunks {
if i < nextChunk {
continue
}
var time1 int64
var data []byte
retries := 0
for {
if keyboardInterrupt {
break
}
time1 = time.Now().UnixNano()
ui.DownloadProgress(progress, actualRate, false, true, retries, ep.Title)
data, err = httpGet(chunklist.BaseUrl+"/"+chunk, []http.Header{ApiHeadersBase, ApiHeadersVideoAdditional}, time.Second*5)
if err != nil {
if retries == MaxRetries {
return err
}
retries++
continue
}
break
}
if keyboardInterrupt {
break
}
var dtDownload float64 = float64(time.Now().UnixNano()-time1) / 1000000000.0
rate := float64(len(data)) / dtDownload
actualRate = rate - max(rate-args.Ratelimit, 0)
progress = float32(i+1) / float32(len(chunklist.Chunks))
delayNow := bufferDt > RatelimitDelayAfter
ui.DownloadProgress(progress, actualRate, delayNow, false, retries, ep.Title)
if delayNow {
bufferDt = 0
// this simulates that the buffering is finished and the player is playing
time.Sleep(time.Duration(RatelimitDelay * float64(time.Second)))
} else if rate > args.Ratelimit {
// slow down, we are too fast.
deferTime := (rate - args.Ratelimit) / args.Ratelimit * dtDownload
time.Sleep(time.Duration(deferTime * float64(time.Second)))
}
videoFile.Write(data)
nextChunk++
infoFile.Truncate(0)
infoFile.Seek(0, io.SeekStart)
infoFile.Write([]byte(strconv.Itoa(nextChunk)))
var dtIteration float64 = float64(time.Now().UnixNano()-time1) / 1000000000.0
if !delayNow {
bufferDt += dtIteration
}
}
infoFile.Close()
if !keyboardInterrupt {
err := os.Remove(infoFilename)
if err != nil {
return err
}
}
return nil
}
func GetStreamEpisode(episode string) (StreamEpisode, error) {
ep := StreamEpisode{}
ep.Episode = episode
info_data, err := httpGet(
fmt.Sprintf(ApiBaseurlStreamEpisodeInfo, episode),
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
if err != nil {
return ep, err
}
// Title
json.Unmarshal(info_data, &ep)
ep.Title = strings.ToValidUTF8(ep.Title, "")
// Sort Chapters, correct offset and set index
sort.Slice(ep.Chapters, func(i int, j int) bool {
return ep.Chapters[i].Offset < ep.Chapters[j].Offset
})
for i := range ep.Chapters {
ep.Chapters[i].Offset = ep.Chapters[i].Offset * time.Second
ep.Chapters[i].Index = i
}
// Formats
playlist_url_data, err := httpGet(
fmt.Sprintf(ApiBaseurlStreamEpisodePlInfo, episode),
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
if err != nil {
return ep, err
}
json.Unmarshal(playlist_url_data, &ep)
playlist_data, err := httpGet(
ep.PlaylistUrl,
[]http.Header{ApiHeadersBase, ApiHeadersMetaAdditional},
time.Second*10,
)
ep.Formats = parseAvailFormatsFromM3u8(string(playlist_data))
return ep, err
}
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 {
return ChunkList{}, err
}
chunklist, err := parseChunkListFromM3u8(string(data), baseUrl)
return chunklist, err
}

View file

@ -1,8 +1,13 @@
package core
type UserInterface interface {
DownloadProgress(progress float32, rate float64, delaying bool, waiting bool, retries int, title string)
InfoMessage(msg string)
ErrorMessage(err error)
Aborted()
type DownloadProgress struct {
Aborted bool
Error error
Success bool
Delaying bool
Progress float32
Rate float64
Retries int
Title string
Waiting bool
}