xkcd/xkcd.go
2023-06-01 21:24:08 +02:00

141 lines
2.7 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/karlseguin/typed"
)
func get(url string) ([]byte, error) {
res, err := http.DefaultClient.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return data, nil
}
func getJSON(url string) (typed.Typed, error) {
data, err := get(url)
if err != nil {
return nil, err
}
m, err := typed.Json(data)
if err != nil {
return nil, err
}
return m, nil
}
func getComicMetadata(id uint) (typed.Typed, error) {
return getJSON(fmt.Sprintf("https://xkcd.com/%d/info.0.json", id))
}
func getLatestID() (uint, error) {
m, err := getJSON("https://xkcd.com/info.0.json")
if err != nil {
return 0, err
}
id, ok := m.IntIf("num")
if !ok {
return 0, errors.New("key 'num' does not exist")
}
return uint(id), nil
}
func getImage(url string) ([]byte, error) {
ext := path.Ext(url)
url = strings.TrimSuffix(url, ext)
data, err := get(url + "_2x" + ext)
if err != nil {
data, err = get(url + ext)
}
return data, err
}
// ensureComic fetches comic #id and stores it in out. Returns metadata.
func ensureComic(out string, id uint) (typed.Typed, error) {
err := os.MkdirAll(out, dirPermissions)
if err != nil {
return nil, err
}
metadata, err := getComicMetadata(id)
if err != nil {
return nil, err
}
// Some comics, such as 1608 and 1663, are composed by interactive
// games and cannot be downloaded as images, so we just ignore them.
if imgURL := metadata.StringOr("img", "https://imgs.xkcd.com/comics/"); imgURL != "https://imgs.xkcd.com/comics/" {
imgBytes, err := getImage(imgURL)
if err != nil {
return nil, err
}
imgName := path.Base(imgURL)
err = os.WriteFile(filepath.Join(out, imgName), imgBytes, filePermissions)
if err != nil {
return nil, err
}
metadata["img"] = "./" + imgName
} else {
metadata["img"] = ""
}
infoBytes, err := json.MarshalIndent(metadata, "", " ")
if err != nil {
return nil, err
}
err = os.WriteFile(filepath.Join(out, "info.json"), infoBytes, filePermissions)
if err != nil {
return nil, err
}
return metadata, nil
}
// getComic gets comic #id metadata from out, or fetches from Internet.
func getComic(out string, id uint) (typed.Typed, error) {
_, err := os.Stat(out)
if os.IsNotExist(err) {
return ensureComic(out, id)
} else if err == nil {
data, err := os.ReadFile(filepath.Join(out, "info.json"))
if err != nil {
return nil, err
}
return typed.Json(data)
}
return nil, err
}