index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. const got = require('got').default
  2. const { logout, refreshToken } = require('../index')
  3. const { withGoogleErrorHandling } = require('../../providerErrors')
  4. const { prepareStream } = require('../../../helpers/utils')
  5. const { MAX_AGE_REFRESH_TOKEN } = require('../../../helpers/jwt')
  6. const logger = require('../../../logger')
  7. const Provider = require('../../Provider')
  8. const getBaseClient = ({ token }) => got.extend({
  9. headers: {
  10. authorization: `Bearer ${token}`,
  11. },
  12. })
  13. const getPhotosClient = ({ token }) => getBaseClient({ token }).extend({
  14. prefixUrl: 'https://photoslibrary.googleapis.com/v1',
  15. })
  16. const getOauthClient = ({ token }) => getBaseClient({ token }).extend({
  17. prefixUrl: 'https://www.googleapis.com/oauth2/v1',
  18. })
  19. async function paginate(fn, getter, limit = 5) {
  20. const items = []
  21. let pageToken
  22. for (let i = 0; (i === 0 || pageToken != null); i++) {
  23. if (i >= limit) {
  24. logger.warn(`Hit pagination limit of ${limit}`)
  25. break;
  26. }
  27. const response = await fn(pageToken);
  28. items.push(...getter(response));
  29. pageToken = response.nextPageToken
  30. }
  31. return items
  32. }
  33. /**
  34. * Provider for Google Photos API
  35. */
  36. class GooglePhotos extends Provider {
  37. static get authProvider () {
  38. return 'googlephotos'
  39. }
  40. static get authStateExpiry () {
  41. return MAX_AGE_REFRESH_TOKEN
  42. }
  43. // eslint-disable-next-line class-methods-use-this
  44. async list (options) {
  45. return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.list.error', async () => {
  46. const { directory, query } = options
  47. const { token } = options
  48. const isRoot = !directory
  49. const client = getPhotosClient({ token })
  50. async function fetchAlbums () {
  51. if (!isRoot) return [] // albums are only in the root
  52. return paginate(
  53. (pageToken) => client.get('albums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(),
  54. (response) => response.albums,
  55. )
  56. }
  57. async function fetchSharedAlbums () {
  58. if (!isRoot) return [] // albums are only in the root
  59. return paginate(
  60. (pageToken) => client.get('sharedAlbums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(),
  61. (response) => response.sharedAlbums ?? [], // seems to be undefined if no shared albums
  62. )
  63. }
  64. async function fetchMediaItems () {
  65. if (isRoot) return { mediaItems: [] } // no images in root (album list only)
  66. const resp = await client.post('mediaItems:search', { json: { pageToken: query?.cursor, albumId: directory, pageSize: 50 }, responseType: 'json' }).json();
  67. return resp
  68. }
  69. const [sharedAlbums, albums, { mediaItems, nextPageToken }] = await Promise.all([
  70. fetchSharedAlbums(), fetchAlbums(), fetchMediaItems()
  71. ])
  72. const newSp = new URLSearchParams(Object.entries(query));
  73. if (nextPageToken) newSp.set('cursor', nextPageToken);
  74. const iconSize = 64
  75. const thumbSize = 300
  76. const getIcon = (baseUrl) => `${baseUrl}=w${iconSize}-h${iconSize}-c`
  77. const getThumbnail = (baseUrl) => `${baseUrl}=w${thumbSize}-h${thumbSize}-c`
  78. const adaptedItems = [
  79. ...albums.map((album) => ({
  80. isFolder: true,
  81. icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder',
  82. mimeType: 'application/vnd.google-apps.folder',
  83. thumbnail: getThumbnail(album.coverPhotoBaseUrl),
  84. name: album.title,
  85. id: album.id,
  86. requestPath: album.id,
  87. })),
  88. ...sharedAlbums.map((sharedAlbum) => ({
  89. isFolder: true,
  90. icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder',
  91. mimeType: 'application/vnd.google-apps.folder',
  92. thumbnail: getThumbnail(sharedAlbum.coverPhotoBaseUrl),
  93. name: sharedAlbum.title,
  94. id: sharedAlbum.id,
  95. requestPath: sharedAlbum.id,
  96. })),
  97. ...mediaItems.map((mediaItem) => ({
  98. isFolder: false,
  99. icon: getIcon(mediaItem.baseUrl),
  100. thumbnail: getThumbnail(mediaItem.baseUrl),
  101. name: mediaItem.filename,
  102. id: mediaItem.id,
  103. mimeType: mediaItem.mimeType,
  104. modifiedDate: mediaItem.creationTime,
  105. requestPath: mediaItem.id,
  106. custom: {
  107. imageWidth: mediaItem.photo ? mediaItem.width : undefined,
  108. imageHeight: mediaItem.photo ? mediaItem.height : undefined,
  109. videoWidth: mediaItem.video ? mediaItem.width : undefined,
  110. videoHeight: mediaItem.video ? mediaItem.height : undefined,
  111. },
  112. })),
  113. ];
  114. const { email: username } = await getOauthClient({ token }).get('userinfo').json()
  115. return {
  116. username,
  117. items: adaptedItems,
  118. nextPagePath: newSp.size > 0 ? `${directory ?? ''}?${newSp.toString()}` : null,
  119. }
  120. })
  121. }
  122. // eslint-disable-next-line class-methods-use-this
  123. async download ({ id, token }) {
  124. return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.download.error', async () => {
  125. const client = getPhotosClient({ token })
  126. const { baseUrl } = await client.get(`mediaItems/${encodeURIComponent(id)}`, { responseType: 'json' }).json()
  127. const url = `${baseUrl}=d`;
  128. const stream = got.stream.get(url, { responseType: 'json' })
  129. const { size } = await prepareStream(stream)
  130. return { stream, size }
  131. })
  132. }
  133. // eslint-disable-next-line class-methods-use-this
  134. async logout(...args) {
  135. return logout(...args)
  136. }
  137. // eslint-disable-next-line class-methods-use-this
  138. async refreshToken(...args) {
  139. return refreshToken(...args)
  140. }
  141. }
  142. module.exports = GooglePhotos