Add device support to geodata importers

This commit is contained in:
Eugene Burmakin 2025-08-08 01:19:59 +02:00
parent a96517caf1
commit a5e07def23
9 changed files with 44 additions and 53 deletions

View file

@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Internal data structure for separate devices in a single user account. - Internal data structure for separate devices in a single user account.
- [ ] Immich and Photoprism integrations should fill all possible fields in points table - [ ] Immich and Photoprism integrations should fill all possible fields in points table
- Geodata from Immich and Photoprism now will also write `tracker_id` to the points table. This will allow to group points by device. It's a good idea to delete your existing imports from Photoprism and Immich and import them again. This will remove existing points and re-import them as long as photos are still available.
- [ ] Add tracker_id index to points table

View file

@ -7,6 +7,7 @@ module PointValidation
Point.where( Point.where(
lonlat: params[:lonlat], lonlat: params[:lonlat],
timestamp: params[:timestamp].to_i, timestamp: params[:timestamp].to_i,
tracker_id: params[:tracker_id],
user_id: user_id:
).exists? ).exists?
end end

View file

@ -56,7 +56,8 @@ class Immich::ImportGeodata
latitude: asset['exifInfo']['latitude'], latitude: asset['exifInfo']['latitude'],
longitude: asset['exifInfo']['longitude'], longitude: asset['exifInfo']['longitude'],
lonlat: "SRID=4326;POINT(#{asset['exifInfo']['longitude']} #{asset['exifInfo']['latitude']})", lonlat: "SRID=4326;POINT(#{asset['exifInfo']['longitude']} #{asset['exifInfo']['latitude']})",
timestamp: Time.zone.parse(asset['exifInfo']['dateTimeOriginal']).to_i timestamp: Time.zone.parse(asset['exifInfo']['dateTimeOriginal']).to_i,
tracker_id: asset['deviceId']
} }
end end

View file

@ -66,7 +66,8 @@ class Photoprism::ImportGeodata
latitude: asset['Lat'], latitude: asset['Lat'],
longitude: asset['Lng'], longitude: asset['Lng'],
lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})", lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})",
timestamp: Time.zone.parse(asset['TakenAt']).to_i timestamp: Time.zone.parse(asset['TakenAt']).to_i,
tracker_id: "#{asset['CameraMake']} #{asset['CameraModel']}"
} }
end end

View file

@ -19,7 +19,7 @@ class Photos::Importer
def create_point(point, index) def create_point(point, index)
return 0 unless valid?(point) return 0 unless valid?(point)
return 0 if point_exists?(point, point['timestamp']) return 0 if point_exists?(point, user_id)
Point.create( Point.create(
lonlat: point['lonlat'], lonlat: point['lonlat'],
@ -28,6 +28,7 @@ class Photos::Importer
timestamp: point['timestamp'].to_i, timestamp: point['timestamp'].to_i,
raw_data: point, raw_data: point,
import_id: import.id, import_id: import.id,
tracker_id: point['tracker_id'],
user_id: user_id:
) )

View file

@ -3,6 +3,7 @@
{ {
"assets": [ "assets": [
{ {
"deviceId": "MyString",
"exifInfo": { "exifInfo": {
"dateTimeOriginal": "2022-12-31T23:17:06.170Z", "dateTimeOriginal": "2022-12-31T23:17:06.170Z",
"latitude": 52.0000, "latitude": 52.0000,
@ -10,6 +11,7 @@
} }
}, },
{ {
"deviceId": "MyString",
"exifInfo": { "exifInfo": {
"dateTimeOriginal": "2022-12-31T23:21:53.140Z", "dateTimeOriginal": "2022-12-31T23:21:53.140Z",
"latitude": 52.0000, "latitude": 52.0000,

View file

@ -104,56 +104,13 @@ RSpec.describe PointValidation do
end end
end end
context 'with integration tests', :db do context 'with point existing in device scope' do
# These tests require a database with PostGIS support let(:existing_point) do
# Only run them if using real database integration create(:point, lonlat: 'POINT(10.5 50.5)', timestamp: Time.now.to_i, tracker_id: '123', user_id: user.id)
let(:existing_timestamp) { 1_650_000_000 }
let(:existing_point_params) do
{
lonlat: 'POINT(10.5 50.5)',
timestamp: existing_timestamp,
user_id: user.id
}
end end
before do it 'returns true' do
# Skip this context if not in integration mode expect(validator.point_exists?(existing_point, user.id)).to be true
skip 'Skipping integration tests' unless ENV['RUN_INTEGRATION_TESTS']
# Create a point in the database
existing_point = Point.create!(
lonlat: "POINT(#{existing_point_params[:longitude]} #{existing_point_params[:latitude]})",
timestamp: existing_timestamp,
user_id: user.id
)
end
it 'returns true when a point with same coordinates and timestamp exists' do
params = {
lonlat: 'POINT(10.5 50.5)',
timestamp: existing_timestamp
}
expect(validator.point_exists?(params, user.id)).to be true
end
it 'returns false when a point with different coordinates exists' do
params = {
lonlat: 'POINT(10.6 50.5)',
timestamp: existing_timestamp
}
expect(validator.point_exists?(params, user.id)).to be false
end
it 'returns false when a point with different timestamp exists' do
params = {
lonlat: 'POINT(10.5 50.5)',
timestamp: existing_timestamp + 1
}
expect(validator.point_exists?(params, user.id)).to be false
end end
end end
end end

View file

@ -4,12 +4,22 @@ require 'rails_helper'
RSpec.describe 'Api::V1::Countries::Borders', type: :request do RSpec.describe 'Api::V1::Countries::Borders', type: :request do
describe 'GET /index' do describe 'GET /index' do
let(:user) { create(:user) }
it 'returns a list of countries with borders' do it 'returns a list of countries with borders' do
get '/api/v1/countries/borders' get '/api/v1/countries/borders', headers: { 'Authorization' => "Bearer #{user.api_key}" }
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response.body).to include('AF') expect(response.body).to include('AF')
expect(response.body).to include('ZW') expect(response.body).to include('ZW')
end end
context 'when user is not authenticated' do
it 'returns http unauthorized' do
get '/api/v1/countries/borders'
expect(response).to have_http_status(:unauthorized)
end
end
end end
end end

View file

@ -45,12 +45,28 @@ RSpec.describe Photos::Importer do
context 'when there are points with the same coordinates' do context 'when there are points with the same coordinates' do
let!(:existing_point) do let!(:existing_point) do
create(:point, lonlat: 'POINT(30.0000 59.0000)', timestamp: 978_296_400, user:, device:) create(:point,
lonlat: 'SRID=4326;POINT(30.0000 59.0000)',
timestamp: 978_296_400,
user: user,
device: device,
tracker_id: nil
)
end end
it 'creates only new points' do it 'creates only new points' do
expect { service }.to change { Point.count }.by(1) expect { service }.to change { Point.count }.by(1)
end end
it 'does not create duplicate points' do
service
points = Point.where(
lonlat: 'SRID=4326;POINT(30.0000 59.0000)',
timestamp: 978_296_400,
user_id: user.id
)
expect(points.count).to eq(1)
end
end end
end end
end end