Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Progress bars #40

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 217 additions & 111 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/fatih/color"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor"

cc "github.com/ivanpirog/coloredcobra"
)
Expand Down Expand Up @@ -57,139 +59,219 @@ Note arguments aren't really positional, you can specify them in any order:
manga-downloader --language es 10-20 https://mangadex.org/title/e7eabe96-aa17-476f-b431-2497d5e9d060/black-clover --bundle`),
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
s, errs := grabber.NewSite(getUrlArg(args), &settings)
if len(errs) > 0 {
color.Red("Errors testing site (a site may be down):")
for _, err := range errs {
color.Red(err.Error())
}
Run: Run,
}

// Run is the main function of the root command, the main downloading cmd
func Run(cmd *cobra.Command, args []string) {
s, errs := grabber.NewSite(getUrlArg(args), &settings)
if len(errs) > 0 {
color.Red("Errors testing site (a site may be down):")
for _, err := range errs {
color.Red(err.Error())
}
if s == nil {
color.Yellow("Site not recognised")
os.Exit(1)
}
if s == nil {
color.Yellow("Site not recognised")
os.Exit(1)
}
s.InitFlags(cmd)

// fetch series title
_, err := s.FetchTitle()
cerr(err, "Error fetching title: ")

// fetch all chapters
chapters, errs := s.FetchChapters()
if len(errs) > 0 {
color.Red("Errors fetching chapters:")
for _, err := range errs {
color.Red(err.Error())
}
s.InitFlags(cmd)

// fetch series title
title, err := s.FetchTitle()
cerr(err, "Error fetching title: %s")

// fetch all chapters
chapters, errs := s.FetchChapters()
if len(errs) > 0 {
color.Red("Errors fetching chapters:")
for _, err := range errs {
color.Red(err.Error())
}
os.Exit(1)
os.Exit(1)
}

chapters = chapters.SortByNumber()

var rngs []ranges.Range
// ranges argument is not provided
if len(args) == 1 {
lastChapter := chapters[len(chapters)-1].GetNumber()
prompt := promptui.Prompt{
Label: fmt.Sprintf("Do you want to download all %g chapters", lastChapter),
IsConfirm: true,
}

chapters = chapters.SortByNumber()
_, err := prompt.Run()

var rngs []ranges.Range
// ranges argument is not provided
if len(args) == 1 {
lastChapter := chapters[len(chapters)-1].GetNumber()
prompt := promptui.Prompt{
Label: fmt.Sprintf("Do you want to download all %g chapters", lastChapter),
IsConfirm: true,
}
if err != nil {
color.Yellow("Canceled by user")
os.Exit(0)
}

_, err := prompt.Run()
rngs = []ranges.Range{{Begin: 1, End: int64(lastChapter)}}
} else {
// ranges parsing
settings.Range = getRangesArg(args)
rngs, err = ranges.Parse(settings.Range)
cerr(err, "Error parsing ranges: ")
}

if err != nil {
color.Yellow("Canceled by user")
os.Exit(0)
}
// sort and filter specified ranges
chapters = chapters.FilterRanges(rngs)

rngs = []ranges.Range{{Begin: 1, End: int64(lastChapter)}}
} else {
// ranges parsing
settings.Range = getRangesArg(args)
rngs, err = ranges.Parse(settings.Range)
cerr(err, "Error parsing ranges: %s")
}
if len(chapters) == 0 {
color.Yellow("No chapters found for the specified ranges")
os.Exit(1)
}

// sort and filter specified ranges
chapters = chapters.FilterRanges(rngs)
// download chapters
wg := sync.WaitGroup{}
g := make(chan struct{}, s.GetMaxConcurrency().Chapters)
downloaded := grabber.Filterables{}
// progress bar
p := mpb.New(
mpb.WithWidth(40),
mpb.WithOutput(color.Output),
mpb.WithAutoRefresh(),
)

if len(chapters) == 0 {
color.Yellow("No chapters found for the specified ranges")
os.Exit(1)
}
green, blue := color.New(color.FgGreen), color.New(color.FgBlue)

// download chapters
wg := sync.WaitGroup{}
g := make(chan struct{}, s.GetMaxConcurrency().Chapters)
downloaded := grabber.Filterables{}

for _, chap := range chapters {
g <- struct{}{}
wg.Add(1)
go func(chap grabber.Filterable) {
defer wg.Done()
chapter, err := s.FetchChapter(chap)
if err != nil {
color.Red("- error fetching chapter %s: %s", chap.GetTitle(), err.Error())
<-g
return
}
fmt.Printf("fetched %s %s\n", color.CyanString(title), color.HiBlackString(chapter.GetTitle()))
for _, chap := range chapters {
g <- struct{}{}
wg.Add(1)

files, err := downloader.FetchChapter(s, chapter)
if err != nil {
color.Red("- error downloading chapter %s: %s", chapter.GetTitle(), err.Error())
<-g
return
}
go func(chap grabber.Filterable) {
defer wg.Done()

d := &packer.DownloadedChapter{
Chapter: chapter,
Files: files,
}
chapter, err := s.FetchChapter(chap)
if err != nil {
color.Red("- error fetching chapter %s: %s", chap.GetTitle(), err.Error())
<-g
return
}

if !settings.Bundle {
filename, err := packer.PackSingle(settings.OutputDir, s, d)
if err == nil {
fmt.Printf("- %s %s\n", color.GreenString("saved file"), color.HiBlackString(filename))
} else {
color.Red(err.Error())
}
// chapter download progress bar
title := fmt.Sprintf("%s:", truncateString(chap.GetTitle(), 30))
cdbar := p.AddBar(chapter.PagesCount,
mpb.PrependDecorators(
decor.Name(title, decor.WCSyncWidthR),
decor.Meta(decor.Name("downloading", decor.WC{C: decor.DextraSpace}), toMetaFunc(blue)),
decor.CountersNoUnit("%d / %d", decor.WC{C: decor.DextraSpace}),
),
mpb.AppendDecorators(
decor.OnCompleteMeta(
decor.OnComplete(decor.Percentage(decor.WC{W: 4}), "dld."),
toMetaFunc(green),
),
),
)
// save chapter progress bar
scbar := p.AddBar(chapter.PagesCount,
mpb.BarQueueAfter(cdbar),
mpb.BarFillerClearOnComplete(),
mpb.PrependDecorators(
decor.Name(title, decor.WCSyncWidthR),
decor.OnCompleteMeta(
decor.OnComplete(
decor.Meta(decor.Name("archiving", decor.WC{C: decor.DextraSpace}), toMetaFunc(blue)),
"done!",
),
toMetaFunc(green),
),
decor.OnComplete(decor.CountersNoUnit("%d / %d", decor.WC{C: decor.DextraSpace}), ""),
),
mpb.AppendDecorators(
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
),
)

files, err := downloader.FetchChapter(s, chapter, func(page, _ int) {
cdbar.IncrBy(page)
})
if err != nil {
color.Red("- error downloading chapter %s: %s", chapter.GetTitle(), err.Error())
<-g
return
}

d := &packer.DownloadedChapter{
Chapter: chapter,
Files: files,
}

if !settings.Bundle {
_, err := packer.PackSingle(settings.OutputDir, s, d, func(page, _ int) {
scbar.IncrBy(page)
})
// filename, err := packer.PackSingle(settings.OutputDir, s, d)
if err == nil {
// fmt.Printf("- %s %s\n", color.GreenString("saved file"), color.HiBlackString(filename))
} else {
// avoid adding it to memory if we're not gonna use it
downloaded = append(downloaded, d)
color.Red(err.Error())
}
} else {
// avoid adding it to memory if we're not gonna use it
downloaded = append(downloaded, d)
}

// release guard
<-g
}(chap)
}
wg.Wait()
close(g)
// release guard
<-g
}(chap)
}
// wait for all routines to finish
wg.Wait()
close(g)

if !settings.Bundle {
// if we're not bundling, just finish it
os.Exit(0)
}
if !settings.Bundle {
// if we're not bundling, we're done
os.Exit(0)
}

// resort downloaded
downloaded = downloaded.SortByNumber()
// resort downloaded
downloaded = downloaded.SortByNumber()

dc := []*packer.DownloadedChapter{}
// convert slice back to DownloadedChapter
for _, d := range downloaded {
dc = append(dc, d.(*packer.DownloadedChapter))
}
dc := []*packer.DownloadedChapter{}
tp := 0
// convert slice back to DownloadedChapter
for _, d := range downloaded {
chapter := d.(*packer.DownloadedChapter)
dc = append(dc, chapter)
tp += int(chapter.PagesCount)
}

filename, err := packer.PackBundle(settings.OutputDir, s, dc, settings.Range)
if err != nil {
color.Red(err.Error())
os.Exit(1)
}
// bundle progress bar
bbar := p.AddBar(int64(tp),
mpb.PrependDecorators(
decor.Name("Bundle", decor.WCSyncWidthR),
decor.OnCompleteMeta(
decor.OnComplete(
decor.Meta(decor.Name("bundling", decor.WC{C: decor.DextraSpace}), toMetaFunc(blue)),
"done!",
),
toMetaFunc(green),
),
decor.OnComplete(decor.CountersNoUnit("%d / %d", decor.WC{C: decor.DextraSpace}), ""),
),
mpb.AppendDecorators(
decor.OnCompleteMeta(
decor.OnComplete(decor.Percentage(decor.WC{W: 4}), "done"),
toMetaFunc(green),
),
),
)

filename, err := packer.PackBundle(settings.OutputDir, s, dc, settings.Range, func(page, _ int) {
bbar.IncrBy(page)
})

if err != nil {
color.Red(err.Error())
os.Exit(1)
}

fmt.Printf("- %s %s\n", color.GreenString("saved file"), color.HiBlackString(filename))
},
fmt.Printf("- %s %s\n", color.GreenString("saved file"), color.HiBlackString(filename))
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand Down Expand Up @@ -276,3 +358,27 @@ func getUrlArg(args []string) string {

return args[1]
}

// truncateString truncates the input string at a specified maximum length
// without cutting words. It finds the last space within the limit and truncates there.
func truncateString(input string, maxLength int) string {
if len(input) <= maxLength {
return input
}

// Find the last index of a space before maxLength
truncationPoint := strings.LastIndex(input[:maxLength], " ")
if truncationPoint == -1 {
// No spaces found, force to maxLength (cuts the word)
return input[:maxLength] + "..."
}

// Return substring up to the last found space
return input[:truncationPoint] + "..."
}

func toMetaFunc(c *color.Color) func(string) string {
return func(s string) string {
return c.Sprint(s)
}
}
Loading
Loading