mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Metadata: Use "Local" as identifier for the local time zone #4946
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -174,6 +174,10 @@ export default class Config {
|
||||
$event.publish("dialog.update", { values });
|
||||
}
|
||||
|
||||
if (values.DefaultLocale && options.DefaultLocale !== values.DefaultLocale) {
|
||||
options.SetDefaultLocale(values.DefaultLocale);
|
||||
}
|
||||
|
||||
for (let key in values) {
|
||||
if (values.hasOwnProperty(key) && values[key] != null) {
|
||||
this.set(key, values[key]);
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Locale = () => {
|
||||
export let Options = [
|
||||
{
|
||||
text: "English", // English
|
||||
translated: "English",
|
||||
value: "en",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ export const YearUnknown = -1;
|
||||
export const MonthUnknown = -1;
|
||||
export const DayUnknown = -1;
|
||||
export const TimeZoneUTC = "UTC";
|
||||
export const TimeZoneLocal = "Local";
|
||||
|
||||
export let BatchSize = 156;
|
||||
|
||||
@@ -788,8 +789,8 @@ export class Photo extends RestModel {
|
||||
month: formats.long,
|
||||
year: formats.num,
|
||||
});
|
||||
} else if (timeZone) {
|
||||
return this.localDate().toLocaleString(showTimeZone ? formats.DATE_FULL_TZ : formats.DATE_FULL);
|
||||
} else if (showTimeZone && timeZone && timeZone !== TimeZoneLocal) {
|
||||
return this.localDate().toLocaleString(formats.DATE_FULL_TZ);
|
||||
}
|
||||
|
||||
return this.localDate().toLocaleString(DateTime.DATE_HUGE);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const GmtOffsets = [
|
||||
|
||||
export const TimeZones = (defaultName) =>
|
||||
[
|
||||
{ ID: "", Name: defaultName ? defaultName : $gettext("Local Time") },
|
||||
{ ID: "Local", Name: defaultName ? defaultName : $gettext("Local") },
|
||||
{ ID: "UTC", Name: "UTC" },
|
||||
]
|
||||
.concat(timeZonesNames)
|
||||
@@ -114,13 +114,34 @@ export const MonthsShort = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// Specifies the default language locale.
|
||||
export let DefaultLocale = "en";
|
||||
|
||||
// Change the default language locale.
|
||||
export const SetDefaultLocale = (locale) => {
|
||||
if (!locale || locale === DefaultLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
DefaultLocale = FindLocale(locale);
|
||||
};
|
||||
|
||||
// Available locales sorted by region and alphabet.
|
||||
export const Languages = () => (window.__LOCALES__ ? window.__LOCALES__ : locales.Options);
|
||||
|
||||
// Returns the language name (text) and locale (value) to use when no other choice is available.
|
||||
export const FallbackLanguage = () => {
|
||||
if (locales?.Options?.length > 0) {
|
||||
return locales.Options[0];
|
||||
}
|
||||
|
||||
return { text: "English", value: "en" };
|
||||
};
|
||||
|
||||
// Finds the best matching language by locale.
|
||||
export const FindLanguage = (locale) => {
|
||||
if (!locale || locale.length < 2) {
|
||||
return null;
|
||||
locale = DefaultLocale;
|
||||
}
|
||||
|
||||
let found;
|
||||
@@ -144,8 +165,10 @@ export const FindLanguage = (locale) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Specifies the default language locale string.
|
||||
export var DefaultLocale = "en";
|
||||
// Returns the fallback locale to use when no other choice is available.
|
||||
export const FallbackLocale = () => {
|
||||
return FallbackLanguage().value;
|
||||
};
|
||||
|
||||
// Finds the best matching language locale based on the specified locale;
|
||||
export const FindLocale = (locale) => {
|
||||
@@ -159,8 +182,9 @@ export const FindLocale = (locale) => {
|
||||
return language.value;
|
||||
}
|
||||
|
||||
return DefaultLocale;
|
||||
return FallbackLocale();
|
||||
};
|
||||
|
||||
export const ItemsPerPage = () => [
|
||||
{ text: "10", title: "10", value: 10 },
|
||||
{ text: "20", title: "20", value: 20 },
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
tabindex="2"
|
||||
item-value="ID"
|
||||
item-title="Name"
|
||||
:items="options.TimeZones($gettext('Default'))"
|
||||
:items="options.TimeZones($gettext('Local'))"
|
||||
:label="$gettext('Time Zone')"
|
||||
:menu-props="{ maxHeight: 346 }"
|
||||
class="input-timezone"
|
||||
|
||||
@@ -39,7 +39,6 @@ type ClientConfig struct {
|
||||
ApiUri string `json:"apiUri"`
|
||||
ContentUri string `json:"contentUri"`
|
||||
VideoUri string `json:"videoUri"`
|
||||
WallpaperUri string `json:"wallpaperUri"`
|
||||
SiteUrl string `json:"siteUrl"`
|
||||
SiteDomain string `json:"siteDomain"`
|
||||
SiteAuthor string `json:"siteAuthor"`
|
||||
@@ -53,6 +52,9 @@ type ClientConfig struct {
|
||||
AppMode string `json:"appMode"`
|
||||
AppIcon string `json:"appIcon"`
|
||||
AppColor string `json:"appColor"`
|
||||
DefaultLocale string `json:"defaultLocale"`
|
||||
DefaultTimezone string `json:"defaultTimezone"`
|
||||
WallpaperUri string `json:"wallpaperUri"`
|
||||
Restart bool `json:"restart"`
|
||||
Debug bool `json:"debug"`
|
||||
Trace bool `json:"trace"`
|
||||
@@ -295,6 +297,8 @@ func (c *Config) ClientPublic() *ClientConfig {
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
DefaultLocale: c.DefaultLocale(),
|
||||
DefaultTimezone: c.DefaultTimezone().String(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
@@ -388,6 +392,8 @@ func (c *Config) ClientShare() *ClientConfig {
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
DefaultLocale: c.DefaultLocale(),
|
||||
DefaultTimezone: c.DefaultTimezone().String(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
@@ -489,6 +495,8 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
DefaultLocale: c.DefaultLocale(),
|
||||
DefaultTimezone: c.DefaultTimezone().String(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
|
||||
@@ -23,12 +23,12 @@ func (c *Config) DefaultLocale() string {
|
||||
// DefaultTimezone returns the default time zone, e.g. for scheduling backups
|
||||
func (c *Config) DefaultTimezone() *time.Location {
|
||||
if c.options.DefaultTimezone == "" {
|
||||
return time.UTC
|
||||
return time.Local
|
||||
}
|
||||
|
||||
// Returns time zone if a valid identifier name was provided and UTC otherwise.
|
||||
if timeZone, err := time.LoadLocation(c.options.DefaultTimezone); err != nil {
|
||||
return time.UTC
|
||||
if timeZone, err := time.LoadLocation(c.options.DefaultTimezone); err != nil || timeZone == nil {
|
||||
return time.Local
|
||||
} else {
|
||||
return timeZone
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ var Flags = CliFlags{
|
||||
Name: "default-timezone",
|
||||
Aliases: []string{"tz"},
|
||||
Usage: "default time zone `NAME`, e.g. for scheduling backups",
|
||||
Value: time.UTC.String(),
|
||||
Value: time.Local.String(),
|
||||
EnvVars: EnvVars("DEFAULT_TIMEZONE"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
|
||||
@@ -213,4 +213,10 @@ var DialectMySQL = Migrations{
|
||||
Stage: "pre",
|
||||
Statements: []string{"ALTER TABLE auth_users_settings CHANGE COLUMN IF EXISTS default_page ui_start_page VARCHAR(64) DEFAULT 'default';"},
|
||||
},
|
||||
{
|
||||
ID: "20250416-000001",
|
||||
Dialect: "mysql",
|
||||
Stage: "main",
|
||||
Statements: []string{"UPDATE photos SET time_zone = 'Local' WHERE time_zone = '' OR time_zone IS NULL;"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -129,4 +129,10 @@ var DialectSQLite3 = Migrations{
|
||||
Stage: "pre",
|
||||
Statements: []string{"ALTER TABLE auth_users_settings RENAME COLUMN default_page TO ui_start_page;"},
|
||||
},
|
||||
{
|
||||
ID: "20250416-000001",
|
||||
Dialect: "sqlite3",
|
||||
Stage: "main",
|
||||
Statements: []string{"UPDATE photos SET time_zone = 'Local' WHERE time_zone = '' OR time_zone IS NULL;"},
|
||||
},
|
||||
}
|
||||
|
||||
1
internal/entity/migrate/mysql/20250416-000001.sql
Normal file
1
internal/entity/migrate/mysql/20250416-000001.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE photos SET time_zone = 'Local' WHERE time_zone = '' OR time_zone IS NULL;
|
||||
1
internal/entity/migrate/sqlite3/20250416-000001.sql
Normal file
1
internal/entity/migrate/sqlite3/20250416-000001.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE photos SET time_zone = 'Local' WHERE time_zone = '' OR time_zone IS NULL;
|
||||
@@ -61,7 +61,7 @@ type Photo struct {
|
||||
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
||||
PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"`
|
||||
PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"`
|
||||
TimeZone string `gorm:"type:VARBINARY(64);" json:"TimeZone" yaml:"TimeZone,omitempty"`
|
||||
TimeZone string `gorm:"type:VARBINARY(64);default:'Local'" json:"TimeZone" yaml:"TimeZone,omitempty"`
|
||||
PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID" yaml:"-"`
|
||||
PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"`
|
||||
CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"-"`
|
||||
|
||||
@@ -13,7 +13,7 @@ func (m *Photo) TrustedTime() bool {
|
||||
return false
|
||||
} else if m.TakenAt.IsZero() || m.TakenAtLocal.IsZero() {
|
||||
return false
|
||||
} else if m.TimeZone == "" {
|
||||
} else if m.TimeZone == "" || m.TimeZone == time.Local.String() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,37 +21,50 @@ func (m *Photo) TrustedTime() bool {
|
||||
}
|
||||
|
||||
// SetTakenAt changes the photo date if not empty and from the same source.
|
||||
func (m *Photo) SetTakenAt(taken, local time.Time, zone, source string) {
|
||||
if taken.IsZero() || taken.Year() < 1000 || taken.Year() > txt.YearMax {
|
||||
func (m *Photo) SetTakenAt(utc, local time.Time, zone, source string) {
|
||||
if utc.IsZero() || utc.Year() < 1000 || utc.Year() > txt.YearMax {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent the existing time from being overwritten by lower priority sources.
|
||||
if SrcPriority[source] < SrcPriority[m.TakenSrc] && !m.TakenAt.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove time zone if time was extracted from file name.
|
||||
if source == SrcName {
|
||||
zone = ""
|
||||
}
|
||||
|
||||
// Round times to avoid jitter.
|
||||
taken = taken.UTC().Truncate(time.Second)
|
||||
utc = utc.UTC().Truncate(time.Second)
|
||||
|
||||
// Default local time to taken if zero or invalid.
|
||||
if local.IsZero() || local.Year() < 1000 {
|
||||
local = taken
|
||||
local = utc
|
||||
} else {
|
||||
local = local.Truncate(time.Second)
|
||||
}
|
||||
|
||||
// If no zone is specified, assume the current zone or try to determine
|
||||
// the time zone based on the time offset. Otherwise, default to Local.
|
||||
if source == SrcName && m.TimeZone == "" {
|
||||
// Assume Local timezone if the time was extracted from a filename.
|
||||
zone = time.Local.String()
|
||||
} else if zone == "" {
|
||||
if m.TimeZone != "" {
|
||||
zone = m.TimeZone
|
||||
} else if !utc.Equal(local) {
|
||||
zone = txt.UtcOffset(utc, local, "")
|
||||
}
|
||||
|
||||
if zone == "" {
|
||||
zone = time.Local.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Don't update older date.
|
||||
if SrcPriority[source] <= SrcPriority[SrcAuto] && !m.TakenAt.IsZero() && taken.After(m.TakenAt) {
|
||||
if SrcPriority[source] <= SrcPriority[SrcAuto] && !m.TakenAt.IsZero() && utc.After(m.TakenAt) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set UTC time and date source.
|
||||
m.TakenAt = taken
|
||||
m.TakenAt = utc
|
||||
m.TakenAtLocal = local
|
||||
m.TakenSrc = source
|
||||
|
||||
@@ -66,7 +79,7 @@ func (m *Photo) SetTakenAt(taken, local time.Time, zone, source string) {
|
||||
m.TimeZone = zone
|
||||
// Keep UTC?
|
||||
if m.TimeZoneUTC() {
|
||||
m.TakenAtLocal = taken
|
||||
m.TakenAtLocal = utc
|
||||
}
|
||||
} else if m.TimeZone != "" {
|
||||
// Apply existing time zone.
|
||||
@@ -87,7 +100,7 @@ func (m *Photo) UpdateTimeZone(zone string) {
|
||||
return
|
||||
}
|
||||
|
||||
if SrcPriority[m.TakenSrc] >= SrcPriority[SrcManual] && m.TimeZone != "" {
|
||||
if SrcPriority[m.TakenSrc] >= SrcPriority[SrcManual] && m.TimeZone != "" && m.TimeZone != time.Local.String() {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestPhoto_TrustedTime(t *testing.T) {
|
||||
})
|
||||
t.Run("MissingTimeZone", func(t *testing.T) {
|
||||
n := Now()
|
||||
m := Photo{ID: 1, TakenAt: n, TakenAtLocal: n, TakenSrc: SrcMeta, TimeZone: ""}
|
||||
m := Photo{ID: 1, TakenAt: n, TakenAtLocal: n, TakenSrc: SrcMeta, TimeZone: time.Local.String()}
|
||||
assert.False(t, m.TrustedTime())
|
||||
})
|
||||
t.Run("SrcAuto", func(t *testing.T) {
|
||||
@@ -56,21 +56,21 @@ func TestPhoto_SetTakenAt(t *testing.T) {
|
||||
})
|
||||
t.Run("FromName", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
m.TimeZone = ""
|
||||
m.TimeZone = time.Local.String()
|
||||
m.TakenSrc = SrcAuto
|
||||
|
||||
assert.Equal(t, time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC), m.TakenAt)
|
||||
assert.Equal(t, time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC), m.TakenAtLocal)
|
||||
assert.Equal(t, "", m.TimeZone)
|
||||
assert.Equal(t, "Local", m.TimeZone)
|
||||
assert.Equal(t, SrcAuto, m.TakenSrc)
|
||||
|
||||
m.SetTakenAt(time.Date(2011, 12, 11, 9, 7, 18, 0, time.UTC),
|
||||
time.Date(2019, 11, 11, 10, 7, 18, 0, time.UTC), "America/New_York", SrcName)
|
||||
|
||||
assert.Equal(t, "", m.TimeZone)
|
||||
assert.Equal(t, "America/New_York", m.TimeZone)
|
||||
assert.Equal(t, SrcName, m.TakenSrc)
|
||||
|
||||
assert.Equal(t, time.Date(2011, 12, 11, 9, 7, 18, 0, time.UTC), m.TakenAt)
|
||||
assert.Equal(t, time.Date(2019, 11, 11, 15, 7, 18, 0, time.UTC), m.TakenAt)
|
||||
assert.Equal(t, time.Date(2019, 11, 11, 10, 7, 18, 0, time.UTC), m.TakenAtLocal)
|
||||
})
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
@@ -81,6 +81,7 @@ func TestPhoto_SetTakenAt(t *testing.T) {
|
||||
m.SetTakenAt(time.Date(2019, 12, 11, 9, 7, 18, 0, time.UTC),
|
||||
time.Date(2019, 12, 11, 10, 7, 18, 0, time.UTC), "", SrcMeta)
|
||||
|
||||
assert.Equal(t, "Europe/Berlin", m.TimeZone)
|
||||
assert.Equal(t, time.Date(2019, 12, 11, 9, 7, 18, 0, time.UTC), m.TakenAt)
|
||||
assert.Equal(t, time.Date(2019, 12, 11, 10, 7, 18, 0, time.UTC), m.TakenAtLocal)
|
||||
})
|
||||
@@ -99,30 +100,32 @@ func TestPhoto_SetTakenAt(t *testing.T) {
|
||||
assert.Equal(t, time.Date(2013, time.November, 11, 9, 7, 18, 0, time.UTC), m.TakenAt)
|
||||
assert.Equal(t, time.Date(2013, time.November, 11, 9, 7, 18, 0, time.UTC), m.TakenAtLocal)
|
||||
|
||||
newTime := time.Date(2013, time.November, 11, 9, 7, 18, 0, time.UTC)
|
||||
|
||||
expected := time.Date(2013, time.November, 11, 8, 7, 18, 0, time.UTC)
|
||||
utcTime := time.Date(2013, time.November, 11, 8, 7, 18, 0, time.UTC)
|
||||
localTime := time.Date(2013, time.November, 11, 9, 7, 18, 0, time.UTC)
|
||||
|
||||
m.TimeZone = "Europe/Berlin"
|
||||
|
||||
m.SetTakenAt(newTime, newTime, "", SrcName)
|
||||
m.SetTakenAt(utcTime, localTime, "", SrcName)
|
||||
|
||||
assert.Equal(t, expected, m.TakenAt)
|
||||
assert.Equal(t, m.TimeZone, m.TimeZone)
|
||||
assert.Equal(t, utcTime, m.TakenAt)
|
||||
assert.Equal(t, m.GetTakenAt(), m.TakenAt)
|
||||
assert.Equal(t, localTime, m.TakenAtLocal)
|
||||
assert.Equal(t, m.GetTakenAtLocal(), m.TakenAtLocal)
|
||||
})
|
||||
t.Run("TimeZone", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
|
||||
zone := "Europe/Berlin"
|
||||
timeZone := "Europe/Berlin"
|
||||
loc := txt.TimeZone(timeZone)
|
||||
utcTime := time.Date(2013, 11, 11, 9, 7, 18, 0, loc)
|
||||
localTime := utcTime
|
||||
|
||||
loc := txt.TimeZone(zone)
|
||||
m.SetTakenAt(utcTime, localTime, timeZone, SrcName)
|
||||
|
||||
newTime := time.Date(2013, 11, 11, 9, 7, 18, 0, loc)
|
||||
|
||||
m.SetTakenAt(newTime, newTime, zone, SrcName)
|
||||
|
||||
assert.Equal(t, newTime.UTC(), m.TakenAt)
|
||||
assert.Equal(t, newTime, m.TakenAtLocal)
|
||||
assert.Equal(t, timeZone, m.TimeZone)
|
||||
assert.Equal(t, utcTime.UTC(), m.TakenAt)
|
||||
assert.Equal(t, localTime, m.TakenAtLocal)
|
||||
})
|
||||
t.Run("InvalidYear", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
@@ -167,7 +170,7 @@ func TestPhoto_SetTakenAt(t *testing.T) {
|
||||
photo.SetTakenAt(time.Date(2014, 12, 11, 9, 7, 18, 0, time.UTC),
|
||||
time.Date(2014, 12, 11, 10, 7, 18, 0, time.UTC), "", SrcManual)
|
||||
assert.Equal(t, time.Date(2014, 12, 11, 9, 7, 18, 0, time.UTC), photo.TakenAt)
|
||||
assert.Equal(t, time.Date(2014, 12, 11, 10, 07, 18, 0, time.UTC), photo.TakenAtLocal)
|
||||
assert.Equal(t, time.Date(2014, 12, 11, 9, 7, 18, 0, time.UTC), photo.TakenAtLocal)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,7 +182,8 @@ func TestPhoto_UpdateTimeZone(t *testing.T) {
|
||||
takenJerusalemUtc := time.Date(2015, time.May, 17, 20, 2, 46, 0, time.UTC)
|
||||
takenShanghaiUtc := time.Date(2015, time.May, 17, 15, 2, 46, 0, time.UTC)
|
||||
|
||||
assert.Equal(t, "", m.TimeZone)
|
||||
assert.Equal(t, "Local", m.TimeZone)
|
||||
assert.Equal(t, time.Local.String(), m.TimeZone)
|
||||
assert.Equal(t, takenLocal, m.TakenAt)
|
||||
assert.Equal(t, takenLocal, m.TakenAtLocal)
|
||||
|
||||
@@ -275,19 +279,20 @@ func TestPhoto_UpdateTimeZone(t *testing.T) {
|
||||
t.Run("Europe/Berlin", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo12")
|
||||
|
||||
zone := "Europe/Berlin"
|
||||
|
||||
takenAt := m.TakenAt
|
||||
takenAtLocal := m.TakenAtLocal
|
||||
|
||||
assert.Equal(t, "Local", m.TimeZone)
|
||||
assert.Equal(t, takenAt, m.TakenAt)
|
||||
assert.Equal(t, takenAtLocal, m.TakenAtLocal)
|
||||
assert.Equal(t, "", m.TimeZone)
|
||||
|
||||
m.UpdateTimeZone(zone)
|
||||
timeZone := "Europe/Berlin"
|
||||
m.UpdateTimeZone(timeZone)
|
||||
|
||||
assert.Equal(t, timeZone, m.TimeZone)
|
||||
assert.Equal(t, m.GetTakenAt(), m.TakenAt)
|
||||
assert.Equal(t, takenAtLocal, m.TakenAtLocal)
|
||||
assert.Equal(t, m.GetTakenAtLocal(), m.TakenAtLocal)
|
||||
})
|
||||
|
||||
t.Run("America/New_York", func(t *testing.T) {
|
||||
|
||||
@@ -175,7 +175,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -235,7 +235,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: CellFixtures.Pointer("caravan park").Place,
|
||||
PlaceID: CellFixtures.Pointer("caravan park").Place.ID,
|
||||
PlaceSrc: "meta",
|
||||
@@ -421,7 +421,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: true,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -480,7 +480,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -717,7 +717,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: PlaceFixtures.Pointer("emptyNameLongCity"),
|
||||
PlaceID: PlaceFixtures.Pointer("emptyNameLongCity").ID,
|
||||
PlaceSrc: "",
|
||||
@@ -776,7 +776,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: PlaceFixtures.Pointer("emptyNameShortCity"),
|
||||
PlaceID: PlaceFixtures.Pointer("emptyNameShortCity").ID,
|
||||
PlaceSrc: "",
|
||||
@@ -1012,7 +1012,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: true,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -1071,7 +1071,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
@@ -1132,7 +1132,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
@@ -1194,7 +1194,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -1253,7 +1253,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
@@ -1371,7 +1371,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
@@ -1737,7 +1737,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: SrcAuto,
|
||||
@@ -3558,7 +3558,7 @@ var PhotoFixtures = PhotoMap{
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
|
||||
@@ -295,14 +295,15 @@ func (m *Photo) UnknownCountry() bool {
|
||||
return m.CountryCode() == UnknownCountry.ID
|
||||
}
|
||||
|
||||
// GetTimeZone uses PhotoLat and PhotoLng to guess the time zone of the photo.
|
||||
func (m *Photo) GetTimeZone() string {
|
||||
result := "UTC"
|
||||
// LocationTimeZone uses the GPS location to determine the time zone of the photo,
|
||||
// and returns "Local" if no location is set or no time zone is found.
|
||||
func (m *Photo) LocationTimeZone() string {
|
||||
result := time.Local.String()
|
||||
|
||||
if m.HasLatLng() {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lat: float64(m.PhotoLat),
|
||||
Lon: float64(m.PhotoLng),
|
||||
Lat: m.PhotoLat,
|
||||
Lon: m.PhotoLng,
|
||||
})
|
||||
|
||||
if err == nil && len(zones) > 0 {
|
||||
@@ -388,7 +389,8 @@ func (m *Photo) UpdateLocation() (keywords []string, labels classify.Labels) {
|
||||
m.PhotoCountry = loc.CountryCode()
|
||||
|
||||
if changed && m.TakenSrc != SrcManual {
|
||||
m.UpdateTimeZone(m.GetTimeZone())
|
||||
// Use GPS location to determine time zone.
|
||||
m.UpdateTimeZone(m.LocationTimeZone())
|
||||
}
|
||||
|
||||
FirstOrCreateCountry(NewCountry(loc.CountryCode(), loc.CountryName()))
|
||||
|
||||
@@ -343,12 +343,12 @@ func TestPhoto_HasPlace(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_GetTimeZone(t *testing.T) {
|
||||
func TestPhoto_LocationTimeZone(t *testing.T) {
|
||||
m := Photo{}
|
||||
m.PhotoLat = 48.533905555
|
||||
m.PhotoLng = 9.01
|
||||
|
||||
result := m.GetTimeZone()
|
||||
result := m.LocationTimeZone()
|
||||
|
||||
if result != "Europe/Berlin" {
|
||||
t.Fatalf("time zone should be Europe/Berlin: %s", result)
|
||||
@@ -361,7 +361,7 @@ func TestPhoto_GetTakenAt(t *testing.T) {
|
||||
m.PhotoLng = 9.01
|
||||
m.TakenAt, _ = time.Parse(time.RFC3339, "2020-02-04T11:54:34Z")
|
||||
m.TakenAtLocal, _ = time.Parse(time.RFC3339, "2020-02-04T11:54:34Z")
|
||||
m.TimeZone = m.GetTimeZone()
|
||||
m.TimeZone = m.LocationTimeZone()
|
||||
|
||||
if m.TimeZone != "Europe/Berlin" {
|
||||
t.Fatalf("time zone should be Europe/Berlin: %s", m.TimeZone)
|
||||
|
||||
@@ -311,7 +311,7 @@ func TestPhotosResults_Merged(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -371,7 +371,7 @@ func TestPhotosResults_Merged(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -442,7 +442,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "123",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -502,7 +502,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "456",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -570,7 +570,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
||||
TakenAt: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "uid123",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -634,7 +634,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
||||
TakenAt: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "uid123",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -699,7 +699,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
||||
TakenAt: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "uid123",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestPhotoResults_ViewerJSON(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "123",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
@@ -76,7 +76,7 @@ func TestPhotoResults_ViewerJSON(t *testing.T) {
|
||||
TakenAt: time.Time{},
|
||||
TakenAtLocal: time.Time{},
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
TimeZone: "Local",
|
||||
PhotoUID: "456",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
|
||||
@@ -283,7 +283,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
||||
// Set UTC offset as time zone?
|
||||
if data.TimeZone != "" && data.TimeZone != "UTC" || data.TakenAt.IsZero() {
|
||||
// Don't change existing time zone.
|
||||
} else if utcOffset := txt.UtcOffset(data.TakenAtLocal, data.TakenAt, data.TimeOffset); utcOffset != "" {
|
||||
} else if utcOffset := txt.UtcOffset(data.TakenAt, data.TakenAtLocal, data.TimeOffset); utcOffset != "" {
|
||||
data.TimeZone = utcOffset
|
||||
log.Infof("metadata: %s has time offset %s (exiftool)", logName, clean.Log(utcOffset))
|
||||
} else if data.TimeOffset != "" {
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
|
||||
// TimeZone returns a time zone for the given UTC offset string.
|
||||
func TimeZone(offset string) *time.Location {
|
||||
if offset == "" || strings.EqualFold(offset, "local") {
|
||||
// Local time.
|
||||
if offset == "" || offset == time.Local.String() {
|
||||
// Local Time.
|
||||
return time.Local
|
||||
} else if offset == "UTC" || offset == "Z" {
|
||||
// Coordinated Universal Time.
|
||||
return time.UTC
|
||||
} else if seconds, err := TimeOffset(offset); err == nil {
|
||||
if h := seconds / 3600; h > 0 || h < 0 {
|
||||
@@ -21,7 +23,7 @@ func TimeZone(offset string) *time.Location {
|
||||
return zone
|
||||
}
|
||||
|
||||
return time.FixedZone("", 0)
|
||||
return time.FixedZone("GMT", 0)
|
||||
}
|
||||
|
||||
// NormalizeUtcOffset returns a normalized UTC time offset string.
|
||||
@@ -89,7 +91,7 @@ func NormalizeUtcOffset(s string) string {
|
||||
}
|
||||
|
||||
// UtcOffset returns the time difference as UTC offset string.
|
||||
func UtcOffset(local, utc time.Time, offset string) string {
|
||||
func UtcOffset(utc, local time.Time, offset string) string {
|
||||
if offset = NormalizeUtcOffset(offset); offset != "" {
|
||||
return offset
|
||||
}
|
||||
@@ -116,6 +118,8 @@ func UtcOffset(local, utc time.Time, offset string) string {
|
||||
// TimeOffset returns the UTC time offset in seconds or an error if it is invalid.
|
||||
func TimeOffset(utcOffset string) (seconds int, err error) {
|
||||
switch utcOffset {
|
||||
case "Z", "GMT", "Etc/GMT", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00":
|
||||
seconds = 0
|
||||
case "-12", "-12:00", "UTC-12", "UTC-12:00":
|
||||
seconds = -12 * 3600
|
||||
case "-11", "-11:00", "UTC-11", "UTC-11:00":
|
||||
@@ -164,8 +168,6 @@ func TimeOffset(utcOffset string) (seconds int, err error) {
|
||||
seconds = 11 * 3600
|
||||
case "12:00", "+12", "+12:00", "UTC+12", "UTC+12:00":
|
||||
seconds = 12 * 3600
|
||||
case "Z", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00":
|
||||
seconds = 0
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid UTC offset")
|
||||
}
|
||||
|
||||
@@ -12,15 +12,18 @@ func TestTimeZone(t *testing.T) {
|
||||
assert.Equal(t, time.UTC.String(), TimeZone(time.UTC.String()).String())
|
||||
assert.Equal(t, time.UTC.String(), TimeZone("Z").String())
|
||||
assert.Equal(t, time.UTC.String(), TimeZone("UTC").String())
|
||||
assert.Equal(t, time.UTC, TimeZone("UTC"))
|
||||
})
|
||||
t.Run("LocalTime", func(t *testing.T) {
|
||||
assert.Equal(t, "", TimeZone("").String())
|
||||
assert.Equal(t, "", TimeZone("local").String())
|
||||
assert.Equal(t, "", TimeZone("Local").String())
|
||||
assert.Equal(t, "", TimeZone("LOCAL").String())
|
||||
assert.Equal(t, "", TimeZone("0").String())
|
||||
assert.Equal(t, "", TimeZone("UTC+0").String())
|
||||
assert.Equal(t, "", TimeZone("UTC+00:00").String())
|
||||
t.Run("GMT", func(t *testing.T) {
|
||||
assert.Equal(t, "GMT", TimeZone("0").String())
|
||||
assert.Equal(t, "GMT", TimeZone("UTC+0").String())
|
||||
assert.Equal(t, "GMT", TimeZone("UTC+00:00").String())
|
||||
})
|
||||
t.Run("Local", func(t *testing.T) {
|
||||
assert.Equal(t, "Local", TimeZone("").String())
|
||||
assert.Equal(t, time.Local, TimeZone(""))
|
||||
assert.Equal(t, "Local", TimeZone("Local").String())
|
||||
assert.Equal(t, time.Local, TimeZone("Local"))
|
||||
})
|
||||
t.Run("UTC+2", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:20:17 +00:00")
|
||||
@@ -35,7 +38,7 @@ func TestTimeZone(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
timeZone := UtcOffset(local, utc, "")
|
||||
timeZone := UtcOffset(utc, local, "")
|
||||
|
||||
assert.Equal(t, "UTC+2", timeZone)
|
||||
|
||||
@@ -90,7 +93,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, ""))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, ""))
|
||||
})
|
||||
t.Run("UTC", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:20:17 +00:00")
|
||||
@@ -105,9 +108,9 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, "00:00"))
|
||||
assert.Equal(t, "", UtcOffset(local, utc, "+00:00"))
|
||||
assert.Equal(t, "UTC", UtcOffset(local, utc, "Z"))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, "00:00"))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, "+00:00"))
|
||||
assert.Equal(t, "UTC", UtcOffset(utc, local, "Z"))
|
||||
})
|
||||
t.Run("UTC+2", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:20:17 +00:00")
|
||||
@@ -122,7 +125,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
timeZone := UtcOffset(local, utc, "")
|
||||
timeZone := UtcOffset(utc, local, "")
|
||||
|
||||
assert.Equal(t, "UTC+2", timeZone)
|
||||
|
||||
@@ -143,7 +146,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "UTC+2", UtcOffset(local, utc, "02:00"))
|
||||
assert.Equal(t, "UTC+2", UtcOffset(utc, local, "02:00"))
|
||||
})
|
||||
t.Run("UTC+2.5", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:50:17 +00:00")
|
||||
@@ -158,7 +161,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, ""))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, ""))
|
||||
})
|
||||
t.Run("+02:30", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:50:17 +00:00")
|
||||
@@ -173,7 +176,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, "+02:30"))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, "+02:30"))
|
||||
})
|
||||
t.Run("UTC-14", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 00:20:17 +00:00")
|
||||
@@ -188,7 +191,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, ""))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, ""))
|
||||
})
|
||||
t.Run("UTC-15", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 00:20:17 +00:00")
|
||||
@@ -203,7 +206,7 @@ func TestUtcOffset(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", UtcOffset(local, utc, ""))
|
||||
assert.Equal(t, "", UtcOffset(utc, local, ""))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user