1
0
Fork 0
timelinize/datasources/googlelocation/recognize.go
Dominik Roszkowski d0cd6f3ce9
googlelocation: add Polish translation for 'Timeline' when recognizing on-device export (#143)
Small change to handle Google Location export when using Polish langauge
2025-10-13 10:22:27 -06:00

143 lines
4.3 KiB
Go

package googlelocation
import (
"encoding/json"
"path"
"strings"
"github.com/timelinize/timelinize/timeline"
)
const filenameFromLegacyTakeout = "Records.json"
// Apparently, filenames exported from Google Maps depend on locale (see issue #111).
// It would be nice if we don't have to open and read from every single JSON file we
// come across in a walk. So for now, we can start making a codex of international
// translations for "Timeline" and other known export filenames. Note: In #111 it
// was reported that the file might be named something different on different devices,
// like "Chronologie.json" on one device, and "Vos trajets.json" on another, despite
// both being set to French. This might be a futile effort, we'll see.
var (
filenameFromiOSDeviceContains = []string{
"location-history", // English (confirmed)
}
filenameFromAndroidDeviceContains = []string{
"Timeline", // English (confirmed)
"Cronología", // Spanish
"Linha do tempo", // Portuguese
"Chronologie", // French (confirmed)
"Vos trajets", // French again! issue #111 (confirmed)
"Zeitleiste", // German
"Tijdlijn", // Dutch
"الجدول الزمني", // Arabic
"时间线", //nolint:gosmopolitan // Chinese
"ไทม์ไลน์", // Thai
"समय", // Hindi
"タイムライン", // Japanese
"타임라인", // Korean (confirmed)
"Хронологія", // Ukrainian
"Хронология", // Russian
"Oś czasu", // Polish
}
)
func (FileImporter) recognizeLegacyTakeoutFormat(dirEntry timeline.DirEntry) timeline.Recognition {
if dirEntry.IsDir() {
// see if it's a Takeout-structured location history (a folder with Records.json in it)
if strings.Contains(dirEntry.Name(), "Location History") && dirEntry.FileExists(filenameFromLegacyTakeout) {
return timeline.Recognition{Confidence: 1}
}
}
return timeline.Recognition{}
}
func (FileImporter) recognizeOnDevice2024iOSFormat(dirEntry timeline.DirEntry) (timeline.Recognition, error) {
// avoid opening all JSON files (can be slow especially in archives)...
if !filenameHasJSONExtAndContains(dirEntry.Name(), filenameFromiOSDeviceContains) {
return timeline.Recognition{}, nil
}
f, err := dirEntry.Open(".")
if err != nil {
return timeline.Recognition{}, err
}
defer f.Close()
dec := json.NewDecoder(f)
if token, err := dec.Token(); err == nil { // read what should be the opening delimiter
if _, ok := token.(json.Delim); ok {
var loc onDeviceLocationiOS2024
if err := dec.Decode(&loc); err == nil {
if !loc.StartTime.IsZero() && !loc.EndTime.IsZero() {
return timeline.Recognition{Confidence: 1}, nil
}
}
}
}
return timeline.Recognition{}, nil
}
func (FileImporter) recognizeOnDevice2025AndroidFormat(dirEntry timeline.DirEntry) (timeline.Recognition, error) {
// avoid opening all JSON files (can be slow especially in archives)...
if !filenameHasJSONExtAndContains(dirEntry.Name(), filenameFromAndroidDeviceContains) {
return timeline.Recognition{}, nil
}
f, err := dirEntry.Open(".")
if err != nil {
return timeline.Recognition{}, err
}
defer f.Close()
dec := json.NewDecoder(f)
// consume the first few tokens until we get to the meat of the file
expect := []func(json.Token) bool{
func(t json.Token) bool {
d, ok := t.(json.Delim)
return ok && d == '{'
},
func(t json.Token) bool {
s, ok := t.(string)
return ok && s == "semanticSegments"
},
func(t json.Token) bool {
d, ok := t.(json.Delim)
return ok && d == '['
},
}
for _, expected := range expect {
token, err := dec.Token()
if err != nil {
return timeline.Recognition{}, nil
}
if !expected(token) {
return timeline.Recognition{}, nil
}
}
// see if the first entry "fits the bill," at least
var loc semanticSegmentAndroid2025
if err := dec.Decode(&loc); err == nil {
if !loc.StartTime.IsZero() && !loc.EndTime.IsZero() {
return timeline.Recognition{Confidence: 1}, nil
}
}
return timeline.Recognition{}, nil
}
// filenameHasJSONExtAndContains returns true if filename has a ".json" extension
// and contains one of the strings in the list.
func filenameHasJSONExtAndContains(filename string, list []string) bool {
if path.Ext(filename) != ".json" {
return false
}
for _, s := range list {
if strings.Contains(filename, s) {
return true
}
}
return false
}