├── .godir ├── .gitignore ├── Procfile ├── config_example ├── README.markdown ├── main_test.go └── main.go /.godir: -------------------------------------------------------------------------------- 1 | awwimage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: awwimage -------------------------------------------------------------------------------- /config_example: -------------------------------------------------------------------------------- 1 | copy_and_paste_tumblr_consumer_key_here -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | This is an API for a random cute image from tumblr. The list is refreshed every hour. 4 | 5 | Demo (refresh to see a new image every time) 6 | ------- 7 | * http://awwimage.herokuapp.com/random/pug/preview 8 | * http://awwimage.herokuapp.com/random/corgi/preview 9 | * http://awwimage.herokuapp.com/random/shiba/preview 10 | * http://awwimage.herokuapp.com/random/cat/preview 11 | * http://awwimage.herokuapp.com/random/giraffe/preview 12 | 13 | Ramdom 14 | ------ 15 | * http://awwimage.herokuapp.com/random/pug 16 | * http://awwimage.herokuapp.com/random/corgi 17 | * http://awwimage.herokuapp.com/random/shiba 18 | * http://awwimage.herokuapp.com/random/cat 19 | * http://awwimage.herokuapp.com/random/giraffe 20 | 21 | Bomb 22 | ---- 23 | * http://awwimage.herokuapp.com/bomb/pug 24 | * http://awwimage.herokuapp.com/bomb/corgi 25 | * http://awwimage.herokuapp.com/bomb/shiba 26 | * http://awwimage.herokuapp.com/bomb/cat 27 | * http://awwimage.herokuapp.com/bomb/giraffe 28 | 29 | Direct link 30 | ----------- 31 | * http://awwimage.herokuapp.com/random/pug/url 32 | * http://awwimage.herokuapp.com/random/corgi/url 33 | * http://awwimage.herokuapp.com/random/shiba/url 34 | * http://awwimage.herokuapp.com/random/cat/url 35 | * http://awwimage.herokuapp.com/random/giraffe/url 36 | 37 | heroku deploy 38 | ------------- 39 | * heroku create / heroku git:remote -a repo_name 40 | * heroku config:set TUMBLR_KEY=copy_and_paste_tumblr_consumer_key_here BUILDPACK_URL=https://github.com/kr/heroku-buildpack-go.git 41 | 42 | appengine 43 | --------- 44 | ~/code/appengine/appcfg.py --oauth2 update . 45 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | var page1 = ` 13 | { 14 | "meta": { 15 | "status": 200, 16 | "msg": "OK" 17 | }, 18 | "response": [ 19 | { 20 | "blog_name": "marangio", 21 | "id": 111415925944, 22 | "post_url": "http://marangio.tumblr.com/post/111415925944/lbungeejumping", 23 | "slug": "lbungeejumping", 24 | "type": "photo", 25 | "date": "2015-02-18 23:44:08 GMT", 26 | "timestamp": 1424303048, 27 | "state": "published", 28 | "format": "html", 29 | "reblog_key": "J558MRGe", 30 | "tags": [ 31 | "bungeejumping", 32 | "jump", 33 | "mouse", 34 | "giraffe", 35 | "illustrator", 36 | "illustration", 37 | "ilustração", 38 | "ilustración", 39 | "abbildung", 40 | "graphic", 41 | "graphicart", 42 | "graphicillustration", 43 | "graphicline", 44 | "graphicdraw", 45 | "graphicdesign", 46 | "art", 47 | "draw", 48 | "line" 49 | ], 50 | "short_url": "http://tmblr.co/ZVKBKm1dmw1ou", 51 | "highlighted": [ 52 | ], 53 | "note_count": 0, 54 | "caption": "

LBUNGEEJUMPING

", 55 | "image_permalink": "http://marangio.tumblr.com/image/111415925944", 56 | "photos": [ 57 | { 58 | "caption": "", 59 | "alt_sizes": [ 60 | { 61 | "width": 1280, 62 | "height": 1280, 63 | "url": "http://41.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_1280.jpg" 64 | }, 65 | { 66 | "width": 500, 67 | "height": 500, 68 | "url": "http://36.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_500.jpg" 69 | }, 70 | { 71 | "width": 400, 72 | "height": 400, 73 | "url": "http://36.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_400.jpg" 74 | }, 75 | { 76 | "width": 250, 77 | "height": 250, 78 | "url": "http://40.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_250.jpg" 79 | }, 80 | { 81 | "width": 100, 82 | "height": 100, 83 | "url": "http://40.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_100.jpg" 84 | }, 85 | { 86 | "width": 75, 87 | "height": 75, 88 | "url": "http://40.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_75sq.jpg" 89 | } 90 | ], 91 | "original_size": { 92 | "width": 1280, 93 | "height": 1280, 94 | "url": "http://41.media.tumblr.com/c8dcfcb11f07801f41db2d2ff46b13ba/tumblr_njzr9kqbUI1u0ep4qo1_1280.jpg" 95 | } 96 | } 97 | ] 98 | }, 99 | { 100 | "blog_name": "piecomic", 101 | "id": 111409833242, 102 | "post_url": "http://piecomic.tumblr.com/post/111409833242", 103 | "slug": "", 104 | "type": "photo", 105 | "date": "2015-02-18 22:30:13 GMT", 106 | "timestamp": 1424298613, 107 | "state": "published", 108 | "format": "html", 109 | "reblog_key": "bcMPdZhi", 110 | "tags": [ 111 | "cartoon", 112 | "lol", 113 | "animals", 114 | "giraffe" 115 | ], 116 | "short_url": "http://tmblr.co/ZU7Znx1dmYoKQ", 117 | "highlighted": [ 118 | ], 119 | "note_count": 165, 120 | "source_url": "http://www.piecomic.com", 121 | "source_title": "piecomic.com", 122 | "caption": "", 123 | "link_url": "http://www.piecomic.com", 124 | "image_permalink": "http://piecomic.tumblr.com/image/111409833242", 125 | "photos": [ 126 | { 127 | "caption": "", 128 | "alt_sizes": [ 129 | { 130 | "width": 500, 131 | "height": 552, 132 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_500.jpg" 133 | }, 134 | { 135 | "width": 400, 136 | "height": 442, 137 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_400.jpg" 138 | }, 139 | { 140 | "width": 250, 141 | "height": 276, 142 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_250.jpg" 143 | }, 144 | { 145 | "width": 100, 146 | "height": 110, 147 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_100.jpg" 148 | }, 149 | { 150 | "width": 75, 151 | "height": 75, 152 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_75sq.jpg" 153 | } 154 | ], 155 | "original_size": { 156 | "width": 500, 157 | "height": 552, 158 | "url": "http://40.media.tumblr.com/dfbaff69a8390c1bc3c048a2c41b7ab0/tumblr_njznudWb0D1qhnegdo1_500.jpg" 159 | } 160 | } 161 | ] 162 | } 163 | ] 164 | } 165 | ` 166 | 167 | type testFetcher struct{} 168 | 169 | func (fetcher testFetcher) Fetch(url string) ([]byte, error) { 170 | return []byte(page1), nil 171 | } 172 | 173 | func reset_image_mapping() { 174 | image_mapping = make(map[string][]string) 175 | } 176 | 177 | type ResponseJson struct { 178 | Url string 179 | } 180 | 181 | func TestPopulateImageMapping(t *testing.T) { 182 | var kind = "pug" 183 | var image_limit_was = image_limit 184 | image_limit = 20 185 | fetcher := testFetcher{} 186 | populate(kind, &fetcher) 187 | if _, ok := image_mapping[kind]; !ok { 188 | t.Error("populate failed") 189 | } 190 | if len(image_mapping[kind]) != 20 { 191 | t.Error("populate count failed") 192 | } 193 | log.Println(image_limit_was) 194 | image_limit = image_limit_was 195 | reset_image_mapping() 196 | } 197 | 198 | func TestRandom(t *testing.T) { 199 | var url = "http://example.com/1.jpg" 200 | image_mapping["pug"] = []string{url} 201 | ts := httptest.NewServer(getHttpHandler()) 202 | defer ts.Close() 203 | res, err := http.Get(ts.URL + "/random/pug") 204 | if err != nil { 205 | t.Error("random failed", err) 206 | } 207 | if res.StatusCode != 200 { 208 | t.Error("random failed", res.StatusCode) 209 | } 210 | body_bytes, err := ioutil.ReadAll(res.Body) 211 | res.Body.Close() 212 | response_json := ResponseJson{} 213 | json.Unmarshal(body_bytes, &response_json) 214 | if response_json.Url != url { 215 | t.Error("random failed", response_json.Url) 216 | } 217 | reset_image_mapping() 218 | } 219 | 220 | func TestRandomPreview(t *testing.T) { 221 | var url = "http://example.com/1.jpg" 222 | image_mapping["pug"] = []string{url} 223 | ts := httptest.NewServer(getHttpHandler()) 224 | defer ts.Close() 225 | res, _ := http.Get(ts.URL + "/random/pug/preview") 226 | body_bytes, _ := ioutil.ReadAll(res.Body) 227 | res.Body.Close() 228 | if string(body_bytes) != "" { 229 | t.Error("random preview failed", string(body_bytes)) 230 | } 231 | reset_image_mapping() 232 | } 233 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/bmizerany/pat" 17 | ) 18 | 19 | var image_limit = 300 20 | var last_refreshed_at time.Time 21 | var image_mapping = make(map[string][]string) 22 | var api_key string 23 | var kinds = []string{"pug", "corgi", "shiba", "cat", "giraffe"} 24 | 25 | type PhotoProperty struct { 26 | Url string 27 | } 28 | 29 | type Photo struct { 30 | OriginalPhoto PhotoProperty `json:"original_size"` 31 | } 32 | 33 | // TODO Timestamp sometime is string 34 | type Blog struct { 35 | Timestamp int 36 | Photos []Photo 37 | } 38 | 39 | type TaggedApiResponse struct { 40 | Blogs []Blog `json:"response"` 41 | } 42 | 43 | type FetcherInterface interface { 44 | Fetch(url string) ([]byte, error) 45 | } 46 | 47 | type tumblrFetcher struct{} 48 | 49 | func instruction(res http.ResponseWriter, req *http.Request) { 50 | fmt.Fprint(res, get_json_string(endpoints())) 51 | } 52 | 53 | func count(res http.ResponseWriter, req *http.Request) { 54 | kind := req.URL.Query().Get(":kind") 55 | fmt.Fprint(res, get_json_string(&map[string]int{"count": len(image_mapping[kind])})) 56 | } 57 | 58 | func random(res http.ResponseWriter, req *http.Request) { 59 | var err error 60 | refresh_every_hour() 61 | kind := req.URL.Query().Get(":kind") 62 | err = check_kind(kind) 63 | if err != nil { 64 | res.WriteHeader(400) 65 | fmt.Fprint(res, get_json_string(&map[string]string{"error": err.Error()})) 66 | return 67 | } 68 | action := req.URL.Query().Get(":action") 69 | if len(image_mapping[kind]) == 0 { 70 | err = wait_for_populating(kind) 71 | if err != nil { 72 | res.WriteHeader(500) 73 | fmt.Fprint(res, get_json_string(&map[string]string{"error": err.Error()})) 74 | return 75 | } 76 | } 77 | index := rand.Intn(len(image_mapping[kind])) 78 | url := image_mapping[kind][index] 79 | if action == "preview" { 80 | fmt.Fprint(res, "") 81 | } else if action == "url" { 82 | fmt.Fprint(res, url) 83 | } else { 84 | fmt.Fprint(res, get_json_string(&map[string]string{"url": url})) 85 | } 86 | } 87 | 88 | func bomb(res http.ResponseWriter, req *http.Request) { 89 | var err error 90 | refresh_every_hour() 91 | var result []string 92 | kind := req.URL.Query().Get(":kind") 93 | number := req.URL.Query().Get(":number") 94 | if len(image_mapping[kind]) == 0 { 95 | err = wait_for_populating(kind) 96 | if err != nil { 97 | res.WriteHeader(500) 98 | fmt.Fprint(res, get_json_string(&map[string]string{"error": err.Error()})) 99 | return 100 | } 101 | } 102 | 103 | if number == "" { 104 | number = "4" 105 | } 106 | number_str, _ := strconv.Atoi(number) 107 | permutation := rand.Perm(len(image_mapping[kind])) 108 | for _, pos := range permutation[:number_str] { 109 | result = append(result, image_mapping[kind][pos]) 110 | } 111 | fmt.Fprint(res, get_json_string(&map[string][]string{"urls": result})) 112 | } 113 | 114 | func panic_on_error(err error) { 115 | if err != nil { 116 | panic(err) 117 | } 118 | } 119 | 120 | func log_on_error(err error) { 121 | if err != nil { 122 | log.Println("Error: ", err) 123 | } 124 | } 125 | 126 | func endpoints() *map[string]map[string]string { 127 | return &map[string]map[string]string{ 128 | "DEMO": { 129 | "pug": "http://awwimage.herokuapp.com/random/pug/preview", 130 | "corgi": "http://awwimage.herokuapp.com/random/corgi/preview", 131 | "shiba": "http://awwimage.herokuapp.com/random/shiba/preview", 132 | "cat": "http://awwimage.herokuapp.com/random/cat/preview", 133 | "giraffe": "http://awwimage.herokuapp.com/random/giraffe/preview", 134 | }, 135 | "ENDPOINT": { 136 | "/instruction": "Get a random image. Supported keywords: pug, corgi, shiba, cat, giraffe", 137 | "/count/:keyword": "Number of images available", 138 | "/random/:keyword/:action": "Get a random image. Optional action: url (get the link directly), preview (preview the image)", 139 | "/bomb/:keyword/:number": "Get a number of images. Default to 4", 140 | }, 141 | "ABOUT": { 142 | "source": "http://github.com/zhengjia/awwimage", 143 | }, 144 | } 145 | } 146 | 147 | func wait_for_populating(kind string) (err error) { 148 | done := make(chan bool) 149 | go check_image_presence(kind, done) 150 | select { 151 | case <-done: 152 | return 153 | case <-time.After(time.Second * 30): 154 | return errors.New("Timeout") 155 | } 156 | } 157 | 158 | func (*tumblrFetcher) Fetch(url string) ([]byte, error) { 159 | var err error 160 | var resp *http.Response 161 | var body_bytes []byte 162 | resp, err = http.Get(url) 163 | log_on_error(err) 164 | body_bytes, err = ioutil.ReadAll(resp.Body) 165 | resp.Body.Close() 166 | log_on_error(err) 167 | return body_bytes, err 168 | } 169 | 170 | func populate(kind string, fetcher FetcherInterface) { 171 | var timestamp int 172 | var url string 173 | var url_template string 174 | var err error 175 | var body_bytes []byte 176 | var tagged_api_response *TaggedApiResponse 177 | var results []string 178 | url_template = "http://api.tumblr.com/v2/tagged?api_key=" + api_key + "&tag=" + kind 179 | for len(results) < image_limit { 180 | if timestamp == 0 { 181 | url = url_template 182 | } else { 183 | url = url_template + "&before=" + strconv.Itoa(timestamp) 184 | } 185 | body_bytes, err = fetcher.Fetch(url) 186 | if err != nil { 187 | continue 188 | } 189 | err = json.Unmarshal(body_bytes, &tagged_api_response) 190 | log_on_error(err) 191 | for _, Blog := range tagged_api_response.Blogs { 192 | timestamp = Blog.Timestamp 193 | for _, Photo := range Blog.Photos { 194 | results = append(results, Photo.OriginalPhoto.Url) 195 | } 196 | } 197 | } 198 | image_mapping[kind] = results 199 | } 200 | 201 | func populate_mapping() { 202 | last_refreshed_at = time.Now() 203 | fetcher := tumblrFetcher{} 204 | for _, kind := range kinds { 205 | go populate(kind, &fetcher) 206 | } 207 | } 208 | 209 | func refresh_every_hour() { 210 | if time.Now().Sub(last_refreshed_at) > time.Minute*60 { 211 | populate_mapping() 212 | } 213 | } 214 | 215 | func check_image_presence(kind string, done chan bool) { 216 | for len(image_mapping[kind]) == 0 { 217 | time.Sleep(time.Second) 218 | } 219 | done <- true 220 | } 221 | 222 | func check_kind(kind string) (err error) { 223 | for _, k := range kinds { 224 | if k == kind { 225 | return 226 | } 227 | } 228 | err = errors.New("Image type not supported") 229 | return 230 | } 231 | 232 | func get_json_string(v interface{}) string { 233 | result, err := json.MarshalIndent(v, "", " ") 234 | panic_on_error(err) 235 | return string(result) 236 | } 237 | 238 | func get_port() string { 239 | port := os.Getenv("PORT") 240 | if port == "" { 241 | port = "4000" 242 | } 243 | return port 244 | } 245 | 246 | func set_api_key() { 247 | var err error 248 | config, err := ioutil.ReadFile("config") 249 | if err == nil { 250 | api_key = strings.TrimSpace(string(config)) 251 | } else { 252 | api_key = os.Getenv("TUMBLR_KEY") 253 | if api_key == "" { 254 | panic_on_error(errors.New("TUMBLR_KEY isn't set")) 255 | } 256 | } 257 | } 258 | 259 | func getHttpHandler() http.Handler { 260 | m := pat.New() 261 | m.Get("/", http.HandlerFunc(instruction)) 262 | m.Get("/instruction", http.HandlerFunc(instruction)) 263 | m.Get("/count/:kind", http.HandlerFunc(count)) 264 | m.Get("/random/:kind", http.HandlerFunc(random)) 265 | m.Get("/random/:kind/:action", http.HandlerFunc(random)) 266 | m.Get("/bomb/:kind", http.HandlerFunc(bomb)) 267 | m.Get("/bomb/:kind/:number", http.HandlerFunc(bomb)) 268 | return m 269 | } 270 | 271 | func initialize() { 272 | set_api_key() 273 | populate_mapping() 274 | rand.Seed(time.Now().UTC().UnixNano()) 275 | } 276 | 277 | func main() { 278 | initialize() 279 | handler := getHttpHandler() 280 | http.Handle("/", handler) 281 | http.ListenAndServe(":"+get_port(), nil) 282 | } 283 | --------------------------------------------------------------------------------