Skip to content

Commit

Permalink
Add progress bars
Browse files Browse the repository at this point in the history
refs #24
  • Loading branch information
elboletaire committed Jan 2, 2025
1 parent 2ad147a commit 8b22c29
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 30 deletions.
83 changes: 71 additions & 12 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"regexp"
"strings"
"sync"
"syscall"

"github.com/elboletaire/manga-downloader/downloader"
"github.com/elboletaire/manga-downloader/grabber"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/spf13/cobra"
"github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor"
"golang.org/x/term"

cc "github.com/ivanpirog/coloredcobra"
)
Expand Down Expand Up @@ -78,7 +80,7 @@ func Run(cmd *cobra.Command, args []string) {
s.InitFlags(cmd)

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

// fetch all chapters
Expand Down Expand Up @@ -138,6 +140,10 @@ func Run(cmd *cobra.Command, args []string) {

green, blue := color.New(color.FgGreen), color.New(color.FgBlue)

// Get terminal width for title truncation
termWidth := getTerminalWidth()
mangaLen, chapterLen := calculateTitleLengths(termWidth)

for _, chap := range chapters {
g <- struct{}{}
wg.Add(1)
Expand All @@ -152,12 +158,27 @@ func Run(cmd *cobra.Command, args []string) {
return
}

// generate the filename for the chapter
filename, err := packer.NewFilenameFromTemplate(s.GetFilenameTemplate(), packer.NewChapterFileTemplateParts(title, chapter))
if err != nil {
color.Red("- error creating filename for chapter %s: %s", chapter.GetTitle(), err.Error())
<-g
return
}
filename += ".cbz"

// chapter download progress bar
title := fmt.Sprintf("%s:", truncateString(chap.GetTitle(), 30))
barTitle := fmt.Sprintf("%s - %s:", truncateString(title, mangaLen), truncateString(chap.GetTitle(), chapterLen))
status := "downloading"
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.Name(barTitle, decor.WCSyncWidthR),
decor.Any(func(s decor.Statistics) string {
if strings.HasPrefix(status, "error:") {
return color.New(color.FgRed).Sprint(status)
}
return blue.Sprint(status)
}, decor.WC{C: decor.DextraSpace}),
decor.CountersNoUnit("%d / %d", decor.WC{C: decor.DextraSpace}),
),
mpb.AppendDecorators(
Expand All @@ -172,11 +193,11 @@ func Run(cmd *cobra.Command, args []string) {
mpb.BarQueueAfter(cdbar),
mpb.BarFillerClearOnComplete(),
mpb.PrependDecorators(
decor.Name(title, decor.WCSyncWidthR),
decor.OnComplete(decor.Name(barTitle, decor.WC{}), ""),
decor.OnCompleteMeta(
decor.OnComplete(
decor.Meta(decor.Name("archiving", decor.WC{C: decor.DextraSpace}), toMetaFunc(blue)),
"done!",
filename+" saved",
),
toMetaFunc(green),
),
Expand All @@ -187,8 +208,13 @@ func Run(cmd *cobra.Command, args []string) {
),
)

files, err := downloader.FetchChapter(s, chapter, func(page, _ int) {
cdbar.IncrBy(page)
files, err := downloader.FetchChapter(s, chapter, func(page int, progress int, err error) {
if err != nil {
status = "error: " + err.Error()
cdbar.SetCurrent(int64(progress))
} else {
cdbar.IncrBy(page)
}
})
if err != nil {
color.Red("- error downloading chapter %s: %s", chapter.GetTitle(), err.Error())
Expand All @@ -205,10 +231,7 @@ func Run(cmd *cobra.Command, args []string) {
_, 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 {
if err != nil {
color.Red(err.Error())
}
} else {
Expand Down Expand Up @@ -359,9 +382,45 @@ func getUrlArg(args []string) string {
return args[1]
}

// getTerminalWidth returns the current terminal width or a default value if it can't be determined
func getTerminalWidth() int {
width, _, err := term.GetSize(int(syscall.Stdin))
if err != nil {
return 80 // default terminal width
}
return width
}

// calculateTitleLengths calculates how much space to allocate for manga and chapter titles
func calculateTitleLengths(termWidth int) (mangaLen, chapterLen int) {
// Reserve space for other elements in the progress bar:
// - status (e.g. "downloading", "archiving", etc.): ~15 chars
// - progress counter (e.g. "123/456"): ~10 chars
// - percentage: ~5 chars
// - decorators (spaces, colons, etc.): ~5 chars
reservedSpace := 35
availableSpace := termWidth - reservedSpace

// Allocate 60% to manga title and 40% to chapter title if there's enough space
if availableSpace > 20 {
mangaLen = (availableSpace * 60) / 100
chapterLen = (availableSpace * 40) / 100
} else {
// If space is very limited, use minimal lengths
mangaLen = 10
chapterLen = 10
}

return
}

// 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 maxLength <= 0 {
return ""
}

if len(input) <= maxLength {
return input
}
Expand Down
44 changes: 27 additions & 17 deletions downloader/fetch.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package downloader

import (
"fmt"
"io"
"sort"
"sync"
Expand All @@ -15,12 +16,16 @@ type File struct {
Page uint
}

// ProgressCallback is a function type for progress updates with optional error
type ProgressCallback func(page, progress int, err error)

// FetchChapter downloads all the pages of a chapter
func FetchChapter(site grabber.Site, chapter *grabber.Chapter, onprogress func(page, progress int)) (files []*File, err error) {
func FetchChapter(site grabber.Site, chapter *grabber.Chapter, onprogress ProgressCallback) (files []*File, err error) {
wg := sync.WaitGroup{}
guard := make(chan struct{}, site.GetMaxConcurrency().Pages)
errChan := make(chan error, 1)
errChan := make(chan error, len(chapter.Pages)) // Buffer for all possible page errors
done := make(chan bool)
var mu sync.Mutex

for _, page := range chapter.Pages {
guard <- struct{}{}
Expand All @@ -35,35 +40,40 @@ func FetchChapter(site grabber.Site, chapter *grabber.Chapter, onprogress func(p
Referer: site.BaseUrl(),
}, uint(page.Number))

pn := int(page.Number)
cp := pn * 100 / len(chapter.Pages)

if err != nil {
select {
case errChan <- err:
default:
}
errChan <- fmt.Errorf("page %d: %w", page.Number, err)
onprogress(pn, cp, err)
return
}

mu.Lock()
files = append(files, file)
pn := int(page.Number)
cp := pn * 100 / len(chapter.Pages)
mu.Unlock()

onprogress(pn, cp)
onprogress(pn, cp, nil)
}(page)
}

go func() {
wg.Wait()
// signal that all goroutines have completed
close(done)
close(errChan)
}()

select {
// in case of error, return the very first one
case err := <-errChan:
close(guard)
return nil, err
case <-done:
// all goroutines finished successfully, continue
// Collect all errors
var errors []error
for err := range errChan {
errors = append(errors, err)
}

<-done
close(guard)

if len(errors) > 0 {
return files, fmt.Errorf("failed to download %d pages", len(errors))
}

// sort files by page number
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down

0 comments on commit 8b22c29

Please sign in to comment.