commit 5e68ef5b2e7e6806dd57586e9b7ed4f97f64dba0
parent 29a18b921839bb2c98ee0caf4b1cc5f2e4cf1707
Author: Agastya Chandrakant <me@hanabi.in>
Date: Sun, 27 Feb 2022 21:15:42 +0530
+ Major refactoring,
+ Adding explainxkcd link,
+ Refer to xkcd.com instead of just image for interactive comics,
+ Save large version of the image, when larger version is available,
+ `xkcd 1000' now works like `xkcd get 1000',
+ `xkcd explain' added.
Diffstat:
23 files changed, 389 insertions(+), 124 deletions(-)
diff --git a/README b/README
@@ -13,27 +13,31 @@ Assuming you have golang compiler, run the following commands:
$ git clone http://git.hanabi.in/repos/xkcd-go.git
$ cd xkcd-go
-$ make install # or, `go build -o xkcd src/main.go && mv xkcd /usr/local/bin/`
+# make install # or, `go build -o xkcd src/main.go && mv xkcd /usr/local/bin/`
Usage
=====
-`xkcd' can fetch the latest xkcd comic, fetch a random comic, or fetch a
-specific comic.
+`xkcd' can fetch the latest xkcd comic, fetch a random comic, fetch a specific
+comic, or save a specific comic.
Run `xkcd help' to show help message.
+ `xkcd' to show the latest xkcd comic.
+ `xkcd latest' to show the latest xkcd comic.
+ `xkcd random' to fetch random xkcd comic.
++ `xkcd get' will function same as `xkcd latest'.
+ `xkcd get 303', for example, to fetch the comic numbered 303.
++ `xkcd 807' will function same as `xkcd get 807'.
+ `xkcd save', to save the latest comic to `/tmp', or
+ `xkcd save 1312', to save the comic numbered 1312 to `/tmp'.
++ `xkcd explain', to explain the latest comic.
++ `xkcd explain 1000', to explain the comic numbered 1000.
Source code
===========
The source code uses git for version control.
Get the source code by running:
-$ git clone http://git.hanabi.in/repos/wordle-cli.git
+$ git clone http://git.hanabi.in/repos/xkcd-go.git
The source code has three branches dev, prod and master.
+ dev: new features are added committed to dev.
@@ -48,7 +52,9 @@ tool) -- tag named as a tribute for suggesting me to build this.
+ Other tags being semantic versioning (semver) of the software release.
The functionality of the software is detailed in the Usage section. I don't
-intend to complicate the software.
+intend to complicate the software. Genuine feature additions, consistency,
+code clean-ups and bug-fixes are welcomed. Consider the UNIX philosophy for
+addition of new features.
Please email patches to <me+git@hanabi.in>
@@ -64,4 +70,3 @@ License
This source code is licened under the GNU AGPLv3 license. Read the LICENSE file
to see what you can do with it, or read
<https://www.gnu.org/licenses/agpl-3.0.en.html>
-
diff --git a/makefile b/makefile
@@ -3,4 +3,4 @@ build:
install:
make build && mv xkcd /usr/local/bin
clean:
- rm ./xkcd
+ rm -f ./xkcd
diff --git a/src/main.go b/src/main.go
@@ -1,24 +1,33 @@
package main
import (
+ "fmt"
+ "log"
"os"
xkcd "git.hanabi.in/dev/xkcd/src/xkcd"
)
func main() {
- option, num := xkcd.GetOption()
+ option, num, err := xkcd.GetOption(os.Args)
+ if err != nil {
+ log.Fatal(err)
+ }
+ var msg string
if option == "help" {
- xkcd.ShowHelp(os.Stdout)
+ msg = xkcd.ShowHelp()
} else if option == "random" {
- xkcd.FetchRandomXKCD(os.Stdout)
+ msg = xkcd.FetchRandomXKCD()
} else if option == "latest" {
- xkcd.FetchLatestXKCD(os.Stdout)
+ msg = xkcd.FetchLatestXKCD()
} else if option == "get" {
- xkcd.FetchThisXKCD(num, os.Stdout)
+ msg = xkcd.FetchThisXKCD(num)
} else if option == "save" {
- xkcd.SaveXKCD(num, os.Stdout)
+ msg = xkcd.SaveXKCD(num)
+ } else if option == "explain" {
+ msg = xkcd.ExplainXKCD(num)
} else {
- xkcd.DefaultBehaviour(os.Stdout)
+ msg = xkcd.DefaultBehaviour(option)
}
+ fmt.Print(msg)
}
diff --git a/src/xkcd/default-behaviour.go b/src/xkcd/default-behaviour.go
@@ -0,0 +1,12 @@
+package xkcd
+
+import "strconv"
+
+func DefaultBehaviour(option string) (msg string) {
+ if maybenum, err := strconv.Atoi(option); err == nil {
+ msg = FetchThisXKCD(maybenum)
+ } else {
+ msg = FetchLatestXKCD()
+ }
+ return msg
+}
diff --git a/src/xkcd/detect-api.go b/src/xkcd/detect-api.go
@@ -0,0 +1,10 @@
+package xkcd
+
+// Detect if a specific API is to be used, or the latest API.
+func detectAPI(num int) (api string) {
+ if num == 0 {
+ return latestAPI()
+ } else {
+ return specificAPI(num)
+ }
+}
diff --git a/src/xkcd/detect-api_test.go b/src/xkcd/detect-api_test.go
@@ -0,0 +1,24 @@
+package xkcd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestLatestAPI(t *testing.T) {
+ expected := "https://xkcd.com/info.0.json"
+ received := detectAPI(0)
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func TestSpecificAPI(t *testing.T) {
+ expected := "https://xkcd.com/541/info.0.json"
+ received := detectAPI(541)
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
diff --git a/src/xkcd/explain-xkcd.go b/src/xkcd/explain-xkcd.go
@@ -0,0 +1,15 @@
+package xkcd
+
+import (
+ "fmt"
+)
+
+// For a given xkcd comic number `num', provide the explainxkcd.com link.
+func ExplainXKCD(num int) (link string) {
+ if num == 0 {
+ link = fmt.Sprintf("%sMain_Page\n", explainxkcd)
+ } else {
+ link = fmt.Sprintf("%s%d\n", explainxkcd, num)
+ }
+ return link
+}
diff --git a/src/xkcd/explain-xkcd_test.go b/src/xkcd/explain-xkcd_test.go
@@ -0,0 +1,33 @@
+package xkcd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestExplainLatest(t *testing.T) {
+ received := ExplainXKCD(0)
+ expected := "https://www.explainxkcd.com/wiki/index.php/Main_Page\n"
+ if received != expected {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func TestExplainSpecific(t *testing.T) {
+ received := ExplainXKCD(413)
+ expected := "https://www.explainxkcd.com/wiki/index.php/413\n"
+ if received != expected {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func TestExplain404(t *testing.T) { // No reason 404 explain xkcd should not be printed.
+ received := ExplainXKCD(404)
+ expected := "https://www.explainxkcd.com/wiki/index.php/404\n"
+ if received != expected {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
diff --git a/src/xkcd/fetch-latest-xkcd.go b/src/xkcd/fetch-latest-xkcd.go
@@ -0,0 +1,10 @@
+package xkcd
+
+// Return message about the latest xkcd.
+func FetchLatestXKCD() (msg string) {
+ api := latestAPI()
+ data := xkcdAPI(api)
+ num := data.Num
+ msg = FetchThisXKCD(num)
+ return msg
+}
diff --git a/src/xkcd/fetch-random-xkcd.go b/src/xkcd/fetch-random-xkcd.go
@@ -0,0 +1,9 @@
+package xkcd
+
+// return the message about a random xkcd.
+func FetchRandomXKCD() (msg string) {
+ max := getNumOfLatestComic()
+ rnd_idx := getRndXKCDIdx(max)
+ msg = FetchThisXKCD(rnd_idx)
+ return msg
+}
diff --git a/src/xkcd/fetch-this-xkcd.go b/src/xkcd/fetch-this-xkcd.go
@@ -0,0 +1,18 @@
+package xkcd
+
+import (
+ "fmt"
+)
+
+// return message for comic numbered num.
+func FetchThisXKCD(num int) (msg string) {
+ if num == 404 {
+ msg = handle404()
+ } else {
+ api := detectAPI(num)
+ data := xkcdAPI(api)
+ comic := fmt.Sprintf("https://xkcd.com/%d", data.Num)
+ msg = fmt.Sprintf("Comic from %s-%s-%s:\n\"%s (%d)\"\n\n%s\n\n\"%s\"\n\nNeed explainer?\n%s%d\n", data.Year, data.Month, data.Day, data.SafeTitle, data.Num, comic, data.Alt, explainxkcd, data.Num)
+ }
+ return msg
+}
diff --git a/src/xkcd/fns.go b/src/xkcd/fns.go
@@ -2,93 +2,26 @@ package xkcd
import (
"encoding/json"
- "fmt"
- "io"
"io/ioutil"
"log"
"math/rand"
"net/http"
- "os"
- "strconv"
"strings"
"time"
)
-type xkcdResp struct {
- Month string `json:"month"`
- Num int `json:"num"`
- Link string `json:"link"`
- Year string `json:"year"`
- News string `json:"news"`
- SafeTitle string `json:"safe_title"`
- Transcript string `json:"transcript"`
- Alt string `json:"alt"`
- Img string `json:"img"`
- Title string `json:"title"`
- Day string `json:"day"`
-}
-
-func ShowHelp(w io.Writer) {
- msg := []byte("Run `xkcd help' to show this message.\n")
- w.Write(msg)
- msg = []byte("+ `xkcd' to show the latest xkcd comic.\n")
- w.Write(msg)
- msg = []byte("+ `xkcd latest' to show the latest xkcd comic.\n")
- w.Write(msg)
- msg = []byte("+ `xkcd random' to fetch random xkcd comic.\n")
- w.Write(msg)
- msg = []byte("+ `xkcd get 303', for example, to fetch the comic numbered 303.\n")
- w.Write(msg)
- msg = []byte("+ `xkcd save', to save the latest comic to `/tmp', or\n")
- w.Write(msg)
- msg = []byte("+ `xkcd save 1312', to save the comic numbered 1312 to `/tmp'.\n")
- w.Write(msg)
-}
-
-func GetOption() (option string, num int) {
- args := os.Args
- if len(args) > 1 {
- option = args[1]
- }
- if len(args) > 2 {
- maybenum, err := strconv.Atoi(args[2])
- if err != nil {
- log.Fatal("Please specify a valid number.\n")
- }
- num = maybenum
- }
- return option, num
-}
-
-func FetchThisXKCD(num int, w io.Writer) {
- API := fmt.Sprintf("https://xkcd.com/%d/info.0.json", num)
- data := xkcdAPI(API)
- msg := []byte(fmt.Sprintf("Comic from %s-%s-%s:\n\"%s (%d)\"\n\n%s\n\n\"%s\"\n", data.Year, data.Month, data.Day, data.SafeTitle, data.Num, data.Img, data.Alt))
- w.Write(msg)
-}
+const (
+ explainxkcd = "https://www.explainxkcd.com/wiki/index.php/"
+)
-func SaveXKCD(num int, w io.Writer) {
- API := fmt.Sprintf("https://xkcd.com/%d/info.0.json", num)
- if num == 0 {
- API = fmt.Sprint("https://xkcd.com/info.0.json")
- }
- data := xkcdAPI(API)
- img := data.Img
- num = data.Num
- ext := getExt(img)
- img_body := httpGet(img)
- defer img_body.Close()
- filename := fmt.Sprintf("/tmp/xkcd-%d.%s", num, ext)
- file, err := os.Create(filename)
- if err != nil {
- log.Fatalf("Unable to save file. See %v.\n", err)
- }
- defer file.Close()
- if _, err := io.Copy(file, img_body); err != nil {
- log.Fatal(err)
+func getImg(img, link, ext string) (res string) {
+ res = img
+ if strings.HasPrefix(link, "https://xkcd.com/") {
+ full_ext := "." + ext
+ first_part := strings.Replace(img, full_ext, "", -1)
+ res = first_part + "_large" + full_ext
}
- msg := []byte("Saved to /tmp.\n")
- w.Write(msg)
+ return res
}
func getExt(str string) (ext string) {
@@ -96,21 +29,17 @@ func getExt(str string) (ext string) {
return arr[len(arr)-1]
}
-func httpGet(url string) io.ReadCloser {
+func httpGet(url string) *http.Response {
client := &http.Client{}
resp, err := client.Get(url)
if err != nil {
log.Fatalf("Unable to access XKCD, see: %v.\n", err)
}
- return resp.Body
+ return resp
}
-func xkcdAPI(API string) (data xkcdResp) {
- client := &http.Client{}
- resp, err := client.Get(API)
- if err != nil {
- log.Fatalf("Unable to access XKCD, see: %v.\n", err)
- }
+func xkcdAPI(api string) (data xkcdResp) {
+ resp := httpGet(api)
defer resp.Body.Close()
respIsJson := resp.Header.Get("Content-Type") == "application/json"
if !respIsJson {
@@ -126,32 +55,8 @@ func xkcdAPI(API string) (data xkcdResp) {
return data
}
-func DefaultBehaviour(w io.Writer) {
- FetchLatestXKCD(w)
-}
-
-func FetchLatestXKCD(w io.Writer) {
- API := "https://xkcd.com/info.0.json"
- data := xkcdAPI(API)
- msg := []byte(fmt.Sprintf("Latest comic (%s-%s-%s):\n\"%s (%d)\"\n\n%s\n\n\"%s\"\n", data.Year, data.Month, data.Day, data.SafeTitle, data.Num, data.Img, data.Alt))
- w.Write(msg)
-}
-
-func FetchRandomXKCD(w io.Writer) {
- max := getNumOfLatest()
- rnd_idx := getRndXKCDIdx(max)
- FetchThisXKCD(rnd_idx, w)
-}
-
-func getNumOfLatest() (latest int) {
- API := "https://xkcd.com/info.0.json"
- data := xkcdAPI(API)
- return data.Num
-}
-
func getRndXKCDIdx(max int) (rnd_idx int) {
rand.Seed(time.Now().UnixNano())
- for rnd_idx = rand.Intn(max+1) + 1; rnd_idx == 404; rnd_idx = rand.Intn(max+1) + 1 {
- }
+ rnd_idx = rand.Intn(max+1) + 1
return rnd_idx
}
diff --git a/src/xkcd/get-num-of-latest-comic.go b/src/xkcd/get-num-of-latest-comic.go
@@ -0,0 +1,9 @@
+package xkcd
+
+// Get the number of the latest xkcd.
+func getNumOfLatestComic() (latest int) {
+ api := latestAPI()
+ data := xkcdAPI(api)
+ latest = data.Num
+ return latest
+}
diff --git a/src/xkcd/get-options.go b/src/xkcd/get-options.go
@@ -0,0 +1,22 @@
+package xkcd
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// Option is of the type `xkcd action [num]'.
+// Return (action, num, err) -- err, if any.
+func GetOption(args []string) (option string, num int, err error) {
+ if len(args) > 1 {
+ option = args[1]
+ }
+ if len(args) > 2 {
+ maybenum, e := strconv.Atoi(args[2])
+ if e != nil {
+ err = fmt.Errorf("Please specify a valid number.\n")
+ }
+ num = maybenum
+ }
+ return option, num, err
+}
diff --git a/src/xkcd/get-options_test.go b/src/xkcd/get-options_test.go
@@ -0,0 +1,42 @@
+package xkcd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestNoArgs(t *testing.T) {
+ input := []string{"bin"}
+ option, num, err := GetOption(input)
+ if option != "" || num != 0 || err != nil {
+ msg := fmt.Sprintf("Expected return empty string, 0 and nil error, got `%s', `%d', `%v'.\n", option, num, err)
+ t.Error(msg)
+ }
+}
+
+func TestOneValidArg(t *testing.T) {
+ var input = []string{"bin", "alif"}
+ option, num, err := GetOption(input)
+ if option != "alif" || num != 0 || err != nil {
+ msg := fmt.Sprintf("Expected return `alif', 0, and nil error, got `%s', `%d', `%v'.\n", option, num, err)
+ t.Error(msg)
+ }
+}
+
+func TestTwoValidArgs(t *testing.T) {
+ var input = []string{"bin", "alif", "465"}
+ option, num, err := GetOption(input)
+ if option != "alif" || num != 465 || err != nil {
+ msg := fmt.Sprintf("Expected return `alif', 465, and nil error, got `%s', `%d', `%v'.\n", option, num, err)
+ t.Error(msg)
+ }
+}
+
+func TestOneValidOtherInvalidArg(t *testing.T) {
+ var input = []string{"bin", "alif", "bet"}
+ _, _, err := GetOption(input)
+ if err == nil {
+ msg := fmt.Sprintf("Expected some err, received `%v'.\n", err)
+ t.Error(msg)
+ }
+}
diff --git a/src/xkcd/handle-404.go b/src/xkcd/handle-404.go
@@ -0,0 +1,7 @@
+package xkcd
+
+// comic number 404 was an easter egg, handle it separately.
+func handle404() (msg string) {
+ msg = "Comic from 2008-4-1:\n\"404 Not Found (404)\"\n\nhttps://xkcd.com/404\n\nNeed explainer?\nhttps://www.explainxkcd.com/wiki/index.php/404\n"
+ return msg
+}
diff --git a/src/xkcd/latest-api.go b/src/xkcd/latest-api.go
@@ -0,0 +1,7 @@
+package xkcd
+
+// Return the URL to fetch the latest xkcd comic data.
+func latestAPI() (api string) {
+ api = "https://xkcd.com/info.0.json"
+ return api
+}
diff --git a/src/xkcd/latest-api_test.go b/src/xkcd/latest-api_test.go
@@ -0,0 +1,15 @@
+package xkcd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ received := latestAPI()
+ expected := "https://xkcd.com/info.0.json"
+ if received != expected {
+ msg := fmt.Sprintf("Expected `%s', received `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
diff --git a/src/xkcd/save-xkcd.go b/src/xkcd/save-xkcd.go
@@ -0,0 +1,31 @@
+package xkcd
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+)
+
+// Save xkcd numbered num in /tmp
+func SaveXKCD(num int) (msg string) {
+ api := detectAPI(num)
+ data := xkcdAPI(api)
+ num = data.Num
+ ext := getExt(data.Img)
+ img := getImg(data.Img, data.Link, ext)
+ img_resp := httpGet(img)
+ img_body := img_resp.Body
+ defer img_body.Close()
+ filename := fmt.Sprintf("/tmp/xkcd-%d.%s", num, ext)
+ file, err := os.Create(filename)
+ if err != nil {
+ log.Fatalf("Unable to save file. See %v.\n", err)
+ }
+ defer file.Close()
+ if _, err := io.Copy(file, img_body); err != nil {
+ log.Fatal(err)
+ }
+ msg = fmt.Sprintf("Saved %d to /tmp.\n", num)
+ return msg
+}
diff --git a/src/xkcd/show-help.go b/src/xkcd/show-help.go
@@ -0,0 +1,16 @@
+package xkcd
+
+func ShowHelp() (msg string) {
+ msg = "Run `xkcd help' to show this message.\n"
+ msg += "+ `xkcd' to show the latest xkcd comic.\n"
+ msg += "+ `xkcd latest' to show the latest xkcd comic.\n"
+ msg += "+ `xkcd random' to fetch random xkcd comic.\n"
+ msg += "+ `xkcd get' will function same as `xkcd latest'.\n"
+ msg += "+ `xkcd get 303', for example, to fetch the comic numbered 303.\n"
+ msg += "+ `xkcd 807' will function same as `xkcd get 807'.\n"
+ msg += "+ `xkcd save', to save the latest comic to `/tmp', or\n"
+ msg += "+ `xkcd save 1312', to save the comic numbered 1312 to `/tmp'.\n"
+ msg += "+ `xkcd explain', open link to explainxkcd for the latest comic.\n"
+ msg += "+ `xkcd explain 1000', open link to explainxkcd for comic numbered 1000.\n"
+ return msg
+}
diff --git a/src/xkcd/specific-api.go b/src/xkcd/specific-api.go
@@ -0,0 +1,9 @@
+package xkcd
+
+import "fmt"
+
+// Return link to specific api, to fetch data about the comic numbered num.
+func specificAPI(num int) (api string) {
+ api = fmt.Sprintf("https://xkcd.com/%d/info.0.json", num)
+ return api
+}
diff --git a/src/xkcd/specific-api_test.go b/src/xkcd/specific-api_test.go
@@ -0,0 +1,42 @@
+package xkcd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Test1(t *testing.T) {
+ expected := "https://xkcd.com/1000/info.0.json"
+ received := specificAPI(1000)
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received: `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func Test2(t *testing.T) {
+ expected := "https://xkcd.com/404/info.0.json"
+ received := specificAPI(404)
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received: `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func Test3(t *testing.T) {
+ expected := "https://xkcd.com/0/info.0.json"
+ received := specificAPI(0) // specificAPI does not care about number, it just expects number.
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received: `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
+
+func Test4(t *testing.T) {
+ expected := "https://xkcd.com/-30/info.0.json"
+ received := specificAPI(-30) // specificAPI does not care about number, it just expects number.
+ if expected != received {
+ msg := fmt.Sprintf("Expected `%s', received: `%s'.\n", expected, received)
+ t.Error(msg)
+ }
+}
diff --git a/src/xkcd/xkcd-resp.go b/src/xkcd/xkcd-resp.go
@@ -0,0 +1,15 @@
+package xkcd
+
+type xkcdResp struct {
+ Month string `json:"month"`
+ Num int `json:"num"`
+ Link string `json:"link"`
+ Year string `json:"year"`
+ News string `json:"news"`
+ SafeTitle string `json:"safe_title"`
+ Transcript string `json:"transcript"`
+ Alt string `json:"alt"`
+ Img string `json:"img"`
+ Title string `json:"title"`
+ Day string `json:"day"`
+}