mirror of https://github.com/perkeep/perkeep.git
284 lines
8.2 KiB
Go
284 lines
8.2 KiB
Go
/*
|
|
Copyright 2014 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 picasa implements an importer for picasa.com accounts.
|
|
package picasa
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"camlistore.org/pkg/blob"
|
|
"camlistore.org/pkg/context"
|
|
"camlistore.org/pkg/importer"
|
|
"camlistore.org/pkg/schema"
|
|
|
|
"camlistore.org/third_party/code.google.com/p/goauth2/oauth"
|
|
"camlistore.org/third_party/github.com/tgulacsi/picago"
|
|
)
|
|
|
|
const (
|
|
apiURL = "https://api.picasa.com/v2/"
|
|
authURL = "https://accounts.google.com/o/oauth2/auth"
|
|
tokenURL = "https://accounts.google.com/o/oauth2/token"
|
|
scopeURL = "https://picasaweb.google.com/data/"
|
|
)
|
|
|
|
func init() {
|
|
importer.Register("picasa", newImporter())
|
|
}
|
|
|
|
var _ importer.ImporterSetupHTMLer = (*imp)(nil)
|
|
|
|
type imp struct {
|
|
importer.ExtendedOAuth2
|
|
}
|
|
|
|
func newImporter() *imp {
|
|
return &imp{
|
|
importer.NewExtendedOAuth2(
|
|
oauth.Config{AuthURL: authURL, TokenURL: tokenURL, Scope: scopeURL},
|
|
func(ctx *context.Context, accessToken string) (*importer.UserInfo, error) {
|
|
u, err := getUserInfo(ctx, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
firstName, lastName := u.Name, ""
|
|
i := strings.LastIndex(u.Name, " ")
|
|
if i >= 0 {
|
|
firstName, lastName = u.Name[:i], u.Name[i+1:]
|
|
}
|
|
return &importer.UserInfo{
|
|
ID: u.ID,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
}, nil
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (im imp) AccountSetupHTML(host *importer.Host) string {
|
|
base := host.ImporterBaseURL() + "picasa"
|
|
return fmt.Sprintf(`
|
|
<h1>Configuring Picasa</h1>
|
|
<p>Visit <a href='https://console.developers.google.com/'>https://console.developers.google.com/</a>
|
|
and click "CREATE PROJECT".</p>
|
|
<p>Then under "APIs & auth" click on "Credentials", then "CREATE NEW CLIENT ID".</p>
|
|
<p>Use the following settings:</p>
|
|
<ul>
|
|
<li>Web application</li>
|
|
<li>Authorized JavaScript origins: <b>%s</b></li>
|
|
<li>Authorized Redirect URI: <b>%s</b></li>
|
|
</ul>
|
|
<p>Click "Create Client ID". Copy the "Client ID" and "Client Secret" into the boxes above.</p>
|
|
`, base, base+"/callback")
|
|
}
|
|
|
|
// A run is our state for a given run of the importer.
|
|
type run struct {
|
|
*importer.RunContext
|
|
im *imp
|
|
client *http.Client
|
|
}
|
|
|
|
func (im *imp) Run(ctx *importer.RunContext) error {
|
|
clientId, secret, err := ctx.Credentials()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client := ctx.Host.HTTPClient()
|
|
client.Transport = &oauth.Transport{
|
|
Config: &oauth.Config{
|
|
ClientId: clientId,
|
|
ClientSecret: secret,
|
|
AuthURL: authURL,
|
|
TokenURL: tokenURL,
|
|
Scope: scopeURL,
|
|
},
|
|
Token: &oauth.Token{
|
|
AccessToken: ctx.AccountNode().Attr(importer.AcctAttrAccessToken),
|
|
},
|
|
}
|
|
r := &run{RunContext: ctx, im: im, client: client}
|
|
if err := r.importAlbums(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *run) importAlbums() error {
|
|
albums, err := picago.GetAlbums(r.client, "default")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
albumsNode, err := r.getTopLevelNode("albums", "Albums")
|
|
for _, album := range albums {
|
|
if r.Context.IsCanceled() {
|
|
return context.ErrCanceled
|
|
}
|
|
if err := r.importAlbum(albumsNode, album, r.client); err != nil {
|
|
return fmt.Errorf("picasa importer: error importing album %s: %v", album, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *run) importAlbum(albumsNode *importer.Object, album picago.Album, client *http.Client) error {
|
|
albumNode, err := albumsNode.ChildPathObject(album.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Data reference: https://developers.google.com/picasa-web/docs/2.0/reference
|
|
// TODO(tgulacsi): add more album info
|
|
if err = albumNode.SetAttrs(
|
|
"picasaId", album.ID,
|
|
"camliNodeType", "picasaweb.google.com:album",
|
|
importer.AttrTitle, album.Title,
|
|
importer.AttrLocationText, album.Location,
|
|
); err != nil {
|
|
return fmt.Errorf("error setting album attributes: %v", err)
|
|
}
|
|
|
|
photos, err := picago.GetPhotos(client, "default", album.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("Importing %d photos from album %q (%s)", len(photos), albumNode.Attr("title"),
|
|
albumNode.PermanodeRef())
|
|
|
|
for _, photo := range photos {
|
|
if r.Context.IsCanceled() {
|
|
return context.ErrCanceled
|
|
}
|
|
// TODO(tgulacsi): check when does the photo.ID changes
|
|
|
|
attr := "camliPath:" + photo.ID + "-" + photo.Filename()
|
|
if refString := albumNode.Attr(attr); refString != "" {
|
|
// Check the photoNode's modtime - skip only if it hasn't changed.
|
|
if photoRef, ok := blob.Parse(refString); !ok {
|
|
log.Printf("error parsing attr %s (%s) as ref: %v", attr, refString, err)
|
|
} else {
|
|
photoNode, err := r.Host.ObjectFromRef(photoRef)
|
|
if err != nil {
|
|
log.Printf("error getting object %s: %v", refString, err)
|
|
} else {
|
|
modtime := photoNode.Attr("dateModified")
|
|
switch modtime {
|
|
case "": // no modtime to check against - import again
|
|
log.Printf("No dateModified on %s, re-import.", refString)
|
|
case schema.RFC3339FromTime(photo.Updated):
|
|
// Assume we have this photo already and don't need to refetch.
|
|
log.Printf("Skipping photo with %s, we already have it.", attr)
|
|
continue
|
|
default: // modtimes differ - import again
|
|
switch filepath.Ext(photo.Filename()) {
|
|
case ".mp4", ".m4v":
|
|
log.Printf("photo %s is a video, cannot rely on its modtime, so not importing again.",
|
|
attr[10:])
|
|
continue
|
|
default:
|
|
log.Printf("photo %s imported(%s) != remote(%s), so importing again",
|
|
attr[10:], modtime, schema.RFC3339FromTime(photo.Updated))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("importing %s", attr[10:])
|
|
photoNode, err := r.importPhoto(albumNode, photo, client)
|
|
if err != nil {
|
|
log.Printf("error importing photo %s: %v", photo.URL, err)
|
|
continue
|
|
}
|
|
err = albumNode.SetAttr(attr, photoNode.PermanodeRef().String())
|
|
if err != nil {
|
|
log.Printf("Error adding photo to album: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *run) importPhoto(albumNode *importer.Object, photo picago.Photo, client *http.Client) (*importer.Object, error) {
|
|
body, err := picago.DownloadPhoto(client, photo.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileRef, err := schema.WriteFileFromReader(
|
|
r.Host.Target(),
|
|
fmt.Sprintf("%s--%s-%s",
|
|
strings.Replace(albumNode.Attr("name"), "/", "--", -1),
|
|
photo.ID,
|
|
photo.Filename()),
|
|
body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error writing file: %v", err)
|
|
}
|
|
if !fileRef.Valid() {
|
|
return nil, fmt.Errorf("Error slurping photo: %s", photo.URL)
|
|
}
|
|
photoNode, err := r.Host.NewObject()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating photo permanode %s under %s: %v",
|
|
photo.Filename(), albumNode.Attr("name"), err)
|
|
}
|
|
|
|
// TODO(tgulacsi): add more attrs (comments ?)
|
|
// for names, see http://schema.org/ImageObject and http://schema.org/CreativeWork
|
|
if err := photoNode.SetAttrs(
|
|
"camliContent", fileRef.String(),
|
|
"picasaId", photo.ID,
|
|
importer.AttrTitle, photo.Title,
|
|
"caption", photo.Summary,
|
|
importer.AttrDescription, photo.Description,
|
|
importer.AttrLocationText, photo.Location,
|
|
"latitude", fmt.Sprintf("%f", photo.Latitude),
|
|
"longitude", fmt.Sprintf("%f", photo.Longitude),
|
|
"dateModified", schema.RFC3339FromTime(photo.Updated),
|
|
"datePublished", schema.RFC3339FromTime(photo.Published),
|
|
); err != nil {
|
|
return nil, fmt.Errorf("error adding file to photo node: %v", err)
|
|
}
|
|
if err := photoNode.SetAttrValues("tag", photo.Keywords); err != nil {
|
|
return nil, fmt.Errorf("error setting photoNode's tags: %v", err)
|
|
}
|
|
|
|
return photoNode, nil
|
|
}
|
|
|
|
func (r *run) getTopLevelNode(path string, title string) (*importer.Object, error) {
|
|
childObject, err := r.RootNode().ChildPathObject(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := childObject.SetAttr("title", title); err != nil {
|
|
return nil, err
|
|
}
|
|
return childObject, nil
|
|
}
|
|
|
|
func getUserInfo(ctx *context.Context, accessToken string) (picago.User, error) {
|
|
return picago.GetUser(ctx.HTTPClient(), "")
|
|
}
|