summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndinus <andinus@nand.sh>2020-03-25 00:13:51 +0530
committerAndinus <andinus@nand.sh>2020-03-25 00:13:51 +0530
commit3ee3715ae7777dd6c1804974e2e293406fd1fe54 (patch)
treef035e76ff9ce48d03299381d03906eaff76140dc
parent28e35c0b0b996e17bd94a5b9e78fda90086ba85e (diff)
downloadcetus-3ee3715ae7777dd6c1804974e2e293406fd1fe54.tar.gz
Add apod support & fix errors
-rw-r--r--apod/json.go4
-rw-r--r--cache/getdir_darwin.go4
-rw-r--r--cache/getdir_unix.go2
-rw-r--r--cmd/cetus/apod.go176
-rw-r--r--cmd/cetus/env.go18
-rw-r--r--cmd/cetus/main.go96
-rw-r--r--cmd/cetus/usage.go15
-rw-r--r--notification/notif.go8
-rw-r--r--notification/notify_darwin.go25
-rw-r--r--notification/notify_unix.go21
-rw-r--r--request/request.go (renamed from request.go)7
11 files changed, 370 insertions, 6 deletions
diff --git a/apod/json.go b/apod/json.go
index c6da815..eae5fb2 100644
--- a/apod/json.go
+++ b/apod/json.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"regexp"
 
-	"framagit.org/andinus/cetus/pkg/request"
+	"tildegit.org/andinus/cetus/request"
 )
 
 // APOD holds the response from the api. Not every field is returned
@@ -27,7 +27,7 @@ type APOD struct {
 }
 
 // UnmarshalJson will take body as input & unmarshal it to res.
-func UnmarshalJson(res *Res, body string) error {
+func UnmarshalJson(res *APOD, body string) error {
 	err := json.Unmarshal([]byte(body), res)
 	if err != nil {
 		err = fmt.Errorf("json.go: unmarshalling json failed\n%s",
diff --git a/cache/getdir_darwin.go b/cache/getdir_darwin.go
index fc47e7e..733e94e 100644
--- a/cache/getdir_darwin.go
+++ b/cache/getdir_darwin.go
@@ -10,13 +10,13 @@ import (
 // GetDir returns cetus cache directory. Default cache directory on
 // macOS is $HOME/Library/Caches.
 func GetDir() string {
-	cacheDir = fmt.Sprintf("%s/%s/%s",
+	cacheDir := fmt.Sprintf("%s/%s/%s",
 		os.Getenv("HOME"),
 		"Library",
 		"Caches")
 
 	// Cetus cache directory is cacheDir/cetus
-	cetusCacheDir = fmt.Sprintf("%s/%s", cacheDir,
+	cetusCacheDir := fmt.Sprintf("%s/%s", cacheDir,
 		"cetus")
 
 	return cetusCacheDir
diff --git a/cache/getdir_unix.go b/cache/getdir_unix.go
index b1358d7..62dd5ed 100644
--- a/cache/getdir_unix.go
+++ b/cache/getdir_unix.go
@@ -22,7 +22,7 @@ func GetDir() string {
 	}
 
 	// Cetus cache directory is cacheDir/cetus.
-	cetusCacheDir = fmt.Sprintf("%s/%s", cacheDir,
+	cetusCacheDir := fmt.Sprintf("%s/%s", cacheDir,
 		"cetus")
 
 	return cetusCacheDir
diff --git a/cmd/cetus/apod.go b/cmd/cetus/apod.go
new file mode 100644
index 0000000..c1af115
--- /dev/null
+++ b/cmd/cetus/apod.go
@@ -0,0 +1,176 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+
+	"tildegit.org/andinus/cetus/apod"
+	"tildegit.org/andinus/cetus/background"
+	"tildegit.org/andinus/cetus/cache"
+	"tildegit.org/andinus/cetus/notification"
+)
+
+var (
+	err     error
+	body    string
+	file    string
+	reqInfo map[string]string
+)
+
+func execAPOD() {
+	apodApi := getEnv("APOD_API", "https://api.nasa.gov/planetary/apod")
+	apodKey := getEnv("APOD_KEY", "DEMO_KEY")
+
+	// reqInfo holds all the parameters that needs to be sent with
+	// the request. GetJson() will pack apiKey & date in params
+	// map before sending it to another function. Adding params
+	// here will not change the behaviour of the function, changes
+	// have to be made in GetJson() too.
+	reqInfo = make(map[string]string)
+	reqInfo["api"] = apodApi
+	reqInfo["apiKey"] = apodKey
+	reqInfo["date"] = apodDate
+
+	if random {
+		reqInfo["date"] = apod.RandDate()
+	}
+
+	cacheDir := fmt.Sprintf("%s/%s", cache.GetDir(), "apod")
+	os.MkdirAll(cacheDir, os.ModePerm)
+
+	// Check if the file is available locally, if it is then don't
+	// download it again and get it from disk
+	file = fmt.Sprintf("%s/%s.json", cacheDir, reqInfo["date"])
+
+	if _, err := os.Stat(file); err == nil {
+		data, err := ioutil.ReadFile(file)
+
+		// Not being able to read from the cache file is a
+		// small error and the program shouldn't exit but
+		// should continue after printing the log so that the
+		// user can investigate it later.
+		if err != nil {
+			err = fmt.Errorf("%s%s\n%s",
+				"apod.go: failed to read file to data: ", file,
+				err.Error())
+			log.Println(err)
+			dlAndCacheBody()
+		}
+		body = string(data)
+
+	} else if os.IsNotExist(err) {
+		dlAndCacheBody()
+
+	} else {
+		// If file existed then that is handled by the if
+		// block, if it didn't exist then that is handled by
+		// the else if block. If we reach here then that means
+		// it's Schrödinger's file & something else went
+		// wrong.
+		log.Fatal(err)
+	}
+
+	if dump {
+		fmt.Printf(body)
+	}
+
+	res := apod.APOD{}
+	err = apod.UnmarshalJson(&res, body)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// res.Msg will be returned when there is error on user input
+	// or the api server.
+	if len(res.Msg) != 0 {
+		fmt.Printf("Message: %s", res.Msg)
+		os.Exit(1)
+	}
+
+	// Send a desktop notification if notify flag was passed.
+	if notify {
+		n := notification.Notif{}
+		n.Title = res.Title
+		n.Message = fmt.Sprintf("%s\n\n%s",
+			res.Date,
+			res.Explanation)
+
+		err = n.Notify()
+		if err != nil {
+			log.Println(err)
+		}
+	}
+
+	if print {
+		fmt.Printf("Title: %s\n\n", res.Title)
+		if len(res.Copyright) != 0 {
+			fmt.Printf("Copyright: %s\n", res.Copyright)
+		}
+		fmt.Printf("Date: %s\n\n", res.Date)
+		fmt.Printf("Media Type: %s\n", res.MediaType)
+		if res.MediaType == "image" {
+			fmt.Printf("URL: %s\n\n", res.HDURL)
+		} else {
+			fmt.Printf("URL: %s\n\n", res.URL)
+		}
+		fmt.Printf("Explanation: %s\n", res.Explanation)
+	}
+
+	// Proceed only if the command was set because if it was fetch
+	// then it's already finished & should exit now.
+	if os.Args[1] == "fetch" {
+		os.Exit(0)
+	}
+
+	// Try to set background only if the media type is an image.
+	// First it downloads the image to the cache directory and
+	// then tries to set it with feh. If the download fails then
+	// it exits with a non-zero exit code.
+	if res.MediaType != "image" {
+		os.Exit(0)
+	}
+	imgFile := fmt.Sprintf("%s/%s", cacheDir, res.Title)
+
+	// Check if the file is available locally, if it is then don't
+	// download it again and set it from disk.
+	if _, err := os.Stat(imgFile); os.IsNotExist(err) {
+		err = background.Download(imgFile, res.HDURL)
+		if err != nil {
+			log.Fatal(err)
+		}
+	} else {
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+
+	err = background.SetFromFile(imgFile)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dlAndCacheBody() {
+	body, err = apod.GetJson(reqInfo)
+	if err != nil {
+		err = fmt.Errorf("%s\n%s",
+			"apod.go: failed to get json response from api",
+			err.Error())
+		log.Fatal(err)
+	}
+
+	// Write body to the cache so that it can be read later.
+	err = ioutil.WriteFile(file, []byte(body), 0644)
+
+	// Not being able to write to the cache file is a small error
+	// and the program shouldn't exit but should continue after
+	// printing the log so that the user can investigate it later.
+	if err != nil {
+		err = fmt.Errorf("%s\n%s",
+			"apod.go: failed to write body to file: ", file,
+			err.Error())
+		log.Println(err)
+	}
+}
diff --git a/cmd/cetus/env.go b/cmd/cetus/env.go
new file mode 100644
index 0000000..9288c35
--- /dev/null
+++ b/cmd/cetus/env.go
@@ -0,0 +1,18 @@
+package main
+
+import "os"
+
+// getEnv will check if the the key exists, if it does then it'll
+// return the value otherwise it will return fallback string.
+func getEnv(key, fallback string) string {
+	// We use os.LookupEnv instead of using os.GetEnv and checking
+	// if the length equals 0 because environment variable can be
+	// set and be of length 0. User could've set key="" which
+	// means the variable was set but the length is 0. There is no
+	// reason why user would want to do this over here though.
+	value, exists := os.LookupEnv(key)
+	if !exists {
+		value = fallback
+	}
+	return value
+}
diff --git a/cmd/cetus/main.go b/cmd/cetus/main.go
new file mode 100644
index 0000000..c7ffd40
--- /dev/null
+++ b/cmd/cetus/main.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"math/rand"
+	"os"
+	"time"
+)
+
+var (
+	version string = "v0.6.0"
+	dump    bool
+	random  bool
+	notify  bool
+	print   bool
+
+	apodDate string
+	// bpodApi string = getEnv("BPOD_API", "https://www.bing.com/HPImageArchive.aspx")
+)
+
+func main() {
+
+	// Early Check: If command was not passed then print usage and
+	// exit. Later command & service both are checked, this check
+	// is for version command. If not checked then running cetus
+	// without any args will fail because os.Args[1] will panic
+	// the program & produce runtime error.
+	if len(os.Args) == 1 {
+		printUsage()
+		os.Exit(0)
+	}
+
+	parseArgs()
+}
+
+// parseArgs will be parsing the arguments, it will verify if they are
+// correct. Flag values are also set by parseArgs.
+func parseArgs() {
+	// Running just `cetus` would've paniced the program if length
+	// of os.Args was not checked beforehand because there would
+	// be no os.Args[1].
+	switch os.Args[1] {
+	case "version", "-version", "--version", "-v":
+		fmt.Printf("Cetus %s\n", version)
+		os.Exit(0)
+
+	case "help", "-help", "--help", "-h":
+		// If help was passed then the program shouldn't exit
+		// with non-zero error code.
+		printUsage()
+		os.Exit(0)
+
+	case "set", "fetch":
+		// If command & service was not passed then print
+		// usage and exit.
+		if len(os.Args) < 3 {
+			printUsage()
+			os.Exit(1)
+		}
+
+	default:
+		fmt.Printf("Invalid command: %q\n", os.Args[1])
+		printUsage()
+		os.Exit(1)
+	}
+
+	rand.Seed(time.Now().Unix())
+
+	// If the program has reached this far then that means a valid
+	// command was passed & now we should check if a valid service
+	// was passed and parse the flags.
+	cetus := flag.NewFlagSet("cetus", flag.ExitOnError)
+
+	// We first declare common flags then service specific flags.
+	cetus.BoolVar(&dump, "dump", false, "Dump the response")
+	cetus.BoolVar(&notify, "notify", false, "Send a desktop notification with info")
+	cetus.BoolVar(&print, "print", false, "Print information")
+	cetus.BoolVar(&random, "random", false, "Choose a random image")
+
+	switch os.Args[2] {
+	case "apod", "nasa":
+		defDate := time.Now().UTC().Format("2006-01-02")
+		cetus.StringVar(&apodDate, "date", defDate, "Date of NASA APOD to retrieve")
+		cetus.Parse(os.Args[3:])
+
+		execAPOD()
+	// case "bpod", "bing":
+	// 	parseFlags()
+	// 	execBPOD()
+	default:
+		fmt.Printf("Invalid service: %q\n", os.Args[2])
+		printUsage()
+		os.Exit(1)
+	}
+}
diff --git a/cmd/cetus/usage.go b/cmd/cetus/usage.go
new file mode 100644
index 0000000..e9e2f71
--- /dev/null
+++ b/cmd/cetus/usage.go
@@ -0,0 +1,15 @@
+package main
+
+import "fmt"
+
+func printUsage() {
+	fmt.Println("Usage: cetus <command> <service> [<flags>]")
+	fmt.Println("\nCommands: ")
+	fmt.Println(" set     Set the background")
+	fmt.Println(" fetch   Fetch the response only")
+	fmt.Println(" help    Print help")
+	fmt.Println(" version Print Cetus version")
+	fmt.Println("\nServices: ")
+	fmt.Println(" apod   NASA Astronomy Picture of the Day")
+	fmt.Println(" bpod   Bing Photo of the Day")
+}
diff --git a/notification/notif.go b/notification/notif.go
new file mode 100644
index 0000000..b436c19
--- /dev/null
+++ b/notification/notif.go
@@ -0,0 +1,8 @@
+package notification
+
+// Notif struct holds information about the notification. Other
+// parameters like urgency & timeout could be added when required.
+type Notif struct {
+	Title   string
+	Message string
+}
diff --git a/notification/notify_darwin.go b/notification/notify_darwin.go
new file mode 100644
index 0000000..bddaf9e
--- /dev/null
+++ b/notification/notify_darwin.go
@@ -0,0 +1,25 @@
+// +build darwin
+
+package notification
+
+import (
+	"fmt"
+	"os/exec"
+	"strconv"
+)
+
+// Notify sends a desktop notification to the user using osascript. It
+// handles information in the form of Notif struct. It returns an
+// error (if exists).
+func (n Notif) Notify() error {
+	// This script cuts out parts of notification, this bug was
+	// confirmed on macOS Catalina 10.15.3, fix not yet known.
+	err := exec.Command("osascript", "-e",
+		`display notification `+strconv.Quote(n.Message)+` with title `+strconv.Quote(n.Title)).Run()
+	if err != nil {
+		err = fmt.Errorf("%s\n%s",
+			"notify_darwin.go: failed to sent notification with osascript",
+			err.Error())
+	}
+	return err
+}
diff --git a/notification/notify_unix.go b/notification/notify_unix.go
new file mode 100644
index 0000000..a6fc3ac
--- /dev/null
+++ b/notification/notify_unix.go
@@ -0,0 +1,21 @@
+// +build linux netbsd openbsd freebsd dragonfly
+
+package notification
+
+import (
+	"fmt"
+	"os/exec"
+)
+
+// Notify sends a desktop notification to the user using libnotify. It
+// handles information in the form of Notif struct. It returns an
+// error (if exists).
+func (n Notif) Notify() error {
+	err := exec.Command("notify-send", n.Title, n.Message).Run()
+	if err != nil {
+		err = fmt.Errorf("%s\n%s",
+			"notify_unix.go: failed to sent notification with notify-send",
+			err.Error())
+	}
+	return err
+}
diff --git a/request.go b/request/request.go
index b5088f9..c5a2dd1 100644
--- a/request.go
+++ b/request/request.go
@@ -10,9 +10,14 @@ import (
 
 // GetRes takes api and params as input and returns the body and
 // error.
-func (c http.Client) GetRes(api string, params map[string]string) (string, error) {
+func GetRes(api string, params map[string]string) (string, error) {
 	var body string
 
+	c := http.Client{
+		// TODO: timeout should be configurable by the user
+		Timeout: time.Second * 64,
+	}
+
 	req, err := http.NewRequest(http.MethodGet, api, nil)
 	if err != nil {
 		err = fmt.Errorf("%s\n%s",