Skip to content

Instantly share code, notes, and snippets.

@int128
Created June 22, 2011 16:21
Show Gist options
  • Save int128/1040475 to your computer and use it in GitHub Desktop.
Save int128/1040475 to your computer and use it in GitHub Desktop.
Picasa Album Arrangement Service
package org.hidetake.lab.controller.admin.motionPicasa;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Logger;
import org.hidetake.lab.motionPicasa.PhotoArrangementService;
import org.hidetake.lab.motionPicasa.PicasaService;
import org.hidetake.lab.motionPicasa.PicasaServiceLocator;
import org.hidetake.lab.motionPicasa.ScheduleUtil;
import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.util.DateUtil;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.gdata.data.photos.PhotoEntry;
public class DailyRotateController extends Controller
{
private static final Logger logger = Logger.getLogger(DailyRotateController.class.getName());
// 次のタスクを 2:00 にスケジュールする
private static final int TASK_TIME = 2;
@Override
public Navigation run() throws Exception
{
TimeZoneLocator.set(TimeZone.getTimeZone("Asia/Tokyo"));
PicasaService picasaService = PicasaServiceLocator.getInstance(Constants.CREDENTIAL);
PhotoArrangementService photoArrangementService = new PhotoArrangementService(picasaService);
List<PhotoEntry> photos = picasaService.getAlbum(Constants.DROPBOX_ID).getPhotoEntries();
logger.info(photos.size() + " photos in the dropbox will be arranged.");
photoArrangementService.arrangeByDate(photos);
Calendar nextTime = ScheduleUtil.getNearestTimeAt(TASK_TIME);
TaskOptions task = TaskOptions.Builder.withMethod(Method.GET)
.url(request.getRequestURI())
.etaMillis(nextTime.getTimeInMillis());
QueueFactory.getDefaultQueue().add(task);
logger.info("An next task has been scheduled at " + DateUtil.toString(nextTime));
return null;
}
}
package org.hidetake.lab.controller.admin.motionPicasa;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Logger;
import org.hidetake.lab.motionPicasa.PhotoArrangementService;
import org.hidetake.lab.motionPicasa.PicasaService;
import org.hidetake.lab.motionPicasa.PicasaServiceLocator;
import org.hidetake.lab.motionPicasa.ScheduleUtil;
import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.util.DateUtil;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.gdata.data.photos.AlbumEntry;
public class DeleteOldAlbumsController extends Controller
{
// 7日前までのアルバムは残す
private static final int DAYS_PRESERVE = 7;
// 次のタスクを 3:00 にスケジュールする
private static final int TASK_TIME = 3;
private static final Logger logger = Logger.getLogger(DeleteOldAlbumsController.class.getName());
@Override
public Navigation run() throws Exception
{
TimeZoneLocator.set(TimeZone.getTimeZone("Asia/Tokyo"));
PicasaService picasaService = PicasaServiceLocator.getInstance(Constants.CREDENTIAL);
PhotoArrangementService photoArrangementService = new PhotoArrangementService(picasaService);
List<AlbumEntry> deletedAlbums = photoArrangementService.deleteOldAlbums(DAYS_PRESERVE);
logger.info(deletedAlbums.size() + " albums has been deleted.");
Calendar nextTime = ScheduleUtil.getNearestTimeAt(TASK_TIME);
TaskOptions task = TaskOptions.Builder.withMethod(Method.GET)
.url(request.getRequestURI())
.etaMillis(nextTime.getTimeInMillis());
QueueFactory.getDefaultQueue().add(task);
logger.info("An next task has been scheduled at " + DateUtil.toString(nextTime));
return null;
}
}
package org.hidetake.lab.motionPicasa;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slim3.util.DateUtil;
import org.slim3.util.TimeZoneLocator;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.photos.AlbumEntry;
import com.google.gdata.data.photos.PhotoEntry;
import com.google.gdata.util.ServiceException;
/**
* 写真を整理するサービス。
* @author hidetake.org
*/
public class PhotoArrangementService
{
private final PicasaService picasaService;
public PhotoArrangementService(PicasaService picasaService)
{
this.picasaService = picasaService;
}
/**
* 古いアルバムを削除する。
* @param days 残す日数(days=1なら1日前は残す)
* @throws IOException
* @throws ServiceException
*/
public List<AlbumEntry> deleteOldAlbums(int days) throws IOException, ServiceException
{
Calendar baseTime = DateUtil.toCalendar(new Date());
DateUtil.clearTimePart(baseTime);
baseTime.add(Calendar.DAY_OF_MONTH, -days);
List<AlbumEntry> deletedAlbums = new ArrayList<AlbumEntry>();
List<AlbumEntry> albums = this.picasaService.getAlbumList();
for (final AlbumEntry albumEntry : albums) {
if (albumEntry.getPublished().getValue() < baseTime.getTimeInMillis()) {
this.picasaService.execute(new PicasaCallable()
{
@Override
public <V> V call() throws ServiceException, IOException
{
albumEntry.delete();
return null;
}
});
deletedAlbums.add(albumEntry);
}
}
return deletedAlbums;
}
/**
* 写真を日付アルバムに移動する。
* @param photos
* @throws IOException
* @throws ServiceException
*/
public List<PhotoEntry> arrangeByDate(List<PhotoEntry> photos) throws IOException, ServiceException
{
// アルバムが存在しなければ作成する
Map<String, AlbumEntry> albumTitleMap = this.picasaService.getAlbumTitleMap();
Map<Date, List<PhotoEntry>> dailyPhotosMap = getDateMap(photos);
Map<AlbumEntry, List<PhotoEntry>> targetAlbumMap = new HashMap<AlbumEntry, List<PhotoEntry>>(
dailyPhotosMap.size());
for (Map.Entry<Date, List<PhotoEntry>> entry : dailyPhotosMap.entrySet()) {
String albumTitle = DateUtil.toString(entry.getKey(), DateUtil.ISO_DATE_PATTERN);
Calendar albumDate = DateUtil.toCalendar(entry.getKey());
albumDate.add(Calendar.HOUR_OF_DAY, TimeZoneLocator.get().getOffset(albumDate.getTimeInMillis()));
AlbumEntry targetAlbum = albumTitleMap.get(albumTitle);
if (targetAlbum == null) {
targetAlbum = new AlbumEntry();
targetAlbum.setTitle(new PlainTextConstruct(albumTitle));
targetAlbum.setDate(albumDate.getTime());
targetAlbum.setAccess("private");
targetAlbum = this.picasaService.createAlbum(targetAlbum);
}
targetAlbumMap.put(targetAlbum, entry.getValue());
}
// 写真をアルバムIDを書き換える
for (Map.Entry<AlbumEntry, List<PhotoEntry>> entry : targetAlbumMap.entrySet()) {
AlbumEntry targetAlbum = entry.getKey();
String targetAlbumId = basename(targetAlbum.getId());
for (final PhotoEntry photo : entry.getValue()) {
photo.setAlbumId(targetAlbumId);
this.picasaService.execute(new PicasaCallable()
{
@Override
public <V> V call() throws ServiceException, IOException
{
photo.update();
return null;
}
});
}
}
return photos;
}
/**
* 公開日ごとの写真リストを取得する。
* @param album アルバム
* @return 公開日と写真リストのマップ
* @throws ServiceException
*/
public static Map<Date, List<PhotoEntry>> getDateMap(List<PhotoEntry> photos) throws ServiceException
{
Map<Date, List<PhotoEntry>> dateAndPhotoMap = new HashMap<Date, List<PhotoEntry>>();
for (PhotoEntry photo : photos) {
Date date = DateUtil.clearTimePart(photo.getTimestamp());
List<PhotoEntry> photoEntries = dateAndPhotoMap.get(date);
if (photoEntries == null) {
photoEntries = new ArrayList<PhotoEntry>();
dateAndPhotoMap.put(date, photoEntries);
}
photoEntries.add(photo);
}
return dateAndPhotoMap;
}
private static String basename(String uri)
{
return uri.substring(uri.lastIndexOf('/') + 1, uri.length());
}
}
package org.hidetake.lab.motionPicasa;
import java.io.IOException;
import com.google.gdata.util.ServiceException;
public interface PicasaCallable
{
public <V> V call() throws ServiceException, IOException;
}
package org.hidetake.lab.motionPicasa;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import com.google.gdata.client.photos.PicasawebService;
import com.google.gdata.data.photos.AlbumEntry;
import com.google.gdata.data.photos.AlbumFeed;
import com.google.gdata.data.photos.UserFeed;
import com.google.gdata.util.PreconditionFailedException;
import com.google.gdata.util.ServiceException;
/**
* Picasa APIサービス。
* @author hidetake.org
*/
public class PicasaService
{
private static final Logger logger = Logger.getLogger(PicasaService.class.getName());
private static final String USER_FEED_URL = "https://picasaweb.google.com/data/feed/api/user/default";
private final PicasawebService picasawebService;
private int retryLimit = 3;
private int retrySleep = 10 * 1000;
public PicasaService(PicasawebService picasawebService)
{
this.picasawebService = picasawebService;
}
public PicasawebService getService()
{
return this.picasawebService;
}
/**
* リトライ回数を設定する。デフォルトは3回。
* @param retryCount
*/
public void setRetryLimit(int retryCount)
{
this.retryLimit = retryCount;
}
/**
* リトライ時のスリープ時間(ms)を設定する。デフォルトは10秒。
* @param retrySleep
*/
public void setRetrySleep(int retrySleep)
{
this.retrySleep = retrySleep;
}
/**
* APIを実行する。
* @param <V>
* @param picasaCallable
* @return
* @throws ServiceException
* @throws IOException
*/
public <V> V execute(PicasaCallable picasaCallable) throws ServiceException, IOException
{
for (int retry = 0;; retry++) {
try {
return picasaCallable.call();
}
catch (RuntimeException e) {
if (retry > this.retryLimit) {
throw e;
}
logger.warning("Retrying (" + retry + "/" + this.retryLimit + "): " + e);
}
catch (PreconditionFailedException e) {
if (retry > this.retryLimit) {
throw e;
}
logger.warning("Retrying (" + retry + "/" + this.retryLimit + "): " + e);
}
catch (ServiceException e) {
if (retry > this.retryLimit) {
throw e;
}
logger.warning("Retrying (" + retry + "/" + this.retryLimit + "): " + e);
}
catch (IOException e) {
if (retry > this.retryLimit) {
throw e;
}
logger.warning("Retrying (" + retry + "/" + this.retryLimit + "): " + e);
}
try {
Thread.sleep(this.retrySleep);
}
catch (InterruptedException e) {
// should not be thrown in single thread environment
throw new RuntimeException(e);
}
}
}
/**
* アルバムを作成する。
* @param albumEntry
* @return
* @throws IOException
* @throws ServiceException
*/
public AlbumEntry createAlbum(final AlbumEntry albumEntry) throws IOException, ServiceException
{
try {
final URL url = new URL(USER_FEED_URL);
return this.execute(new PicasaCallable()
{
@SuppressWarnings("unchecked")
@Override
public AlbumEntry call() throws IOException, ServiceException
{
return picasawebService.insert(url, albumEntry);
}
});
}
catch (MalformedURLException e) {
// should not be thrown
throw new RuntimeException(e);
}
}
/**
* アルバムを取得する。
* @param albumId
* @return
* @throws IOException
* @throws ServiceException
*/
public AlbumFeed getAlbum(String albumId) throws IOException, ServiceException
{
try {
final URL url = new URL(USER_FEED_URL + "/albumid/" + albumId);
return this.execute(new PicasaCallable()
{
@SuppressWarnings("unchecked")
@Override
public AlbumFeed call() throws IOException, ServiceException
{
return picasawebService.getFeed(url, AlbumFeed.class);
}
});
}
catch (MalformedURLException e) {
// should not be thrown
throw new RuntimeException(e);
}
}
/**
* アルバムの一覧を取得する。
* @return
* @throws IOException
* @throws ServiceException
*/
public List<AlbumEntry> getAlbumList() throws IOException, ServiceException
{
try {
final URL url = new URL(USER_FEED_URL + "?kind=album");
return this.execute(new PicasaCallable()
{
@SuppressWarnings("unchecked")
@Override
public List<AlbumEntry> call() throws IOException, ServiceException
{
return picasawebService.getFeed(url, UserFeed.class).getAlbumEntries();
}
});
}
catch (MalformedURLException e) {
// should not be thrown
throw new RuntimeException(e);
}
}
/**
* アルバムのタイトルをキーとしたマップを取得する。
* @return
* @throws IOException
* @throws ServiceException
*/
public Map<String, AlbumEntry> getAlbumTitleMap() throws IOException, ServiceException
{
List<AlbumEntry> albumList = this.getAlbumList();
Map<String, AlbumEntry> albumMap = new HashMap<String, AlbumEntry>(albumList.size());
for (AlbumEntry album : albumList) {
String title = album.getTitle().getPlainText();
albumMap.put(title, album);
}
return albumMap;
}
}
package org.hidetake.lab.motionPicasa;
import java.util.Calendar;
import java.util.Date;
import org.slim3.util.DateUtil;
public class ScheduleUtil
{
/**
* 指定された時に最も近い日時を返す。
* すなわち、次のタスク実行日時を返す。
* @param hour 毎時(0-23)
* @return
*/
public static Calendar getNearestTimeAt(int hour)
{
return getNearestTimeAt(hour, new Date());
}
public static Calendar getNearestTimeAt(int hour, Date current)
{
Calendar schedule = DateUtil.toCalendar(current);
DateUtil.clearTimePart(schedule);
schedule.add(Calendar.HOUR_OF_DAY, hour);
if (schedule.getTime().before(current)) {
schedule.add(Calendar.DAY_OF_MONTH, 1);
}
return schedule;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment