Skip to content

Commit 914a3db

Browse files
authored
feat: google music release exporter (#1469)
Enables the support for exporting music releases from Google.
1 parent a448108 commit 914a3db

File tree

4 files changed

+199
-4
lines changed

4 files changed

+199
-4
lines changed

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import javax.annotation.Nullable;
3333
import org.datatransferproject.api.launcher.Monitor;
3434
import org.datatransferproject.datatransfer.google.common.GoogleCredentialFactory;
35+
import org.datatransferproject.datatransfer.google.musicModels.ExportReleaseResponse;
3536
import org.datatransferproject.datatransfer.google.musicModels.GoogleArtist;
3637
import org.datatransferproject.datatransfer.google.musicModels.GooglePlaylist;
3738
import org.datatransferproject.datatransfer.google.musicModels.GooglePlaylistItem;
@@ -98,8 +99,7 @@ public GoogleMusicExporter(
9899
public ExportResult<MusicContainerResource> export(
99100
UUID jobId, TokensAndUrlAuthData authData, Optional<ExportInformation> exportInformation)
100101
throws IOException, InvalidTokenException, PermissionDeniedException, ParseException {
101-
// TODO: Remove the logic when testing is finished.
102-
// Local demo-server test usage. Start transfer job without ExportInformation.
102+
// Used in production
103103
if (!exportInformation.isPresent()) {
104104
StringPaginationToken paginationToken = new StringPaginationToken(PLAYLIST_TOKEN_PREFIX);
105105
return exportPlaylists(authData, Optional.of(paginationToken), jobId);
@@ -125,8 +125,7 @@ public ExportResult<MusicContainerResource> export(
125125
return new ExportResult<>(ResultType.END, null, null);
126126
} else if (paginationDataPresent
127127
&& paginationToken.getToken().startsWith(RELEASE_TOKEN_PREFIX)) {
128-
// TODO: export releases
129-
return new ExportResult<>(ResultType.END, null, null);
128+
return exportReleases(authData, Optional.of(paginationToken), jobId);
130129
} else {
131130
// There is nothing to export.
132131
return new ExportResult<>(ResultType.END, null, null);
@@ -237,6 +236,57 @@ ExportResult<MusicContainerResource> exportPlaylistItems(
237236
return new ExportResult<>(ResultType.CONTINUE, containerResource, continuationData);
238237
}
239238

239+
@VisibleForTesting
240+
ExportResult<MusicContainerResource> exportReleases(TokensAndUrlAuthData authData,
241+
Optional<PaginationData> paginationData,
242+
UUID jobId) throws InvalidTokenException, PermissionDeniedException, IOException {
243+
Optional<String> paginationToken = getToken(RELEASE_TOKEN_PREFIX, paginationData);
244+
ExportReleaseResponse exportReleaseResponse = getOrCreateMusicHttpApi(authData).exportReleases(paginationToken);
245+
246+
PaginationData nextPageData = null;
247+
String token = exportReleaseResponse.getNextPageToken();
248+
ResultType resultType = ResultType.END;
249+
if (!Strings.isNullOrEmpty(token)) {
250+
nextPageData = new StringPaginationToken(RELEASE_TOKEN_PREFIX + token);
251+
resultType = ResultType.CONTINUE;
252+
}
253+
254+
ContinuationData continuationData = new ContinuationData(nextPageData);
255+
MusicContainerResource containerResource = null;
256+
GoogleRelease[] googleReleases = exportReleaseResponse.getReleases();
257+
List<MusicRelease> exportableReleases = new ArrayList<>();
258+
259+
if (googleReleases != null && googleReleases.length > 0) {
260+
for (GoogleRelease googleRelease : googleReleases) {
261+
exportableReleases.add(convertRelease(googleRelease));
262+
monitor.debug(
263+
() ->
264+
String.format(
265+
"%s: Google Music exporting release item: [release title: %s, release icpn: %s]",
266+
jobId,
267+
googleRelease.getTitle(),
268+
googleRelease.getIcpn()));
269+
}
270+
containerResource = new MusicContainerResource(null, null, null, exportableReleases);
271+
}
272+
return new ExportResult<>(resultType, containerResource, continuationData);
273+
}
274+
275+
private Optional<String> getToken(String prefix, Optional<PaginationData> paginationData){
276+
Optional<String> paginationToken = Optional.empty();
277+
if (paginationData.isPresent()) {
278+
String token = ((StringPaginationToken) paginationData.get()).getToken();
279+
Preconditions.checkArgument(
280+
token.startsWith(prefix), "Invalid pagination token %s",
281+
token);
282+
if (prefix.length() < token.length()) {
283+
paginationToken = Optional.of(token.substring(prefix.length()));
284+
}
285+
}
286+
return paginationToken;
287+
}
288+
289+
240290
private int getTokenPrefixLength(String token) {
241291
final ImmutableList<String> knownPrefixes =
242292
ImmutableList.of(
@@ -267,6 +317,11 @@ private int getTokenPrefixLength(String token) {
267317
return musicGroups;
268318
}
269319

320+
private MusicRelease convertRelease(GoogleRelease googleRelease){
321+
return new MusicRelease(googleRelease.getIcpn(), googleRelease.getTitle(), null);
322+
323+
}
324+
270325
private MusicPlaylistItem convertPlaylistItem(
271326
String playlistId, GooglePlaylistItem googlePlaylistItem) throws ParseException {
272327
GoogleTrack track = googlePlaylistItem.getTrack();

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.datatransferproject.datatransfer.google.common.GoogleCredentialFactory;
5353
import org.datatransferproject.datatransfer.google.musicModels.BatchPlaylistItemRequest;
5454
import org.datatransferproject.datatransfer.google.musicModels.BatchPlaylistItemResponse;
55+
import org.datatransferproject.datatransfer.google.musicModels.ExportReleaseResponse;
5556
import org.datatransferproject.datatransfer.google.musicModels.GooglePlaylist;
5657
import org.datatransferproject.datatransfer.google.musicModels.ImportPlaylistRequest;
5758
import org.datatransferproject.datatransfer.google.musicModels.PlaylistItemExportResponse;
@@ -72,13 +73,15 @@ public class GoogleMusicHttpApi {
7273

7374
private static final String BASE_URL =
7475
"https://youtubemediaconnect.googleapis.com/v1/users/me/musicLibrary/";
76+
private static final String RELEASE_BASE_URL = "https://youtubemediaconnect.googleapis.com/v1/releases";
7577
private static final int PLAYLIST_PAGE_SIZE = 20;
7678
private static final int PLAYLIST_ITEM_PAGE_SIZE = 50;
7779
private static final String PLAYLIST_ID_KEY = "playlistId";
7880
private static final String PAGE_SIZE_KEY = "pageSize";
7981
private static final String TOKEN_KEY = "pageToken";
8082
private static final String ORIGINAL_PLAYLIST_ID_KEY = "originalPlaylistId";
8183
private static final String ACCESS_TOKEN_KEY = "access_token";
84+
private static final int RELEASE_ITEM_PAGE_SIZE = 50;
8285

8386
private final ObjectMapper objectMapper =
8487
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -128,6 +131,20 @@ PlaylistItemExportResponse exportPlaylistItems(String playlistId, Optional<Strin
128131
PlaylistItemExportResponse.class);
129132
}
130133

134+
ExportReleaseResponse exportReleases(Optional<String> pageToken)
135+
throws InvalidTokenException, PermissionDeniedException, IOException {
136+
137+
Map<String, String> params = new LinkedHashMap<>();
138+
params.put(PAGE_SIZE_KEY, String.valueOf(RELEASE_ITEM_PAGE_SIZE));
139+
if (pageToken.isPresent()) {
140+
params.put(TOKEN_KEY, pageToken.get());
141+
}
142+
return makeGetRequest(
143+
RELEASE_BASE_URL, Optional.of(params),
144+
ExportReleaseResponse.class);
145+
146+
}
147+
131148
GooglePlaylist importPlaylist(GooglePlaylist playlist, String playlistId)
132149
throws IOException, InvalidTokenException, PermissionDeniedException {
133150
ImportPlaylistRequest importPlaylistRequest = new ImportPlaylistRequest(playlist, playlistId);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.datatransferproject.datatransfer.google.musicModels;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public class ExportReleaseResponse {
6+
7+
@JsonProperty("releases")
8+
private GoogleRelease[] releases;
9+
10+
11+
@JsonProperty("nextPageToken")
12+
private String nextPageToken;
13+
14+
public GoogleRelease[] getReleases() {
15+
return releases;
16+
}
17+
18+
public String getNextPageToken() {
19+
return nextPageToken;
20+
}
21+
22+
}

extensions/data-transfer/portability-data-transfer-google/src/test/java/org/datatransferproject/datatransfer/google/music/GoogleMusicExporterTest.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.datatransferproject.datatransfer.google.music.GoogleMusicExporter.GOOGLE_PLAYLIST_NAME_PREFIX;
2020
import static org.datatransferproject.datatransfer.google.music.GoogleMusicExporter.PLAYLIST_TOKEN_PREFIX;
21+
import static org.datatransferproject.datatransfer.google.music.GoogleMusicExporter.RELEASE_TOKEN_PREFIX;
2122
import static org.junit.jupiter.api.Assertions.assertEquals;
2223
import static com.google.common.truth.Truth.assertThat;
2324
import static org.mockito.ArgumentMatchers.any;
@@ -36,6 +37,7 @@
3637
import java.util.stream.Collectors;
3738
import org.datatransferproject.api.launcher.Monitor;
3839
import org.datatransferproject.datatransfer.google.common.GoogleCredentialFactory;
40+
import org.datatransferproject.datatransfer.google.musicModels.ExportReleaseResponse;
3941
import org.datatransferproject.datatransfer.google.musicModels.GooglePlaylist;
4042
import org.datatransferproject.datatransfer.google.musicModels.GooglePlaylistItem;
4143
import org.datatransferproject.datatransfer.google.musicModels.GoogleRelease;
@@ -46,6 +48,7 @@
4648
import org.datatransferproject.spi.transfer.types.ContinuationData;
4749
import org.datatransferproject.spi.transfer.types.InvalidTokenException;
4850
import org.datatransferproject.spi.transfer.types.PermissionDeniedException;
51+
import org.datatransferproject.types.common.ExportInformation;
4952
import org.datatransferproject.types.common.PaginationData;
5053
import org.datatransferproject.types.common.StringPaginationToken;
5154
import org.datatransferproject.types.common.models.ContainerResource;
@@ -62,6 +65,7 @@ public class GoogleMusicExporterTest {
6265

6366
static final String PLAYLIST_PAGE_TOKEN = "playlist_page_token";
6467
static final String PLAYLIST_ITEM_TOKEN = "playlist_item_token";
68+
static final String RELEASE_ITEM_TOKEN = "release_item_token";
6569

6670
private final UUID uuid = UUID.randomUUID();
6771

@@ -70,6 +74,7 @@ public class GoogleMusicExporterTest {
7074

7175
private PlaylistExportResponse playlistExportResponse;
7276
private PlaylistItemExportResponse playlistItemExportResponse;
77+
private ExportReleaseResponse exportReleaseResponse;
7378

7479
@BeforeEach
7580
public void setUp() throws IOException, InvalidTokenException, PermissionDeniedException {
@@ -84,10 +89,12 @@ public void setUp() throws IOException, InvalidTokenException, PermissionDeniedE
8489

8590
playlistExportResponse = mock(PlaylistExportResponse.class);
8691
playlistItemExportResponse = mock(PlaylistItemExportResponse.class);
92+
exportReleaseResponse = mock(ExportReleaseResponse.class);
8793

8894
when(musicHttpApi.exportPlaylists(any(Optional.class))).thenReturn(playlistExportResponse);
8995
when(musicHttpApi.exportPlaylistItems(any(String.class), any(Optional.class)))
9096
.thenReturn(playlistItemExportResponse);
97+
when(musicHttpApi.exportReleases(any(Optional.class))).thenReturn(exportReleaseResponse);
9198

9299
verifyNoInteractions(credentialFactory);
93100
}
@@ -229,6 +236,93 @@ public void exportPlaylistItemSubsequentSet()
229236
assertThat(paginationToken).isNull();
230237
}
231238

239+
@Test
240+
public void exportReleaseFirstSet()
241+
throws InvalidTokenException, PermissionDeniedException, IOException, ParseException {
242+
GoogleRelease release = setUpSingleRelease("Test", "R_icpn");
243+
when(exportReleaseResponse.getReleases())
244+
.thenReturn(new GoogleRelease[]{release});
245+
when(exportReleaseResponse.getNextPageToken()).thenReturn(null);
246+
StringPaginationToken inputPaginationToken = new StringPaginationToken(RELEASE_TOKEN_PREFIX);
247+
ExportInformation exportInformation = new ExportInformation(inputPaginationToken, null);
248+
ExportResult<MusicContainerResource> result = googleMusicExporter.export(uuid, null, Optional.of(exportInformation));
249+
250+
// Check results
251+
// Verify correct methods were called
252+
verify(musicHttpApi).exportReleases(Optional.empty());
253+
verify(exportReleaseResponse).getReleases();
254+
255+
// Check pagination token
256+
ContinuationData continuationData = result.getContinuationData();
257+
StringPaginationToken paginationToken = (StringPaginationToken) continuationData.getPaginationData();
258+
assertThat(paginationToken).isNull();
259+
}
260+
261+
@Test
262+
public void exportReleaseSubsequentSet()
263+
throws InvalidTokenException, PermissionDeniedException, IOException, ParseException {
264+
GoogleRelease release = setUpSingleRelease("Test", "R_icpn");
265+
when(exportReleaseResponse.getReleases())
266+
.thenReturn(new GoogleRelease[]{release});
267+
when(exportReleaseResponse.getNextPageToken()).thenReturn(null);
268+
StringPaginationToken inputPaginationToken = new StringPaginationToken(RELEASE_TOKEN_PREFIX + RELEASE_ITEM_TOKEN);
269+
ExportInformation exportInformation = new ExportInformation(inputPaginationToken, null);
270+
ExportResult<MusicContainerResource> result = googleMusicExporter.export(uuid, null, Optional.of(exportInformation));
271+
272+
// Check results
273+
// Verify correct methods were called
274+
verify(musicHttpApi).exportReleases(Optional.of(RELEASE_ITEM_TOKEN));
275+
verify(exportReleaseResponse).getReleases();
276+
277+
// Check pagination token
278+
ContinuationData continuationData = result.getContinuationData();
279+
StringPaginationToken paginationToken = (StringPaginationToken) continuationData.getPaginationData();
280+
assertThat(paginationToken).isNull();
281+
}
282+
283+
@Test
284+
public void exportReleaseWithNextPage()
285+
throws InvalidTokenException, PermissionDeniedException, IOException, ParseException {
286+
GoogleRelease release = setUpSingleRelease("Test", "R_icpn");
287+
when(exportReleaseResponse.getReleases())
288+
.thenReturn(new GoogleRelease[]{release});
289+
when(exportReleaseResponse.getNextPageToken()).thenReturn(RELEASE_ITEM_TOKEN);
290+
StringPaginationToken inputPaginationToken = new StringPaginationToken(RELEASE_TOKEN_PREFIX + RELEASE_ITEM_TOKEN);
291+
ExportInformation exportInformation = new ExportInformation(inputPaginationToken, null);
292+
ExportResult<MusicContainerResource> result = googleMusicExporter.export(uuid, null, Optional.of(exportInformation));
293+
294+
// Check results
295+
// Verify correct methods were called
296+
verify(musicHttpApi).exportReleases(Optional.of(RELEASE_ITEM_TOKEN));
297+
verify(exportReleaseResponse).getReleases();
298+
299+
// Check pagination token
300+
ContinuationData continuationData = result.getContinuationData();
301+
StringPaginationToken paginationToken = (StringPaginationToken) continuationData.getPaginationData();
302+
assertEquals(RELEASE_TOKEN_PREFIX+RELEASE_ITEM_TOKEN, paginationToken.getToken());
303+
}
304+
305+
@Test
306+
public void exportReleaseWithNoData()
307+
throws InvalidTokenException, PermissionDeniedException, IOException, ParseException {
308+
when(exportReleaseResponse.getReleases())
309+
.thenReturn(null);
310+
when(exportReleaseResponse.getNextPageToken()).thenReturn(null);
311+
StringPaginationToken inputPaginationToken = new StringPaginationToken(RELEASE_TOKEN_PREFIX);
312+
ExportInformation exportInformation = new ExportInformation(inputPaginationToken, null);
313+
ExportResult<MusicContainerResource> result = googleMusicExporter.export(uuid, null, Optional.of(exportInformation));
314+
315+
// Check results
316+
// Verify correct methods were called
317+
verify(musicHttpApi).exportReleases(Optional.empty());
318+
verify(exportReleaseResponse).getReleases();
319+
320+
// Check pagination token
321+
ContinuationData continuationData = result.getContinuationData();
322+
StringPaginationToken paginationToken = (StringPaginationToken) continuationData.getPaginationData();
323+
assertThat(paginationToken).isNull();
324+
}
325+
232326
/**
233327
* Sets up a response with a single playlist, containing a single playlist item
234328
*/
@@ -255,4 +349,11 @@ private GooglePlaylistItem setUpSinglePlaylistItem(String isrc, String icpn) {
255349
playlistItemEntry.setTrack(track);
256350
return playlistItemEntry;
257351
}
352+
353+
private GoogleRelease setUpSingleRelease(String title, String icpn){
354+
GoogleRelease release = new GoogleRelease();
355+
release.setIcpn(icpn);
356+
release.setReleaseTitle(title);
357+
return release;
358+
}
258359
}

0 commit comments

Comments
 (0)