From be45af95fb9b6e2549d0f8968c47f63ca4fc5d25 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 2 Dec 2024 18:21:12 +0100 Subject: [PATCH] Implement photos serializer --- app/controllers/api/v1/photos_controller.rb | 18 +++--- app/javascript/controllers/maps_controller.js | 9 ++- app/serializers/api/photo_serializer.rb | 61 +++++++++++++++++++ app/services/immich/request_photos.rb | 2 +- app/services/photoprism/request_photos.rb | 22 ++++--- app/services/photos/request.rb | 2 +- .../photoprism/request_photos_spec.rb | 8 +-- 7 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 app/serializers/api/photo_serializer.rb diff --git a/app/controllers/api/v1/photos_controller.rb b/app/controllers/api/v1/photos_controller.rb index c023a2d6..a31e03ea 100644 --- a/app/controllers/api/v1/photos_controller.rb +++ b/app/controllers/api/v1/photos_controller.rb @@ -10,7 +10,14 @@ class Api::V1::PhotosController < ApiController end def thumbnail - response = Rails.cache.fetch("photo_thumbnail_#{params[:id]}", expires_in: 1.day) do + response = fetch_cached_thumbnail + handle_thumbnail_response(response) + end + + private + + def fetch_cached_thumbnail + Rails.cache.fetch("photo_thumbnail_#{params[:id]}", expires_in: 1.day) do HTTParty.get( "#{current_api_user.settings['immich_url']}/api/assets/#{params[:id]}/thumbnail?size=preview", headers: { @@ -19,14 +26,11 @@ class Api::V1::PhotosController < ApiController } ) end + end + def handle_thumbnail_response(response) if response.success? - send_data( - response.body, - type: 'image/jpeg', - disposition: 'inline', - status: :ok - ) + send_data(response.body, type: 'image/jpeg', disposition: 'inline', status: :ok) else render json: { error: 'Failed to fetch thumbnail' }, status: response.code end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 20e058ee..338444cf 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -137,10 +137,13 @@ export default class extends Controller { this.map.addControl(this.drawControl); } if (e.name === 'Photos') { - if (!this.userSettings.immich_url || !this.userSettings.immich_api_key) { + if ( + (!this.userSettings.immich_url || !this.userSettings.immich_api_key) && + (!this.userSettings.photoprism_url || !this.userSettings.photoprism_api_key) + ) { showFlashMessage( 'error', - 'Immich integration is not configured. Please check your settings.' + 'Photos integration is not configured. Please check your integrations settings.' ); return; } @@ -836,7 +839,7 @@ export default class extends Controller {

${photo.originalFileName}

Taken: ${new Date(photo.localDateTime).toLocaleString()}

Location: ${photo.exifInfo.city}, ${photo.exifInfo.state}, ${photo.exifInfo.country}

- ${photo.type === 'VIDEO' ? '🎥 Video' : '📷 Photo'} + ${photo.type === 'video' ? '🎥 Video' : '📷 Photo'} `; marker.bindPopup(popupContent); diff --git a/app/serializers/api/photo_serializer.rb b/app/serializers/api/photo_serializer.rb new file mode 100644 index 00000000..b063c541 --- /dev/null +++ b/app/serializers/api/photo_serializer.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class Api::PhotoSerializer + def initialize(photo) + @photo = photo + end + + def call + { + id: id, + latitude: latitude, + longitude: longitude, + localDateTime: local_date_time, + originalFileName: original_file_name, + city: city, + state: state, + country: country, + type: type + } + end + + private + + attr_reader :photo + + def id + photo['id'] || photo['ID'] + end + + def latitude + photo.dig('exifInfo', 'latitude') || photo['Lat'] + end + + def longitude + photo.dig('exifInfo', 'longitude') || photo['Lng'] + end + + def local_date_time + photo['localDateTime'] || photo['TakenAtLocal'] + end + + def original_file_name + photo['originalFileName'] || photo['OriginalName'] + end + + def city + photo.dig('exifInfo', 'city') || photo['PlaceCity'] + end + + def state + photo.dig('exifInfo', 'state') || photo['PlaceState'] + end + + def country + photo.dig('exifInfo', 'country') || photo['PlaceCountry'] + end + + def type + (photo['type'] || photo['Type']).downcase + end +end diff --git a/app/services/immich/request_photos.rb b/app/services/immich/request_photos.rb index 0f1eabc7..034a6452 100644 --- a/app/services/immich/request_photos.rb +++ b/app/services/immich/request_photos.rb @@ -5,7 +5,7 @@ class Immich::RequestPhotos def initialize(user, start_date: '1970-01-01', end_date: nil) @user = user - @immich_api_base_url = "#{user.settings['immich_url']}/api/search/metadata" + @immich_api_base_url = URI.parse("#{user.settings['immich_url']}/api/search/metadata") @immich_api_key = user.settings['immich_api_key'] @start_date = start_date @end_date = end_date diff --git a/app/services/photoprism/request_photos.rb b/app/services/photoprism/request_photos.rb index 2eb89bab..4cf84810 100644 --- a/app/services/photoprism/request_photos.rb +++ b/app/services/photoprism/request_photos.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true class Photoprism::RequestPhotos - class Error < StandardError; end attr_reader :user, :photoprism_api_base_url, :photoprism_api_key, :start_date, :end_date def initialize(user, start_date: '1970-01-01', end_date: nil) @user = user - @photoprism_api_base_url = "#{user.settings['photoprism_url']}/api/v1/photos" + @photoprism_api_base_url = URI.parse("#{user.settings['photoprism_url']}/api/v1/photos") @photoprism_api_key = user.settings['photoprism_api_key'] @start_date = start_date @end_date = end_date @@ -18,7 +17,9 @@ class Photoprism::RequestPhotos data = retrieve_photoprism_data - time_framed_data(data) + return [] if data[0]['error'].present? + + time_framed_data(data, start_date, end_date) end private @@ -29,15 +30,14 @@ class Photoprism::RequestPhotos while offset < 1_000_000 response_data = fetch_page(offset) - break unless response_data + break if response_data.blank? || response_data[0]['error'].present? data << response_data - break if response_data.empty? offset += 1000 end - data + data.flatten end def fetch_page(offset) @@ -47,7 +47,10 @@ class Photoprism::RequestPhotos query: request_params(offset) ) - raise Error, "Photoprism API returned #{response.code}: #{response.body}" if response.code != 200 + if response.code != 200 + Rails.logger.info "Photoprism API returned #{response.code}: #{response.body}" + Rails.logger.debug "Photoprism API request params: #{request_params(offset).inspect}" + end JSON.parse(response.body) end @@ -71,12 +74,11 @@ class Photoprism::RequestPhotos public: true, quality: 3, after: start_date, - count: 1000, - photo: 'yes' + count: 1000 } end - def time_framed_data(data) + def time_framed_data(data, start_date, end_date) data.flatten.select do |photo| taken_at = DateTime.parse(photo['TakenAtLocal']) end_date ||= Time.current diff --git a/app/services/photos/request.rb b/app/services/photos/request.rb index 7c490651..5e0fe828 100644 --- a/app/services/photos/request.rb +++ b/app/services/photos/request.rb @@ -15,7 +15,7 @@ class Photos::Request photos << request_immich if user.immich_integration_configured? photos << request_photoprism if user.photoprism_integration_configured? - photos + photos.flatten.map { |photo| Api::PhotoSerializer.new(photo).call } end private diff --git a/spec/services/photoprism/request_photos_spec.rb b/spec/services/photoprism/request_photos_spec.rb index fb09fd51..e8bcaaeb 100644 --- a/spec/services/photoprism/request_photos_spec.rb +++ b/spec/services/photoprism/request_photos_spec.rb @@ -201,10 +201,10 @@ RSpec.describe Photoprism::RequestPhotos do .to_return(status: 401, body: 'Unauthorized') end - it 'raises an error' do - expect do - service.call - end.to raise_error(Photoprism::RequestPhotos::Error, 'Photoprism API returned 401: Unauthorized') + it 'logs the error' do + expect(Rails.logger).to receive(:error).with('Photoprism API returned 401: Unauthorized') + + service.call end end