summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
}