Created
June 22, 2011 16:21
-
-
Save int128/1040475 to your computer and use it in GitHub Desktop.
Picasa Album Arrangement Service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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