Skip to content

Commit f3643c8

Browse files
Merge pull request #8 from robinovitch61/6-cbz
Add cbz output format option
2 parents 9bfac3a + 0266944 commit f3643c8

3 files changed

Lines changed: 162 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*.jpg
44
*.jpeg
55
*.pdf
6+
*.cbz
67
.DS_Store
78
webtoon-dl
89
opt

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# webtoon-dl
22

3-
Download [webtoon](https://www.webtoons.com/en/) comics as PDFs using a terminal/command line.
3+
Download [webtoon](https://www.webtoons.com/en/) comics as PDF or CBZ using a terminal/command line.
44

55
## Usage
66

@@ -11,6 +11,9 @@ webtoon-dl "<your-webtoon-episode-url>"
1111
# download entire series, default 10 episodes per pdf
1212
webtoon-dl "<your-webtoon-series-url>"
1313

14+
# download as cbz (default is pdf)
15+
webtoon-dl --format cbz "<your-webtoon-series-url>"
16+
1417
# specify a range of episodes (inclusive on both ends)
1518
webtoon-dl --min-ep=10 --max-ep=20 "<your-webtoon-series-url>"
1619

main.go

Lines changed: 157 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"archive/zip"
45
"bytes"
56
"encoding/json"
67
"flag"
@@ -31,6 +32,97 @@ type EpisodeBatch struct {
3132
maxEp int
3233
}
3334

35+
type ComicFile interface {
36+
addImage([]byte) error
37+
save(outFile string) error
38+
}
39+
40+
type PDFComicFile struct {
41+
pdf *gopdf.GoPdf
42+
}
43+
44+
// validate PDFComicFile implements ComicFile
45+
var _ ComicFile = &PDFComicFile{}
46+
47+
func newPDFComicFile() *PDFComicFile {
48+
pdf := gopdf.GoPdf{}
49+
pdf.Start(gopdf.Config{Unit: gopdf.UnitPT, PageSize: *gopdf.PageSizeA4})
50+
return &PDFComicFile{pdf: &pdf}
51+
}
52+
53+
func (c *PDFComicFile) addImage(img []byte) error {
54+
holder, err := gopdf.ImageHolderByBytes(img)
55+
if err != nil {
56+
return err
57+
}
58+
59+
d, _, err := image.DecodeConfig(bytes.NewReader(img))
60+
if err != nil {
61+
return err
62+
}
63+
64+
// gopdf assumes dpi 128 https://github.com/signintech/gopdf/issues/168
65+
// W and H are in points, 1 point = 1/72 inch
66+
// convert pixels (Width and Height) to points
67+
// subtract 1 point to account for margins
68+
c.pdf.AddPageWithOption(gopdf.PageOption{PageSize: &gopdf.Rect{
69+
W: float64(d.Width)*72/128 - 1,
70+
H: float64(d.Height)*72/128 - 1,
71+
}})
72+
return c.pdf.ImageByHolder(holder, 0, 0, nil)
73+
}
74+
75+
func (c *PDFComicFile) save(outputPath string) error {
76+
return c.pdf.WritePdf(outputPath)
77+
}
78+
79+
type CBZComicFile struct {
80+
zipWriter *zip.Writer
81+
buffer *bytes.Buffer
82+
numFiles int
83+
}
84+
85+
// validate CBZComicFile implements ComicFile
86+
var _ ComicFile = &CBZComicFile{}
87+
88+
func newCBZComicFile() (*CBZComicFile, error) {
89+
buffer := new(bytes.Buffer)
90+
zipWriter := zip.NewWriter(buffer)
91+
return &CBZComicFile{zipWriter: zipWriter, buffer: buffer, numFiles: 0}, nil
92+
}
93+
94+
func (c *CBZComicFile) addImage(img []byte) error {
95+
f, err := c.zipWriter.Create(fmt.Sprintf("%010d.jpg", c.numFiles))
96+
if err != nil {
97+
return err
98+
}
99+
_, err = f.Write(img)
100+
if err != nil {
101+
return err
102+
}
103+
c.numFiles++
104+
return nil
105+
}
106+
107+
func (c *CBZComicFile) save(outputPath string) error {
108+
if err := c.zipWriter.Close(); err != nil {
109+
return err
110+
}
111+
file, err := os.Create(outputPath)
112+
if err != nil {
113+
return err
114+
}
115+
defer func(file *os.File) {
116+
err := file.Close()
117+
if err != nil {
118+
fmt.Println(err.Error())
119+
os.Exit(1)
120+
}
121+
}(file)
122+
_, err = c.buffer.WriteTo(file)
123+
return err
124+
}
125+
34126
func getOzPageImgLinks(doc soup.Root) []string {
35127
// regex find the documentURL, e.g:
36128
// viewerOptions: {
@@ -253,37 +345,37 @@ func fetchImage(imgLink string) []byte {
253345
return buff.Bytes()
254346
}
255347

256-
func addImgToPdf(pdf *gopdf.GoPdf, imgLink string) error {
257-
img := fetchImage(imgLink)
258-
holder, err := gopdf.ImageHolderByBytes(img)
259-
if err != nil {
260-
return err
261-
}
262-
263-
d, _, err := image.DecodeConfig(bytes.NewReader(img))
264-
if err != nil {
265-
return err
348+
func getComicFile(format string) ComicFile {
349+
var comic ComicFile
350+
var err error
351+
comic = newPDFComicFile()
352+
if format == "cbz" {
353+
comic, err = newCBZComicFile()
354+
if err != nil {
355+
fmt.Println(err.Error())
356+
os.Exit(1)
357+
}
266358
}
359+
return comic
360+
}
267361

268-
// gopdf assumes dpi 128 https://github.com/signintech/gopdf/issues/168
269-
// W and H are in points, 1 point = 1/72 inch
270-
// convert pixels (Width and Height) to points
271-
// subtract 1 point to account for margins
272-
pdf.AddPageWithOption(gopdf.PageOption{PageSize: &gopdf.Rect{
273-
W: float64(d.Width)*72/128 - 1,
274-
H: float64(d.Height)*72/128 - 1,
275-
}})
276-
return pdf.ImageByHolder(holder, 0, 0, nil)
362+
type Opts struct {
363+
url string
364+
minEp int
365+
maxEp int
366+
epsPerFile int
367+
format string
277368
}
278369

279-
func main() {
280-
if len(os.Args) < 2 {
370+
func parseOpts(args []string) Opts {
371+
if len(args) < 2 {
281372
fmt.Println("Usage: webtoon-dl <url>")
282373
os.Exit(1)
283374
}
284375
minEp := flag.Int("min-ep", 0, "Minimum episode number to download (inclusive)")
285376
maxEp := flag.Int("max-ep", math.MaxInt, "Maximum episode number to download (inclusive)")
286377
epsPerFile := flag.Int("eps-per-file", 10, "Number of episodes to put in each PDF file")
378+
format := flag.String("format", "pdf", "Output format (pdf or cbz)")
287379
flag.Parse()
288380
if *minEp > *maxEp {
289381
fmt.Println("min-ep must be less than or equal to max-ep")
@@ -299,50 +391,72 @@ func main() {
299391
}
300392

301393
url := os.Args[len(os.Args)-1]
302-
episodeBatches := getEpisodeBatches(url, *minEp, *maxEp, *epsPerFile)
394+
return Opts{
395+
url: url,
396+
minEp: *minEp,
397+
maxEp: *maxEp,
398+
epsPerFile: *epsPerFile,
399+
format: *format,
400+
}
401+
}
402+
403+
func getOutFile(opts Opts, episodeBatch EpisodeBatch) string {
404+
outURL := strings.ReplaceAll(opts.url, "http://", "")
405+
outURL = strings.ReplaceAll(outURL, "https://", "")
406+
outURL = strings.ReplaceAll(outURL, "www.", "")
407+
outURL = strings.ReplaceAll(outURL, "webtoons.com/", "")
408+
outURL = strings.Split(outURL, "?")[0]
409+
outURL = strings.ReplaceAll(outURL, "/viewer", "")
410+
outURL = strings.ReplaceAll(outURL, "/", "-")
411+
if episodeBatch.minEp != episodeBatch.maxEp {
412+
outURL = fmt.Sprintf("%s-epNo%d-epNo%d.%s", outURL, episodeBatch.minEp, episodeBatch.maxEp, opts.format)
413+
} else {
414+
outURL = fmt.Sprintf("%s-epNo%d.%s", outURL, episodeBatch.minEp, opts.format)
415+
}
416+
return outURL
417+
}
303418

419+
func main() {
420+
opts := parseOpts(os.Args)
421+
episodeBatches := getEpisodeBatches(opts.url, opts.minEp, opts.maxEp, opts.epsPerFile)
304422
totalPages := 0
305423
for _, episodeBatch := range episodeBatches {
306424
totalPages += len(episodeBatch.imgLinks)
307425
}
308426
totalEpisodes := episodeBatches[len(episodeBatches)-1].maxEp - episodeBatches[0].minEp + 1
309427
fmt.Println(fmt.Sprintf("found %d total image links across %d episodes", totalPages, totalEpisodes))
310-
fmt.Println(fmt.Sprintf("saving into %d files with max of %d episodes per file", len(episodeBatches), *epsPerFile))
428+
fmt.Println(fmt.Sprintf("saving into %d files with max of %d episodes per file", len(episodeBatches), opts.epsPerFile))
311429

312430
for _, episodeBatch := range episodeBatches {
313-
pdf := gopdf.GoPdf{}
314-
pdf.Start(gopdf.Config{Unit: gopdf.UnitPT, PageSize: *gopdf.PageSizeA4})
431+
var err error
432+
outFile := getOutFile(opts, episodeBatch)
433+
comicFile := getComicFile(opts.format)
315434
for idx, imgLink := range episodeBatch.imgLinks {
316435
if strings.Contains(imgLink, ".gif") {
317436
fmt.Println(fmt.Sprintf("WARNING: skipping gif %s", imgLink))
318437
continue
319438
}
320-
err := addImgToPdf(&pdf, imgLink)
439+
err := comicFile.addImage(fetchImage(imgLink))
321440
if err != nil {
322441
fmt.Println(err.Error())
323442
os.Exit(1)
324443
}
325-
fmt.Println(fmt.Sprintf("saving episodes %d through %d: added page %d/%d", episodeBatch.minEp, episodeBatch.maxEp, idx+1, len(episodeBatch.imgLinks)))
326-
}
327-
328-
outURL := strings.ReplaceAll(url, "http://", "")
329-
outURL = strings.ReplaceAll(outURL, "https://", "")
330-
outURL = strings.ReplaceAll(outURL, "www.", "")
331-
outURL = strings.ReplaceAll(outURL, "webtoons.com/", "")
332-
outURL = strings.Split(outURL, "?")[0]
333-
outURL = strings.ReplaceAll(outURL, "/viewer", "")
334-
outURL = strings.ReplaceAll(outURL, "/", "-")
335-
if episodeBatch.minEp != episodeBatch.maxEp {
336-
outURL = fmt.Sprintf("%s-epNo%d-epNo%d", outURL, episodeBatch.minEp, episodeBatch.maxEp)
337-
} else {
338-
outURL = fmt.Sprintf("%s-epNo%d", outURL, episodeBatch.minEp)
444+
fmt.Println(
445+
fmt.Sprintf(
446+
"saving episodes %d through %d of %d: added page %d/%d",
447+
episodeBatch.minEp,
448+
episodeBatch.maxEp,
449+
totalEpisodes,
450+
idx+1,
451+
len(episodeBatch.imgLinks),
452+
),
453+
)
339454
}
340-
outPath := outURL + ".pdf"
341-
err := pdf.WritePdf(outPath)
455+
err = comicFile.save(outFile)
342456
if err != nil {
343457
fmt.Println(err.Error())
344458
os.Exit(1)
345459
}
346-
fmt.Println(fmt.Sprintf("saved to %s", outPath))
460+
fmt.Println(fmt.Sprintf("saved to %s", outFile))
347461
}
348462
}

0 commit comments

Comments
 (0)