Skip to content

Commit 30c0180

Browse files
jzacshseehamrun
andauthored
Google media exporter: installs along side Photos, but will replace photos in the future (#1218)
`GoogleMediaExporter` class mosty written by seehamrun@ with later additional commits by jzacsh@ * Create MediaContainerResource to be used by the new MediaExporters and MediaImporters This combines the photo and video resources * Account for videos in the transmogrification logic * Fix typo in the Transmogrification config * Add Video limits to transmogrification config * add clean title to the transmogrification config for videos * undo rename of the videomodel (let this get handled when the PR to rename gets pushed) * fix warning abut overriding equals but not hashcode * remove logic to split up the albums in the media resource * slight fix to the transmogrification function -- move up the root album to the main function * Update MediaContainer to use the new VideoModel instead of VideoObject * Use Album and Video transmogrification specific variables * Fix return case when maxLength <= 0 * Initial skeleton of the GoogleMediaExporter This is currently just a copy of the GooglePhotosExporter without all of the content for exporting videos * Hookup the mediaexporter to the transfer extension * Add support for video exports in the GoogleMediaExporter * fix importer * move convertToVideoModel and convertToPhotoModel into the GoogleMediaItem class * use getFileName as the title for videos * Rename PopulateContainedPhotosList method to PopulateContainedMediaList * Rename `exportPhotos` to `exportMedia` * Add video support for the exportMediaContainerResource function jzacsh@'s continued commits: * fix broken tests: have new data & exceptions both video and photo models have a new "upload time" field so this commit _starts_ to introduce that. also tweaked some of the existing code that was written before Upload...Exception was added to some signatures. * nit: consistency in inline-documentation * nit: stick to existing pattern (of always having a setter) * noop(docs) leave trail to cleanup since this PR is forking code * noop(refactor) mv Temp{Photo,Media}Data noop as this is just a rename of current temp-store logic that is hardcoded to Photos. Later work will come along to extend the TempMediaData object to handle new data (added TODO to this point atop the class). * WIP: fork of Photos tests trivial "passing" * noop(refactor) make name match the forked purpose of this token prefix * WIP: sets roadmap to complete unit test comparing to the history of the forked Exporter _itself_ - the core thing that changed was adding video, but most of the code stayed identical. this unit test then leaves the core "photo set" unit test in place and simply adds another for video, trying to mirror the fork of the exporter itself as closely as possible. * noop(prefactor) clarify photo-logic in tests this clarifies what the photo-specific fakes are between a unit test's arrange and assert phases. that's not always clear for _other_ consts that are pulled to the top of the test file, but I'm trying to merely unblock the addition of video logic here (not do a _general_ cleanup). * bugfix: all the fakes should be readonly * finishes unit test by fixing URL assertion while I'm at it, also refactoring the Media objects involved so google adapter logic isn't doing deep property-null-checking as an API (`isVideo` `isPhoto`). --------- Co-authored-by: Siham Hussein <sihamh@google.com>
1 parent 3e33a07 commit 30c0180

11 files changed

Lines changed: 986 additions & 97 deletions

File tree

extensions/data-transfer/portability-data-transfer-google/src/main/java/org/datatransferproject/datatransfer/google/GoogleTransferExtension.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.datatransferproject.types.common.models.DataVertical.CALENDAR;
55
import static org.datatransferproject.types.common.models.DataVertical.CONTACTS;
66
import static org.datatransferproject.types.common.models.DataVertical.MAIL;
7+
import static org.datatransferproject.types.common.models.DataVertical.MEDIA;
78
import static org.datatransferproject.types.common.models.DataVertical.PHOTOS;
89
import static org.datatransferproject.types.common.models.DataVertical.SOCIAL_POSTS;
910
import static org.datatransferproject.types.common.models.DataVertical.TASKS;
@@ -27,6 +28,7 @@
2728
import org.datatransferproject.datatransfer.google.gplus.GooglePlusExporter;
2829
import org.datatransferproject.datatransfer.google.mail.GoogleMailExporter;
2930
import org.datatransferproject.datatransfer.google.mail.GoogleMailImporter;
31+
import org.datatransferproject.datatransfer.google.media.GoogleMediaExporter;
3032
import org.datatransferproject.datatransfer.google.photos.GooglePhotosExporter;
3133
import org.datatransferproject.datatransfer.google.photos.GooglePhotosImporter;
3234
import org.datatransferproject.datatransfer.google.tasks.GoogleTasksExporter;
@@ -50,7 +52,7 @@ public class GoogleTransferExtension implements TransferExtension {
5052
// TODO: centralized place, or enum type for these
5153
private static final ImmutableList<DataVertical> SUPPORTED_SERVICES =
5254
ImmutableList.of(
53-
BLOBS, CALENDAR, CONTACTS, MAIL, PHOTOS, SOCIAL_POSTS, TASKS, VIDEOS);
55+
BLOBS, CALENDAR, CONTACTS, MAIL, PHOTOS, SOCIAL_POSTS, TASKS, VIDEOS, MEDIA);
5456
private ImmutableMap<DataVertical, Importer> importerMap;
5557
private ImmutableMap<DataVertical, Exporter> exporterMap;
5658
private boolean initialized = false;
@@ -132,6 +134,8 @@ public void initialize(ExtensionContext context) {
132134
exporterBuilder.put(
133135
PHOTOS, new GooglePhotosExporter(credentialFactory, jobStore, jsonFactory, monitor));
134136
exporterBuilder.put(VIDEOS, new GoogleVideosExporter(credentialFactory, jsonFactory));
137+
exporterBuilder.put(
138+
MEDIA, new GoogleMediaExporter(credentialFactory, jobStore, jsonFactory, monitor));
135139

136140
exporterMap = exporterBuilder.build();
137141

extensions/data-transfer/portability-data-transfer-google/src/main/java/org/datatransferproject/datatransfer/google/media/GoogleMediaExporter.java

Lines changed: 422 additions & 0 deletions
Large diffs are not rendered by default.

extensions/data-transfer/portability-data-transfer-google/src/main/java/org/datatransferproject/datatransfer/google/mediaModels/GoogleMediaItem.java

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package org.datatransferproject.datatransfer.google.mediaModels;
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.google.common.base.Preconditions;
21+
import java.util.Date;
22+
import java.util.Optional;
23+
import org.datatransferproject.types.common.models.photos.PhotoModel;
24+
import org.datatransferproject.types.common.models.videos.VideoModel;
2025

21-
/**
22-
Media item returned by queries to the Google Photos API. Represents what is stored by Google.
23-
*/
26+
/** Media item returned by queries to the Google Photos API. Represents what is stored by Google. */
2427
public class GoogleMediaItem {
2528
@JsonProperty("id")
2629
private String id;
@@ -43,31 +46,118 @@ public class GoogleMediaItem {
4346
@JsonProperty("productUrl")
4447
private String productUrl;
4548

46-
public String getId() { return id; }
49+
@JsonProperty("uploadedTime")
50+
private Date uploadedTime;
4751

48-
public String getDescription() { return description; }
52+
public boolean isPhoto() {
53+
return this.getMediaMetadata().getPhoto() != null;
54+
}
55+
public boolean isVideo() {
56+
return this.getMediaMetadata().getVideo() != null;
57+
}
4958

50-
public String getBaseUrl() { return baseUrl; }
5159

52-
public String getMimeType() { return mimeType; }
60+
public String getFetchableUrl() {
61+
if (this.isPhoto()) {
62+
return this.getBaseUrl() + "=d";
63+
} else if (this.isVideo()) {
64+
// dv = download video otherwise you only get a thumbnail
65+
return this.getBaseUrl() + "=dv";
66+
} else {
67+
throw new IllegalArgumentException("unimplemented media type");
68+
}
69+
}
5370

54-
public String getFilename() { return filename; }
71+
public static VideoModel convertToVideoModel(
72+
Optional<String> albumId, GoogleMediaItem mediaItem) {
73+
Preconditions.checkArgument(mediaItem.isVideo());
74+
75+
return new VideoModel(
76+
mediaItem.getFilename(),
77+
mediaItem.getFetchableUrl(),
78+
mediaItem.getDescription(),
79+
mediaItem.getMimeType(),
80+
mediaItem.getId(),
81+
albumId.orElse(null),
82+
false /*inTempStore*/,
83+
mediaItem.getUploadedTime());
84+
}
5585

56-
public String getProductUrl() {
57-
return productUrl;
86+
public static PhotoModel convertToPhotoModel(
87+
Optional<String> albumId, GoogleMediaItem mediaItem) {
88+
Preconditions.checkArgument(mediaItem.isPhoto());
89+
90+
return new PhotoModel(
91+
mediaItem.getFilename(),
92+
mediaItem.getFetchableUrl(),
93+
mediaItem.getDescription(),
94+
mediaItem.getMimeType(),
95+
mediaItem.getId(),
96+
albumId.orElse(null),
97+
false /*inTempStore*/,
98+
null /*sha1*/,
99+
mediaItem.getUploadedTime());
100+
}
101+
102+
public String getId() {
103+
return id;
104+
}
105+
106+
public void setId(String id) {
107+
this.id = id;
108+
}
109+
110+
public String getDescription() {
111+
return description;
112+
}
113+
114+
public void setDescription(String description) {
115+
this.description = description;
116+
}
117+
118+
public String getBaseUrl() {
119+
return baseUrl;
120+
}
121+
122+
public void setBaseUrl(String baseUrl) {
123+
this.baseUrl = baseUrl;
124+
}
125+
126+
public String getMimeType() {
127+
return mimeType;
128+
}
129+
130+
public void setMimeType(String mimeType) {
131+
this.mimeType = mimeType;
58132
}
59133

60-
public MediaMetadata getMediaMetadata() { return mediaMetadata; }
134+
public String getFilename() {
135+
return filename;
136+
}
61137

62-
public void setDescription(String description) { this.description = description; }
138+
public void setFilename(String filename) {
139+
this.filename = filename;
140+
}
63141

64-
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
142+
// TODO(zacsh) investigate why/if there's no setter for this; do we need setters or does the java
143+
// annotation do the work for us somehow?
144+
public String getProductUrl() {
145+
return productUrl;
146+
}
65147

66-
public void setId(String id) { this.id = id; }
148+
public MediaMetadata getMediaMetadata() {
149+
return mediaMetadata;
150+
}
67151

68-
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
152+
public void setMediaMetadata(MediaMetadata mediaMetadata) {
153+
this.mediaMetadata = mediaMetadata;
154+
}
69155

70-
public void setFilename(String filename) { this.filename = filename; }
156+
public Date getUploadedTime() {
157+
return this.uploadedTime;
158+
}
71159

72-
public void setMediaMetadata(MediaMetadata mediaMetadata) { this.mediaMetadata = mediaMetadata; }
160+
public void setUploadedTime(Date date) {
161+
this.uploadedTime = date;
162+
}
73163
}

extensions/data-transfer/portability-data-transfer-google/src/main/java/org/datatransferproject/datatransfer/google/photos/GooglePhotosExporter.java

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import org.datatransferproject.spi.transfer.types.ContinuationData;
4545
import org.datatransferproject.spi.transfer.types.InvalidTokenException;
4646
import org.datatransferproject.spi.transfer.types.PermissionDeniedException;
47-
import org.datatransferproject.spi.transfer.types.TempPhotosData;
47+
import org.datatransferproject.spi.transfer.types.TempMediaData;
4848
import org.datatransferproject.spi.transfer.types.UploadErrorException;
4949
import org.datatransferproject.types.common.ExportInformation;
5050
import org.datatransferproject.types.common.PaginationData;
@@ -58,6 +58,8 @@
5858
// Not ready for prime-time!
5959
// TODO: fix duplication problems introduced by exporting all photos in 'root' directory first
6060

61+
// TODO WARNING DO NOT MODIFY THIS CLASS! (unless you're willing to mirror your changes to
62+
// GoogleMediaExporter too). This class is deprecated in favor. TODO here is to delete this class.
6163
public class GooglePhotosExporter
6264
implements Exporter<TokensAndUrlAuthData, PhotosContainerResource> {
6365

@@ -165,7 +167,7 @@ private ExportResult<PhotosContainerResource> exportPhotosContainer(
165167
for (PhotoModel photo : container.getPhotos()) {
166168
GoogleMediaItem googleMediaItem =
167169
getOrCreatePhotosInterface(authData).getMediaItem(photo.getDataId());
168-
photosBuilder.add(convertToPhotoModel(Optional.empty(), googleMediaItem));
170+
photosBuilder.add(GoogleMediaItem.convertToPhotoModel(Optional.empty(), googleMediaItem));
169171
}
170172

171173
PhotosContainerResource photosContainerResource =
@@ -274,8 +276,8 @@ ExportResult<PhotosContainerResource> exportPhotos(
274276
void populateContainedPhotosList(UUID jobId, TokensAndUrlAuthData authData)
275277
throws IOException, InvalidTokenException, PermissionDeniedException, UploadErrorException {
276278
// This method is only called once at the beginning of the transfer, so we can start by
277-
// initializing a new TempPhotosData to be store in the job store.
278-
TempPhotosData tempPhotosData = new TempPhotosData(jobId);
279+
// initializing a new TempMediaData to be store in the job store.
280+
TempMediaData tempMediaData = new TempMediaData(jobId);
279281

280282
String albumToken = null;
281283
AlbumListResponse albumListResponse;
@@ -293,7 +295,7 @@ void populateContainedPhotosList(UUID jobId, TokensAndUrlAuthData authData)
293295
.listMediaItems(Optional.of(albumId), Optional.ofNullable(photoToken));
294296
if (containedMediaSearchResponse.getMediaItems() != null) {
295297
for (GoogleMediaItem mediaItem : containedMediaSearchResponse.getMediaItems()) {
296-
tempPhotosData.addContainedPhotoId(mediaItem.getId());
298+
tempMediaData.addContainedPhotoId(mediaItem.getId());
297299
}
298300
}
299301
photoToken = containedMediaSearchResponse.getNextPageToken();
@@ -305,7 +307,7 @@ void populateContainedPhotosList(UUID jobId, TokensAndUrlAuthData authData)
305307

306308
// TODO: if we see complaints about objects being too large for JobStore in other places, we
307309
// should consider putting logic in JobStore itself to handle it
308-
InputStream stream = convertJsonToInputStream(tempPhotosData);
310+
InputStream stream = convertJsonToInputStream(tempMediaData);
309311
jobStore.create(jobId, createCacheKey(), stream);
310312
}
311313

@@ -332,10 +334,10 @@ private List<PhotoModel> convertPhotosList(
332334
Optional<String> albumId, GoogleMediaItem[] mediaItems, UUID jobId) throws IOException {
333335
List<PhotoModel> photos = new ArrayList<>(mediaItems.length);
334336

335-
TempPhotosData tempPhotosData = null;
337+
TempMediaData tempMediaData = null;
336338
InputStream stream = jobStore.getStream(jobId, createCacheKey()).getStream();
337339
if (stream != null) {
338-
tempPhotosData = new ObjectMapper().readValue(stream, TempPhotosData.class);
340+
tempMediaData = new ObjectMapper().readValue(stream, TempMediaData.class);
339341
stream.close();
340342
}
341343

@@ -344,12 +346,12 @@ private List<PhotoModel> convertPhotosList(
344346
// TODO: address videos
345347
boolean shouldUpload = albumId.isPresent();
346348

347-
if (tempPhotosData != null) {
348-
shouldUpload = shouldUpload || !tempPhotosData.isContainedPhotoId(mediaItem.getId());
349+
if (tempMediaData != null) {
350+
shouldUpload = shouldUpload || !tempMediaData.isContainedPhotoId(mediaItem.getId());
349351
}
350352

351353
if (shouldUpload) {
352-
PhotoModel photoModel = convertToPhotoModel(albumId, mediaItem);
354+
PhotoModel photoModel = GoogleMediaItem.convertToPhotoModel(albumId, mediaItem);
353355
photos.add(photoModel);
354356

355357
monitor.debug(
@@ -360,19 +362,6 @@ private List<PhotoModel> convertPhotosList(
360362
return photos;
361363
}
362364

363-
private PhotoModel convertToPhotoModel(Optional<String> albumId, GoogleMediaItem mediaItem) {
364-
Preconditions.checkArgument(mediaItem.getMediaMetadata().getPhoto() != null);
365-
366-
return new PhotoModel(
367-
mediaItem.getFilename(),
368-
mediaItem.getBaseUrl() + "=d",
369-
mediaItem.getDescription(),
370-
mediaItem.getMimeType(),
371-
mediaItem.getId(),
372-
albumId.orElse(null),
373-
false);
374-
}
375-
376365
private synchronized GooglePhotosInterface getOrCreatePhotosInterface(
377366
TokensAndUrlAuthData authData) {
378367
return photosInterface == null ? makePhotosInterface(authData) : photosInterface;
@@ -385,6 +374,6 @@ private synchronized GooglePhotosInterface makePhotosInterface(TokensAndUrlAuthD
385374
}
386375

387376
private static String createCacheKey() {
388-
return "tempPhotosData";
377+
return "tempMediaData";
389378
}
390379
}

extensions/data-transfer/portability-data-transfer-google/src/main/java/org/datatransferproject/datatransfer/google/photos/GooglePhotosInterface.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public class GooglePhotosInterface {
9595
private final GoogleCredentialFactory credentialFactory;
9696
private final RateLimiter writeRateLimiter;
9797

98-
GooglePhotosInterface(
98+
public GooglePhotosInterface(
9999
GoogleCredentialFactory credentialFactory,
100100
Credential credential,
101101
JsonFactory jsonFactory,
@@ -108,7 +108,7 @@ public class GooglePhotosInterface {
108108
writeRateLimiter = RateLimiter.create(writesPerSecond);
109109
}
110110

111-
AlbumListResponse listAlbums(Optional<String> pageToken)
111+
public AlbumListResponse listAlbums(Optional<String> pageToken)
112112
throws IOException, InvalidTokenException, PermissionDeniedException {
113113
Map<String, String> params = new LinkedHashMap<>();
114114
params.put(PAGE_SIZE_KEY, String.valueOf(ALBUM_PAGE_SIZE));
@@ -118,18 +118,18 @@ AlbumListResponse listAlbums(Optional<String> pageToken)
118118
return makeGetRequest(BASE_URL + "albums", Optional.of(params), AlbumListResponse.class);
119119
}
120120

121-
GoogleAlbum getAlbum(String albumId) throws IOException, InvalidTokenException, PermissionDeniedException{
121+
public GoogleAlbum getAlbum(String albumId) throws IOException, InvalidTokenException, PermissionDeniedException{
122122
Map<String, String> params = new LinkedHashMap<>();
123123
return makeGetRequest(BASE_URL + "albums/" + albumId, Optional.of(params), GoogleAlbum.class);
124124
}
125125

126-
GoogleMediaItem getMediaItem(String mediaId) throws IOException, InvalidTokenException, PermissionDeniedException {
126+
public GoogleMediaItem getMediaItem(String mediaId) throws IOException, InvalidTokenException, PermissionDeniedException {
127127
Map<String, String> params = new LinkedHashMap<>();
128128
return makeGetRequest(BASE_URL + "mediaItems/" + mediaId, Optional.of(params), GoogleMediaItem
129129
.class);
130130
}
131131

132-
MediaItemSearchResponse listMediaItems(Optional<String> albumId, Optional<String> pageToken)
132+
public MediaItemSearchResponse listMediaItems(Optional<String> albumId, Optional<String> pageToken)
133133
throws IOException, InvalidTokenException, PermissionDeniedException, UploadErrorException {
134134
Map<String, Object> params = new LinkedHashMap<>();
135135
params.put(PAGE_SIZE_KEY, String.valueOf(MEDIA_PAGE_SIZE));

0 commit comments

Comments
 (0)