diff options
-rw-r--r-- | README.markdown | 53 | ||||
-rw-r--r-- | config.go | 63 | ||||
-rw-r--r-- | conflang.go | 160 | ||||
-rw-r--r-- | earthporn.go | 216 | ||||
-rw-r--r-- | http_getter/http_getter.go | 19 | ||||
-rw-r--r-- | interval/interval.go | 33 | ||||
-rw-r--r-- | links.go | 12 | ||||
-rw-r--r-- | main.go | 207 | ||||
-rw-r--r-- | reddit_background/reddit_background.go | 231 | ||||
-rw-r--r-- | template.html | 35 | ||||
-rw-r--r-- | weather/weather.go | 59 |
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 + } +} @@ -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 -} @@ -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 } |