summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaria Carolin Chabowski <laria@laria.me>2020-10-04 22:58:03 +0200
committerLaria Carolin Chabowski <laria@laria.me>2020-10-04 22:58:03 +0200
commit5b456afb49bfa6ff1567510bc1d9362377d32216 (patch)
treee9f13973251405095dd8b0a31930ca10caa57163
parent1f700deeb1a26ab289178b76518a33faa3f51545 (diff)
downloadstartpage-5b456afb49bfa6ff1567510bc1d9362377d32216.tar.gz
startpage-5b456afb49bfa6ff1567510bc1d9362377d32216.tar.bz2
startpage-5b456afb49bfa6ff1567510bc1d9362377d32216.zip
New config format and new features
We're now using json as a config format instead of our weird own format. The weather icon is now optional, simply don't define WeatherPlace in your config. You can now specify which subreddit to get background images from. The default is still /r/EarthPorn.
-rw-r--r--README.markdown53
-rw-r--r--config.go63
-rw-r--r--conflang.go160
-rw-r--r--earthporn.go216
-rw-r--r--http_getter/http_getter.go19
-rw-r--r--interval/interval.go33
-rw-r--r--links.go12
-rw-r--r--main.go207
-rw-r--r--reddit_background/reddit_background.go231
-rw-r--r--template.html35
-rw-r--r--weather/weather.go59
11 files changed, 511 insertions, 577 deletions
diff --git a/README.markdown b/README.markdown
index 8f3eb15..aa87dbb 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,4 +1,4 @@
-A simple start page with a background image from [/r/EarthPorn](http://www.reddit.com/r/earthporn), weather from [yr.no](http://www.yr.no) and customizable links.
+A simple start page with a background image from a subreddit ([/r/EarthPorn](http://www.reddit.com/r/earthporn) by default), weather from [yr.no](http://www.yr.no) and customizable links.
## Screenshot
![Screenshot](http://i.imgur.com/u42QOZe.png)
@@ -9,43 +9,32 @@ A simple start page with a background image from [/r/EarthPorn](http://www.reddi
## Configuration
-The startpage configuration is located in the file ~/.startpagerc. It is a list of commands. A command has a name and can optionally have parameters separated by spaces or tabs. A backspace `\` will interpret the next charcter literally (can be used to escape whitespace, linebreaks and backspaces). Commands are separated by newlines.
+The optional startpage configuration is a JSON file located at `~/.config/startpage/config.json`.
-These commands are implemented:
+Here is an example with all fields filled out.
-### `set-weather-place`
+ {
+ // The place for which to get the weather data. If omitted, no weather will be shown
+ "WeatherPlace": "Germany/Hamburg/Hamburg",
-Takes one argument, the place used for weather info. startpage uses [yr.no](http://www.yr.no) to get weather data. Use the search box on that page to search for your place. You will then be redirected to an URL like this: `http://www.yr.no/place/<myplace>`. Put the `<myplace>` part after the `set-weather-place` command like this:
+ // A list of links to show. Can be omitted.
+ "Links": [
+ {
+ "Title": "example",
+ "URL": "https://www.example.com"
+ }
+ ],
- set-weather-place <myplace>
+ // If set, background images can be saved here
+ "BackgroundSavepath": "/home/laria/Pictures/cool-backgrounds",
-### `add-link`
+ // If set, this limits the background image size, the default is DEFAULT_BACKGROUND_MAXDIM (=2500)
+ "BackgroundMaxdim": 4000,
-Add a link that is displayed on the startpage. First argument is the title, second one the URL.
-
-Example:
-
- add-link github http://www.github.com
- add-link reddit http://www.reddit.com
- add-link go http://www.golang.org
- add-link another\ example http://www.example.org
-
-### `set-earthporn-savepath`
-
-Sets the diretory to save EarthPorn images to.
-
-Example:
-
- set-earthporn-savepath /home/foobar/Pictures/earthporn
-
-### `set-maxdim`
-
-Sets the maximum width/height of an EarthPorn image. If the image is larger, it will be resized for the background (it will be saved in it's original size).
-
-Example:
-
- set-maxdim 4000
+ // Get background images from this subreddit. Defaults to "EarthPorn"
+ "ImageSubreddit": "ruralporn"
+ }
## Running
-If `$GOPATH/bin` is in your `$PATH`, you can run startpage with the command `startpage`. By default, startpage listens on port 25145. You can change that with a command line switch: `startpage -laddr :<port>` \ No newline at end of file
+If `$GOPATH/bin` is in your `$PATH`, you can run startpage with the command `startpage`. By default, startpage listens on port 25145. You can change that with a command line switch: `startpage -laddr :<port>`
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..c32364e
--- /dev/null
+++ b/config.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "encoding/json"
+ "github.com/adrg/xdg"
+ "os"
+)
+
+// Config contains all configuration options that are read from .config/startpage/config.json
+type Config struct {
+ // The place for which to get the weather data. If omitted, no weather will be shown
+ WeatherPlace string
+
+ // A list of links to show
+ Links []Link
+
+ // If set, background images can be saved here
+ BackgroundSavepath string
+
+ // If set, this limits the background image size, the default is DEFAULT_BACKGROUND_MAXDIM
+ BackgroundMaxdim *int
+
+ // Get background images from this subreddit. Defaults to "EarthPorn"
+ ImageSubreddit string
+}
+
+func LoadConfig() (*Config, error) {
+ path, err := xdg.ConfigFile("startpage/config.json")
+ if err != nil {
+ return nil, err
+ }
+
+ file, err := os.Open(path)
+ switch {
+ case err == nil:
+ // All OK, we can continue
+ case os.IsNotExist(err):
+ return &Config{}, nil
+ default:
+ return nil, err
+ }
+
+ defer file.Close()
+
+ decoder := json.NewDecoder(file)
+ var config Config
+ err = decoder.Decode(&config)
+ if err == nil {
+ return &config, nil
+ } else {
+ return nil, err
+ }
+}
+
+const DEFAULT_BACKGROUND_MAXDIM = 2500
+
+func (c Config) GetBackgroundMaxdim() int {
+ if c.BackgroundMaxdim == nil {
+ return DEFAULT_BACKGROUND_MAXDIM
+ } else {
+ return *c.BackgroundMaxdim
+ }
+}
diff --git a/conflang.go b/conflang.go
deleted file mode 100644
index 0b4a856..0000000
--- a/conflang.go
+++ /dev/null
@@ -1,160 +0,0 @@
-package main
-
-import (
- "bufio"
- "fmt"
- "io"
-)
-
-type toktype int
-
-const (
- tText toktype = iota
- tNextCmd
-)
-
-type token struct {
- Type toktype
- Data string
- Line int
-}
-
-func scan(r io.Reader, tokens chan<- token, errch chan<- error) {
- emit := func(t toktype, d []byte, line int) {
- if t == tText && len(d) == 0 {
- return
- }
- tokens <- token{t, string(d), line}
- }
-
- err := func() error {
- br := bufio.NewReader(r)
-
- escaped := false
- data := []byte{}
- line := 1
-
- for {
- b, err := br.ReadByte()
-
- switch err {
- case nil:
- case io.EOF:
- return nil
- default:
- return ErrorAtLine{line, err}
- }
-
- if b == '\n' {
- line++
- }
-
- if escaped {
- data = append(data, b)
- escaped = false
- continue
- }
-
- switch b {
- case '\\':
- escaped = true
- case ' ', '\t':
- emit(tText, data, line)
- data = data[:0]
- case '\n':
- emit(tText, data, line)
- data = data[:0]
- emit(tNextCmd, nil, line)
- default:
- data = append(data, b)
- }
- }
-
- emit(tText, data, line)
- emit(tNextCmd, nil, line)
- return nil
- }()
-
- close(tokens)
- errch <- err
-}
-
-type command struct {
- Name string
- Params []string
- Line int
-}
-
-func parse(tokens <-chan token, cmds chan<- command) {
- defer close(cmds)
-
- startcmd := true
- cmd := command{"", make([]string, 0), 0}
-
- for tok := range tokens {
- switch tok.Type {
- case tText:
- if startcmd {
- cmd.Name = tok.Data
- cmd.Line = tok.Line
- startcmd = false
- } else {
- cmd.Params = append(cmd.Params, tok.Data)
- }
- case tNextCmd:
- if !startcmd {
- cmds <- cmd
- cmd.Name = ""
- cmd.Params = make([]string, 0)
- startcmd = true
- }
- }
- }
-}
-
-type cmdfunc func(params []string) error
-
-var commands = map[string]cmdfunc{
- "nop": func(_ []string) error { return nil },
-}
-
-func RegisterCommand(name string, f cmdfunc) {
- commands[name] = f
-}
-
-type ErrorAtLine struct {
- Line int
- Err error
-}
-
-func (err ErrorAtLine) Error() string {
- return fmt.Sprintf("%s (at line %d)", err.Err, err.Line)
-}
-
-type CommandNotFound string
-
-func (c CommandNotFound) Error() string {
- return fmt.Sprintf("Command \"%s\" not found", c)
-}
-
-func RunCommands(r io.Reader) error {
- errch := make(chan error)
- tokens := make(chan token)
- cmds := make(chan command)
-
- go scan(r, tokens, errch)
- go parse(tokens, cmds)
-
- for cmd := range cmds {
- f, ok := commands[cmd.Name]
- if !ok {
- return ErrorAtLine{cmd.Line, CommandNotFound(cmd.Name)}
- }
-
- if err := f(cmd.Params); err != nil {
- return ErrorAtLine{cmd.Line, err}
- }
- }
-
- return <-errch
-}
diff --git a/earthporn.go b/earthporn.go
deleted file mode 100644
index 29ff208..0000000
--- a/earthporn.go
+++ /dev/null
@@ -1,216 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "github.com/nfnt/resize"
- "image"
- _ "image/gif"
- "image/jpeg"
- _ "image/png"
- "io"
- "log"
- "mime"
- "net/http"
- "os"
- "path"
- "strconv"
- "strings"
-)
-
-type redditList struct {
- Data struct {
- Children []struct {
- Data *EarthPorn `json:"data"`
- } `json:"children"`
- } `json:"data"`
-}
-
-type EarthPorn struct {
- Title string `json:"title"`
- URL string `json:"url,omitempty"`
- Permalink string `json:"permalink"`
- Domain string `json:"domain"`
- Saved bool
- data, origdata []byte
- mediatype string
-}
-
-const earthPornURL = "http://www.reddit.com/r/EarthPorn.json"
-
-func GetEarthPorn() (EarthPorn, error) {
- resp, err := http.Get(earthPornURL)
- if err != nil {
- return EarthPorn{}, err
- }
- defer resp.Body.Close()
-
- var list redditList
- dec := json.NewDecoder(resp.Body)
- if err := dec.Decode(&list); err != nil {
- return EarthPorn{}, err
- }
-
- for _, el := range list.Data.Children {
- p := el.Data
-
- if p.Domain == "self.EarthPorn" {
- continue
- }
-
- if p.URL == "" {
- continue
- }
-
- if p.fetch() {
- return *p, nil
- }
- }
-
- return EarthPorn{}, errors.New("Could not get EarthPorn: No image could be extracted")
-}
-
-func (p *EarthPorn) fetch() bool {
- // TODO: We can do further processing here (e.g. if we get a link to flickr, extract the image).
- // For now, we will simply test, if the URL points to an image.
-
- resp, err := http.Get(p.URL)
- if err != nil {
- return false
- }
- defer resp.Body.Close()
-
- t := resp.Header.Get("Content-Type")
- if t == "" {
- log.Printf("could not get image of %s: no Content-Type", p.Permalink)
- return false
- }
- mt, _, err := mime.ParseMediaType(t)
- if err != nil {
- log.Printf("could not get image of %s: %s", p.Permalink, err)
- return false
- }
-
- if strings.Split(mt, "/")[0] != "image" {
- log.Printf("could not get image of %s: not an image", p.Permalink)
- return false
- }
-
- buf := new(bytes.Buffer)
- if _, err := io.Copy(buf, resp.Body); err != nil {
- log.Printf("could not get image of %s: %s", p.Permalink, err)
- return false
- }
-
- p.mediatype = t
- p.origdata = buf.Bytes()
- p.data = p.origdata
- p.resize()
- return true
-}
-
-var maxdim = 2500
-
-func setMaxdimCmd(params []string) error {
- if len(params) != 1 {
- return errors.New("set-maxdim needs one parameter, which must be a positive number")
- }
-
- newmaxdim, err := strconv.ParseUint(params[0], 10, 32)
- if err != nil {
- return fmt.Errorf("1st parameter of set-maxdim could not be parsed as a number: %s", err)
- }
- maxdim = int(newmaxdim)
-
- return nil
-}
-
-// resize resizes image, if it's very large
-func (p *EarthPorn) resize() {
- im, _, err := image.Decode(bytes.NewReader(p.origdata))
- if err != nil {
- log.Printf("Failed decoding in resize(): %s", err)
- return
- }
-
- size := im.Bounds().Size()
- if !(size.X > maxdim || size.Y > maxdim) {
- return
- }
-
- var w, h int
- if size.X > size.Y {
- h = maxdim * (size.Y / size.X)
- w = maxdim
- } else {
- w = maxdim * (size.X / size.Y)
- h = maxdim
- }
-
- im = resize.Resize(uint(w), uint(h), im, resize.Bicubic)
-
- buf := new(bytes.Buffer)
-
- if err := jpeg.Encode(buf, im, &jpeg.Options{Quality: 90}); err != nil {
- log.Printf("Failed encoding in resize(): %s", err)
- return
- }
-
- p.data = buf.Bytes()
- p.mediatype = "image/jpeg"
-}
-
-var extensions = map[string]string{
- "image/png": "png",
- "image/jpeg": "jpg",
- "image/gif": "gif",
- "image/x-ms-bmp": "bmp",
- "image/x-bmp": "bmp",
- "image/bmp": "bmp",
- "image/tiff": "tiff",
- "image/tiff-fx": "tiff",
- "image/x-targa": "tga",
- "image/x-tga": "tga",
- "image/webp": "webp",
-}
-
-var savepath = ""
-
-func setSavepathCmd(params []string) error {
- if len(params) != 1 {
- return errors.New("set-earthporn-savepath needs one parameter")
- }
-
- savepath = params[0]
- return nil
-}
-
-const maxTitleLenInFilename = 100
-
-func (p *EarthPorn) save() error {
- ext := extensions[p.mediatype]
- pp := strings.Split(p.Permalink, "/")
- threadid := pp[len(pp)-3]
-
- title := strings.Replace(p.Title, "/", "-", -1)
- tRunes := []rune(title)
- if len(tRunes) > maxTitleLenInFilename {
- title = string(tRunes[0:maxTitleLenInFilename])
- }
-
- f, err := os.Create(path.Join(savepath, threadid+" - "+title+"."+ext))
- if err != nil {
- return fmt.Errorf("Could not save earthporn: %s", err)
- }
- defer f.Close()
-
- if _, err := f.Write(p.origdata); err != nil {
- return fmt.Errorf("Could not save earthporn: %s", err)
- }
-
- p.Saved = true
-
- return nil
-}
diff --git a/http_getter/http_getter.go b/http_getter/http_getter.go
new file mode 100644
index 0000000..5df3ac2
--- /dev/null
+++ b/http_getter/http_getter.go
@@ -0,0 +1,19 @@
+package http_getter
+
+import (
+ "net/http"
+)
+
+// Get is like http.Get, but we're sending our own user agent.
+func Get(url string) (*http.Response, error) {
+ client := &http.Client{}
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("User-Agent", "github.com/slivasur/startpage")
+
+ return client.Do(req)
+}
diff --git a/interval/interval.go b/interval/interval.go
new file mode 100644
index 0000000..35996bb
--- /dev/null
+++ b/interval/interval.go
@@ -0,0 +1,33 @@
+package interval
+
+import (
+ "time"
+)
+
+// IntervalRunner is used to run a function again only after a certain time
+type IntervalRunner struct {
+ durationSuccess time.Duration
+ durationFailure time.Duration
+ lastRun time.Time
+ success bool
+}
+
+func NewIntervalRunner(durationSuccess, durationFailure time.Duration) *IntervalRunner {
+ return &IntervalRunner{durationSuccess: durationSuccess, durationFailure: durationFailure}
+}
+
+// Run runs the passed in fn function, if the last run is a certain duration ago.
+func (ir *IntervalRunner) Run(fn func() bool) {
+ var duration time.Duration
+ if ir.success {
+ duration = ir.durationSuccess
+ } else {
+ duration = ir.durationFailure
+ }
+
+ now := time.Now()
+ if ir.lastRun.Add(duration).Before(now) {
+ ir.success = fn()
+ ir.lastRun = now
+ }
+}
diff --git a/links.go b/links.go
index bd7b624..de88f63 100644
--- a/links.go
+++ b/links.go
@@ -1,7 +1,6 @@
package main
import (
- "errors"
"html/template"
)
@@ -9,14 +8,3 @@ type Link struct {
Title string
URL template.URL
}
-
-var links = []Link{}
-
-func addLinkCmd(params []string) error {
- if len(params) != 2 {
- return errors.New("add-link needs 2 parameters: title url")
- }
-
- links = append(links, Link{params[0], template.URL(params[1])})
- return nil
-}
diff --git a/main.go b/main.go
index 911fd13..e1ee16c 100644
--- a/main.go
+++ b/main.go
@@ -4,90 +4,16 @@ import (
"errors"
"flag"
"fmt"
+ "github.com/silvasur/startpage/reddit_background"
+ "github.com/silvasur/startpage/weather"
"html/template"
"log"
"net/http"
"os"
"path"
"strings"
- "time"
- "github.com/silvasur/startpage/weather"
)
-var porn EarthPorn
-var curWeather weather.Weather
-
-func trylater(ch chan<- bool) {
- log.Println("Will try again later...")
- time.Sleep(1 * time.Minute)
- ch <- true
-}
-
-func earthPornUpdater(ch chan bool) {
- for _ = range ch {
- newporn, err := GetEarthPorn()
- if err != nil {
- log.Print(err)
- go trylater(ch)
- continue
- }
-
- porn = newporn
- log.Println("New fap material!")
- }
-}
-
-var place = ""
-
-func setPlaceCmd(params []string) error {
- if len(params) != 1 {
- return errors.New("set-weather-place needs one parameter")
- }
-
- place = params[0]
- return nil
-}
-
-func weatherUpdater(ch chan bool) {
- for _ = range ch {
- newW, err := weather.CurrentWeather(place)
- if err != nil {
- log.Printf("Failed getting latest weather data: %s", err)
- go trylater(ch)
- continue
- }
-
- curWeather = newW
- log.Println("New weather data")
- }
-}
-
-func intervalUpdates(d time.Duration, stopch <-chan bool, chans ...chan<- bool) {
- send := func(chans ...chan<- bool) {
- for _, ch := range chans {
- go func(ch chan<- bool) {
- ch <- true
- }(ch)
- }
- }
-
- send(chans...)
-
- tick := time.NewTicker(d)
- for {
- select {
- case <-tick.C:
- send(chans...)
- case <-stopch:
- tick.Stop()
- for _, ch := range chans {
- close(ch)
- }
- return
- }
- }
-}
-
var tpl *template.Template
func loadTemplate() {
@@ -103,23 +29,21 @@ func loadTemplate() {
panic(errors.New("could not find template in $GOPATH/src/github.com/silvasur/startpage"))
}
-func initCmds() {
- RegisterCommand("add-link", addLinkCmd)
- RegisterCommand("set-earthporn-savepath", setSavepathCmd)
- RegisterCommand("set-weather-place", setPlaceCmd)
- RegisterCommand("set-maxdim", setMaxdimCmd)
+func buildWeatherProvider(config Config) *weather.WeatherProvider {
+ if config.WeatherPlace == "" {
+ return nil
+ }
+
+ return weather.NewWeatherProvider(config.WeatherPlace)
}
-func runConf() {
- f, err := os.Open(os.ExpandEnv("$HOME/.startpagerc"))
- if err != nil {
- log.Fatalf("Could not open startpagerc: %s", err)
+func buildRedditImageProvider(config Config) *reddit_background.RedditImageProvider {
+ subreddit := config.ImageSubreddit
+ if subreddit == "" {
+ subreddit = "EarthPorn"
}
- defer f.Close()
- if err := RunCommands(f); err != nil {
- log.Fatal(err)
- }
+ return reddit_background.NewRedditImageProvider(config.GetBackgroundMaxdim(), subreddit)
}
func main() {
@@ -127,67 +51,90 @@ func main() {
flag.Parse()
loadTemplate()
- initCmds()
- runConf()
- pornch := make(chan bool)
- weatherch := make(chan bool)
- stopch := make(chan bool)
+ config, err := LoadConfig()
+ if err != nil {
+ log.Fatalf("Failed loading config: %s", err)
+ }
+
+ redditImageProvider := buildRedditImageProvider(*config)
- go intervalUpdates(30*time.Minute, stopch, pornch, weatherch)
- go weatherUpdater(weatherch)
- go earthPornUpdater(pornch)
+ http.HandleFunc("/", startpage(*config, redditImageProvider))
+ http.HandleFunc("/bgimg", bgimg(redditImageProvider))
- defer func(stopch chan<- bool) {
- stopch <- true
- }(stopch)
+ if config.BackgroundSavepath != "" {
+ http.HandleFunc("/savebg", savebg(redditImageProvider, config.BackgroundSavepath))
+ }
- http.HandleFunc("/", startpage)
- http.HandleFunc("/bgimg", bgimg)
- http.HandleFunc("/savebg", savebg)
log.Fatal(http.ListenAndServe(*laddr, nil))
}
type TplData struct {
- Porn *EarthPorn
- Weather *weather.Weather
- Links []Link
+ BgImage *reddit_background.RedditImage
+ Weather *weather.Weather
+ Links []Link
+ CanSaveBg bool
}
-func startpage(rw http.ResponseWriter, req *http.Request) {
- defer req.Body.Close()
+func startpage(config Config, redditImageProvider *reddit_background.RedditImageProvider) http.HandlerFunc {
+ weatherProvider := buildWeatherProvider(config)
+
+ return func(rw http.ResponseWriter, req *http.Request) {
+ defer req.Body.Close()
+
+ var curWeather *weather.Weather = nil
+ if weatherProvider != nil {
+ var err error
+ if curWeather, err = weatherProvider.CurrentWeather(); err != nil {
+ log.Printf("Failed getting weather: %s", err)
+ }
+ }
- if err := tpl.Execute(rw, &TplData{&porn, &curWeather, links}); err != nil {
- log.Printf("Failed executing template: %s\n", err)
+ if err := tpl.Execute(rw, &TplData{
+ redditImageProvider.Image(),
+ curWeather,
+ config.Links,
+ config.BackgroundSavepath != "",
+ }); err != nil {
+ log.Printf("Failed executing template: %s\n", err)
+ }
}
}
-func bgimg(rw http.ResponseWriter, req *http.Request) {
- defer req.Body.Close()
+func bgimg(redditImageProvider *reddit_background.RedditImageProvider) http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ defer req.Body.Close()
- if len(porn.data) == 0 {
- rw.WriteHeader(http.StatusNotFound)
- }
+ image := redditImageProvider.Image()
- rw.Header().Add("Content-Type", porn.mediatype)
- if _, err := rw.Write(porn.data); err != nil {
- log.Printf("Failed serving background: %s", err)
+ if image == nil || len(image.Data) == 0 {
+ rw.WriteHeader(http.StatusNotFound)
+ }
+
+ rw.Header().Add("Content-Type", image.Mediatype)
+ if _, err := rw.Write(image.Data); err != nil {
+ log.Printf("Failed serving background: %s", err)
+ }
}
}
-func savebg(rw http.ResponseWriter, req *http.Request) {
- defer req.Body.Close()
+func savebg(redditImageProvider *reddit_background.RedditImageProvider, savepath string) http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ defer req.Body.Close()
- if len(porn.data) == 0 {
- fmt.Fprintln(rw, "No earth porn available")
- return
- }
+ image := redditImageProvider.Image()
- if err := (&porn).save(); err != nil {
- log.Println(err)
- fmt.Fprintln(rw, err)
- }
+ if image == nil || len(image.Data) == 0 {
+ fmt.Fprintln(rw, "No background image available")
+ return
+ }
- rw.Header().Add("Location", "/")
- rw.WriteHeader(http.StatusFound)
+ if err := image.Save(savepath); err != nil {
+ log.Println(err)
+ fmt.Fprintln(rw, err)
+ }
+
+ rw.Header().Add("Location", "/")
+ rw.WriteHeader(http.StatusFound)
+ }
}
diff --git a/reddit_background/reddit_background.go b/reddit_background/reddit_background.go
new file mode 100644
index 0000000..67b25d0
--- /dev/null
+++ b/reddit_background/reddit_background.go
@@ -0,0 +1,231 @@
+package reddit_background
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/nfnt/resize"
+ "github.com/silvasur/startpage/http_getter"
+ "github.com/silvasur/startpage/interval"
+ "image"
+ _ "image/gif"
+ "image/jpeg"
+ _ "image/png"
+ "io"
+ "log"
+ "mime"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+type redditList struct {
+ Data struct {
+ Children []struct {
+ Data *RedditImage `json:"data"`
+ } `json:"children"`
+ } `json:"data"`
+}
+
+type RedditImage struct {
+ Title string `json:"title"`
+ URL string `json:"url,omitempty"`
+ Permalink string `json:"permalink"`
+ Domain string `json:"domain"`
+ Saved bool `json:"-"`
+ Data []byte `json:"-"`
+ origdata []byte `json:"-"`
+ Mediatype string `json:"-"`
+}
+
+func GetRedditImage(maxsize int, subreddit string) (*RedditImage, error) {
+ subredditUrl := fmt.Sprintf("https://www.reddit.com/r/%s.json", subreddit)
+
+ resp, err := http_getter.Get(subredditUrl)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var list redditList
+ dec := json.NewDecoder(resp.Body)
+ if err := dec.Decode(&list); err != nil {
+ return nil, err
+ }
+
+ for _, el := range list.Data.Children {
+ ri := el.Data
+
+ if ri.Domain == fmt.Sprintf("self.%s", subreddit) {
+ continue
+ }
+
+ if ri.URL == "" {
+ continue
+ }
+
+ if ri.fetch(maxsize) {
+ return ri, nil
+ }
+ }
+
+ return nil, errors.New("Could not get RedditImage: No image could be extracted")
+}
+
+func (ri *RedditImage) fetch(maxsize int) bool {
+ // TODO: We can do further processing here (e.g. if we get a link to flickr, extract the image).
+ // For now, we will simply test, if the URL points to an image.
+
+ resp, err := http.Get(ri.URL)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+
+ t := resp.Header.Get("Content-Type")
+ if t == "" {
+ log.Printf("could not get image of %s: no Content-Type", ri.Permalink)
+ return false
+ }
+ mt, _, err := mime.ParseMediaType(t)
+ if err != nil {
+ log.Printf("could not get image of %s: %s", ri.Permalink, err)
+ return false
+ }
+
+ if strings.Split(mt, "/")[0] != "image" {
+ log.Printf("could not get image of %s: not an image", ri.Permalink)
+ return false
+ }
+
+ buf := new(bytes.Buffer)
+ if _, err := io.Copy(buf, resp.Body); err != nil {
+ log.Printf("could not get image of %s: %s", ri.Permalink, err)
+ return false
+ }
+
+ ri.Mediatype = t
+ ri.origdata = buf.Bytes()
+ ri.Data = ri.origdata
+ ri.resize(maxsize)
+ return true
+}
+
+// resize resizes image, if it's very large
+func (ri *RedditImage) resize(maxdim int) {
+ im, _, err := image.Decode(bytes.NewReader(ri.origdata))
+ if err != nil {
+ log.Printf("Failed decoding in resize(): %s", err)
+ return
+ }
+
+ size := im.Bounds().Size()
+ if !(size.X > maxdim || size.Y > maxdim) {
+ return
+ }
+
+ var w, h int
+ if size.X > size.Y {
+ h = maxdim * (size.Y / size.X)
+ w = maxdim
+ } else {
+ w = maxdim * (size.X / size.Y)
+ h = maxdim
+ }
+
+ im = resize.Resize(uint(w), uint(h), im, resize.Bicubic)
+
+ buf := new(bytes.Buffer)
+
+ if err := jpeg.Encode(buf, im, &jpeg.Options{Quality: 90}); err != nil {
+ log.Printf("Failed encoding in resize(): %s", err)
+ return
+ }
+
+ ri.Data = buf.Bytes()
+ ri.Mediatype = "image/jpeg"
+}
+
+var extensions = map[string]string{
+ "image/png": "png",
+ "image/jpeg": "jpg",
+ "image/gif": "gif",
+ "image/x-ms-bmp": "bmp",
+ "image/x-bmp": "bmp",
+ "image/bmp": "bmp",
+ "image/tiff": "tiff",
+ "image/tiff-fx": "tiff",
+ "image/x-targa": "tga",
+ "image/x-tga": "tga",
+ "image/webp": "webp",
+}
+
+const maxTitleLenInFilename = 100
+
+func (p *RedditImage) Save(savepath string) error {
+ ext := extensions[p.Mediatype]
+ pp := strings.Split(p.Permalink, "/")
+ threadid := pp[len(pp)-3]
+
+ title := strings.Replace(p.Title, "/", "-", -1)
+ tRunes := []rune(title)
+ if len(tRunes) > maxTitleLenInFilename {
+ title = string(tRunes[0:maxTitleLenInFilename])
+ }
+
+ f, err := os.Create(path.Join(savepath, threadid+" - "+title+"."+ext))
+ if err != nil {
+ return fmt.Errorf("Could not save image: %s", err)
+ }
+ defer f.Close()
+
+ if _, err := f.Write(p.origdata); err != nil {
+ return fmt.Errorf("Could not save image: %s", err)
+ }
+
+ p.Saved = true
+
+ return nil
+}
+
+const (
+ UPDATE_INTERVAL = 30 * time.Minute
+ RETRY_INTERVAL = 1 * time.Minute
+)
+
+type RedditImageProvider struct {
+ intervalRunner *interval.IntervalRunner
+ maxsize int
+ subreddit string
+ image *RedditImage
+}
+
+func NewRedditImageProvider(maxsize int, subreddit string) *RedditImageProvider {
+ return &RedditImageProvider{
+ intervalRunner: interval.NewIntervalRunner(UPDATE_INTERVAL, RETRY_INTERVAL),
+ maxsize: maxsize,
+ subreddit: subreddit,
+ }
+}
+
+func (rip *RedditImageProvider) Image() *RedditImage {
+ rip.intervalRunner.Run(func() bool {
+ log.Printf("Getting new RedditImage")
+
+ var err error
+ rip.image, err = GetRedditImage(rip.maxsize, rip.subreddit)
+
+ if err == nil {
+ log.Printf("Successfully loaded RedditImage")
+ } else {
+ log.Printf("Failed loading RedditImage: %s", err)
+ }
+
+ return err == nil
+ })
+
+ return rip.image
+}
diff --git a/template.html b/template.html
index e0f3a30..ffb833c 100644
--- a/template.html
+++ b/template.html
@@ -4,11 +4,16 @@
<title>Startpage</title>
<style type="text/css">
body {
- background: black url(/bgimg) no-repeat center center fixed;
+ background: grey;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
+ {{ if .BgImage }}
+ body {
+ background: black url(/bgimg) no-repeat center center fixed;
+ }
+ {{ end }}
a {
color: #eee;
text-decoration: none;
@@ -31,7 +36,7 @@
left: 10mm;
background: black;
}
- #earthporninfo {
+ #imageinfo {
position: fixed;
bottom: 2mm;
right: 10mm;
@@ -72,10 +77,12 @@
</style>
</head>
<body>
- <div id="weather">
- <a href="{{ .Weather.URL }}"><img src="{{ .Weather.Icon }}" alt="" /></a>
- <span id="temp">{{ .Weather.Temp.Value }}°</span>
- </div>
+ {{ if .Weather }}
+ <div id="weather">
+ <a href="{{ .Weather.URL }}"><img src="{{ .Weather.Icon }}" alt="" /></a>
+ <span id="temp">{{ .Weather.Temp.Value }}°</span>
+ </div>
+ {{ end }}
{{ if .Links }}
<div id="links">
<ul>
@@ -88,13 +95,17 @@
<footer>
- <div id="yr_no_credit">Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK</div>
+ {{ if .Weather }}
+ <div id="yr_no_credit">Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK</div>
+ {{ end }}
- <span id="earthporninfo">
- <a href="http://reddit.com{{ .Porn.Permalink }}">{{ .Porn.Title }}</a>
- |
- {{ if .Porn.Saved }}saved{{ else }}<a href="/savebg">save</a>{{ end }}
- </span>
+ {{ if .BgImage }}
+ <span id="imageinfo">
+ <a href="http://reddit.com{{ .BgImage.Permalink }}">{{ .BgImage.Title }}</a>{{ if .CanSaveBg}}
+ | {{ if .BgImage.Saved }}saved{{ else }}<a href="/savebg">save</a>{{ end }}
+ {{ end }}
+ </span>
+ {{ end}}
</footer>
</body>
</html>
diff --git a/weather/weather.go b/weather/weather.go
index 87b7a83..2a1d23f 100644
--- a/weather/weather.go
+++ b/weather/weather.go
@@ -3,7 +3,9 @@ package weather
import (
"encoding/xml"
- "net/http"
+ "github.com/silvasur/startpage/http_getter"
+ "github.com/silvasur/startpage/interval"
+ "log"
"time"
)
@@ -35,33 +37,60 @@ type weatherdata struct {
Forecast []*Weather `xml:"forecast>tabular>time"`
}
-func CurrentWeather(place string) (Weather, error) {
+func CurrentWeather(place string) (*Weather, error) {
url := "http://www.yr.no/place/" + place + "/forecast_hour_by_hour.xml"
- client := &http.Client{}
-
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return Weather{}, err
- }
-
- req.Header.Add("User-Agent", "github.com/slivasur/startpage/weather")
-
- resp, err := client.Do(req)
+ resp, err := http_getter.Get(url)
if err != nil {
- return Weather{}, err
+ return nil, err
}
defer resp.Body.Close()
var wd weatherdata
dec := xml.NewDecoder(resp.Body)
if err := dec.Decode(&wd); err != nil {
- return Weather{}, err
+ return nil, err
}
w := wd.Forecast[0]
w.URL = "http://www.yr.no/place/" + place
w.prepIcon()
- return *w, nil
+ return w, nil
+}
+
+type WeatherProvider struct {
+ place string
+ intervalRunner *interval.IntervalRunner
+ weather *Weather
+ err error
+}
+
+const (
+ UPDATE_INTERVAL = 30 * time.Minute
+ RETRY_INTERVAL = 1 * time.Minute
+)
+
+func NewWeatherProvider(place string) *WeatherProvider {
+ return &WeatherProvider{
+ place: place,
+ intervalRunner: interval.NewIntervalRunner(UPDATE_INTERVAL, RETRY_INTERVAL),
+ }
+}
+
+func (wp *WeatherProvider) CurrentWeather() (*Weather, error) {
+ wp.intervalRunner.Run(func() bool {
+ log.Printf("Getting new weather data")
+ wp.weather, wp.err = CurrentWeather(wp.place)
+
+ if wp.err == nil {
+ log.Printf("Successfully loaded weather data")
+ } else {
+ log.Printf("Failed loading weather data: %s", wp.err)
+ }
+
+ return wp.err == nil
+ })
+
+ return wp.weather, wp.err
}