commit 22a02d75a509cbf7ddbe59d5d3cf8fa1705624f1 Author: abs3nt Date: Tue Oct 17 11:33:29 2023 -0700 initial diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..008c0d7 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +build: wallhaven_dl + +wallhaven_dl: $(shell find . -name '*.go') + go build -o wallhaven_dl . + +run: + go run main.go + +tidy: + go mod tidy + +clean: + rm -f wallhaven_dl + +uninstall: + rm -f /usr/bin/wallhaven_dl + +install: + cp wallhaven_dl /usr/bin diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ede469c --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module main + +go 1.21.3 + +require github.com/alecthomas/kong v0.8.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..021aabe --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a9312bc --- /dev/null +++ b/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + "os" + "os/exec" + "path" + "time" + + "github.com/alecthomas/kong" + + "main/wallhaven" +) + +var cli struct { + Search struct { + Query string `arg:"" name:"query" help:"what to search for." type:"string"` + } `cmd:"" help:"search for wallpaper"` + Top struct { + Purity string `arg:"" name:"purity" optional:"" help:"purity of results"` + } `cmd:"" help:"random toplist wallpaper"` +} + +func main() { + ctx := kong.Parse(&cli) + switch ctx.Command() { + case "search ": + err := searchAndSet(cli.Search.Query) + if err != nil { + panic(err) + } + case "top", "top ": + err := setTop(cli.Top.Purity) + if err != nil { + panic(err) + } + default: + panic(ctx.Command()) + } +} + +func searchAndSet(query string) error { + seed := rand.NewSource(time.Now().UnixNano()) + r := rand.New(seed) + results, err := wallhaven.SearchWallpapers(&wallhaven.Search{ + Query: wallhaven.Q{ + Tags: []string{query}, + }, + Categories: "111", + Purities: "110", + Sorting: wallhaven.Relevance, + Order: wallhaven.Desc, + AtLeast: wallhaven.Resolution{Width: 2560, Height: 1400}, + Ratios: []wallhaven.Ratio{ + {Horizontal: 16, Vertical: 9}, + {Horizontal: 16, Vertical: 10}, + }, + Page: 1, + }) + if err != nil { + return err + } + result, err := getOrDownload(results, r) + if err != nil { + return nil + } + err = setWallPaperAndRestartStuff(result) + if err != nil { + return err + } + return nil +} + +func setTop(purity string) error { + seed := rand.NewSource(time.Now().UnixNano()) + r := rand.New(seed) + s := &wallhaven.Search{ + Categories: "010", + Purities: "110", + Sorting: wallhaven.Toplist, + Order: wallhaven.Desc, + TopRange: "1y", + AtLeast: wallhaven.Resolution{Width: 2560, Height: 1400}, + Ratios: []wallhaven.Ratio{ + {Horizontal: 16, Vertical: 9}, + {Horizontal: 16, Vertical: 10}, + }, + Page: r.Intn(5) + 1, + } + if purity != "" { + s.Purities = purity + } + results, err := wallhaven.SearchWallpapers(s) + if err != nil { + return err + } + result, err := getOrDownload(results, r) + if err != nil { + return err + } + err = setWallPaperAndRestartStuff(result) + if err != nil { + return err + } + return nil +} + +func setWallPaperAndRestartStuff(result wallhaven.Wallpaper) error { + homedir, _ := os.UserHomeDir() + _, err := exec.Command("wal", "--cols16", "-i", path.Join(homedir, "Pictures/Wallpapers", path.Base(result.Path)), "-n", "-a", "85"). + Output() + if err != nil { + return err + } + _, err = exec.Command("swww", "img", path.Join(homedir, "/Pictures/Wallpapers", path.Base(result.Path)), "--transition-step=20", "--transition-fps=60"). + Output() + if err != nil { + return err + } + _, err = exec.Command("restart_dunst"). + Output() + if err != nil { + return err + } + _, err = exec.Command("pywalfox", "update"). + Output() + if err != nil { + return err + } + source, err := os.Open(path.Join(homedir, ".cache/wal/discord-wal.theme.css")) + if err != nil { + return err + } + defer source.Close() + destination, err := os.Create(path.Join(homedir, ".config/Vencord/themes/discord-wal.theme.css")) + if err != nil { + return err + } + _, err = io.Copy(destination, source) + if err != nil { + return err + } + return nil +} + +func getOrDownload(results *wallhaven.SearchResults, r *rand.Rand) (wallhaven.Wallpaper, error) { + if len(results.Data) == 0 { + return wallhaven.Wallpaper{}, fmt.Errorf("no wallpapers found") + } + homedir, _ := os.UserHomeDir() + result := results.Data[r.Intn(len(results.Data))] + if _, err := os.Stat(path.Join(homedir, "Pictures/Wallpapers", path.Base(result.Path))); err != nil { + err = result.Download(path.Join(homedir, "Pictures/Wallpapers")) + if err != nil { + return wallhaven.Wallpaper{}, err + } + } + return result, nil +} diff --git a/wallhaven/search.go b/wallhaven/search.go new file mode 100644 index 0000000..4c0462c --- /dev/null +++ b/wallhaven/search.go @@ -0,0 +1,318 @@ +package wallhaven + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" +) + +// Search Types + +// Category is an enum used to represent wallpaper categories +type Category string + +// Purity is an enum used to represent +type Purity string + +// Sort enum specifies the various sort types accepted by WH api +type Sort int + +// Sort Enum Values +const ( + DateAdded Sort = iota + 1 + Relevance + Random + Views + Favorites + Toplist +) + +func (s Sort) String() string { + str := [...]string{"", "date_added", "relevance", "random", "views", "favorites", "toplist"} + return str[s] +} + +// Order enum specifies the sort orders accepted by WH api +type Order int + +// Sort Enum Values +const ( + Desc Order = iota + 1 + Asc +) + +func (o Order) String() string { + str := [...]string{"", "desc", "asc"} + return str[o] +} + +// Privacy enum specifies the collection privacy returned by WH api +type Privacy int + +// Privacy Enum Values +const ( + Private Privacy = iota + Public +) + +func (p Privacy) String() string { + str := [...]string{"private", "public"} + return str[p] +} + +// TopRange is used to specify the time window for 'top' result when topList is chosen as sort param +type TopRange int + +// Enum for TopRange values +const ( + Day TopRange = iota + 1 + ThreeDay + Week + Month + ThreeMonth + SixMonth + Year +) + +func (t TopRange) String() string { + str := [...]string{"1d", "3d", "1w", "1M", "3M", "6M", "1y"} + return str[t] +} + +// Resolution specifies the image resolution to find +type Resolution struct { + Width int64 + Height int64 +} + +func (r Resolution) String() string { + return fmt.Sprintf("%vx%v", r.Width, r.Height) +} + +func (r Resolution) isValid() bool { + return r.Width > 0 && r.Height > 0 +} + +// Ratio may be used to specify the aspect ratio of the search +type Ratio struct { + Horizontal int + Vertical int +} + +func (r Ratio) String() string { + return fmt.Sprintf("%vx%v", r.Horizontal, r.Vertical) +} + +func (r Ratio) isValid() bool { + return r.Vertical > 0 && r.Horizontal > 0 +} + +// WallpaperID is a string representing a wallpaper +type WallpaperID string + +// Q is used to hold the Q params for various fulltext options that the WH Search supports +type Q struct { + Tags []string + ExcudeTags []string + UserName string + TagID int + Type string // Type is one of png/jpg + Like WallpaperID +} + +func (q Q) toQuery() url.Values { + var sb strings.Builder + for _, tag := range q.Tags { + sb.WriteString("+") + sb.WriteString(tag) + } + for _, etag := range q.ExcudeTags { + sb.WriteString("-") + sb.WriteString(etag) + } + if len(q.UserName) > 0 { + sb.WriteString("@") + sb.WriteString(q.UserName) + } + if len(q.Type) > 0 { + sb.WriteString("type:") + sb.WriteString(q.Type) + } + out := url.Values{} + val := sb.String() + if len(val) > 0 { + out.Set("q", val) + } + return out +} + +// Search provides various parameters to search for on wallhaven +type Search struct { + Query Q + Categories string + Purities string + Sorting Sort + Order Order + TopRange string + AtLeast Resolution + Resolutions []Resolution + Ratios []Ratio + Colors []string // Colors is an array of hex colors represented as strings in #RRGGBB format + Page int +} + +func (s Search) toQuery() url.Values { + v := s.Query.toQuery() + if s.Categories != "" { + v.Add("categories", s.Categories) + } + if s.Purities != "" { + v.Add("purity", s.Purities) + } + if s.Sorting > 0 { + v.Add("sorting", s.Sorting.String()) + } + if s.Order > 0 { + v.Add("order", s.Order.String()) + } + if s.TopRange != "" && s.Sorting == Toplist { + v.Add("topRange", s.TopRange) + } + if s.AtLeast.isValid() { + v.Add("atleast", s.AtLeast.String()) + } + if len(s.Resolutions) > 0 { + outRes := []string{} + for _, res := range s.Resolutions { + if res.isValid() { + outRes = append(outRes, res.String()) + } + } + if len(outRes) > 0 { + v.Add("resolutions", strings.Join(outRes, ",")) + } + } + if len(s.Ratios) > 0 { + outRat := []string{} + for _, rat := range s.Ratios { + if rat.isValid() { + outRat = append(outRat, rat.String()) + } + } + if len(outRat) > 0 { + v.Add("ratios", strings.Join(outRat, ",")) + } + } + if len(s.Colors) > 0 { + v.Add("colors", strings.Join([]string(s.Colors), ",")) + } + if s.Page > 0 { + v.Add("page", strconv.Itoa(s.Page)) + } + return v +} + +// SearchWallpapers performs a search on WH given a set of criteria. +// Note that this API behaves slightly differently than the various +// single item apis as it also includes the metadata for paging purposes +func SearchWallpapers(search *Search) (*SearchResults, error) { + resp, err := getWithValues("/search/", search.toQuery()) + if err != nil { + return nil, err + } + + out := &SearchResults{} + err = processResponse(resp, out) + if err != nil { + return nil, err + } + return out, nil +} + +func processResponse(resp *http.Response, out interface{}) error { + byt, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + return json.Unmarshal(byt, out) +} + +// Result Structs -- server responses + +// SearchResults a wrapper containing search results from wh +type SearchResults struct { + Data []Wallpaper `json:"data"` +} + +// Wallpaper information about a given wallpaper +type Wallpaper struct { + Path string `json:"path"` +} + +// Tag full data on a given wallpaper tag +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` + Alias string `json:"alias"` + CategoryID int `json:"category_id"` + Category string `json:"category"` + Purity string `json:"purity"` + CreatedAt string `json:"created_at"` +} + +const baseURL = "https://wallhaven.cc/api/v1" + +func getWithBase(p string) string { + return baseURL + p +} + +func getWithValues(p string, v url.Values) (*http.Response, error) { + u, err := url.Parse(getWithBase(p)) + if err != nil { + return nil, err + } + u.RawQuery = v.Encode() + return getAuthedResponse(u.String()) +} + +func getAuthedResponse(url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-API-Key", os.Getenv("WH_API_KEY")) + return client.Do(req) +} + +var client = &http.Client{} + +func download(filepath string, resp *http.Response) error { + defer resp.Body.Close() + + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +// Download downloads a wallpaper given the local filepath to save the wallpaper to +func (w *Wallpaper) Download(dir string) error { + resp, err := getAuthedResponse(w.Path) + if err != nil { + return err + } + path := filepath.Join(dir, path.Base(w.Path)) + return download(path, resp) +} diff --git a/wallhaven_dl b/wallhaven_dl new file mode 100755 index 0000000..d5321f7 Binary files /dev/null and b/wallhaven_dl differ