Merge pull request #539 from Eduard-Gimaev/import_google_formats

feat: added google_phone_takeout and google_semantic_histo into impor…
This commit is contained in:
Evgenii Burmakin 2024-12-25 11:54:10 +01:00 committed by GitHub
commit 3644a4f7b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 538 additions and 1222 deletions

3
.gitignore vendored
View file

@ -52,6 +52,9 @@
.DS_Store
.env
.byebug_history
.devcontainer/.onCreateCommandMarker
.devcontainer/.postCreateCommandMarker
.devcontainer/.updateContentCommandMarker

View file

@ -4,9 +4,11 @@ class ImportJob < ApplicationJob
queue_as :imports
def perform(user_id, import_id)
user = User.find(user_id)
import = user.imports.find(import_id)
import.process!
end
end

View file

@ -4,16 +4,19 @@ class Imports::Watcher
class UnsupportedSourceError < StandardError; end
WATCHED_DIR_PATH = Rails.root.join('tmp/imports/watched')
SUPPORTED_FORMATS = %w[.gpx .json .rec].freeze
def call
user_directories.each do |user_email|
user = User.find_by(email: user_email)
next unless user
puts "Processing directory for user: #{user.email}"
user_directory_path = File.join(WATCHED_DIR_PATH, user_email)
file_names = file_names(user_directory_path)
file_names.each do |file_name|
puts "Processing file: #{file_name}"
process_file(user, user_directory_path, file_name)
end
end
@ -36,7 +39,7 @@ class Imports::Watcher
def file_names(directory_path)
Dir.entries(directory_path).select do |file|
['.gpx', '.json'].include?(File.extname(file))
SUPPORTED_FORMATS.include?(File.extname(file))
end
end
@ -50,8 +53,11 @@ class Imports::Watcher
import.raw_data = raw_data(file_path, import.source)
import.save!
puts "Import saved for file: #{file_name}"
ImportJob.perform_later(user.id, import.id)
puts "ImportJob enqueued for user_id: #{user.id}, import_id: #{import.id}"
end
def find_or_initialize_import(user, file_name)
@ -72,9 +78,19 @@ class Imports::Watcher
end
def source(file_name)
case file_name.split('.').last
when 'json' then :geojson
when 'gpx' then :gpx
case file_name.split('.').last.downcase
when 'json'
if file_name.match?(/location-history/i)
:google_phone_takeout
elsif file_name.match?(/Records/i)
:google_records
elsif file_name.match?(/\d{4}_\w+/i)
:google_semantic_history
else
:geojson
end
when 'rec' then :owntracks
when 'gpx' then :gpx
else raise UnsupportedSourceError, 'Unsupported source '
end
end
@ -82,6 +98,15 @@ class Imports::Watcher
def raw_data(file_path, source)
file = File.read(file_path)
source.to_sym == :gpx ? Hash.from_xml(file) : JSON.parse(file)
case source.to_sym
when :gpx
Hash.from_xml(file)
when :json, :geojson, :google_phone_takeout, :google_records, :google_semantic_history
JSON.parse(file)
when :owntracks
OwnTracks::RecParser.new(file).call
else
raise UnsupportedSourceError, "Unsupported source: #{source}"
end
end
end
end

View file

@ -0,0 +1,89 @@
[
{
"endTime": "2023-08-27T17:04:26.999-05:00",
"startTime": "2023-08-27T15:48:56.000-05:00",
"visit": {
"hierarchyLevel": "0",
"topCandidate": {
"probability": "0.785181",
"semanticType": "Unknown",
"placeID": "ChIJxxP_Qwb2aIYRTwDNDLkUmD0",
"placeLocation": "geo:27.720022,-97.347951"
},
"probability": "0.710000"
}
},
{
"endTime": "2023-08-27T22:00:00.000Z",
"startTime": "2023-08-27T20:00:00.000Z",
"timelinePath": [
{
"point": "geo:27.720007,-97.348044",
"durationMinutesOffsetFromStartTime": "49"
}
]
},
{
"endTime": "2023-09-02T23:25:59.000-06:00",
"startTime": "2023-08-27T14:48:56.000-06:00",
"timelineMemory": {
"destinations": [
{
"identifier": "ChIJs9KSYYBfaIYRj5AOiZNQ0a4"
},
{
"identifier": "ChIJw6lCfj2sZ4YRl6q2LNNyojk"
},
{
"identifier": "ChIJA89FstRIAYcRr9I2aBzR89A"
},
{
"identifier": "ChIJtWVg4r5DFIcRr0zkOeDPEfY"
}
],
"distanceFromOriginKms": "1594"
}
},
{
"endTime": "2023-08-28T00:00:00.000Z",
"startTime": "2023-08-27T22:00:00.000Z",
"timelinePath": [
{
"point": "geo:27.701123,-97.362988",
"durationMinutesOffsetFromStartTime": "4"
},
{
"point": "geo:27.701123,-97.362988",
"durationMinutesOffsetFromStartTime": "4"
},
{
"point": "geo:27.687173,-97.363743",
"durationMinutesOffsetFromStartTime": "7"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "10"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "10"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "108"
},
{
"point": "geo:27.696576,-97.376949",
"durationMinutesOffsetFromStartTime": "109"
},
{
"point": "geo:27.709617,-97.375988",
"durationMinutesOffsetFromStartTime": "112"
},
{
"point": "geo:27.709617,-97.375988",
"durationMinutesOffsetFromStartTime": "112"
}
]
}
]

View file

@ -0,0 +1,70 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
14.3439906,
50.0506312
]
},
"properties": {
"timestamp": "2023-01-01T08:00:00Z"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
14.3439906,
50.0506312
]
},
"properties": {
"timestamp": "2023-01-01T10:00:00Z"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
14.42076,
50.08804
]
},
"properties": {
"timestamp": "2023-01-02T12:00:00Z"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
14.42076,
50.08804
]
},
"properties": {
"timestamp": "2023-01-02T14:00:00Z"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
14.42076,
50.08804
]
},
"properties": {
"timestamp": "2023-01-02T16:00:00Z"
}
}
]
}

View file

@ -0,0 +1,40 @@
{
"locations": [
{
"latitudeE7": 533690550,
"longitudeE7": 836950010,
"accuracy": 150,
"source": "UNKNOWN",
"timestamp": "2012-12-15T14:21:29.460Z"
},
{
"latitudeE7": 533563380,
"longitudeE7": 837616500,
"accuracy": 18000,
"source": "UNKNOWN",
"timestamp": "2013-01-04T10:22:43.225Z"
},
{
"latitudeE7": 533690589,
"longitudeE7": 836951347,
"accuracy": 22,
"source": "WIFI",
"deviceTag": 1184882232,
"timestamp": "2013-03-01T05:17:39.849Z"
},
{
"latitudeE7": 533700000,
"longitudeE7": 836960000,
"accuracy": 50,
"source": "GPS",
"timestamp": "2013-04-01T12:00:00.000Z"
},
{
"latitudeE7": 533710000,
"longitudeE7": 836970000,
"accuracy": 30,
"source": "GPS",
"timestamp": "2013-05-01T08:30:00.000Z"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
[
{
"endTime": "2023-08-27T17:04:26.999-05:00",
"startTime": "2023-08-27T15:48:56.000-05:00",
"visit": {
"hierarchyLevel": "0",
"topCandidate": {
"probability": "0.785181",
"semanticType": "Unknown",
"placeID": "ChIJxxP_Qwb2aIYRTwDNDLkUmD0",
"placeLocation": "geo:27.720022,-97.347951"
},
"probability": "0.710000"
}
},
{
"endTime": "2023-08-27T22:00:00.000Z",
"startTime": "2023-08-27T20:00:00.000Z",
"timelinePath": [
{
"point": "geo:27.720007,-97.348044",
"durationMinutesOffsetFromStartTime": "49"
}
]
},
{
"endTime": "2023-09-02T23:25:59.000-06:00",
"startTime": "2023-08-27T14:48:56.000-06:00",
"timelineMemory": {
"destinations": [
{
"identifier": "ChIJs9KSYYBfaIYRj5AOiZNQ0a4"
},
{
"identifier": "ChIJw6lCfj2sZ4YRl6q2LNNyojk"
},
{
"identifier": "ChIJA89FstRIAYcRr9I2aBzR89A"
},
{
"identifier": "ChIJtWVg4r5DFIcRr0zkOeDPEfY"
}
],
"distanceFromOriginKms": "1594"
}
},
{
"endTime": "2023-08-28T00:00:00.000Z",
"startTime": "2023-08-27T22:00:00.000Z",
"timelinePath": [
{
"point": "geo:27.701123,-97.362988",
"durationMinutesOffsetFromStartTime": "4"
},
{
"point": "geo:27.701123,-97.362988",
"durationMinutesOffsetFromStartTime": "4"
},
{
"point": "geo:27.687173,-97.363743",
"durationMinutesOffsetFromStartTime": "7"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "10"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "10"
},
{
"point": "geo:27.686129,-97.381865",
"durationMinutesOffsetFromStartTime": "108"
},
{
"point": "geo:27.696576,-97.376949",
"durationMinutesOffsetFromStartTime": "109"
},
{
"point": "geo:27.709617,-97.375988",
"durationMinutesOffsetFromStartTime": "112"
},
{
"point": "geo:27.709617,-97.375988",
"durationMinutesOffsetFromStartTime": "112"
}
]
}
]

View file

@ -0,0 +1,13 @@
2024-03-01T09:03:09Z * {"bs":2,"p":100.266,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.332,"vel":0,"t":"p","BSSID":"b0:f2:8:45:94:33","SSID":"Home Wifi","conn":"w","vac":4,"acc":10,"tst":1709283789,"lat":52.225,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T17:46:02Z * {"bs":1,"p":100.28,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.333,"t":"p","vel":0,"BSSID":"b0:f2:8:45:94:33","conn":"w","SSID":"Home Wifi","vac":3,"cog":98,"acc":9,"tst":1709315162,"lat":52.226,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T18:26:55Z * {"lon":13.334,"acc":5,"wtst":1696359532,"event":"leave","rid":"5f1d1b","desc":"home","topic":"owntracks/test/iPhone 12 Pro/event","lat":52.227,"t":"c","tst":1709317615,"tid":"RO","_type":"transition","_http":true}
2024-03-01T18:26:55Z * {"cog":40,"batt":85,"lon":13.335,"acc":5,"bs":1,"p":100.279,"vel":3,"vac":3,"lat":52.228,"topic":"owntracks/test/iPhone 12 Pro","t":"c","conn":"m","m":1,"tst":1709317615,"alt":36,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:28:30Z * {"cog":38,"batt":85,"lon":13.336,"acc":5,"bs":1,"p":100.349,"vel":3,"vac":3,"lat":52.229,"topic":"owntracks/test/iPhone 12 Pro","t":"v","conn":"m","m":1,"tst":1709317710,"alt":35,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:33:03Z * {"cog":18,"batt":85,"lon":13.337,"acc":5,"bs":1,"p":100.347,"vel":4,"vac":3,"lat":52.230,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709317983,"alt":36,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.338,"acc":5,"bs":1,"p":100.348,"vel":6,"vac":3,"lat":52.231,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:42:57Z * {"cog":320,"batt":85,"lon":13.339,"acc":5,"bs":1,"p":100.353,"vel":3,"vac":3,"lat":52.232,"topic":"owntracks/test/iPhone 12 Pro","t":"v","conn":"m","m":1,"tst":1709318577,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:08Z lwt {"_type":"lwt","tst":1717459208}
2024-03-01T18:40:09Z waypoints {"_type":"waypoint","desc":"Home","lat":52.232,"lon":13.339,"rad":50,"tst":1717459768}
2024-03-01T18:40:10Z event {"_type":"transition","acc":5,"desc":"Home","event":"enter","lat":52.232,"lon":13.339,"t":"l","tid":"s8","tst":1717460098,"wtst":1717459768}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.338,"acc":5,"bs":1,"p":100.348,"vel":6,"vac":3,"lat":52.231,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:40:11Z * {"cog":43,"batt":85,"lon":13.341,"acc":5,"bs":1,"p":100.348,"created_at":1709318940,"vel":6,"vac":3,"lat":52.234,"topic":"owntracks/test/iPhone 12 Pro","conn":"m","m":1,"tst":1709318411,"alt":37,"_type":"location","tid":"RO","_http":true}

View file

@ -11,8 +11,11 @@ RSpec.describe Imports::Watcher do
before do
stub_const('Imports::Watcher::WATCHED_DIR_PATH', watched_dir_path)
Sidekiq::Testing.inline!
end
after { Sidekiq::Testing.fake! }
context 'when there are no files in the watched directory' do
it 'does not call ImportJob' do
expect(ImportJob).not_to receive(:perform_later)
@ -22,26 +25,34 @@ RSpec.describe Imports::Watcher do
end
context 'when there are files in the watched directory' do
Sidekiq::Testing.inline! do
context 'when the file has a valid user email' do
it 'creates an import for the user' do
expect { service }.to change(user.imports, :count).by(2)
end
context 'when the file has a valid user email' do
it 'creates an import for the user' do
expect { service }.to change(user.imports, :count).by(6)
end
context 'when the file has an invalid user email' do
it 'does not create an import' do
expect { service }.not_to change(Import, :count)
end
it 'creates points for the user' do
initial_point_count = Point.count
service
expect(Point.count).to be > initial_point_count
end
end
context 'when the import already exists' do
it 'does not create a new import' do
create(:import, user:, name: 'export_same_points.json')
create(:import, user:, name: 'gpx_track_single_segment.gpx')
context 'when the file has an invalid user email' do
it 'does not create an import' do
expect { service }.not_to change(Import, :count)
end
end
expect { service }.not_to change(Import, :count)
end
context 'when the import already exists' do
it 'does not create a new import' do
create(:import, user:, name: '2023_January.json')
create(:import, user:, name: 'export_same_points.json')
create(:import, user:, name: 'gpx_track_single_segment.gpx')
create(:import, user:, name: 'location-history.json')
create(:import, user:, name: 'owntracks.rec')
create(:import, user:, name: 'Records.json')
expect { service }.not_to change(Import, :count)
end
end
end