mirror of
https://github.com/NohamR/papeer.git
synced 2026-05-25 20:00:47 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008e4ebd7a | ||
|
|
5e735f9c52 | ||
|
|
29008185a8 | ||
|
|
ff3d09c727 | ||
|
|
0009435769 | ||
|
|
4e9b0611e8 | ||
|
|
e7ffd8c66c | ||
|
|
84e6ad8585 | ||
|
|
d593a74e6e | ||
|
|
d5971a2819 | ||
|
|
2d1d5a964a |
14
Makefile
Normal file
14
Makefile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
format:
|
||||||
|
gofmt -s -w .
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test github.com/lapwat/papeer/book
|
||||||
|
|
||||||
|
install:
|
||||||
|
go install
|
||||||
|
|
||||||
|
clean:
|
||||||
|
find . -maxdepth 1 -not -name 'README.md' -name '*.md' -delete
|
||||||
|
find . -maxdepth 1 -name '*.epub' -delete
|
||||||
|
find . -maxdepth 1 -name '*.mobi' -delete
|
||||||
|
find . -maxdepth 1 -name 'papeer-v*' -delete
|
||||||
161
README.md
161
README.md
@@ -1,53 +1,6 @@
|
|||||||
```
|
# Papeer
|
||||||
❯ papeer get --format epub --recursive --delay 500 --limit 10 https://news.ycombinator.com/
|
|
||||||
6s [===============================================>--------------------] 70% Status: 7 out of 10 chapters
|
|
||||||
0s [====================================================================] 100% 1. Three ex-US intelligence officers admit hacking for UAE
|
|
||||||
0s [====================================================================] 100% 2. Show HN: Time Travel Debugger
|
|
||||||
0s [====================================================================] 100% 3. How much faster is Java 17?
|
|
||||||
0s [====================================================================] 100% 4. The First Webcam Was Invented to Keep an Eye on a Coffee Pot
|
|
||||||
0s [====================================================================] 100% 5. Nikon's 2021 Photomicrography Competition Winners
|
|
||||||
0s [====================================================================] 100% 6. HTTP Status 418 – I'm a teapot
|
|
||||||
0s [====================================================================] 100% 7. H3: Hexagonal hierarchical geospatial indexing system
|
|
||||||
--- [--------------------------------------------------------------------] 0% 8. Automatic cipher suite ordering in Go’s crypto/tls
|
|
||||||
--- [--------------------------------------------------------------------] 0% 9. Find engineering roles at over 800 YC-funded startups
|
|
||||||
--- [--------------------------------------------------------------------] 0% 10. Futarchy: Robin Hanson on prediction markets
|
|
||||||
Ebook saved to "Hacker_News.epub"
|
|
||||||
```
|
|
||||||
|
|
||||||
# Installation
|
Papeer is a tool that lets you scrape content from the internet. It can scrape any web page, keeping only relevant content (formatted text and images) and removing ads and menus. You can export the content to Markdown, EPUB or MOBI files.
|
||||||
|
|
||||||
## From source
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go get -u github.com/lapwat/papeer
|
|
||||||
```
|
|
||||||
|
|
||||||
## From binary
|
|
||||||
|
|
||||||
### On Linux / MacOS
|
|
||||||
|
|
||||||
```sh
|
|
||||||
platform=linux
|
|
||||||
# platform=darwin for MacOS
|
|
||||||
curl -L https://github.com/lapwat/papeer/releases/download/v0.1.0/papeer-v0.1.0-$platform-amd64 > papeer
|
|
||||||
chmod +x papeer
|
|
||||||
sudo mv papeer /usr/local/bin
|
|
||||||
```
|
|
||||||
|
|
||||||
### On Windows
|
|
||||||
|
|
||||||
Download [latest release](https://github.com/lapwat/papeer/releases/download/v0.1.0/papeer-v0.1.0-windows-amd64.exe).
|
|
||||||
|
|
||||||
## Install kindlegen to export websites to MOBI (optional)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
TMPDIR=$(mktemp -d -t papeer-XXXXX)
|
|
||||||
curl -L https://github.com/lapwat/papeer/releases/download/kindlegen/kindlegen_linux_2.6_i386_v2_9.tar.gz > $TMPDIR/kindlegen.tar.gz
|
|
||||||
tar xzvf $TMPDIR/kindlegen.tar.gz -C $TMPDIR
|
|
||||||
chmod +x $TMPDIR/kindlegen
|
|
||||||
sudo mv $TMPDIR/kindlegen /usr/local/bin
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
@@ -66,20 +19,120 @@ Available Commands:
|
|||||||
version Print the version number of papeer
|
version Print the version number of papeer
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
-a, --author string book author
|
||||||
-d, --delay int time to wait before downloading next chapter, in milliseconds (default -1)
|
-d, --delay int time to wait before downloading next chapter, in milliseconds (default -1)
|
||||||
-f, --format string file format [md, epub, mobi] (default "md")
|
-f, --format string file format [stdout, md, epub, mobi] (default "stdout")
|
||||||
-h, --help help for papeer
|
-h, --help help for papeer
|
||||||
|
--images retrieve images only
|
||||||
-i, --include include URL as first chapter, in resursive mode
|
-i, --include include URL as first chapter, in resursive mode
|
||||||
-l, --limit int limit number of chapters, in recursive mode (default -1)
|
-l, --limit int limit number of chapters, in recursive mode (default -1)
|
||||||
-o, --output string output file
|
-n, --name string book name (default: page title)
|
||||||
-q, --quiet do not show logs
|
-o, --offset int skip first chapters, in recursive mode
|
||||||
|
--output string file name (default: book name)
|
||||||
|
-q, --quiet hide progress bar
|
||||||
-r, --recursive create one chapter per natigation item
|
-r, --recursive create one chapter per natigation item
|
||||||
-s, --selector string table of content CSS selector
|
-s, --selector string table of content CSS selector, in resursive mode
|
||||||
--stdout print to standard output
|
-t, --threads int download concurrency, in recursive mode (default -1)
|
||||||
|
|
||||||
Use "papeer [command] --help" for more information about a command.
|
Use "papeer [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Grab a single page
|
||||||
|
|
||||||
|
The `get` command lets you retrieve the content of a web page.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
papeer get https://www.eff.org/cyberspace-independence
|
||||||
|
# A Declaration of the Independence of Cyberspace
|
||||||
|
# ===============================================
|
||||||
|
|
||||||
|
# Governments of the Industrial World, you weary giants of flesh and steel, I come from Cyberspace, the new home of Mind. On behalf of the future, I ask you of the past to leave us alone. You are not welcome among us. You have no sovereignty where we gather...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grab several pages (recursive mode)
|
||||||
|
|
||||||
|
The `recursive` option lets you extract the table of content of a website, then scrape the content of each one of its pages.
|
||||||
|
|
||||||
|
### Display table of content
|
||||||
|
|
||||||
|
Before trying the `recursive` option, it is a good idea to use the `ls` option, which lets you vizualize the content that will be retrieved. You can use several options to customize the table of content extraction, such as `selector`, `limit`, `offset` and `include`. Type `papeer help` for more information about those options.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
papeer ls https://12factor.net/ -s 'section.concrete > article > h2 > a'
|
||||||
|
# # NAME URL
|
||||||
|
# 1 I. Codebase https://12factor.net/codebase
|
||||||
|
# 2 II. Dependencies https://12factor.net/dependencies
|
||||||
|
# 3 III. Config https://12factor.net/config
|
||||||
|
# 4 IV. Backing services https://12factor.net/backing-services
|
||||||
|
# 5 V. Build, release, run https://12factor.net/build-release-run
|
||||||
|
# 6 VI. Processes https://12factor.net/processes
|
||||||
|
# 7 VII. Port binding https://12factor.net/port-binding
|
||||||
|
# 8 VIII. Concurrency https://12factor.net/concurrency
|
||||||
|
# 9 IX. Disposability https://12factor.net/disposability
|
||||||
|
# 10 X. Dev/prod parity https://12factor.net/dev-prod-parity
|
||||||
|
# 11 XI. Logs https://12factor.net/logs
|
||||||
|
# 12 XII. Admin processes https://12factor.net/admin-processes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scrape time
|
||||||
|
|
||||||
|
Once you are satisfied with the table of content listed by the `ls` command, you can actually scrape the content of those pages. You can use the same options that you specified for the `ls` command. In recursive mode, you also have the possibility to use `delay` and `threads` options.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
papeer get https://12factor.net/ --recursive -s 'section.concrete > article > h2 > a' --format=md
|
||||||
|
# [======================================>-----------------------------] Chapters 7 / 12
|
||||||
|
# [====================================================================] 1. I. Codebase
|
||||||
|
# [====================================================================] 2. II. Dependencies
|
||||||
|
# [====================================================================] 3. III. Config
|
||||||
|
# [====================================================================] 4. IV. Backing services
|
||||||
|
# [====================================================================] 5. V. Build, release, run
|
||||||
|
# [====================================================================] 6. VI. Processes
|
||||||
|
# [====================================================================] 7. VII. Port binding
|
||||||
|
# [--------------------------------------------------------------------] 8. VIII. Concurrency
|
||||||
|
# [--------------------------------------------------------------------] 9. IX. Disposability
|
||||||
|
# [--------------------------------------------------------------------] 10. X. Dev/prod parity
|
||||||
|
# [--------------------------------------------------------------------] 11. XI. Logs
|
||||||
|
# [--------------------------------------------------------------------] 12. XII. Admin processes
|
||||||
|
# Markdown saved to "The_Twelve-Factor_App.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## From source
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get -u github.com/lapwat/papeer
|
||||||
|
```
|
||||||
|
|
||||||
|
## From binary
|
||||||
|
|
||||||
|
### On Linux / MacOS
|
||||||
|
|
||||||
|
```sh
|
||||||
|
platform=linux # use platform=darwin for MacOS
|
||||||
|
release=0.3.3
|
||||||
|
curl -L https://github.com/lapwat/papeer/releases/download/v$release/papeer-v$release-$platform-amd64 > papeer
|
||||||
|
chmod +x papeer
|
||||||
|
sudo mv papeer /usr/local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### On Windows
|
||||||
|
|
||||||
|
Download [latest release](https://github.com/lapwat/papeer/releases/download/3/papeer-v0.3.3-windows-amd64.exe).
|
||||||
|
|
||||||
|
## Install kindlegen to export websites to MOBI (optional)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
TMPDIR=$(mktemp -d -t papeer-XXXXX)
|
||||||
|
curl -L https://github.com/lapwat/papeer/releases/download/kindlegen/kindlegen_linux_2.6_i386_v2_9.tar.gz > $TMPDIR/kindlegen.tar.gz
|
||||||
|
tar xzvf $TMPDIR/kindlegen.tar.gz -C $TMPDIR
|
||||||
|
chmod +x $TMPDIR/kindlegen
|
||||||
|
sudo mv $TMPDIR/kindlegen /usr/local/bin
|
||||||
|
rm -rf $TMPDIR
|
||||||
|
```
|
||||||
|
|
||||||
# Autocompletion
|
# Autocompletion
|
||||||
|
|
||||||
Execute this command in your current shell, or add it to your `.bashrc`.
|
Execute this command in your current shell, or add it to your `.bashrc`.
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
package book
|
package book
|
||||||
|
|
||||||
type chapter struct {
|
type chapter struct {
|
||||||
name string
|
body string
|
||||||
author string
|
name string
|
||||||
content string
|
author string
|
||||||
|
content string
|
||||||
|
subChapters []chapter
|
||||||
|
config *ScrapeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChapter(name, author, content string) chapter {
|
func NewChapter(body, name, author, content string, subChapters []chapter, config *ScrapeConfig) chapter {
|
||||||
return chapter{name, author, content}
|
return chapter{body, name, author, content, subChapters, config}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c chapter) Body() string {
|
||||||
|
return c.body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c chapter) Name() string {
|
func (c chapter) Name() string {
|
||||||
@@ -21,3 +28,7 @@ func (c chapter) Author() string {
|
|||||||
func (c chapter) Content() string {
|
func (c chapter) Content() string {
|
||||||
return c.content
|
return c.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c chapter) SubChapters() []chapter {
|
||||||
|
return c.subChapters
|
||||||
|
}
|
||||||
|
|||||||
143
book/format.go
Normal file
143
book/format.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package book
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
epub "github.com/bmaupin/go-epub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Filename(name string) string {
|
||||||
|
filename := name
|
||||||
|
|
||||||
|
filename = strings.ReplaceAll(filename, " ", "_")
|
||||||
|
filename = strings.ReplaceAll(filename, "/", "")
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarkdown(c chapter) string {
|
||||||
|
markdown := ""
|
||||||
|
|
||||||
|
if c.config.include {
|
||||||
|
// title
|
||||||
|
markdown += fmt.Sprintf("%s\n", c.Name())
|
||||||
|
markdown += fmt.Sprintf("%s\n\n", strings.Repeat("=", len(c.Name())))
|
||||||
|
|
||||||
|
// convert content to markdown
|
||||||
|
content, err := md.NewConverter("", true, nil).ConvertString(c.Content())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
markdown += fmt.Sprintf("%s\n\n\n", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sc := range c.SubChapters() {
|
||||||
|
// subchapters content
|
||||||
|
markdown += fmt.Sprintf("%s\n\n\n", ToMarkdown(sc))
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToEpub(c chapter, filename string) string {
|
||||||
|
if len(filename) == 0 {
|
||||||
|
filename = fmt.Sprintf("%s.epub", Filename(c.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// init ebook
|
||||||
|
e := epub.NewEpub(c.Name())
|
||||||
|
e.SetAuthor(c.Author())
|
||||||
|
|
||||||
|
AppendToEpub(e, c)
|
||||||
|
|
||||||
|
err := e.Write(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendToEpub(e *epub.Epub, c chapter) {
|
||||||
|
content := ""
|
||||||
|
|
||||||
|
if c.config.include {
|
||||||
|
|
||||||
|
if c.config.imagesOnly == false {
|
||||||
|
content = c.Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse content
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(c.Content()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// download images and replace src in img tags of content
|
||||||
|
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||||
|
src, _ := s.Attr("src")
|
||||||
|
src = strings.Split(src, "?")[0] // remove query part
|
||||||
|
imagePath, _ := e.AddImage(src, "")
|
||||||
|
|
||||||
|
if c.config.imagesOnly {
|
||||||
|
imageTag, _ := goquery.OuterHtml(s)
|
||||||
|
content += strings.Replace(imageTag, src, imagePath, 1)
|
||||||
|
} else {
|
||||||
|
content = strings.Replace(content, src, imagePath, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
html := ""
|
||||||
|
// add title only if imagesOnly = false
|
||||||
|
if c.config.imagesOnly == false {
|
||||||
|
html += fmt.Sprintf("<h1>%s</h1>", c.Name())
|
||||||
|
}
|
||||||
|
html += content
|
||||||
|
|
||||||
|
// write to epub file
|
||||||
|
_, err = e.AddSection(html, c.Name(), "", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sc := range c.SubChapters() {
|
||||||
|
AppendToEpub(e, sc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMobi(c chapter, filename string) string {
|
||||||
|
if len(filename) == 0 {
|
||||||
|
filename = fmt.Sprintf("%s.mobi", Filename(c.Name()))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// add .mobi extension if not specified
|
||||||
|
if strings.HasSuffix(filename, ".mobi") == false {
|
||||||
|
filename = fmt.Sprintf("%s.mobi", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
filenameEPUB := strings.ReplaceAll(filename, ".mobi", ".epub")
|
||||||
|
ToEpub(c, filenameEPUB)
|
||||||
|
|
||||||
|
exec.Command("kindlegen", filenameEPUB).Run()
|
||||||
|
// exec command always return status 1 even if it succeed
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
err := os.Remove(filenameEPUB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
79
book/format_test.go
Normal file
79
book/format_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package book
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilename(t *testing.T) {
|
||||||
|
|
||||||
|
got := Filename("This is a chapter / book")
|
||||||
|
want := "This_is_a_chapter__book"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToMarkdown(t *testing.T) {
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{NewScrapeConfig()}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := ToMarkdown(c)
|
||||||
|
want := "Books\n=====\n\n- [Discours de la Méthode](https://books.lapw.at/posts/ren%C3%A9-descartes-discours-de-la-m%C3%A9thode/)clock 98 min read -\n1637\n\n- [The Twelve-Factor App](https://books.lapw.at/posts/adam-wiggins-the-twelve-factor-app/)clock 22 min read -\n2011\n\n\n"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToEpub(t *testing.T) {
|
||||||
|
|
||||||
|
filename := "Books.epub"
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{NewScrapeConfig()}, 0, func(index int, name string) {})
|
||||||
|
ToEpub(c, "")
|
||||||
|
|
||||||
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("%s does not exist: %v", filename, err)
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(filename); err != nil {
|
||||||
|
t.Errorf("cannot remove %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToEpubFilename(t *testing.T) {
|
||||||
|
|
||||||
|
filename := "ebook.epub"
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{NewScrapeConfig()}, 0, func(index int, name string) {})
|
||||||
|
ToEpub(c, filename)
|
||||||
|
|
||||||
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("%s does not exist: %v", filename, err)
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(filename); err != nil {
|
||||||
|
t.Errorf("cannot remove %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToMobi(t *testing.T) {
|
||||||
|
|
||||||
|
filename := "ebook.mobi"
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{NewScrapeConfig()}, 0, func(index int, name string) {})
|
||||||
|
ToMobi(c, filename)
|
||||||
|
|
||||||
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("%s does not exist: %v", filename, err)
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(filename); err != nil {
|
||||||
|
t.Errorf("cannot remove %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,11 +3,10 @@ package book
|
|||||||
type link struct {
|
type link struct {
|
||||||
href string
|
href string
|
||||||
text string
|
text string
|
||||||
class string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLink(href, text, class string) link {
|
func NewLink(href, text string) link {
|
||||||
return link{href, text, class}
|
return link{href, text}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c link) Href() string {
|
func (c link) Href() string {
|
||||||
@@ -17,7 +16,3 @@ func (c link) Href() string {
|
|||||||
func (c link) Text() string {
|
func (c link) Text() string {
|
||||||
return c.text
|
return c.text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c link) Class() string {
|
|
||||||
return c.class
|
|
||||||
}
|
|
||||||
|
|||||||
59
book/progress.go
Normal file
59
book/progress.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package book
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gosuri/uiprogress"
|
||||||
|
)
|
||||||
|
|
||||||
|
type progress struct {
|
||||||
|
global *uiprogress.Bar
|
||||||
|
individuals []*uiprogress.Bar
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgress(links []link, parent string, depth int) progress {
|
||||||
|
uiprogress.Start()
|
||||||
|
|
||||||
|
global := uiprogress.AddBar(len(links))
|
||||||
|
indentGlobal := strings.Repeat("> ", depth)
|
||||||
|
global.AppendFunc(func(b *uiprogress.Bar) string {
|
||||||
|
return fmt.Sprintf("%v%v (%v / %v)", indentGlobal, parent, b.Current(), len(links))
|
||||||
|
})
|
||||||
|
|
||||||
|
// hide individual bars if more than 50 chapters
|
||||||
|
individuals := []*uiprogress.Bar{}
|
||||||
|
indent := strings.Repeat("- ", depth)
|
||||||
|
if len(links) <= 50 {
|
||||||
|
for index, link := range links {
|
||||||
|
bar := uiprogress.AddBar(1)
|
||||||
|
barText := fmt.Sprintf("%v#%v %v", indent, index+1, link.Text())
|
||||||
|
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
||||||
|
return barText
|
||||||
|
})
|
||||||
|
individuals = append(individuals, bar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress{global, individuals}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progress) IncrementGlobal() {
|
||||||
|
p.global.Incr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progress) Increment(index int) {
|
||||||
|
p.IncrementGlobal()
|
||||||
|
if len(p.individuals) > index {
|
||||||
|
p.individuals[index].Incr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progress) UpdateName(index int, name string) {
|
||||||
|
if len(p.individuals) > index {
|
||||||
|
barText := fmt.Sprintf("%s", name)
|
||||||
|
p.individuals[index].AppendFunc(func(b *uiprogress.Bar) string {
|
||||||
|
return barText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
365
book/scraper.go
365
book/scraper.go
@@ -1,9 +1,12 @@
|
|||||||
package book
|
package book
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
urllib "net/url"
|
urllib "net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -12,73 +15,253 @@ import (
|
|||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
readability "github.com/go-shiori/go-readability"
|
readability "github.com/go-shiori/go-readability"
|
||||||
colly "github.com/gocolly/colly/v2"
|
colly "github.com/gocolly/colly/v2"
|
||||||
"github.com/gosuri/uiprogress"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewBookFromURL(url, selector string, recursive, include bool, limit, delay int) book {
|
type ScrapeConfig struct {
|
||||||
|
depth int
|
||||||
|
selector string
|
||||||
|
limit int
|
||||||
|
offset int
|
||||||
|
delay int
|
||||||
|
threads int
|
||||||
|
include bool
|
||||||
|
imagesOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScrapeConfig() *ScrapeConfig {
|
||||||
|
return &ScrapeConfig{0, "", -1, 0, -1, -1, true, false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScrapeConfigsAjin() []*ScrapeConfig {
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.depth = 0
|
||||||
|
config0.selector = ".dt>a"
|
||||||
|
config0.limit = 3
|
||||||
|
config0.offset = 0
|
||||||
|
config0.delay = 5000
|
||||||
|
config0.include = false
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
config1.depth = 1
|
||||||
|
config1.selector = ".nav_apb>a"
|
||||||
|
config1.limit = 3
|
||||||
|
config1.offset = 1
|
||||||
|
config1.delay = 5000
|
||||||
|
config1.include = false
|
||||||
|
|
||||||
|
config2 := NewScrapeConfig()
|
||||||
|
config2.depth = 2
|
||||||
|
config2.imagesOnly = true
|
||||||
|
|
||||||
|
return []*ScrapeConfig{config0, config1, config2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScrapeConfigsWikipedia() []*ScrapeConfig {
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.depth = 0
|
||||||
|
config0.threads = -1
|
||||||
|
config0.include = true
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
config1.depth = 1
|
||||||
|
config1.include = true
|
||||||
|
|
||||||
|
return []*ScrapeConfig{config0, config1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScrapeConfigFake() *ScrapeConfig {
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
config.include = false
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBookFromURL(url, selector, name, author string, recursive, include, imagesOnly, quiet bool, limit, offset, delay, threads int) book {
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
config1.imagesOnly = imagesOnly
|
||||||
|
|
||||||
|
var chapters []chapter
|
||||||
|
var home chapter
|
||||||
|
|
||||||
if recursive {
|
if recursive {
|
||||||
home := NewChapterFromURL(url)
|
config2 := NewScrapeConfig()
|
||||||
b := New(home.Name(), home.Author())
|
config2.selector = selector
|
||||||
|
config2.limit = limit
|
||||||
chapters := tableOfContent(url, selector, limit, delay)
|
config2.offset = offset
|
||||||
if include {
|
config2.delay = delay
|
||||||
b.AddChapter(home)
|
config2.threads = threads
|
||||||
}
|
config2.include = include
|
||||||
for _, c := range chapters {
|
config2.imagesOnly = imagesOnly
|
||||||
b.AddChapter(c)
|
chapters, home = tableOfContent(url, config2, config1, quiet)
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
} else {
|
} else {
|
||||||
c := NewChapterFromURL(url)
|
chapters = []chapter{NewChapterFromURL(url, []*ScrapeConfig{config1}, 0, func(index int, name string) {})}
|
||||||
b := New(c.Name(), c.Author())
|
home = chapters[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = home.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(author) == 0 {
|
||||||
|
author = home.Author()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := New(name, author)
|
||||||
|
for _, c := range chapters {
|
||||||
b.AddChapter(c)
|
b.AddChapter(c)
|
||||||
return b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChapterFromURL(url string) chapter {
|
|
||||||
article, err := readability.FromURL(url, 30*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to parse %s, %v\n", url, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapter{article.Title, article.Byline, article.Content}
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableOfContent(url, selector string, limit, delay int) []chapter {
|
func NewChapterFromURL(url string, configs []*ScrapeConfig, index int, updateProgressBarName func(index int, name string)) chapter {
|
||||||
|
config := configs[0]
|
||||||
|
|
||||||
base, err := urllib.Parse(url)
|
base, err := urllib.Parse(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
links := GetLinks(base, selector)
|
// get page body
|
||||||
if limit != -1 {
|
response, err := http.Get(url)
|
||||||
limit = int(math.Min(float64(limit), float64(len(links))))
|
if err != nil {
|
||||||
links = links[:limit]
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// duplicate response stream
|
||||||
|
readabilityReader := &bytes.Buffer{}
|
||||||
|
bodyReader := io.TeeReader(response.Body, readabilityReader)
|
||||||
|
|
||||||
|
// extract HTML body
|
||||||
|
body, err := io.ReadAll(bodyReader)
|
||||||
|
|
||||||
|
// extract article content and metadata
|
||||||
|
article, err := readability.FromReader(readabilityReader, base)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse %s, %v\n", url, err)
|
||||||
|
}
|
||||||
|
name := article.Title
|
||||||
|
|
||||||
|
// notify progress bar with new name
|
||||||
|
updateProgressBarName(index, name)
|
||||||
|
|
||||||
|
subchapters := []chapter{}
|
||||||
|
if len(configs) > 1 {
|
||||||
|
// add subchapters
|
||||||
|
|
||||||
|
links, _, err := GetLinks(base, config.selector, config.limit, config.offset, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subchapters = make([]chapter, len(links))
|
||||||
|
progress := NewProgress(links, name, config.depth)
|
||||||
|
|
||||||
|
if config.delay >= 0 {
|
||||||
|
|
||||||
|
// synchronous mode
|
||||||
|
for index, link := range links {
|
||||||
|
// and then use it to parse relative URLs
|
||||||
|
u, err := base.Parse(link.href)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := NewChapterFromURL(u.String(), configs[1:], index, progress.UpdateName)
|
||||||
|
subchapters[index] = sc
|
||||||
|
progress.Increment(index)
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(config.delay) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// asynchronous mode
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
threads := config.threads
|
||||||
|
if threads == -1 {
|
||||||
|
threads = len(links)
|
||||||
|
}
|
||||||
|
semaphore := make(chan bool, threads)
|
||||||
|
|
||||||
|
for index, l := range links {
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- true
|
||||||
|
|
||||||
|
go func(index int, l link) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// and then use it to parse relative URLs
|
||||||
|
u, err := base.Parse(l.href)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := NewChapterFromURL(u.String(), configs[1:], index, progress.UpdateName)
|
||||||
|
subchapters[index] = sc
|
||||||
|
progress.Increment(index)
|
||||||
|
|
||||||
|
<-semaphore
|
||||||
|
}(index, l)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := ""
|
||||||
|
if config.include {
|
||||||
|
|
||||||
|
// we care about the content only if we include this level
|
||||||
|
content = article.Content
|
||||||
|
|
||||||
|
// extract images
|
||||||
|
if config.imagesOnly {
|
||||||
|
|
||||||
|
// parse HTML
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append every image to content
|
||||||
|
content = ""
|
||||||
|
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||||
|
imageTag, _ := goquery.OuterHtml(s)
|
||||||
|
imageTag = strings.ReplaceAll(imageTag, "\n", "")
|
||||||
|
|
||||||
|
content += imageTag
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapter{string(body), name, article.Byline, content, subchapters, config}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableOfContent(url string, config *ScrapeConfig, subConfig *ScrapeConfig, quiet bool) ([]chapter, chapter) {
|
||||||
|
base, err := urllib.Parse(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
links, home, err := GetLinks(base, config.selector, config.limit, config.offset, config.include)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chapters := make([]chapter, len(links))
|
chapters := make([]chapter, len(links))
|
||||||
|
delay := config.delay
|
||||||
|
|
||||||
// init global progress bar
|
var p progress
|
||||||
uiprogress.Start()
|
if quiet == false {
|
||||||
barGlobal := uiprogress.AddBar(len(links)).AppendCompleted().PrependElapsed()
|
p = NewProgress(links, "", 0)
|
||||||
barGlobal.AppendFunc(func(b *uiprogress.Bar) string {
|
|
||||||
return fmt.Sprintf("Status: %d out of %d chapters", b.Current(), len(links))
|
|
||||||
})
|
|
||||||
|
|
||||||
// init progress bars
|
|
||||||
bars := []*uiprogress.Bar{}
|
|
||||||
for index, link := range links {
|
|
||||||
bar := uiprogress.AddBar(1).AppendCompleted().PrependElapsed()
|
|
||||||
barText := fmt.Sprintf("%d. %s", index+1, link.text)
|
|
||||||
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
|
||||||
return barText
|
|
||||||
})
|
|
||||||
bars = append(bars, bar)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if delay >= 0 {
|
if delay >= 0 {
|
||||||
|
// synchronous mode
|
||||||
|
|
||||||
for index, link := range links {
|
for index, link := range links {
|
||||||
// and then use it to parse relative URLs
|
// and then use it to parse relative URLs
|
||||||
u, err := base.Parse(link.href)
|
u, err := base.Parse(link.href)
|
||||||
@@ -86,23 +269,35 @@ func tableOfContent(url, selector string, limit, delay int) []chapter {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chapters[index] = NewChapterFromURL(u.String())
|
chapters[index] = NewChapterFromURL(u.String(), []*ScrapeConfig{subConfig}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
bars[index].Incr()
|
if quiet == false {
|
||||||
barGlobal.Incr()
|
p.Increment(index)
|
||||||
|
|
||||||
// do not wait after downloading last chapter
|
|
||||||
if index < len(links)-1 {
|
|
||||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// short sleep for last chapter to let the progress bar update
|
||||||
|
if index == len(links)-1 {
|
||||||
|
delay = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// asynchronous mode
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
threads := config.threads
|
||||||
|
if threads == -1 {
|
||||||
|
threads = len(links)
|
||||||
|
}
|
||||||
|
semaphore := make(chan bool, threads)
|
||||||
|
|
||||||
for index, l := range links {
|
for index, l := range links {
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
semaphore <- true
|
||||||
|
|
||||||
go func(index int, l link) {
|
go func(index int, l link) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
@@ -112,15 +307,19 @@ func tableOfContent(url, selector string, limit, delay int) []chapter {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chapters[index] = NewChapterFromURL(u.String())
|
chapters[index] = NewChapterFromURL(u.String(), []*ScrapeConfig{subConfig}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
bars[index].Incr()
|
if quiet == false {
|
||||||
barGlobal.Incr()
|
p.Increment(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-semaphore
|
||||||
}(index, l)
|
}(index, l)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
return chapters
|
|
||||||
|
return chapters, home
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPath(elm *goquery.Selection) string {
|
func GetPath(elm *goquery.Selection) string {
|
||||||
@@ -140,8 +339,7 @@ func GetPath(elm *goquery.Selection) string {
|
|||||||
return join
|
return join
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLinks(url *urllib.URL, selector string, limit, offset int, include bool) ([]link, chapter, error) {
|
||||||
func GetLinks(url *urllib.URL, selector string) []link {
|
|
||||||
selectorSet := true
|
selectorSet := true
|
||||||
if selector == "" {
|
if selector == "" {
|
||||||
selector = "a"
|
selector = "a"
|
||||||
@@ -158,13 +356,17 @@ func GetLinks(url *urllib.URL, selector string) []link {
|
|||||||
href := e.Attr("href")
|
href := e.Attr("href")
|
||||||
text := strings.TrimSpace(e.Text)
|
text := strings.TrimSpace(e.Text)
|
||||||
path := GetPath(e.DOM)
|
path := GetPath(e.DOM)
|
||||||
class := e.Attr("class")
|
key := path
|
||||||
key := fmt.Sprintf("%s.%s", path, class)
|
|
||||||
|
// include element class in key if selector is set
|
||||||
|
if !selectorSet {
|
||||||
|
class := e.Attr("class")
|
||||||
|
key = fmt.Sprintf("%s.%s", path, class)
|
||||||
|
}
|
||||||
|
|
||||||
if selectorSet || text != "" {
|
if selectorSet || text != "" {
|
||||||
pathLinks[key] = append(pathLinks[key], NewLink(href, text, class))
|
pathLinks[key] = append(pathLinks[key], NewLink(href, text))
|
||||||
pathCount[key] += len(text)
|
pathCount[key] += len(text)
|
||||||
// pathCount[key]++
|
|
||||||
|
|
||||||
if pathCount[key] > pathCount[pathMax] {
|
if pathCount[key] > pathCount[pathMax] {
|
||||||
pathMax = key
|
pathMax = key
|
||||||
@@ -172,28 +374,25 @@ func GetLinks(url *urllib.URL, selector string) []link {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
c.Visit(url.String())
|
c.Visit(url.String())
|
||||||
return pathLinks[pathMax]
|
|
||||||
|
|
||||||
// // visit and count link classes
|
links := pathLinks[pathMax]
|
||||||
// classesLinks := map[string][]link{}
|
if len(links) == 0 {
|
||||||
// classesCount := map[string]int{}
|
return []link{}, chapter{}, fmt.Errorf("no link found for selector: %s", selector)
|
||||||
// classMax := ""
|
}
|
||||||
|
|
||||||
// c := colly.NewCollector()
|
end := len(links)
|
||||||
// c.OnHTML(selector, func(e *colly.HTMLElement) {
|
if limit != -1 {
|
||||||
// href := e.Attr("href")
|
end = int(math.Min(float64(limit+offset), float64(len(links))))
|
||||||
// text := strings.TrimSpace(e.Text)
|
}
|
||||||
// class := e.Attr("class")
|
|
||||||
|
|
||||||
// if selectorSet || class != "" && text != "" {
|
links = links[offset:end]
|
||||||
// classesLinks[class] = append(classesLinks[class], NewLink(href, text))
|
|
||||||
// classesCount[class]++
|
|
||||||
|
|
||||||
// if classesCount[class] > classesCount[classMax] {
|
home := NewChapterFromURL(url.String(), []*ScrapeConfig{NewScrapeConfig()}, 0, func(index int, name string) {})
|
||||||
// classMax = class
|
|
||||||
// }
|
if include {
|
||||||
// }
|
l := NewLink(url.String(), home.Name())
|
||||||
// })
|
links = append([]link{l}, links...)
|
||||||
// c.Visit(url.String())
|
}
|
||||||
// return classesLinks[classMax]
|
|
||||||
|
return links, home, nil
|
||||||
}
|
}
|
||||||
|
|||||||
184
book/scraper_test.go
Normal file
184
book/scraper_test.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package book
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBody(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Body()
|
||||||
|
want := "<!doctype html>\n<html lang=\"en-us\">\n <head>\n <title>Books</title>\n <link rel=\"shortcut icon\" href=\"/favicon.ico\" />\n <meta charset=\"utf-8\" />\n <meta name=\"generator\" content=\"Hugo 0.59.1\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"author\" content=\"John Doe\" />\n <meta name=\"description\" content=\" \" />\n <link rel=\"stylesheet\" href=\"https://books.lapw.at/css/main.min.88e7083eff65effb7485b6e6f38d10afbec25093a6fac42d734ce9024d3defbd.css\" />\n\n \n <meta name=\"twitter:card\" content=\"summary\"/>\n<meta name=\"twitter:title\" content=\"Books\"/>\n<meta name=\"twitter:description\" content=\" \"/>\n\n <meta property=\"og:title\" content=\"Books\" />\n<meta property=\"og:description\" content=\" \" />\n<meta property=\"og:type\" content=\"website\" />\n<meta property=\"og:url\" content=\"https://books.lapw.at/\" />\n\n\n\n </head>\n <body>\n <header class=\"app-header\">\n <a href=\"https://books.lapw.at/\"><img class=\"app-header-avatar\" src=\"/book.svg\" alt=\"John Doe\" /></a>\n <h1>Books</h1>\n <p> </p>\n <div class=\"app-header-social\">\n \n </div>\n </header>\n <main class=\"app-container\">\n \n <article>\n <h1>Books</h1>\n <ul class=\"posts-list\">\n \n <li class=\"posts-list-item\">\n <a class=\"posts-list-item-title\" href=\"https://books.lapw.at/posts/ren%C3%A9-descartes-discours-de-la-m%C3%A9thode/\">Discours de la Méthode</a>\n <span class=\"posts-list-item-description\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"icon icon-clock\">\n <title>clock</title>\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16 14\"></polyline>\n</svg> 98 min read -\n 1637\n </span>\n </li>\n \n <li class=\"posts-list-item\">\n <a class=\"posts-list-item-title\" href=\"https://books.lapw.at/posts/adam-wiggins-the-twelve-factor-app/\">The Twelve-Factor App</a>\n <span class=\"posts-list-item-description\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"icon icon-clock\">\n <title>clock</title>\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16 14\"></polyline>\n</svg> 22 min read -\n 2011\n </span>\n </li>\n \n </ul>\n \n\n\n\n </article>\n\n </main>\n </body>\n</html>\n"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestName(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Name()
|
||||||
|
want := "Books"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthor(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Author()
|
||||||
|
want := "John Doe"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContent(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Content()
|
||||||
|
want := "<div id=\"readability-page-1\" class=\"page\">\n \n <main>\n \n <article>\n \n <ul>\n \n <li>\n <a href=\"https://books.lapw.at/posts/ren%C3%A9-descartes-discours-de-la-m%C3%A9thode/\">Discours de la Méthode</a>\n <span>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <title>clock</title>\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16 14\"></polyline>\n</svg> 98 min read -\n 1637\n </span>\n </li>\n \n <li>\n <a href=\"https://books.lapw.at/posts/adam-wiggins-the-twelve-factor-app/\">The Twelve-Factor App</a>\n <span>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <title>clock</title>\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16 14\"></polyline>\n</svg> 22 min read -\n 2011\n </span>\n </li>\n \n </ul>\n \n\n\n\n </article>\n\n </main>\n \n\n</div>"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelay(t *testing.T) {
|
||||||
|
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.delay = 500
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config0, config1}, 0, func(index int, name string) {})
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
got := elapsed
|
||||||
|
want := time.Duration(500) * time.Millisecond
|
||||||
|
|
||||||
|
if got < want {
|
||||||
|
t.Errorf("got %v, wanted min %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContentImagesOnly(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
config.imagesOnly = true
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/posts/adam-wiggins-the-twelve-factor-app/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Content()
|
||||||
|
want := "<img src=\"https://books.lapw.at/images/codebase-deploys.png\" alt=\"One codebase maps to many deploys\"/><img src=\"https://books.lapw.at/images/attached-resources.png\" alt=\"A production deploy attached to four backing services.\"/><img src=\"https://books.lapw.at/images/release.png\" alt=\"Code becomes a build, which is combined with config to create a release.\"/><img src=\"https://books.lapw.at/images/process-types.png\" alt=\"Scale is expressed as running processes, workload diversity is expressed as process types.\"/>"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubChapters(t *testing.T) {
|
||||||
|
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config0, config1}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := len(c.SubChapters())
|
||||||
|
want := 2
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubChaptersSelector(t *testing.T) {
|
||||||
|
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.selector = "section.concrete > article > h2 > a"
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://12factor.net/", []*ScrapeConfig{config0, config1}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := len(c.SubChapters())
|
||||||
|
want := 12
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubChaptersLimit(t *testing.T) {
|
||||||
|
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.limit = 1
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config0, config1}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := len(c.SubChapters())
|
||||||
|
want := 1
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubChaptersLimitOver(t *testing.T) {
|
||||||
|
|
||||||
|
config0 := NewScrapeConfig()
|
||||||
|
config0.limit = 3
|
||||||
|
|
||||||
|
config1 := NewScrapeConfig()
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config0, config1}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := len(c.SubChapters())
|
||||||
|
want := 2
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotInclude(t *testing.T) {
|
||||||
|
|
||||||
|
config := NewScrapeConfig()
|
||||||
|
config.include = false
|
||||||
|
|
||||||
|
c := NewChapterFromURL("https://books.lapw.at/", []*ScrapeConfig{config}, 0, func(index int, name string) {})
|
||||||
|
|
||||||
|
got := c.Content()
|
||||||
|
want := ""
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
129
cmd/get.go
129
cmd/get.go
@@ -5,19 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
"github.com/spf13/cobra"
|
||||||
epub "github.com/bmaupin/go-epub"
|
|
||||||
cobra "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/lapwat/papeer/book"
|
"github.com/lapwat/papeer/book"
|
||||||
)
|
)
|
||||||
|
|
||||||
var quiet, stdout, recursive, include bool
|
var recursive, include, images, quiet bool
|
||||||
var format, output, selector string
|
var format, output, selector, name, author string
|
||||||
var limit, delay int
|
var limit, offset, delay, threads int
|
||||||
|
|
||||||
var getCmd = &cobra.Command{
|
var getCmd = &cobra.Command{
|
||||||
Use: "get",
|
Use: "get",
|
||||||
@@ -28,20 +25,16 @@ var getCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatEnum := map[string]bool{
|
formatEnum := map[string]bool{
|
||||||
"md": true,
|
"stdout": true,
|
||||||
"epub": true,
|
"md": true,
|
||||||
"mobi": true,
|
"epub": true,
|
||||||
|
"mobi": true,
|
||||||
}
|
}
|
||||||
if formatEnum[format] != true {
|
if formatEnum[format] != true {
|
||||||
return fmt.Errorf("invalid format specified: %s", format)
|
return fmt.Errorf("invalid format specified: %s", format)
|
||||||
}
|
}
|
||||||
|
|
||||||
if format == "epub" || format == "mobi" {
|
// add .mobi to filename if not specified
|
||||||
if stdout {
|
|
||||||
return errors.New("cannot print EPUB/MOBI file to standard output")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if format == "mobi" {
|
if format == "mobi" {
|
||||||
if len(output) > 0 && strings.HasSuffix(output, ".mobi") == false {
|
if len(output) > 0 && strings.HasSuffix(output, ".mobi") == false {
|
||||||
output = fmt.Sprintf("%s.mobi", output)
|
output = fmt.Sprintf("%s.mobi", output)
|
||||||
@@ -60,100 +53,68 @@ var getCmd = &cobra.Command{
|
|||||||
return errors.New("cannot use limit option if not in recursive mode")
|
return errors.New("cannot use limit option if not in recursive mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("offset") && recursive == false {
|
||||||
|
return errors.New("cannot use offset option if not in recursive mode")
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Flags().Changed("delay") && recursive == false {
|
if cmd.Flags().Changed("delay") && recursive == false {
|
||||||
return errors.New("cannot use delay option if not in recursive mode")
|
return errors.New("cannot use delay option if not in recursive mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("threads") && recursive == false {
|
||||||
|
return errors.New("cannot use threads option if not in recursive mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("delay") && cmd.Flags().Changed("threads") {
|
||||||
|
return errors.New("cannot use delay and threads options at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
url := args[0]
|
url := args[0]
|
||||||
b := book.NewBookFromURL(url, selector, recursive, include, limit, delay)
|
b := book.NewBookFromURL(url, selector, name, author, recursive, include, images, quiet, limit, offset, delay, threads)
|
||||||
|
|
||||||
if len(output) == 0 {
|
fakeConfig := book.NewScrapeConfigFake()
|
||||||
// set default output
|
fakeChapter := book.NewChapter("", b.Name(), b.Author(), "", b.Chapters(), fakeConfig)
|
||||||
output = strings.ReplaceAll(b.Name(), " ", "_")
|
|
||||||
output = strings.ReplaceAll(output, "/", "")
|
if format == "stdout" {
|
||||||
output = fmt.Sprintf("%s.%s", output, format)
|
// TODO: ToMarkdownString
|
||||||
|
markdown := book.ToMarkdown(fakeChapter)
|
||||||
|
fmt.Println(markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
if format == "md" {
|
if format == "md" {
|
||||||
|
// TODO: ToMarkdownFile
|
||||||
|
markdown := book.ToMarkdown(fakeChapter)
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
filename := book.Filename(fakeChapter.Name())
|
||||||
|
output = fmt.Sprintf("%s.md", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write to file
|
||||||
f, err := os.Create(output)
|
f, err := os.Create(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
_, err2 := f.WriteString(markdown)
|
||||||
defer f.Close()
|
if err2 != nil {
|
||||||
|
log.Fatal(err2)
|
||||||
for _, c := range b.Chapters() {
|
|
||||||
content, err := md.NewConverter("", true, nil).ConvertString(c.Content())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
text := fmt.Sprintf("%s\n%s\n\n%s\n\n\n", c.Name(), strings.Repeat("=", len(c.Name())), content)
|
|
||||||
|
|
||||||
if stdout {
|
|
||||||
fmt.Println(text)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
_, err := f.WriteString(text)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
if stdout == false {
|
fmt.Printf("Markdown saved to \"%s\"\n", output)
|
||||||
fmt.Printf("Markdown saved to \"%s\"\n", output)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if format == "epub" {
|
if format == "epub" {
|
||||||
e := epub.NewEpub(b.Name())
|
output = book.ToEpub(fakeChapter, output)
|
||||||
e.SetAuthor(b.Author())
|
|
||||||
|
|
||||||
for _, c := range b.Chapters() {
|
|
||||||
html := fmt.Sprintf("<h1>%s</h1>%s", c.Name(), c.Content())
|
|
||||||
e.AddSection(html, c.Name(), "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := e.Write(output)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Ebook saved to \"%s\"\n", output)
|
fmt.Printf("Ebook saved to \"%s\"\n", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if format == "mobi" {
|
if format == "mobi" {
|
||||||
e := epub.NewEpub(b.Name())
|
output = book.ToMobi(fakeChapter, output)
|
||||||
e.SetAuthor(b.Author())
|
|
||||||
|
|
||||||
for _, chapter := range b.Chapters() {
|
|
||||||
e.AddSection(chapter.Content(), chapter.Name(), "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
outputEPUB := strings.ReplaceAll(output, ".mobi", ".epub")
|
|
||||||
|
|
||||||
err := e.Write(outputEPUB)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Command("kindlegen", outputEPUB).Run()
|
|
||||||
// exec command always return status 1 even if it fails
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fmt.Printf("Ebook saved to \"%s\"\n", output)
|
fmt.Printf("Ebook saved to \"%s\"\n", output)
|
||||||
|
|
||||||
err2 := os.Remove(outputEPUB)
|
|
||||||
if err2 != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
12
cmd/list.go
12
cmd/list.go
@@ -27,11 +27,17 @@ var listCmd = &cobra.Command{
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
links := book.GetLinks(base, selector)
|
links, _, err := book.GetLinks(base, selector, limit, offset, include)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
t := table.NewWriter()
|
t := table.NewWriter()
|
||||||
t.SetOutputMirror(os.Stdout)
|
t.SetOutputMirror(os.Stdout)
|
||||||
t.AppendHeader(table.Row{"#", "Name", "Url", "Class"})
|
t.Style().Options.DrawBorder = false
|
||||||
|
t.Style().Options.SeparateColumns = false
|
||||||
|
t.Style().Options.SeparateHeader = false
|
||||||
|
t.AppendHeader(table.Row{"#", "Name", "Url"})
|
||||||
|
|
||||||
for index, link := range links {
|
for index, link := range links {
|
||||||
u, err := base.Parse(link.Href())
|
u, err := base.Parse(link.Href())
|
||||||
@@ -39,7 +45,7 @@ var listCmd = &cobra.Command{
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.AppendRow([]interface{}{index + 1, link.Text(), u.String(), link.Class()})
|
t.AppendRow([]interface{}{index + 1, link.Text(), u.String()})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Render()
|
t.Render()
|
||||||
|
|||||||
14
cmd/root.go
14
cmd/root.go
@@ -23,15 +23,19 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "md", "file format [md, epub, mobi]")
|
rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "book name (default: page title)")
|
||||||
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "output file")
|
rootCmd.PersistentFlags().StringVarP(&author, "author", "a", "", "book author")
|
||||||
rootCmd.PersistentFlags().StringVarP(&selector, "selector", "s", "", "table of content CSS selector")
|
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "stdout", "file format [stdout, md, epub, mobi]")
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&output, "output", "", "", "file name (default: book name)")
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&selector, "selector", "s", "", "table of content CSS selector, in resursive mode")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&recursive, "recursive", "r", false, "create one chapter per natigation item")
|
rootCmd.PersistentFlags().BoolVarP(&recursive, "recursive", "r", false, "create one chapter per natigation item")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&include, "include", "i", false, "include URL as first chapter, in resursive mode")
|
rootCmd.PersistentFlags().BoolVarP(&include, "include", "i", false, "include URL as first chapter, in resursive mode")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "do not show logs")
|
rootCmd.PersistentFlags().BoolVarP(&images, "images", "", false, "retrieve images only")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&stdout, "stdout", "", false, "print to standard output")
|
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "hide progress bar")
|
||||||
rootCmd.PersistentFlags().IntVarP(&limit, "limit", "l", -1, "limit number of chapters, in recursive mode")
|
rootCmd.PersistentFlags().IntVarP(&limit, "limit", "l", -1, "limit number of chapters, in recursive mode")
|
||||||
|
rootCmd.PersistentFlags().IntVarP(&offset, "offset", "o", 0, "skip first chapters, in recursive mode")
|
||||||
rootCmd.PersistentFlags().IntVarP(&delay, "delay", "d", -1, "time to wait before downloading next chapter, in milliseconds")
|
rootCmd.PersistentFlags().IntVarP(&delay, "delay", "d", -1, "time to wait before downloading next chapter, in milliseconds")
|
||||||
|
rootCmd.PersistentFlags().IntVarP(&threads, "threads", "t", -1, "download concurrency, in recursive mode")
|
||||||
|
|
||||||
rootCmd.AddCommand(getCmd)
|
rootCmd.AddCommand(getCmd)
|
||||||
rootCmd.AddCommand(listCmd)
|
rootCmd.AddCommand(listCmd)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
func getTableOfContent() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,6 @@ var versionCmd = &cobra.Command{
|
|||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print the version number of papeer",
|
Short: "Print the version number of papeer",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("papeer v0.1.1")
|
fmt.Println("papeer v0.3.3")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -31,13 +31,18 @@ require (
|
|||||||
github.com/jedib0t/go-pretty/v6 v6.2.4 // indirect
|
github.com/jedib0t/go-pretty/v6 v6.2.4 // indirect
|
||||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
||||||
|
github.com/schollz/progressbar/v3 v3.8.3 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||||
golang.org/x/text v0.3.6 // indirect
|
golang.org/x/text v0.3.6 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -240,6 +240,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
|||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
@@ -259,9 +260,13 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
|
|||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
@@ -295,6 +300,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
|||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
@@ -302,6 +309,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||||
|
github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8=
|
||||||
|
github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s=
|
github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s=
|
||||||
github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||||
@@ -380,6 +389,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -533,9 +544,14 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
|
||||||
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
version=$1
|
version=$1
|
||||||
platforms=("linux/amd64" "darwin/amd64" "windows/amd64")
|
platforms=("linux/amd64" "darwin/amd64" "windows/amd64")
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Illegal number of parameters"
|
||||||
|
echo "Usage: ./release.sh X.X.X"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
for platform in "${platforms[@]}"
|
for platform in "${platforms[@]}"
|
||||||
do
|
do
|
||||||
platform_split=(${platform//\// })
|
platform_split=(${platform//\// })
|
||||||
GOOS=${platform_split[0]}
|
GOOS=${platform_split[0]}
|
||||||
GOARCH=${platform_split[1]}
|
GOARCH=${platform_split[1]}
|
||||||
output_name='papeer-'$version'-'$GOOS'-'$GOARCH
|
output_name='papeer-v'$version'-'$GOOS'-'$GOARCH
|
||||||
if [ $GOOS = "windows" ]; then
|
if [ $GOOS = "windows" ]; then
|
||||||
output_name+='.exe'
|
output_name+='.exe'
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user