Skip to content

Commit

Permalink
Merge pull request #3 from siriusnottin/develop
Browse files Browse the repository at this point in the history
Refactor, better types and more data displayed to the user!
  • Loading branch information
siriusnottin authored Apr 18, 2023
2 parents 1ed6d14 + 4ad5175 commit 7052904
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 169 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Google Photos Coda Pack

Use this pack to synchronise your photos and albums right inside Coda!

[More information about the pack and how to use it](https://coda.io/@siriusnottin/google-photos-pack)[Demo document]()

## Features

- [x] List Photos and Albums
- [x] Advanced filters (include or exclude certain types of photos or albums)
- [x] Filter photos by favorites
- [x] Filter photos by date
- [x] Filter photos by categories (include)
- [x] Filter photos by categories (exclude) (soon)
- [ ] See which photos belong to what album (soon)
- [ ] Get details informations about an album or a photo (soon)
- [ ] Refactor the code for better readability and organization (I’m still learning)

## Changelog

v1 – 4/15/2023

List photos and albums.

v2 – 4/16/2023

New Media Type column (dissociate photo and video) and filter option for Medias table

v3 – 4/18/2023

Code & Pack refactoring: better performance when filtering by media type, new columns and data from the API for medias table
135 changes: 108 additions & 27 deletions helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as coda from "@codahq/packs-sdk";
import * as types from "./types";
import { allowedNodeEnvironmentFlags } from "process";

export const ApiUrl = "https://photoslibrary.googleapis.com/v1";

Expand All @@ -15,31 +17,110 @@ export async function getConnectionName(context: coda.ExecutionContext) {
return user.name as string;
}

export const MediasContentCategoriesList = {
Animals: "ANIMALS",
Fashion: "FASHION",
Landmarks: "LANDMARKS",
Receipts: "RECEIPTS",
Weddings: "WEDDINGS",
Arts: "ARTS",
Flowers: "FLOWERS",
Landscapes: "LANDSCAPES",
Screenshots: "SCREENSHOTS",
Whiteboards: "WHITEBOARDS",
Birthdays: "BIRTHDAYS",
Food: "FOOD",
Night: "NIGHT",
Selfies: "SELFIES",
Cityscapes: "CITYSCAPES",
Gardens: "GARDENS",
People: "PEOPLE",
Sport: "SPORT",
Crafts: "CRAFTS",
Holidays: "HOLIDAYS",
Performances: "PERFORMANCES",
Travel: "TRAVEL",
Documents: "DOCUMENTS",
Houses: "HOUSES",
Pets: "PETS",
Utility: "UTILITY"
export async function SyncMediaItems(
context: coda.SyncExecutionContext,
filters?: types.MediaItemsFilter,
albumId?: string,
): Promise<coda.GenericSyncFormulaResult> {
if (filters && albumId || !filters && !albumId) {
throw new coda.UserVisibleError("Must provide either filters or albumId");
}
let { continuation } = context.sync;
let body: types.GetMediaItemsPayload = { pageSize: 100 };
if (continuation) {
body.pageToken = continuation.nextPageToken as string;
}
if (filters && !albumId) {
body.filters = filters;
}
if (albumId && !filters) {
body.albumId = albumId;
}
let response = await context.fetcher.fetch({
method: "POST",
url: `${ApiUrl}/mediaItems:search`,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
let mediaItemsRes = response.body.mediaItems as types.MediaItemResponse[];
let nextPageToken;
if (response.body.nextPageToken) {
nextPageToken = response.body.nextPageToken;
}
let result: types.MediaItem[];

if (!mediaItemsRes || mediaItemsRes.length === 0) {
return {
result: [],
continuation: undefined,
};
}

result = mediaItemsRes.map((mediaItem) => {
let { id, filename, mimeType, description, productUrl } = mediaItem;
let { creationTime, photo, video, width, height } = mediaItem.mediaMetadata;
return {
mediaId: id,
filename,
mediaType: (photo) ? "Photo" : "Video",
mimeType,
description,
creationTime,
mediaMetadata: { photo, video },
width,
height,
image: `${mediaItem.baseUrl}=w2048-h1024`,
url: productUrl,
}
});

return {
result,
continuation: nextPageToken ? { nextPageToken } : undefined,
};
}

export async function syncAlbums(
context: coda.SyncExecutionContext,
): Promise<coda.GenericSyncFormulaResult> {
let url = `${ApiUrl}/albums`;
let { continuation } = context.sync;
if (continuation) {
url = coda.withQueryParams(url, { pageToken: continuation.nextPageToken });
}
let response = await context.fetcher.fetch({
method: "GET",
url,
headers: { "Content-Type": "application/json" },
});
let albumsRes = response.body.albums as types.AlbumResponse[];
let nextPageToken;
if (response.body.nextPageToken) {
nextPageToken = response.body.nextPageToken;
}
let result: types.Album[];

if (!albumsRes || albumsRes.length === 0) {
return {
result: [],
continuation: undefined,
};
}

result = albumsRes.map((album) => {
let { id, title, productUrl, coverPhotoBaseUrl } = album;
return {
albumId: id,
title,
url: productUrl,
mediaItems: [],
coverPhoto: `${coverPhotoBaseUrl}=w2048-h1024`,
coverPhotoMediaItem: undefined,
}
});

return {
result,
continuation: nextPageToken ? { nextPageToken } : undefined,
};
}
154 changes: 33 additions & 121 deletions pack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as coda from "@codahq/packs-sdk";
import * as helpers from "./helpers";
import * as params from "./params";
import * as schemas from "./schemas";
import * as types from "./types";
import * as params from "./params";

export const pack = coda.newPack();

Expand Down Expand Up @@ -30,14 +31,14 @@ pack.addSyncTable({
name: "SyncMedias",
description: "Sync medias from the user's library.",
parameters: [
params.MediaDateRangeParam,
params.MediaTypeParam,
params.MediaCategoriesIncludeParam,
params.MediaFavoritesParam
params.MediaDateRange,
params.MediaCategoriesIncludeOpt,
params.MediaCategoriesExcludeOpt,
params.MediaTypeOptional,
params.MediaFavoritesOptional,
params.MediaArchivedOptional,
],
execute: async function ([dateRange, mediaType, categories, favorite], context) {
let url = `${helpers.ApiUrl}/mediaItems:search`;

execute: async function ([dateRange, categoriesToInclude, categoriesToExclude, mediaType, favorite, archived], context) {
function formatDate(date: Date, dateFormatter: Intl.DateTimeFormat) {
const dateParts = dateFormatter.formatToParts(date);
return {
Expand All @@ -53,85 +54,35 @@ pack.addSyncTable({
day: "numeric",
});

interface RequestPayload {
pageSize: number;
filters: {
dateFilter: {
ranges: {
startDate: {
year: string;
month: string;
day: string;
};
endDate: {
year: string;
month: string;
day: string;
};
}[];
};
featureFilter?: {
includedFeatures: string[],
};
contentFilter?: {
includedContentCategories: string[],
};
};
pageToken?: undefined | string;
let filters: types.MediaItemsFilter = {
dateFilter: {
ranges: [{
"startDate": formatDate(dateRange[0], dateFormatter),
"endDate": formatDate(dateRange[1], dateFormatter),
}]
},
};

let payload: RequestPayload = {
pageSize: 100,
filters: {
dateFilter: {
ranges: [{
"startDate": formatDate(dateRange[0], dateFormatter),
"endDate": formatDate(dateRange[1], dateFormatter),
}]
},
featureFilter: (favorite) ? { includedFeatures: ["FAVORITES"] } : undefined,
contentFilter: (categories) ? { includedContentCategories: categories.map(category => (helpers.MediasContentCategoriesList[category])) } : undefined,
},
pageToken: (context.sync.continuation?.nextPageToken) ? context.sync.continuation.nextPageToken : undefined,
}
let response = await context.fetcher.fetch({
method: "POST",
url: url,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
let items = response.body.mediaItems;
if (items && items.length > 0) {
for (let item of items) {
// the api returns item.mediaMetadata.photo and item.mediaMetadata.video, we want to have a single mediaType property.
item.mediaType = (item.mediaMetadata.photo) ? "Photo" : "Video";
item.creationTime = item.mediaMetadata.creationTime
item.width = item.mediaMetadata.width
item.height = item.mediaMetadata.height
if (categoriesToInclude || categoriesToExclude) {
filters.contentFilter = {
includedContentCategories: (categoriesToInclude) ? categoriesToInclude.map((category) => types.MediasContentCategories[category]) : undefined,
excludedContentCategories: (categoriesToExclude) ? categoriesToExclude.map((category) => types.MediasContentCategories[category]) : undefined,
};
};
}

if (mediaType) {
items = items.filter(item => (item.mediaType === mediaType));
filters.mediaTypeFilter = { mediaTypes: [types.MediaTypes[mediaType]] };
}
if (items && items.length > 0) {
for (let item of items) {
// We get the image only after we have filtered the items since it can become quite costly in ressources.
item.image = item.baseUrl + "=w2048-h1024"//TODO: add parameter for image sizes.
};
};
let continuation;
if (response.body.nextPageToken) {
continuation = {
nextPageToken: response.body.nextPageToken
};

if (favorite) {
filters.featureFilter = { includedFeatures: ["FAVORITES"] }
}
return {
result: items,
continuation: continuation,
};
}

if (archived) { filters.includeArchivedMedia = archived }

return helpers.SyncMediaItems(context, filters);

},
},
});

Expand All @@ -144,46 +95,7 @@ pack.addSyncTable({
description: "Sync all albums.",
parameters: [],
execute: async function ([], context) {
let url = `${helpers.ApiUrl}/albums`;

if (context.sync.continuation) {
url = coda.withQueryParams(url, { pageToken: context.sync.continuation })
};

const AlbumsResponse = await context.fetcher.fetch({
method: "GET",
url,
});

let albumsContinuation;
if (AlbumsResponse.body.nextPageToken) {
albumsContinuation = AlbumsResponse.body.nextPageToken
};

const Albums = await AlbumsResponse.body.albums;
for (const album of Albums) {
// we want to search for all medias in the current album.
// let url = coda.withQueryParams(`${helpers.ApiUrl}/mediaItems:search`, { pageSize: 5 });
// let body = { albumId: album.id };
// let mediaItemsInAlbum = [];
// let mediaItemsNextPageToken;

// const mediaItemsInAlbumResponse = await context.fetcher.fetch({
// method: "POST",
// url,
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(body)
// });
// if (mediaItemsInAlbumResponse.body.nextPageToken) {
// continuation.AlbumMediaItemsNextPageToken = mediaItemsInAlbumResponse.body.nextPageToken;
// };
album.medias = [];
album.coverPhoto = album.coverPhotoBaseUrl + "=w2048-h1024"
}
return {
result: Albums,
continuation: albumsContinuation,
};
return helpers.syncAlbums(context);
}
}
});
Loading

0 comments on commit 7052904

Please sign in to comment.