2019-05-22 00:24:11 +08:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-05-27 18:01:53 +08:00
|
|
|
"image"
|
|
|
|
clr "image/color"
|
|
|
|
"image/draw"
|
|
|
|
"image/jpeg"
|
|
|
|
"math"
|
2019-05-22 00:24:11 +08:00
|
|
|
"net/http"
|
|
|
|
nurl "net/url"
|
|
|
|
"os"
|
2019-05-22 17:47:20 +08:00
|
|
|
"os/exec"
|
2019-05-22 00:24:11 +08:00
|
|
|
fp "path/filepath"
|
2019-05-22 17:47:20 +08:00
|
|
|
"runtime"
|
2019-05-22 00:24:11 +08:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2019-05-27 18:01:53 +08:00
|
|
|
"github.com/disintegration/imaging"
|
2019-05-22 00:24:11 +08:00
|
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/go-shiori/shiori/internal/model"
|
2019-05-22 17:47:20 +08:00
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
2019-05-27 18:01:53 +08:00
|
|
|
|
|
|
|
// Add supports for PNG image
|
|
|
|
_ "image/png"
|
2019-05-22 00:24:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
cIndex = color.New(color.FgHiCyan)
|
|
|
|
cSymbol = color.New(color.FgHiMagenta)
|
|
|
|
cTitle = color.New(color.FgHiGreen).Add(color.Bold)
|
|
|
|
cReadTime = color.New(color.FgHiMagenta)
|
|
|
|
cURL = color.New(color.FgHiYellow)
|
|
|
|
cExcerpt = color.New(color.FgHiWhite)
|
|
|
|
cTag = color.New(color.FgHiBlue)
|
|
|
|
|
2019-05-22 17:13:52 +08:00
|
|
|
cInfo = color.New(color.FgHiCyan)
|
|
|
|
cError = color.New(color.FgHiRed)
|
|
|
|
cWarning = color.New(color.FgHiYellow)
|
|
|
|
|
2019-05-22 00:24:11 +08:00
|
|
|
errInvalidIndex = errors.New("Index is not valid")
|
|
|
|
)
|
|
|
|
|
|
|
|
func normalizeSpace(str string) string {
|
2019-05-23 10:22:47 +08:00
|
|
|
str = strings.TrimSpace(str)
|
2019-05-22 00:24:11 +08:00
|
|
|
return strings.Join(strings.Fields(str), " ")
|
|
|
|
}
|
|
|
|
|
|
|
|
func isURLValid(s string) bool {
|
|
|
|
tmp, err := nurl.Parse(s)
|
|
|
|
return err == nil && tmp.Scheme != "" && tmp.Hostname() != ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func clearUTMParams(url *nurl.URL) {
|
|
|
|
queries := url.Query()
|
|
|
|
|
|
|
|
for key := range queries {
|
|
|
|
if strings.HasPrefix(key, "utm_") {
|
|
|
|
queries.Del(key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
url.RawQuery = queries.Encode()
|
|
|
|
}
|
|
|
|
|
2019-05-24 14:25:29 +08:00
|
|
|
func downloadBookImage(url, dstPath string, timeout time.Duration) error {
|
2019-05-22 00:24:11 +08:00
|
|
|
// Fetch data from URL
|
|
|
|
client := &http.Client{Timeout: timeout}
|
|
|
|
resp, err := client.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2019-05-27 18:01:53 +08:00
|
|
|
// Make sure it's JPG or PNG image
|
|
|
|
cp := resp.Header.Get("Content-Type")
|
|
|
|
if !strings.Contains(cp, "image/jpeg") && !strings.Contains(cp, "image/png") {
|
|
|
|
return fmt.Errorf("%s is not a supported image", url)
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, the download has finished successfully.
|
|
|
|
// Prepare destination file.
|
2019-05-22 00:24:11 +08:00
|
|
|
err = os.MkdirAll(fp.Dir(dstPath), os.ModePerm)
|
|
|
|
if err != nil {
|
2019-05-27 18:01:53 +08:00
|
|
|
return fmt.Errorf("failed to create image dir: %v", err)
|
2019-05-22 00:24:11 +08:00
|
|
|
}
|
|
|
|
|
2019-05-27 18:01:53 +08:00
|
|
|
dstFile, err := os.Create(dstPath)
|
2019-05-22 00:24:11 +08:00
|
|
|
if err != nil {
|
2019-05-27 18:01:53 +08:00
|
|
|
return fmt.Errorf("failed to create image file: %v", err)
|
2019-05-22 00:24:11 +08:00
|
|
|
}
|
2019-05-27 18:01:53 +08:00
|
|
|
defer dstFile.Close()
|
2019-05-22 00:24:11 +08:00
|
|
|
|
2019-05-27 18:01:53 +08:00
|
|
|
// Parse image and process it.
|
|
|
|
// If image is smaller than 600x400 or its ratio is less than 4:3, resize.
|
|
|
|
// Else, save it as it is.
|
|
|
|
img, _, err := image.Decode(resp.Body)
|
2019-05-22 00:24:11 +08:00
|
|
|
if err != nil {
|
2019-05-27 18:01:53 +08:00
|
|
|
return fmt.Errorf("failed to parse image %s: %v", url, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
imgRect := img.Bounds()
|
|
|
|
imgWidth := imgRect.Dx()
|
|
|
|
imgHeight := imgRect.Dy()
|
|
|
|
imgRatio := float64(imgWidth) / float64(imgHeight)
|
|
|
|
|
|
|
|
if imgWidth >= 600 && imgHeight >= 400 && imgRatio > 1.3 {
|
|
|
|
err = jpeg.Encode(dstFile, img, nil)
|
|
|
|
} else {
|
|
|
|
// Create background
|
|
|
|
bg := image.NewNRGBA(imgRect)
|
|
|
|
draw.Draw(bg, imgRect, image.NewUniform(clr.White), image.Point{}, draw.Src)
|
|
|
|
draw.Draw(bg, imgRect, img, image.Point{}, draw.Over)
|
|
|
|
|
|
|
|
bg = imaging.Fill(bg, 600, 400, imaging.Center, imaging.Lanczos)
|
|
|
|
bg = imaging.Blur(bg, 150)
|
|
|
|
bg = imaging.AdjustBrightness(bg, 30)
|
|
|
|
|
|
|
|
// Create foreground
|
|
|
|
fg := imaging.Fit(img, 600, 400, imaging.Lanczos)
|
|
|
|
|
|
|
|
// Merge foreground and background
|
|
|
|
bgRect := bg.Bounds()
|
|
|
|
fgRect := fg.Bounds()
|
|
|
|
fgPosition := image.Point{
|
|
|
|
X: bgRect.Min.X - int(math.Round(float64(bgRect.Dx()-fgRect.Dx())/2)),
|
|
|
|
Y: bgRect.Min.Y - int(math.Round(float64(bgRect.Dy()-fgRect.Dy())/2)),
|
|
|
|
}
|
|
|
|
|
|
|
|
draw.Draw(bg, bgRect, fg, fgPosition, draw.Over)
|
|
|
|
|
|
|
|
// Save to file
|
|
|
|
err = jpeg.Encode(dstFile, bg, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to save image %s: %v", url, err)
|
2019-05-22 00:24:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func printBookmarks(bookmarks ...model.Bookmark) {
|
|
|
|
for _, bookmark := range bookmarks {
|
|
|
|
// Create bookmark index
|
|
|
|
strBookmarkIndex := fmt.Sprintf("%d. ", bookmark.ID)
|
|
|
|
strSpace := strings.Repeat(" ", len(strBookmarkIndex))
|
|
|
|
|
|
|
|
// Print bookmark title
|
|
|
|
cIndex.Print(strBookmarkIndex)
|
|
|
|
cTitle.Println(bookmark.Title)
|
|
|
|
|
|
|
|
// Print bookmark URL
|
|
|
|
cSymbol.Print(strSpace + "> ")
|
|
|
|
cURL.Println(bookmark.URL)
|
|
|
|
|
|
|
|
// Print bookmark excerpt
|
|
|
|
if bookmark.Excerpt != "" {
|
|
|
|
cSymbol.Print(strSpace + "+ ")
|
|
|
|
cExcerpt.Println(bookmark.Excerpt)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Print bookmark tags
|
|
|
|
if len(bookmark.Tags) > 0 {
|
|
|
|
cSymbol.Print(strSpace + "# ")
|
|
|
|
for i, tag := range bookmark.Tags {
|
|
|
|
if i == len(bookmark.Tags)-1 {
|
|
|
|
cTag.Println(tag.Name)
|
|
|
|
} else {
|
|
|
|
cTag.Print(tag.Name + ", ")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append new line
|
|
|
|
fmt.Println()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseStrIndices converts a list of indices to their integer values
|
|
|
|
func parseStrIndices(indices []string) ([]int, error) {
|
|
|
|
var listIndex []int
|
|
|
|
for _, strIndex := range indices {
|
|
|
|
if !strings.Contains(strIndex, "-") {
|
|
|
|
index, err := strconv.Atoi(strIndex)
|
|
|
|
if err != nil || index < 1 {
|
|
|
|
return nil, errInvalidIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
listIndex = append(listIndex, index)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.Split(strIndex, "-")
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return nil, errInvalidIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
minIndex, errMin := strconv.Atoi(parts[0])
|
|
|
|
maxIndex, errMax := strconv.Atoi(parts[1])
|
|
|
|
if errMin != nil || errMax != nil || minIndex < 1 || minIndex > maxIndex {
|
|
|
|
return nil, errInvalidIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := minIndex; i <= maxIndex; i++ {
|
|
|
|
listIndex = append(listIndex, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return listIndex, nil
|
|
|
|
}
|
2019-05-22 17:47:20 +08:00
|
|
|
|
|
|
|
// openBrowser tries to open the URL in a browser,
|
|
|
|
// and returns any error if it happened.
|
|
|
|
func openBrowser(url string) error {
|
|
|
|
var args []string
|
|
|
|
switch runtime.GOOS {
|
|
|
|
case "darwin":
|
|
|
|
args = []string{"open"}
|
|
|
|
case "windows":
|
|
|
|
args = []string{"cmd", "/c", "start"}
|
|
|
|
default:
|
|
|
|
args = []string{"xdg-open"}
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command(args[0], append(args[1:], url)...)
|
|
|
|
return cmd.Run()
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTerminalWidth() int {
|
|
|
|
width, _, _ := terminal.GetSize(int(os.Stdin.Fd()))
|
|
|
|
return width
|
|
|
|
}
|