diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterface.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/SmugMugInterface.java similarity index 82% rename from extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterface.java rename to extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/SmugMugInterface.java index 7bc2ab0bf..2afdb5947 100644 --- a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterface.java +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/SmugMugInterface.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.datatransferproject.transfer.smugmug.photos; +package org.datatransferproject.transfer.smugmug; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -39,12 +39,14 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.datatransferproject.transfer.smugmug.photos.SmugMugOauthApi; import org.datatransferproject.transfer.smugmug.photos.model.*; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumImageResponse; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumResponse; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumsResponse; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugImageUploadResponse; import org.datatransferproject.types.common.models.photos.PhotoModel; +import org.datatransferproject.types.common.models.videos.VideoModel; import org.datatransferproject.types.transfer.auth.AppCredentials; import org.datatransferproject.types.transfer.auth.TokenSecretAuthData; @@ -60,7 +62,8 @@ public class SmugMugInterface { private final ObjectMapper mapper; private final SmugMugUser user; - SmugMugInterface(AppCredentials appCredentials, TokenSecretAuthData authData, ObjectMapper mapper) + public SmugMugInterface(AppCredentials appCredentials, TokenSecretAuthData authData, + ObjectMapper mapper) throws IOException { this.oAuthService = new ServiceBuilder(appCredentials.getKey()) @@ -71,7 +74,7 @@ public class SmugMugInterface { this.user = getUserInformation().getUser(); } - SmugMugAlbumImageResponse getListOfAlbumImages(String url) throws IOException { + public SmugMugAlbumImageResponse getListOfAlbumImages(String url) throws IOException { Preconditions.checkArgument( !Strings.isNullOrEmpty(url), "Album URI is required to retrieve album information"); SmugMugAlbumImageResponse response = @@ -82,7 +85,7 @@ SmugMugAlbumImageResponse getListOfAlbumImages(String url) throws IOException { /* Returns the album corresponding to the url provided. If the url is null or empty, this * returns the top level user albums. */ - SmugMugAlbumsResponse getAlbums(String url) throws IOException { + public SmugMugAlbumsResponse getAlbums(String url) throws IOException { if (Strings.isNullOrEmpty(url)) { url = user.getUris().get(ALBUMS_KEY).getUri(); } @@ -91,7 +94,7 @@ SmugMugAlbumsResponse getAlbums(String url) throws IOException { } /* Creates an album with albumName provided. */ - SmugMugAlbumResponse createAlbum(String albumName) throws IOException { + public SmugMugAlbumResponse createAlbum(String albumName) throws IOException { // Set up album Map json = new HashMap<>(); json.put("NiceName", cleanName(albumName)); @@ -120,7 +123,7 @@ SmugMugAlbumResponse createAlbum(String albumName) throws IOException { /* Uploads the resource at photoUrl to the albumId provided * The albumId must exist before calling upload, else the request will fail */ - SmugMugImageUploadResponse uploadImage( + public SmugMugImageUploadResponse uploadImage( PhotoModel photoModel, String albumUri, InputStream inputStream) throws IOException { // Set up photo InputStreamContent content = new InputStreamContent(null, inputStream); @@ -155,6 +158,43 @@ SmugMugImageUploadResponse uploadImage( return Preconditions.checkNotNull(response, "Image upload Response is null"); } + /* Uploads the resource at videoUrl to the albumId provided + * The albumId must exist before calling upload, else the request will fail */ + public SmugMugImageUploadResponse uploadVideo( + VideoModel videoModel, String albumUri, InputStream inputStream) throws IOException { + // Set up photo + InputStreamContent content = new InputStreamContent(null, inputStream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + content.writeTo(outputStream); + byte[] contentBytes = outputStream.toByteArray(); + + // Headers from: https://api.smugmug.com/api/v2/doc/reference/upload.html + Map headersMap = new HashMap<>(); + headersMap.put("X-Smug-AlbumUri", albumUri); + headersMap.put("X-Smug-ResponseType", "JSON"); + headersMap.put("X-Smug-Version", "v2"); + headersMap.put("Content-Type", videoModel.getMimeType()); + + if (!Strings.isNullOrEmpty(videoModel.getName())) { + headersMap.put("X-Smug-Title", cleanHeader(videoModel.getName())); + } + if (!Strings.isNullOrEmpty(videoModel.getDescription())) { + headersMap.put("X-Smug-Caption", cleanHeader(videoModel.getDescription())); + } + + // Upload video + SmugMugImageUploadResponse response = + postRequest( + "https://upload.smugmug.com/", + ImmutableMap.of(), // No content params for video upload + contentBytes, + headersMap, + new TypeReference() {}); + + Preconditions.checkState(response.getStat().equals("ok"), "Failed to upload image"); + return Preconditions.checkNotNull(response, "Image upload Response is null"); + } + private SmugMugUserResponse getUserInformation() throws IOException { return makeRequest(USER_URL, new TypeReference>() {}) .getResponse(); @@ -261,7 +301,7 @@ private T postRequest( } } - static String cleanName(String name) { + public static String cleanName(String name) { // TODO: Handle cases where the entire album name is non-alphanumeric, e.g. all emojis return name.chars() .mapToObj(c -> (char) c) diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporter.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporter.java new file mode 100644 index 000000000..1f7e020ed --- /dev/null +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporter.java @@ -0,0 +1,262 @@ +/* + * Copyright 2023 The Data Transfer Project 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 + * + * https://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 org.datatransferproject.transfer.smugmug.media; + +import static com.google.common.base.Preconditions.checkState; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import org.datatransferproject.api.launcher.Monitor; +import org.datatransferproject.spi.cloud.storage.TemporaryPerJobDataStore; +import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor; +import org.datatransferproject.spi.transfer.provider.ImportResult; +import org.datatransferproject.spi.transfer.provider.Importer; +import org.datatransferproject.transfer.smugmug.SmugMugTransmogrificationConfig; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumResponse; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugImageUploadResponse; +import org.datatransferproject.transfer.smugmug.photos.SmugMugPhotoTempData; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; +import org.datatransferproject.types.common.models.media.MediaAlbum; +import org.datatransferproject.types.common.models.media.MediaContainerResource; +import org.datatransferproject.types.common.models.photos.PhotoModel; +import org.datatransferproject.types.common.models.videos.VideoModel; +import org.datatransferproject.types.transfer.auth.AppCredentials; +import org.datatransferproject.types.transfer.auth.TokenSecretAuthData; + +public class SmugMugMediaImporter + implements Importer { + + private static final String DEFAULT_ALBUM_NAME = "Untitled Album"; + private final TemporaryPerJobDataStore jobStore; + private final AppCredentials appCredentials; + private final ObjectMapper mapper; + private final Monitor monitor; + private final SmugMugTransmogrificationConfig transmogrificationConfig; + private final SmugMugInterface smugMugInterface; + + public SmugMugMediaImporter( + TemporaryPerJobDataStore jobStore, + AppCredentials appCredentials, + ObjectMapper mapper, + Monitor monitor) { + this(null, new SmugMugTransmogrificationConfig(), jobStore, appCredentials, mapper, monitor); + } + + @VisibleForTesting + SmugMugMediaImporter( + SmugMugInterface smugMugInterface, + SmugMugTransmogrificationConfig transmogrificationConfig, + TemporaryPerJobDataStore jobStore, + AppCredentials appCredentials, + ObjectMapper mapper, + Monitor monitor) { + this.smugMugInterface = smugMugInterface; + this.transmogrificationConfig = transmogrificationConfig; + this.jobStore = jobStore; + this.appCredentials = appCredentials; + this.mapper = mapper; + this.monitor = monitor; + } + + @Override + public ImportResult importItem( + UUID jobId, + IdempotentImportExecutor idempotentExecutor, + TokenSecretAuthData authData, + MediaContainerResource data) + throws Exception { + + // Make the data smugmug compatible + data.transmogrify(transmogrificationConfig); + + try { + SmugMugInterface smugMugInterface = getOrCreateSmugMugInterface(authData); + for (MediaAlbum album : data.getAlbums()) { + idempotentExecutor.executeAndSwallowIOExceptions( + album.getId(), + album.getName(), + () -> importSingleAlbum(jobId, album, smugMugInterface)); + } + for (PhotoModel photo : data.getPhotos()) { + idempotentExecutor.executeAndSwallowIOExceptions( + photo.getIdempotentId(), + photo.getTitle(), + () -> importSinglePhoto(jobId, idempotentExecutor, photo, smugMugInterface)); + } + for (VideoModel video : data.getVideos()) { + idempotentExecutor.executeAndSwallowIOExceptions( + video.getIdempotentId(), + video.getName(), + () -> importSingleVideo(jobId, idempotentExecutor, video, smugMugInterface)); + } + + } catch (IOException e) { + monitor.severe(() -> "Error importing", e); + return new ImportResult(e); + } + return ImportResult.OK; + } + + @VisibleForTesting + String importSingleAlbum(UUID jobId, MediaAlbum inputAlbum, SmugMugInterface smugMugInterface) + throws IOException { + String albumName = + Strings.isNullOrEmpty(inputAlbum.getName()) + ? DEFAULT_ALBUM_NAME + : inputAlbum.getName(); + + SmugMugAlbumResponse albumResponse = smugMugInterface.createAlbum(albumName); + SmugMugPhotoTempData tempData = + new SmugMugPhotoTempData( + inputAlbum.getId(), albumName, inputAlbum.getDescription(), albumResponse.getUri()); + jobStore.create(jobId, getTempDataId(inputAlbum.getId()), tempData); + return albumResponse.getUri(); + } + + @VisibleForTesting + String importSinglePhoto( + UUID jobId, + IdempotentImportExecutor idempotentExecutor, + PhotoModel inputPhoto, + SmugMugInterface smugMugInterface) + throws Exception { + InputStream inputStream; + if (inputPhoto.isInTempStore()) { + inputStream = jobStore.getStream(jobId, inputPhoto.getFetchableUrl()).getStream(); + } else { + inputStream = smugMugInterface.getImageAsStream(inputPhoto.getFetchableUrl()); + } + + String originalAlbumId = inputPhoto.getAlbumId(); + SmugMugPhotoTempData albumTempData = + getDestinationAlbumTempData(jobId, idempotentExecutor, originalAlbumId, smugMugInterface); + + SmugMugImageUploadResponse response = + smugMugInterface.uploadImage(inputPhoto, albumTempData.getAlbumUri(), inputStream); + albumTempData.incrementPhotoCount(); + jobStore.update(jobId, getTempDataId(albumTempData.getAlbumExportId()), albumTempData); + + return response.toString(); + } + + @VisibleForTesting + String importSingleVideo( + UUID jobId, + IdempotentImportExecutor idempotentExecutor, + VideoModel inputVideo, + SmugMugInterface smugMugInterface) + throws Exception { + InputStream inputStream; + if (inputVideo.isInTempStore()) { + inputStream = jobStore.getStream(jobId, inputVideo.getFetchableUrl()).getStream(); + } else { + inputStream = smugMugInterface.getImageAsStream(inputVideo.getFetchableUrl()); + } + + String originalAlbumId = inputVideo.getAlbumId(); + SmugMugPhotoTempData albumTempData = + getDestinationAlbumTempData(jobId, idempotentExecutor, originalAlbumId, smugMugInterface); + + SmugMugImageUploadResponse response = + smugMugInterface.uploadVideo(inputVideo, albumTempData.getAlbumUri(), inputStream); + + albumTempData.incrementPhotoCount(); + jobStore.update(jobId, getTempDataId(albumTempData.getAlbumExportId()), albumTempData); + + return response.toString(); + } + + // Returns the provided interface, or a new one specific to the authData provided. + private SmugMugInterface getOrCreateSmugMugInterface(TokenSecretAuthData authData) + throws IOException { + return smugMugInterface == null + ? new SmugMugInterface(appCredentials, authData, mapper) + : smugMugInterface; + } + + /** + * Get the proper album upload information for the photo. Takes into account size limits of the + * albums and completed uploads. + */ + @VisibleForTesting + SmugMugPhotoTempData getDestinationAlbumTempData( + UUID jobId, + IdempotentImportExecutor idempotentExecutor, + String baseAlbumId, + SmugMugInterface smugMugInterface) + throws Exception { + SmugMugPhotoTempData baseAlbumTempData = + jobStore.findData(jobId, getTempDataId(baseAlbumId), SmugMugPhotoTempData.class); + SmugMugPhotoTempData albumTempData = baseAlbumTempData; + int depth = 0; + while (albumTempData.getPhotoCount() >= transmogrificationConfig.getAlbumMaxSize()) { + if (albumTempData.getOverflowAlbumExportId() == null) { + MediaAlbum newAlbum = + createOverflowAlbum( + baseAlbumTempData.getAlbumExportId(), + baseAlbumTempData.getAlbumName(), + baseAlbumTempData.getAlbumDescription(), + depth + 1); + // since the album is full and has no overflow, we need to create a new one + String newUri = + idempotentExecutor.executeOrThrowException( + newAlbum.getId(), + newAlbum.getName(), + () -> importSingleAlbum(jobId, newAlbum, smugMugInterface)); + albumTempData.setOverflowAlbumExportId(newAlbum.getId()); + jobStore.update(jobId, getTempDataId(albumTempData.getAlbumExportId()), albumTempData); + albumTempData = + jobStore.findData( + jobId, + getTempDataId(albumTempData.getOverflowAlbumExportId()), + SmugMugPhotoTempData.class); + } else { + albumTempData = + jobStore.findData( + jobId, + getTempDataId(albumTempData.getOverflowAlbumExportId()), + SmugMugPhotoTempData.class); + } + depth += 1; + } + return albumTempData; + } + + private static String getTempDataId(String albumId) { + return String.format("smugmug-album-temp-data-%s", albumId); + } + + /** + * Create an overflow album using the base album's id, name, and description and the overflow + * album's opy number. E.g. if baseAlbum needs a single overflow album, it will be created with + * createOverflowAlbum("baseAlbumId", "baseAlbumName", "baseAlbumDescription", 1) and result in an + * album PhotoAlbum("baseAlbumId-overflow-1", "baseAlbumName (1)", "baseAlbumDescription") + */ + private static MediaAlbum createOverflowAlbum( + String baseAlbumId, String baseAlbumName, String baseAlbumDescription, int copyNumber) + throws Exception { + checkState(copyNumber > 0, "copyNumber should be > 0"); + return new MediaAlbum( + String.format("%s-overflow-%d", baseAlbumId, copyNumber), + String.format("%s (%d)", baseAlbumName, copyNumber), + baseAlbumDescription); + } +} diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosExporter.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosExporter.java index 4ebd8a9ad..213c868c6 100644 --- a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosExporter.java +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosExporter.java @@ -27,6 +27,7 @@ import org.datatransferproject.spi.transfer.provider.ExportResult.ResultType; import org.datatransferproject.spi.transfer.provider.Exporter; import org.datatransferproject.spi.transfer.types.ContinuationData; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbum; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumImageResponse; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumsResponse; diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporter.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporter.java index 1823b81e3..73d68b426 100644 --- a/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporter.java +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/main/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporter.java @@ -29,6 +29,7 @@ import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor; import org.datatransferproject.spi.transfer.provider.ImportResult; import org.datatransferproject.spi.transfer.provider.Importer; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; import org.datatransferproject.transfer.smugmug.SmugMugTransmogrificationConfig; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumResponse; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugImageUploadResponse; diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporterTest.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporterTest.java new file mode 100644 index 000000000..b3ffb6f2e --- /dev/null +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/media/SmugMugMediaImporterTest.java @@ -0,0 +1,293 @@ +/* + * Copyright 2023 The Data Transfer Project 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 + * + * https://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 org.datatransferproject.transfer.smugmug.media; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import java.io.BufferedInputStream; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.datatransferproject.api.launcher.Monitor; +import org.datatransferproject.cloud.local.LocalJobStore; +import org.datatransferproject.spi.cloud.storage.TemporaryPerJobDataStore; +import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor; +import org.datatransferproject.spi.transfer.provider.ImportResult; +import org.datatransferproject.test.types.FakeIdempotentImportExecutor; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; +import org.datatransferproject.transfer.smugmug.photos.SmugMugPhotoTempData; +import org.datatransferproject.transfer.smugmug.SmugMugTransmogrificationConfig; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbum; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumResponse; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugImageUploadResponse; +import org.datatransferproject.transfer.smugmug.photos.model.SmugMugImageUploadResponse.ImageInfo; +import org.datatransferproject.types.common.models.media.MediaAlbum; +import org.datatransferproject.types.common.models.media.MediaContainerResource; +import org.datatransferproject.types.common.models.photos.PhotoModel; +import org.datatransferproject.types.common.models.videos.VideoModel; +import org.datatransferproject.types.transfer.auth.AppCredentials; +import org.datatransferproject.types.transfer.auth.TokenSecretAuthData; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +public class SmugMugMediaImporterTest { + + private static final String TEMP_DATA_FORMAT = "smugmug-album-temp-data-%s"; + private static final IdempotentImportExecutor EXECUTOR = new FakeIdempotentImportExecutor(); + + private TemporaryPerJobDataStore jobStore = new LocalJobStore(); + + private BufferedInputStream bufferedInputStream = mock(BufferedInputStream.class); + + private Monitor monitor = mock(Monitor.class); + private SmugMugInterface smugMugInterface = mock(SmugMugInterface.class); + + private SmugMugTransmogrificationConfig config = + new SmugMugTransmogrificationConfig() { + public int getAlbumMaxSize() { + return 4; + } + }; + + @Test + public void importStoresAlbumInJobStore() throws Exception { + // setup test objects + UUID jobId = UUID.randomUUID(); + + MediaAlbum mediaAlbum1 = new MediaAlbum("albumId1", "albumName1", "albumDescription1"); + PhotoModel photoModel1 = + new PhotoModel( + "PHOTO_TITLE", + "FETCHABLE_URL", + "PHOTO_DESCRIPTION", + "MEDIA_TYPE", + "photoId1", + mediaAlbum1.getId(), + false); + PhotoModel photoModel2 = + new PhotoModel( + "PHOTO_TITLE", + "FETCHABLE_URL", + "PHOTO_DESCRIPTION", + "MEDIA_TYPE", + "photoId2", + mediaAlbum1.getId(), + false); + PhotoModel photoModel3 = + new PhotoModel( + "PHOTO_TITLE", + "FETCHABLE_URL", + "PHOTO_DESCRIPTION", + "MEDIA_TYPE", + "photoId3", + mediaAlbum1.getId(), + false); + VideoModel videoModel1 = + new VideoModel( + "VIDEO_TITLE", + "FETCHABLE_URL", + "VIDEO_DESCRIPTION", + "MEDIA_TYPE", + "videoId1", + mediaAlbum1.getId(), + false, + null); + VideoModel videoModel2 = + new VideoModel( + "VIDEO_TITLE", + "FETCHABLE_URL", + "VIDEO_DESCRIPTION", + "MEDIA_TYPE", + "videoId2", + mediaAlbum1.getId(), + false, + null); + VideoModel videoModel3 = + new VideoModel( + "VIDEO_TITLE", + "FETCHABLE_URL", + "VIDEO_DESCRIPTION", + "MEDIA_TYPE", + "videoId3", + mediaAlbum1.getId(), + false, + null); + + MediaContainerResource mediaContainerResource1 = + new MediaContainerResource(Collections.singletonList(mediaAlbum1), ImmutableList.of(), + ImmutableList.of()); + MediaContainerResource mediaContainerResource2 = + new MediaContainerResource( + ImmutableList.of(), ImmutableList.of(photoModel1, photoModel2, photoModel3), + ImmutableList.of(videoModel1, videoModel2, videoModel3)); + + SmugMugAlbum smugMugAlbum1 = + new SmugMugAlbum( + "date", + mediaAlbum1.getDescription(), + mediaAlbum1.getName(), + "privacy", + "albumUri1", + "urlname", + "weburi"); + String overflowAlbumName = smugMugAlbum1.getName() + " (1)"; + SmugMugAlbum smugMugAlbum2 = + new SmugMugAlbum( + "date", + mediaAlbum1.getDescription(), + overflowAlbumName, + "privacy", + "albumUri2", + "urlname", + "weburi"); + + SmugMugAlbumResponse mockAlbumResponse1 = + new SmugMugAlbumResponse(smugMugAlbum1.getUri(), "Locator", "LocatorType", smugMugAlbum1); + SmugMugAlbumResponse mockAlbumResponse2 = + new SmugMugAlbumResponse(smugMugAlbum2.getUri(), "Locator", "LocatorType", smugMugAlbum2); + + when(smugMugInterface.createAlbum(eq(smugMugAlbum1.getName()))).thenReturn(mockAlbumResponse1); + when(smugMugInterface.createAlbum(eq(smugMugAlbum2.getName()))).thenReturn(mockAlbumResponse2); + + SmugMugImageUploadResponse smugMugUploadImageResponse = + new SmugMugImageUploadResponse( + "imageUri", + "albumImageUri", + new ImageInfo("imageUri", "albumImageUri", "statusImageReplaceUri", "url")); + when(smugMugInterface.uploadImage(any(), any(), any())).thenReturn(smugMugUploadImageResponse); + when(smugMugInterface.uploadVideo(any(), any(), any())).thenReturn(smugMugUploadImageResponse); + when(smugMugInterface.getImageAsStream(any())).thenReturn(bufferedInputStream); + + // Run test + SmugMugMediaImporter importer = + new SmugMugMediaImporter( + smugMugInterface, + config, + jobStore, + new AppCredentials("key", "secret"), + mock(ObjectMapper.class), + monitor); + ImportResult result = + importer.importItem( + jobId, EXECUTOR, new TokenSecretAuthData("token", "secret"), mediaContainerResource1); + + result = + importer.importItem( + jobId, EXECUTOR, new TokenSecretAuthData("token", "secret"), mediaContainerResource2); + + // Verify + ArgumentCaptor photoUrlsCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor albumNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(smugMugInterface, atLeastOnce()).createAlbum(albumNamesCaptor.capture()); + verify(smugMugInterface, atLeastOnce()).getImageAsStream(photoUrlsCaptor.capture()); + + List capturedAlbumNames = albumNamesCaptor.getAllValues(); + assertTrue(capturedAlbumNames.contains(smugMugAlbum1.getName())); + assertTrue(capturedAlbumNames.contains(smugMugAlbum2.getName())); + + List capturedPhotoUrls = photoUrlsCaptor.getAllValues(); + assertTrue(capturedPhotoUrls.contains(photoModel1.getFetchableUrl())); + assertTrue(capturedPhotoUrls.contains(photoModel2.getFetchableUrl())); + assertTrue(capturedPhotoUrls.contains(photoModel3.getFetchableUrl())); + + String overflowAlbumId = mediaAlbum1.getId() + "-overflow-1"; + assertThat((String) EXECUTOR.getCachedValue(mediaAlbum1.getId())) + .isEqualTo(smugMugAlbum1.getUri()); + assertThat((String) EXECUTOR.getCachedValue(overflowAlbumId)).isEqualTo(smugMugAlbum2.getUri()); + + SmugMugPhotoTempData tempData1 = + new SmugMugPhotoTempData( + mediaAlbum1.getId(), + smugMugAlbum1.getName(), + smugMugAlbum1.getDescription(), + smugMugAlbum1.getUri(), + 4, + overflowAlbumId); + SmugMugPhotoTempData tempData2 = + new SmugMugPhotoTempData( + overflowAlbumId, + smugMugAlbum2.getName(), + smugMugAlbum2.getDescription(), + smugMugAlbum2.getUri(), + 2, + null); + assertThat( + jobStore + .findData( + jobId, + String.format(TEMP_DATA_FORMAT, mediaAlbum1.getId()), + SmugMugPhotoTempData.class) + .toString()) + .isEqualTo(tempData1.toString()); + assertThat( + jobStore + .findData( + jobId, + String.format(TEMP_DATA_FORMAT, overflowAlbumId), + SmugMugPhotoTempData.class) + .toString()) + .isEqualTo(tempData2.toString()); + } + + @Test + public void importEmptyAlbumName() throws Exception { + UUID jobId = UUID.randomUUID(); + MediaAlbum mediaAlbum = new MediaAlbum("albumid", "", "albumDescription"); + MediaContainerResource mediaContainerResource = + new MediaContainerResource(Collections.singletonList(mediaAlbum), ImmutableList.of(), + ImmutableList.of()); + + SmugMugAlbum smugMugAlbum = + new SmugMugAlbum( + "date", + mediaAlbum.getDescription(), + "Untitled Album", + "privacy", + "albumUri1", + "urlname", + "weburi"); + SmugMugAlbumResponse mockAlbumResponse = + new SmugMugAlbumResponse(smugMugAlbum.getUri(), "Locator", "LocatorType", smugMugAlbum); + when(smugMugInterface.createAlbum(eq(smugMugAlbum.getName()))).thenReturn(mockAlbumResponse); + + // Run test + SmugMugMediaImporter importer = + new SmugMugMediaImporter( + smugMugInterface, + config, + jobStore, + new AppCredentials("key", "secret"), + mock(ObjectMapper.class), + monitor); + ImportResult result = + importer.importItem( + jobId, EXECUTOR, new TokenSecretAuthData("token", "secret"), mediaContainerResource); + + // Verify + verify(smugMugInterface, atLeastOnce()).createAlbum( + ArgumentCaptor.forClass(String.class).capture()); + } +} diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterfaceTest.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterfaceTest.java index 71031dec5..02360a8f3 100644 --- a/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterfaceTest.java +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugInterfaceTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; import org.junit.jupiter.api.Test; public class SmugMugInterfaceTest { diff --git a/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporterTest.java b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporterTest.java index e40b87a63..938169585 100644 --- a/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporterTest.java +++ b/extensions/data-transfer/portability-data-transfer-smugmug/src/test/java/org/datatransferproject/transfer/smugmug/photos/SmugMugPhotosImporterTest.java @@ -37,6 +37,7 @@ import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor; import org.datatransferproject.spi.transfer.provider.ImportResult; import org.datatransferproject.test.types.FakeIdempotentImportExecutor; +import org.datatransferproject.transfer.smugmug.SmugMugInterface; import org.datatransferproject.transfer.smugmug.SmugMugTransmogrificationConfig; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbum; import org.datatransferproject.transfer.smugmug.photos.model.SmugMugAlbumResponse;