diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..baa17dd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [siriusnottin] +custom: ["https://buymeacoffee.com/siriusnottin"] diff --git a/.gitignore b/.gitignore index e4fec36..fdc3a13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # Coda.io .coda* +_upload_build + +# Other +*.paw # Logs logs diff --git a/README.md b/README.md index ebac111..4b3557c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,9 @@ Use this pack to synchronise your photos and albums right inside Coda! - [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) + - [x] Filter photos by categories (exclude) +- [x] See which photos belong to what album (soon) [#5](https://github.com/siriusnottin/google-photos-pack/issues/5) +- [ ] Get details informations about an album or a photo (soon) [#6](https://github.com/siriusnottin/google-photos-pack/issues/6) ## Changelog diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..826dd86 --- /dev/null +++ b/api/README.md @@ -0,0 +1,8 @@ +# google-photos-api + +Google photos api client. + +This is a fork of [a PR that was never merged](https://github.com/roopakv/google-photos/pull/27). + +Link to the original repo: +https://github.com/roopakv/google-photos diff --git a/api/albums/index.ts b/api/albums/index.ts new file mode 100644 index 0000000..b0127ba --- /dev/null +++ b/api/albums/index.ts @@ -0,0 +1,32 @@ +import { Transport } from "api/transport"; + +export class Albums { + constructor(public transport: Transport) { } + + list(pageSize = 50, pageToken?: string) { // range: 20-50 + return this.transport.get("albums", { pageSize, pageToken }); + } + + get(albumId: string) { + return this.transport.get(`albums/${albumId}`); + } + + create(title: string, description?: string) { + } + + share(albumId: string) { + } + + unshare(albumId: string) { + } + + addMediaItems(albumId: string, mediaItemIds: string[]) { + } + + removeMediaItems(albumId: string, mediaItemIds: string[]) { + } + + update(albumId: string, title: string, description?: string) { + } + +} diff --git a/api/common/date-filter.ts b/api/common/date-filter.ts new file mode 100644 index 0000000..3523a3e --- /dev/null +++ b/api/common/date-filter.ts @@ -0,0 +1,22 @@ +import { GDate } from "./gdate"; +import { DateRange } from "./date-range"; + +export class DateFilter { + public dates: ReturnType[] = []; + public ranges: DateRange[] = []; + + addDate(date) { + this.dates.push(GDate.newDate(date)); + } + + addRange(startDate, endDate) { + this.ranges.push(new DateRange(startDate, endDate)); + } + + toJSON() { + return { + dates: this.dates.map(d => d.toJSON()), + ranges: this.ranges.map(r => r.toJSON()), + } + } +} diff --git a/api/common/date-range.ts b/api/common/date-range.ts new file mode 100644 index 0000000..a7f60dc --- /dev/null +++ b/api/common/date-range.ts @@ -0,0 +1,16 @@ +import { GDate } from "./gdate"; +import { MediaItemsFilter } from "types/api-types"; + +export class DateRange { + constructor( + public startDate: ReturnType | GDate | Date, + public endDate: ReturnType | GDate | Date + ) { } + + toJSON(): MediaItemsFilter['dateFilter']['ranges'][0] { + return { + startDate: GDate.newDate(this.startDate).toJSON(), + endDate: GDate.newDate(this.endDate).toJSON() + } + } +} diff --git a/api/common/gdate.ts b/api/common/gdate.ts new file mode 100644 index 0000000..5f2623d --- /dev/null +++ b/api/common/gdate.ts @@ -0,0 +1,45 @@ +import { Moment, isMoment } from "moment"; +import { GPhotosDate } from "types/api-types"; + +export class GDate { + constructor(public year?: number, public month?: number, public day?: number) { } + + static fromDate(date: Date) { + if (!(date instanceof Date)) { + throw Error('Not a valid date object'); + } + return new GDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); + } + + static fromMoment(moment: Moment) { + if (!isMoment(moment)) { + throw Error('Not a valid moment object'); + } + return new GDate(moment.year(), moment.month() + 1, moment.date()); + } + + static newDate(date?: GDate | Date | Moment | GPhotosDate) { + if (date instanceof GDate) { + return date; + } + if (date instanceof Date) { + return GDate.fromDate(date); + } + if (isMoment(date)) { + return GDate.fromMoment(date); + } + if (date.year && date.month && date.day) { + return new GDate(date.year, date.month, date.day); + } + return new GDate(); + } + + toJSON(): GPhotosDate { + return { + year: this.year, + month: this.month, + day: this.day + } + } + +} diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..d56f164 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,32 @@ +import * as coda from "@codahq/packs-sdk"; + +import { Transport } from "./transport"; + +import { Albums } from "./albums"; +import { MediaItems } from "./media-items"; + +import { DateFilter } from "./common/date-filter"; +import { MediaTypeFilter } from "./media-items/media-type-filter"; +import { FeatureFilter } from "./media-items/feature-filter" +import { ContentFilter } from "./media-items/content-filter"; +import { Filters } from "./media-items/filters"; +export default class GPhotos { + + public readonly transport: Transport; + + public readonly albums: Albums; + public readonly mediaItems: MediaItems; + + public readonly DateFilter = DateFilter; + public readonly MediaTypeFilter = MediaTypeFilter; + public readonly FeatureFilter = FeatureFilter; + public readonly ContentFilter = ContentFilter; + public readonly Filters = Filters; + + constructor(public readonly fetcher: coda.Fetcher) { + this.transport = new Transport(fetcher); + this.albums = new Albums(this.transport); + this.mediaItems = new MediaItems(this.transport); + } + +} diff --git a/api/media-items/content-filter.ts b/api/media-items/content-filter.ts new file mode 100644 index 0000000..e18ad81 --- /dev/null +++ b/api/media-items/content-filter.ts @@ -0,0 +1,22 @@ +import { MediasContentCategories } from 'types/api-types'; + +export class ContentFilter { + + public includedContentCategories: MediasContentCategories[] = [] + public excludedContentCategories: MediasContentCategories[] = [] + + addIncludedCategory(category: MediasContentCategories) { + this.includedContentCategories.push(category); + } + + addExcludedCategory(category: MediasContentCategories) { + this.excludedContentCategories.push(category); + } + + toJSON() { + return { + includedContentCategories: this.includedContentCategories, + excludedContentCategories: this.excludedContentCategories, + } + } +} diff --git a/api/media-items/feature-filter.ts b/api/media-items/feature-filter.ts new file mode 100644 index 0000000..4dfecc4 --- /dev/null +++ b/api/media-items/feature-filter.ts @@ -0,0 +1,21 @@ + +import { MediaFeature, MediaItemsFilter } from "types/api-types"; + +export class FeatureFilter { + public includedFeatures: [MediaFeature]; + + constructor(includedFeature = MediaFeature.None) { + this.includedFeatures = [includedFeature]; + } + + setFeature(feature: MediaFeature) { + this.includedFeatures = [feature]; + } + + toJSON(): MediaItemsFilter['featureFilter'] { + return { + includedFeatures: this.includedFeatures + } + } + +} diff --git a/api/media-items/filters.ts b/api/media-items/filters.ts new file mode 100644 index 0000000..6e59790 --- /dev/null +++ b/api/media-items/filters.ts @@ -0,0 +1,44 @@ +import { DateFilter } from "../common/date-filter"; +import { MediaTypeFilter } from "./media-type-filter"; +import { ContentFilter } from "./content-filter"; +import { MediaItemsFilter } from "types/api-types"; +import { FeatureFilter } from "./feature-filter"; + +export class Filters { + public dateFilter: DateFilter; + public mediaTypeFilter: MediaTypeFilter; + public contentFilter: ContentFilter; + public featureFilter: FeatureFilter; + + constructor(public includeArchivedMedia = false) { } + + setDateFilter(dateFilter: DateFilter) { + this.dateFilter = dateFilter; + } + + setMediaTypeFilter(mediaTypeFilter: MediaTypeFilter) { + this.mediaTypeFilter = mediaTypeFilter; + } + + setContentFilter(contentFilter: ContentFilter) { + this.contentFilter = contentFilter; + } + + setIncludeArchivedMedia(includeArchivedMedia: boolean) { + this.includeArchivedMedia = includeArchivedMedia; + } + + setFeatureFilter(featureFilter: FeatureFilter) { + this.featureFilter = featureFilter; + } + + toJSON(): MediaItemsFilter { + return { + dateFilter: this.dateFilter, + mediaTypeFilter: this.mediaTypeFilter, + contentFilter: this.contentFilter, + includeArchivedMedia: this.includeArchivedMedia, + featureFilter: this.featureFilter, + } + } +} diff --git a/api/media-items/index.ts b/api/media-items/index.ts new file mode 100644 index 0000000..02c8f32 --- /dev/null +++ b/api/media-items/index.ts @@ -0,0 +1,34 @@ +import { Transport } from "api/transport"; +import { Filters } from "./filters"; +import { FetchResponse } from "@codahq/packs-sdk"; +import { ApiResponse } from "types/api-types"; +export class MediaItems { + constructor(public transport: Transport) { } + + list(pageToken?: string, pageSize = 100) { // range: 25-100 + return this.transport.get("mediaItems", { pageSize, pageToken }); + } + + get(mediaItemId: string) { + return this.transport.get(`mediaItems/${mediaItemId}`); + } + + search(albumId: string, pageToken?: string, pageSize?: number, fields?: string): Promise> + search(filters: Filters, pageToken?: string, pageSize?: number, fields?: string): Promise>; + + search(albumIdOrFilters: string | Filters, pageToken?: string, pageSize = 100, fields?: string) { + const body: { + pageSize?: number; + pageToken?: string; + albumId?: string; + filters?: ReturnType; + } = { pageSize, pageToken } + if (typeof albumIdOrFilters === "string") { + body.albumId = albumIdOrFilters; + } else { + body.filters = albumIdOrFilters.toJSON(); + } + return this.transport.post("mediaItems:search", { fields }, body); + } + +} diff --git a/api/media-items/media-type-filter.ts b/api/media-items/media-type-filter.ts new file mode 100644 index 0000000..76cdfeb --- /dev/null +++ b/api/media-items/media-type-filter.ts @@ -0,0 +1,19 @@ +import { MediaTypes, MediaItemsFilter } from 'types/api-types'; + +export class MediaTypeFilter { + public mediaTypes: MediaTypes[]; + + constructor(type = MediaTypes.All) { + this.mediaTypes = [type]; + } + + setType(type: MediaTypes) { + this.mediaTypes = [type]; + } + + toJSON(): MediaItemsFilter['mediaTypeFilter'] { + return { + mediaTypes: this.mediaTypes + } + } +} diff --git a/api/shared_albums/index.ts b/api/shared_albums/index.ts new file mode 100644 index 0000000..f6fdc01 --- /dev/null +++ b/api/shared_albums/index.ts @@ -0,0 +1,17 @@ +import { Transport } from "api/transport"; + +export class SharedAlbums { + constructor(public transport: Transport) { } + + list(pageSize = 50, pageToken?: string) { + } + + get(id: string) { + } + + join(id: string) { + } + + leave(id: string) { + } +} diff --git a/api/transport.ts b/api/transport.ts new file mode 100644 index 0000000..cda4795 --- /dev/null +++ b/api/transport.ts @@ -0,0 +1,69 @@ +import * as coda from "@codahq/packs-sdk"; +import { ApiResponse } from "types/api-types"; + +const ApiBaseUrl = "https://photoslibrary.googleapis.com/v1"; + +export class Transport { + + constructor(public readonly fetcher: coda.Fetcher) { } + + private readonly headers = { + "Content-Type": "application/json", + } + + private createUrl(endpoint: string, params?: { [key: string]: any }) { + let url = `${ApiBaseUrl}/${endpoint}`; + if (params) { + url = coda.withQueryParams(url, params); + } + return url; + } + + private createRequestParams(method: coda.FetchMethodType, url: string, body?: string): coda.FetchRequest { + // middleware to add header + return { + method, + url, + headers: this.headers, + body, + }; + } + + private async withErrorHandling(apiCall: () => Promise): Promise { + try { + return await apiCall(); + } catch (e) { + if (coda.StatusCodeError.isStatusCodeError(e)) { + let statusError = e as coda.StatusCodeError; + let message = statusError.body?.error?.message; + if (message) { + throw new coda.UserVisibleError(message); + } + } + throw e; + } + } + + get(endpoint: string, params?: { [key: string]: any }): Promise> { + const request = this.createRequestParams( + "GET", + this.createUrl(endpoint, params) + ); + return this.withErrorHandling(() => this.fetcher.fetch(request)); + } + + upload() { + // TODO: implement upload method + } + + post(endpoint: string, params?: { [key: string]: any }, body?: any): Promise> { + + const request = this.createRequestParams( + "POST", + this.createUrl(endpoint, params), + JSON.stringify(body) + ); + return this.withErrorHandling(() => this.fetcher.fetch(request)); + } + +} diff --git a/helpers.ts b/helpers.ts index fd8a66e..4082663 100644 --- a/helpers.ts +++ b/helpers.ts @@ -1,8 +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"; +import * as types from "types/common-types"; +import GPhotos from "./api"; export async function getConnectionName(context: coda.ExecutionContext) { let request: coda.FetchRequest = { @@ -17,46 +15,10 @@ export async function getConnectionName(context: coda.ExecutionContext) { return user.name as string; } -export async function SyncMediaItems( - context: coda.SyncExecutionContext, - filters?: types.MediaItemsFilter, - albumId?: string, -): Promise { - 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) => { +// Parse the response from the API to the format we want to return +// Matches the MediaSchema schema defined in ./schemas.ts +export function mediaItemsParser(mediaItems: types.MediaItemResponse[]): types.MediaItem[] { + return mediaItems.map((mediaItem) => { let { id, filename, mimeType, description, productUrl } = mediaItem; let { creationTime, photo, video, width, height } = mediaItem.mediaMetadata; return { @@ -73,54 +35,44 @@ export async function SyncMediaItems( url: productUrl, } }); - - return { - result, - continuation: nextPageToken ? { nextPageToken } : undefined, - }; } -export async function syncAlbums( - context: coda.SyncExecutionContext, -): Promise { - 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, - }; - } +export async function getMediaItemsFromAlbum(albumId: string, context: coda.ExecutionContext) { + const photos = new GPhotos(context.fetcher); + let mediaItems: types.Album['mediaItems'] = []; + let nextPageToken: string | undefined; + do { + const response = await photos.mediaItems.search(albumId, nextPageToken, 100, 'mediaItems(id),nextPageToken') + const mediaItemsRes = response.body?.mediaItems as types.MediaItemIdRes[]; + if (mediaItemsRes) { + mediaItems = mediaItems.concat(mediaItemsRes.map((mediaItem) => { + return { mediaId: mediaItem.id, filename: "Not found" } + })); + } + nextPageToken = response.body?.nextPageToken as string | undefined; + } while (nextPageToken); + return { mediaItems }; +} - result = albumsRes.map((album) => { - let { id, title, productUrl, coverPhotoBaseUrl } = album; +// Parse the response from the API to the format we want to return +// Matches the AlbumSchema schema defined in ./schemas.ts +export function albumParser(albums: types.AlbumResponse[]): types.Album[] { + return albums.map((album) => { + let { id, title, productUrl, coverPhotoBaseUrl, coverPhotoMediaItemId } = album; + let coverPhotoMediaItem: types.Album['coverPhotoMediaItem'] = undefined; + if (coverPhotoMediaItemId) { + coverPhotoMediaItem = { + filename: "Not found", + mediaId: coverPhotoMediaItemId, + } + } return { albumId: id, title, url: productUrl, mediaItems: [], coverPhoto: `${coverPhotoBaseUrl}=w2048-h1024`, - coverPhotoMediaItem: undefined, + coverPhotoMediaItem, } }); - - return { - result, - continuation: nextPageToken ? { nextPageToken } : undefined, - }; } diff --git a/pack.ts b/pack.ts index 8470b58..6585ea8 100644 --- a/pack.ts +++ b/pack.ts @@ -1,8 +1,9 @@ import * as coda from "@codahq/packs-sdk"; import * as helpers from "./helpers"; import * as schemas from "./schemas"; -import * as types from "./types"; +import * as types from "./types/api-types"; import * as params from "./params"; +import GPhotos from "./api"; export const pack = coda.newPack(); @@ -39,49 +40,52 @@ pack.addSyncTable({ params.MediaArchivedOptional, ], execute: async function ([dateRange, categoriesToInclude, categoriesToExclude, mediaType, favorite, archived], context) { - function formatDate(date: Date, dateFormatter: Intl.DateTimeFormat) { - const dateParts = dateFormatter.formatToParts(date); - return { - year: dateParts.find((part) => part.type === "year").value, - month: dateParts.find((part) => part.type === "month").value, - day: dateParts.find((part) => part.type === "day").value, - }; - } - const dateFormatter = new Intl.DateTimeFormat("en", { - timeZone: context.timezone, // Use the doc's timezone (important!) - year: "numeric", - month: "numeric", - day: "numeric", - }); + let photos = new GPhotos(context.fetcher); - let filters: types.MediaItemsFilter = { - dateFilter: { - ranges: [{ - "startDate": formatDate(dateRange[0], dateFormatter), - "endDate": formatDate(dateRange[1], dateFormatter), - }] - }, - }; + const filters = new photos.Filters(archived); - if (categoriesToInclude || categoriesToExclude) { - filters.contentFilter = { - includedContentCategories: (categoriesToInclude) ? categoriesToInclude.map((category) => types.MediasContentCategories[category]) : undefined, - excludedContentCategories: (categoriesToExclude) ? categoriesToExclude.map((category) => types.MediasContentCategories[category]) : undefined, - }; + // Date filter + if (!dateRange) { + throw new coda.UserVisibleError("Date range is required."); } + const dateFilter = new photos.DateFilter(); + dateFilter.addRange(dateRange[0], dateRange[1]); + filters.setDateFilter(dateFilter); - if (mediaType) { - filters.mediaTypeFilter = { mediaTypes: [types.MediaTypes[mediaType]] }; + // Content filter + const contentFilter = new photos.ContentFilter(); + switch (categoriesToInclude || categoriesToExclude) { + case categoriesToInclude: + categoriesToInclude?.forEach((category) => { + contentFilter.addIncludedCategory(category as types.MediasContentCategories); + }); + break; + case categoriesToExclude: + categoriesToExclude?.forEach((category) => { + contentFilter.addExcludedCategory(category as types.MediasContentCategories); + }); + break; } + filters.setContentFilter(contentFilter); + + // Media type filter + const mediaTypeFilter = new photos.MediaTypeFilter(mediaType as types.MediaTypes); + // mediaTypeFilter.setType(mediaType as types.MediaTypes) + filters.setMediaTypeFilter(mediaTypeFilter); + // Feature filter (favorites) if (favorite) { - filters.featureFilter = { includedFeatures: ["FAVORITES"] } + const featureFilter = new photos.FeatureFilter(types.MediaFeature.Favorites) + filters.setFeatureFilter(featureFilter); } - if (archived) { filters.includeArchivedMedia = archived } - - return helpers.SyncMediaItems(context, filters); + const response = await photos.mediaItems.search(filters, (context.sync.continuation?.nextPageToken as string | undefined)) + const { nextPageToken, mediaItems } = response?.body ?? {}; + return { + result: mediaItems ? helpers.mediaItemsParser(mediaItems as types.MediaItemResponse[]) : null, + continuation: nextPageToken ? { nextPageToken } : undefined + } }, }, }); @@ -95,7 +99,24 @@ pack.addSyncTable({ description: "Sync all albums.", parameters: [], execute: async function ([], context) { - return helpers.syncAlbums(context); + + const photos = new GPhotos(context.fetcher); + const response = await photos.albums.list(20, (context.sync.continuation?.nextPageToken as string | undefined)); + const { albums, nextPageToken } = response?.body; + const parsedAlbums = albums ? helpers.albumParser(albums) : null; + + if (parsedAlbums) { + for (const album of parsedAlbums) { + const { albumId } = album; + const { mediaItems } = await helpers.getMediaItemsFromAlbum(albumId, context); + album.mediaItems = mediaItems ?? undefined; + } + } + + return { + result: parsedAlbums, + continuation: nextPageToken ? { nextPageToken } : undefined + } } } }); diff --git a/package-lock.json b/package-lock.json index 44fd3ca..90011fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@codahq/packs-sdk": "^1.3.3" + "@codahq/packs-sdk": "^1.3.3", + "moment": "^2.29.4" }, "devDependencies": { "@types/chai": "^4.3.4", @@ -5728,6 +5729,14 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", "integrity": "sha512-JIR0m0ybkmTcR8URann+HbwKmodP+OE8UCbsifQDYMLD5J3em1Cdn3MYPpbEd5elGDwmP98T+WbqP/tvzA5Mjg==" }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index 249750a..87e8984 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,15 @@ "name": "google-photos", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { - "@codahq/packs-sdk": "^1.3.3" + "@codahq/packs-sdk": "^1.3.3", + "moment": "^2.29.4" }, "devDependencies": { "@types/chai": "^4.3.4", diff --git a/params.ts b/params.ts index 70b5e74..96b62e3 100644 --- a/params.ts +++ b/params.ts @@ -1,7 +1,5 @@ import * as coda from "@codahq/packs-sdk"; -import * as helpers from "./helpers"; -import * as schemas from "./schemas"; -import * as types from "./types"; +import * as types from "types/api-types"; export const MediaDateRange = coda.makeParameter({ type: coda.ParameterType.DateArray, diff --git a/schemas.ts b/schemas.ts index 759608f..5cd3507 100644 --- a/schemas.ts +++ b/schemas.ts @@ -29,11 +29,8 @@ const MediaMetadataSchema = coda.makeObjectSchema({ export const MediaSchema = coda.makeObjectSchema({ properties: { - mediaId: { - type: coda.ValueType.String, - required: true - }, - filename: { type: coda.ValueType.String, required: true }, + mediaId: { type: coda.ValueType.String }, + filename: { type: coda.ValueType.String }, mediaType: { type: coda.ValueType.String }, mimeType: { type: coda.ValueType.String }, description: { type: coda.ValueType.String }, @@ -61,7 +58,18 @@ export const MediaSchema = coda.makeObjectSchema({ ], }); -const MediaReferenceSchema = coda.makeReferenceSchemaFromObjectSchema(MediaSchema, "Media"); +const MediaReferenceSchema = coda.makeObjectSchema({ + codaType: coda.ValueHintType.Reference, + properties: { + filename: { type: coda.ValueType.String, required: true }, + mediaId: { type: coda.ValueType.String, required: true }, + }, + displayProperty: "filename", + idProperty: "mediaId", + identity: { + name: "Media", + }, +}); export const AlbumSchema = coda.makeObjectSchema({ properties: { @@ -74,7 +82,7 @@ export const AlbumSchema = coda.makeObjectSchema({ description: "Google Photos URL for the album.", codaType: coda.ValueHintType.Url, }, - mediasItems: { + mediaItems: { type: coda.ValueType.Array, items: MediaReferenceSchema }, diff --git a/tsconfig.json b/tsconfig.json index 71a7085..68b722c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,15 @@ { "compilerOptions": { - "lib": ["es2020"] + "lib": [ + "es2020" + ], + "paths": { + "types/*": [ + "./types/*" + ], + "api/*": [ + "./api/*" + ] + } } } diff --git a/types.ts b/types/api-types.ts similarity index 72% rename from types.ts rename to types/api-types.ts index 5ae20ed..f9b4bef 100644 --- a/types.ts +++ b/types/api-types.ts @@ -36,8 +36,10 @@ interface ContributorInfo { displayName: string; } -export interface MediaItemResponse { - id: string; +export interface MediaItemIdRes { + id: string, +}; +export interface MediaItemResponse extends MediaItemIdRes { description: string; productUrl: string; baseUrl: string; @@ -52,7 +54,7 @@ interface SharedAlbumOptions { isCommentable: boolean; } -interface ShareInfo { +export interface ShareInfo { sharedAlbumOptions: SharedAlbumOptions; shareableUrl?: string; shareToken: string; @@ -72,31 +74,10 @@ export interface AlbumResponse { coverPhotoMediaItemId: string; } -// -// Pack types - -export interface MediaItem { - mediaId: string; - filename: string; - mediaType: string; - mimeType: string; - description: string; - photoMetadata?: Photo; - videoMetadata?: Video; - creationTime: string; - width: number; - height: number; - image: string; - url: string; -} - -export interface Album { - albumId: string; - title: string; - url: string; - mediaItems: MediaItem[]; - coverPhoto: string; - coverPhotoMediaItem: string | undefined; +export type GPhotosDate = { + year?: number; + month?: number; + day?: number; } export enum MediasContentCategories { @@ -128,25 +109,18 @@ export enum MediasContentCategories { Whiteboards = "WHITEBOARDS", } -export enum MediaTypes { - Photo = "PHOTO", - Video = "VIDEO", +export enum MediaFeature { + None = "NONE", + Favorites = "FAVORITES", } // filter object when "searching" for media items export interface MediaItemsFilter { - dateFilter: { + dateFilter?: { + dates: GPhotosDate[]; ranges: { - startDate: { - year: string; - month: string; - day: string; - }; - endDate: { - year: string; - month: string; - day: string; - }; + startDate: GPhotosDate; + endDate: GPhotosDate; }[]; }; contentFilter?: { @@ -154,18 +128,24 @@ export interface MediaItemsFilter { excludedContentCategories: string[], }; mediaTypeFilter?: { - mediaTypes: [string] + mediaTypes: MediaTypes[], } featureFilter?: { - includedFeatures: ["NONE" | "FAVORITES"], + includedFeatures: [MediaFeature], }; includeArchivedMedia?: boolean; excludeNonAppCreatedData?: boolean; } -export interface GetMediaItemsPayload { - albumId?: string; - pageSize?: number; - pageToken?: string; - filters?: MediaItemsFilter; +export interface ApiResponse { + mediaItems?: MediaItemResponse[] | MediaItemIdRes[]; + albums?: AlbumResponse[]; + sharedAlbums?: object[]; + nextPageToken?: string; +} + +export enum MediaTypes { + All = "ALL_MEDIA", + Photo = "PHOTO", + Video = "VIDEO", } diff --git a/types/common-types.ts b/types/common-types.ts new file mode 100644 index 0000000..736c46b --- /dev/null +++ b/types/common-types.ts @@ -0,0 +1,2 @@ +export { ApiResponse, MediaItemIdRes, MediaItemResponse, AlbumResponse } from "./api-types"; +export { MediaItemId, MediaItem, Album } from "./pack-types" diff --git a/types/pack-types.ts b/types/pack-types.ts new file mode 100644 index 0000000..7fa1a79 --- /dev/null +++ b/types/pack-types.ts @@ -0,0 +1,32 @@ +import { ShareInfo } from "./api-types"; +// Pack types + +export interface MediaItemId { + mediaId: string, +} + +export interface MediaItem extends MediaItemId { + filename: string; + mediaType: string; + mimeType: string; + description: string; + creationTime: string; + width: number; + height: number; + image: string; + url: string; +} + +export interface MediaItemReference extends MediaItemId { + filename: "Not found"; +} + +export interface Album { + albumId: string; + title: string; + url: string; + shareInfo?: ShareInfo; + mediaItems: MediaItemReference[] | undefined; + coverPhoto: string; + coverPhotoMediaItem: MediaItemReference | undefined; +}