diff --git a/README.md b/README.md index ffc0e21..e7a81bb 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ Usage: tagger [OPTIONS] file.csv Options: -filename-pattern string - filename pattern for generate exiftool command. %d means frame number on the film. (default "FILM_%05d.dng") + filename pattern for generate exiftool command. %d means frame number on the film (default "FILM_%05d.dng") -help - display help message + display help message + -timezone string + location or timezone name used while setting time on EOS 1V, will be used for proper scans timestamping (example: 'Europe/Moscow') (default "UTC") ``` NOTICES @@ -27,9 +29,9 @@ NOTICES * It's in **deep alpha** state * It's **NOT** going to make any changes to real data: just prints exiftool commands to STDOUT * It relies on the data provided by ES-E1 software in CSV format(in EOS 1V Memory just export via `File` -> `Export` -> `CSV`) -* It does **NOT** perform timezone detection since ES-E1 exports timestamps without timezone mark. * **Always** carefully review exiftool commands *before* applying them. * It sets ISO for each frame from the film settings. So if you have set ISO for particular frame to the another value it will still use the one from the film properties. +* It allows you to specify timezone set on EOS 1V to properly timestamp scans so please pay attention to `-timezone` flag **which defaults to UTC timezone** Licence ------- diff --git a/cmd/tagger/flags.go b/cmd/tagger/flags.go index d5f5b88..af9da13 100644 --- a/cmd/tagger/flags.go +++ b/cmd/tagger/flags.go @@ -11,8 +11,9 @@ const ( ) var ( - filenamePattern string = "FILM_%05d.dng" displayHelp bool = false + filenamePattern string = "FILM_%05d.dng" + timezone string = "UTC" ) func parseFlags() { @@ -22,8 +23,9 @@ func parseFlags() { flag.PrintDefaults() } - flag.StringVar(&filenamePattern, "filename-pattern", filenamePattern, "filename pattern for generate exiftool command. %d means frame number on the film.") flag.BoolVar(&displayHelp, "help", displayHelp, "display help message") + flag.StringVar(&filenamePattern, "filename-pattern", filenamePattern, "filename pattern for generate exiftool command. %d means frame number on the film") + flag.StringVar(&timezone, "timezone", timezone, "location or timezone name used while setting time on EOS 1V, will be used for proper scans timestamping (example: 'Europe/Moscow')") flag.Parse() diff --git a/cmd/tagger/main.go b/cmd/tagger/main.go index c64fc39..ca0c091 100644 --- a/cmd/tagger/main.go +++ b/cmd/tagger/main.go @@ -11,7 +11,12 @@ import ( func main() { parseFlags() - t, err := tagger.NewCSVParser(flag.Arg(0)) + tz, err := tagger.LocationByTimeZone(timezone) + if err != nil { + log.Fatalf("error looking up timezone: %s", err) + } + + t, err := tagger.NewCSVParser(flag.Arg(0), tz) if err != nil { log.Fatalf("error initializing CSV parser: %s", err) } diff --git a/csv_parser.go b/csv_parser.go index 2914e75..39919c7 100644 --- a/csv_parser.go +++ b/csv_parser.go @@ -15,6 +15,7 @@ import ( // CSVParser type type CSVParser struct { rc io.ReadCloser + tz *time.Location } var ( @@ -23,7 +24,7 @@ var ( ) // NewCSVParser creates new CSVParser object -func NewCSVParser(fn string) (*CSVParser, error) { +func NewCSVParser(fn string, tz *time.Location) (*CSVParser, error) { fp, err := os.Open(fn) if err != nil { return nil, err @@ -31,16 +32,10 @@ func NewCSVParser(fn string) (*CSVParser, error) { return &CSVParser{ rc: fp, + tz: tz, }, nil } -// NewCSVParserFromReadCloser creats new CSVParser object from ReadCloser -func NewCSVParserFromReadCloser(fp io.ReadCloser) *CSVParser { - return &CSVParser{ - rc: fp, - } -} - // Close ... func (p *CSVParser) Close() error { return p.rc.Close() @@ -61,7 +56,7 @@ func (p *CSVParser) Parse() (Film, error) { } remarks = strings.TrimSpace(strings.Split(remarks, ",")[2]) - film, err := parseFilmData(filmDataStr) + film, err := parseFilmData(filmDataStr, p.tz) if err != nil { return film, err } @@ -80,7 +75,7 @@ func (p *CSVParser) Parse() (Film, error) { continue } - frame, err := parseFrameData(frameStr) + frame, err := parseFrameData(frameStr, p.tz) if err != nil { if err == ErrEmptyFrame { continue @@ -95,11 +90,11 @@ func (p *CSVParser) Parse() (Film, error) { return film, err } -func parseFilmData(s string) (Film, error) { +func parseFilmData(s string, tz *time.Location) (Film, error) { ss := strings.Split(s, ",") ts := fmt.Sprintf("%sT%s", ss[6], ss[7]) - tt, err := time.Parse(TimestampFormat, ts) + tt, err := time.ParseInLocation(TimestampFormat, ts, tz) if err != nil { return Film{}, err } @@ -125,7 +120,7 @@ func parseFilmData(s string) (Film, error) { return f, nil } -func parseFrameData(s string) (Frame, error) { +func parseFrameData(s string, tz *time.Location) (Frame, error) { ss := strings.Split(s, ",") if len(ss) != 21 { return Frame{}, fmt.Errorf("wrong amount of columns for frame: %d: `%s`", len(ss), s) @@ -209,9 +204,9 @@ func parseFrameData(s string) (Frame, error) { FilmAdvanceMode: ss[12], AFMode: ss[13], BulbExposureTime: ss[14], - Timestamp: maybeParseTimestamp(ss[15], ss[16]), + Timestamp: maybeParseTimestamp(ss[15], ss[16], tz), MultipleExposure: ss[17], - BatteryLoadedDate: maybeParseTimestamp(ss[18], ss[19]), + BatteryLoadedDate: maybeParseTimestamp(ss[18], ss[19], tz), Remarks: ss[20], } return f, nil @@ -226,12 +221,12 @@ func isEmptySliceOfStrings(ss []string) bool { return true } -func maybeParseTimestamp(d, t string) time.Time { +func maybeParseTimestamp(d, t string, tz *time.Location) time.Time { if d == "" || t == "" { return time.Time{} } - ts, err := time.Parse(TimestampFormat, fmt.Sprintf("%vT%v", d, t)) + ts, err := time.ParseInLocation(TimestampFormat, fmt.Sprintf("%vT%v", d, t), tz) if err != nil { log.Printf("error parsing timestamp: `%sT%s`: %s", d, t, err) return time.Time{} diff --git a/csv_parser_test.go b/csv_parser_test.go index ef57c76..446a033 100644 --- a/csv_parser_test.go +++ b/csv_parser_test.go @@ -1,6 +1,7 @@ package tagger import ( + "strings" "testing" "time" @@ -10,7 +11,10 @@ import ( func TestCSVParser(t *testing.T) { r := require.New(t) - p, err := NewCSVParser("testdata/sample.CSV") + tz, err := LocationByTimeZone("CET") + r.NoError(err) + + p, err := NewCSVParser("testdata/sample.CSV", tz) r.NoError(err) r.NotNil(p) @@ -24,7 +28,7 @@ func TestCSVParser(t *testing.T) { r.Equal(Film{ ID: "03-758", Title: "Sample", - FilmLoadedTimestamp: mustParseTimestamp("09/01/2010T14:00:00"), + FilmLoadedTimestamp: mustParseTimestamp(t, "09/01/2010T14:00:00", tz), FrameCount: 36, ISO: 200, Frames: []Frame{ @@ -41,7 +45,7 @@ func TestCSVParser(t *testing.T) { ShootingMode: "Program AE", FilmAdvanceMode: "Single-frame", AFMode: "One-Shot AF", - Timestamp: time.Date(2010, 11, 9, 18, 31, 26, 0, time.UTC), + Timestamp: mustParseTimestamp(t, "11/09/2010T18:31:26", tz), MultipleExposure: "OFF", BatteryLoadedDate: time.Time{}, }, @@ -58,7 +62,7 @@ func TestCSVParser(t *testing.T) { ShootingMode: "Program AE", FilmAdvanceMode: "Single-frame", AFMode: "One-Shot AF", - Timestamp: time.Date(2010, 12, 9, 18, 32, 55, 0, time.UTC), + Timestamp: mustParseTimestamp(t, "12/09/2010T18:32:55", tz), MultipleExposure: "OFF", BatteryLoadedDate: time.Time{}, ExposureCompensation: -5, @@ -68,11 +72,15 @@ func TestCSVParser(t *testing.T) { }, film) } -func mustParseTimestamp(ts string) time.Time { - tt, err := time.Parse(TimestampFormat, ts) - if err != nil { - panic(err) - } +func mustParseTimestamp(t *testing.T, ts string, tz *time.Location) time.Time { + r := require.New(t) + + tts := strings.Split(ts, "T") + r.Len(tts, 2) + + tt := maybeParseTimestamp(tts[0], tts[1], tz) + r.NotNil(tt) + r.False(tt.IsZero()) return tt } diff --git a/timezone.go b/timezone.go new file mode 100644 index 0000000..b608449 --- /dev/null +++ b/timezone.go @@ -0,0 +1,8 @@ +package tagger + +import "time" + +// LocationByTimeZone returns time.Location object by timezone name +func LocationByTimeZone(z string) (*time.Location, error) { + return time.LoadLocation(z) +}