2013-10-19 22:49:10 +00:00
|
|
|
/*
|
|
|
|
Copyright 2013 The Camlistore Authors
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Package flickr implements an importer for flickr.com accounts.
|
|
|
|
package flickr
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2013-11-17 03:08:00 +00:00
|
|
|
"errors"
|
2013-10-19 22:49:10 +00:00
|
|
|
"fmt"
|
2013-11-18 05:50:13 +00:00
|
|
|
"log"
|
2013-10-19 22:49:10 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2013-11-19 04:53:46 +00:00
|
|
|
"strings"
|
2013-10-19 22:49:10 +00:00
|
|
|
|
|
|
|
"camlistore.org/pkg/importer"
|
|
|
|
"camlistore.org/pkg/jsonconfig"
|
2013-11-18 05:50:13 +00:00
|
|
|
"camlistore.org/pkg/schema"
|
2013-11-17 03:08:00 +00:00
|
|
|
"camlistore.org/third_party/github.com/garyburd/go-oauth/oauth"
|
2013-10-19 22:49:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2013-11-17 03:08:00 +00:00
|
|
|
apiURL = "http://api.flickr.com/services/rest/"
|
2013-10-19 22:49:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
importer.Register("flickr", newFromConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
type imp struct {
|
2013-11-17 20:29:07 +00:00
|
|
|
host *importer.Host
|
|
|
|
user *userInfo // nil if the user isn't authenticated
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
2013-11-17 03:08:00 +00:00
|
|
|
func newFromConfig(cfg jsonconfig.Obj, host *importer.Host) (importer.Importer, error) {
|
2013-11-19 04:53:46 +00:00
|
|
|
apiKey := cfg.RequiredString("apiKey")
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
parts := strings.Split(apiKey, ":")
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return nil, fmt.Errorf("Flickr importer: Invalid apiKey configuration: %q", apiKey)
|
|
|
|
}
|
2013-11-17 03:08:00 +00:00
|
|
|
oauthClient.Credentials = oauth.Credentials{
|
2013-11-19 04:53:46 +00:00
|
|
|
Token: parts[0],
|
|
|
|
Secret: parts[1],
|
2013-11-17 03:08:00 +00:00
|
|
|
}
|
2013-11-19 04:53:46 +00:00
|
|
|
user, err := readCredentials()
|
|
|
|
if err != nil {
|
2013-10-19 22:49:10 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2013-11-17 03:08:00 +00:00
|
|
|
return &imp{
|
|
|
|
host: host,
|
2013-11-17 20:29:07 +00:00
|
|
|
user: user,
|
2013-11-17 03:08:00 +00:00
|
|
|
}, nil
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) CanHandleURL(url string) bool { return false }
|
|
|
|
func (im *imp) ImportURL(url string) error { panic("unused") }
|
|
|
|
|
|
|
|
func (im *imp) Prefix() string {
|
2013-11-17 20:29:07 +00:00
|
|
|
// This should only get called when we're importing, so it's OK to
|
|
|
|
// assume we're authenticated.
|
|
|
|
return fmt.Sprintf("flickr:%s", im.user.Id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) String() string {
|
|
|
|
// We use this in logging when we're not authenticated, so it should do
|
|
|
|
// something reasonable in that case.
|
|
|
|
userId := "<unauthenticated>"
|
|
|
|
if im.user != nil {
|
|
|
|
userId = im.user.Id
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("flickr:%s", userId)
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type photoMeta struct {
|
|
|
|
Id string
|
|
|
|
Title string
|
|
|
|
Ispublic int
|
|
|
|
Isfriend int
|
|
|
|
Isfamily int
|
|
|
|
Description struct {
|
|
|
|
Content string `json:"_content"`
|
|
|
|
}
|
|
|
|
Dateupload string
|
|
|
|
Datetaken string
|
|
|
|
Originalformat string
|
|
|
|
Lastupdate string
|
|
|
|
Latitude float32
|
|
|
|
Longitude float32
|
|
|
|
Tags string
|
|
|
|
Machinetags string `json:"machine_tags"`
|
|
|
|
Views string
|
|
|
|
Media string
|
|
|
|
URL string `json:"url_o"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type searchPhotosResult struct {
|
|
|
|
Photos struct {
|
|
|
|
Page int
|
|
|
|
Pages int
|
|
|
|
Perpage int
|
|
|
|
Total int `json:",string"`
|
2013-11-18 05:50:13 +00:00
|
|
|
Photo []*photoMeta
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Stat string
|
|
|
|
}
|
|
|
|
|
2013-11-17 03:08:00 +00:00
|
|
|
func (im *imp) Run(intr importer.Interrupt) error {
|
2013-10-19 22:49:10 +00:00
|
|
|
resp := searchPhotosResult{}
|
2013-11-18 05:50:13 +00:00
|
|
|
if err := im.flickrAPIRequest(url.Values{
|
2013-11-17 03:08:00 +00:00
|
|
|
"method": {"flickr.photos.search"},
|
|
|
|
"user_id": {"me"},
|
|
|
|
"extras": {"description, date_upload, date_taken, original_format, last_update, geo, tags, machine_tags, views, media, url_o"}},
|
2013-10-19 22:49:10 +00:00
|
|
|
&resp); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2013-11-18 05:50:13 +00:00
|
|
|
photos, err := im.getPhotosNode()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
log.Printf("Importing %d photos into permanode %s",
|
|
|
|
len(resp.Photos.Photo), photos.PermanodeRef().String())
|
|
|
|
|
2013-10-19 22:49:10 +00:00
|
|
|
for _, item := range resp.Photos.Photo {
|
2013-11-18 05:50:13 +00:00
|
|
|
if err := im.importPhoto(photos, item); err != nil {
|
|
|
|
log.Printf("Flickr importer: error importing %s: %s", item.Id, err)
|
|
|
|
continue
|
|
|
|
}
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
2013-11-18 05:50:13 +00:00
|
|
|
|
2013-10-19 22:49:10 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2013-11-18 05:50:13 +00:00
|
|
|
// TODO(aa):
|
|
|
|
// * Parallelize: http://golang.org/doc/effective_go.html#concurrency
|
|
|
|
// * Record lastmodified and don't reimport photos that haven't changed
|
|
|
|
// * Do more than one "page" worth of results
|
|
|
|
// * Report progress and errors back through host interface
|
|
|
|
// * All the rest of the metadata (see photoMeta)
|
|
|
|
// * What happens when changes at Flickr conflict with changes made through the Camlistore UI?
|
|
|
|
// * Test!
|
|
|
|
func (im *imp) importPhoto(parent *importer.Object, photo *photoMeta) error {
|
|
|
|
filename := fmt.Sprintf("%s.%s", photo.Id, photo.Originalformat)
|
|
|
|
|
|
|
|
res, err := im.flickrRequest(photo.URL, url.Values{})
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Flickr importer: Could not fetch %s: %s", photo.URL, err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
fileRef, err := schema.WriteFileFromReader(im.host.Target(), filename, res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
photoNode, err := parent.ChildPathObject(filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := photoNode.SetAttr("camliContent", fileRef.String()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if photo.Title != "" {
|
|
|
|
photoNode.SetAttr("title", photo.Title)
|
|
|
|
}
|
|
|
|
if photo.Description.Content != "" {
|
|
|
|
photoNode.SetAttr("description", photo.Description.Content)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) getPhotosNode() (*importer.Object, error) {
|
|
|
|
root, err := im.getRootNode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
photos, err := root.ChildPathObject("photos")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
2013-11-18 05:50:13 +00:00
|
|
|
if err := photos.SetAttr("title", "Photos"); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return photos, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) getRootNode() (*importer.Object, error) {
|
|
|
|
root, err := im.host.RootObject()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := root.SetAttr("title", "Flickr Import Root"); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return root, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) flickrAPIRequest(form url.Values, result interface{}) error {
|
2013-11-17 03:08:00 +00:00
|
|
|
form.Set("format", "json")
|
|
|
|
form.Set("nojsoncallback", "1")
|
2013-11-18 05:50:13 +00:00
|
|
|
res, err := im.flickrRequest(apiURL, form)
|
2013-10-19 22:49:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2013-11-18 05:50:13 +00:00
|
|
|
defer res.Body.Close()
|
|
|
|
return json.NewDecoder(res.Body).Decode(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (im *imp) flickrRequest(url string, form url.Values) (*http.Response, error) {
|
|
|
|
if im.user == nil {
|
|
|
|
return nil, errors.New("Not logged in. Go to /importer-flickr/login.")
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := oauthClient.Get(im.host.HTTPClient(), im.user.Cred, url, form)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2013-10-19 22:49:10 +00:00
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
2013-11-18 05:50:13 +00:00
|
|
|
return nil, fmt.Errorf("Auth request failed with: %s", res.Status)
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|
|
|
|
|
2013-11-18 05:50:13 +00:00
|
|
|
return res, nil
|
2013-10-19 22:49:10 +00:00
|
|
|
}
|