From 677cd66e0e5b6641468cd189f323a0d6574721bb Mon Sep 17 00:00:00 2001 From: simulot Date: Thu, 18 Jul 2024 19:50:32 +0200 Subject: [PATCH 01/17] errors when uploading are disturbing the the % of the progression Fixes #376 --- cmd/upload/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/upload/ui.go b/cmd/upload/ui.go index ee312060..c516fd02 100644 --- a/cmd/upload/ui.go +++ b/cmd/upload/ui.go @@ -131,7 +131,7 @@ func (app *UpCmd) runUI(ctx context.Context) error { ui.immichPrepare.SetMaxValue(int(counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo])) ui.immichPrepare.SetValue(int(counts[fileevent.AnalysisAssociatedMetadata])) - ui.immichUpload.SetMaxValue(int(counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo])) + ui.immichUpload.SetMaxValue(int(counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo] - counts[fileevent.UploadServerError])) ui.immichUpload.SetValue(int(counts[fileevent.UploadNotSelected] + counts[fileevent.UploadUpgraded] + counts[fileevent.UploadServerDuplicate] + From df33de7f4aaa7e14e3a4742587027c231a8628ee Mon Sep 17 00:00:00 2001 From: simulot Date: Thu, 18 Jul 2024 20:16:31 +0200 Subject: [PATCH 02/17] add test for Question: report shows way less images uploaded than scanned #390 --- browser/gp/testgp_samples_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index 5853d950..aa63eaa4 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -220,3 +220,16 @@ func issue68ForgottenDuplicates() *inMemFS { addImage("Takeout/Google Photos/Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", 1). addImage("Takeout/Google Photos/Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", 2) } + +// #390 Question: report shows way less images uploaded than scanned +func issue390WrongCount() *inMemFS { + return newInMemFS(). + addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG.json", "IMG_0170.JPG", takenTime("20180101")). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG", 10). + addJSONAlbum("Takeout/Google Photos/2018 - Cambodia/metadata.json", "2018 - Cambodia"). + addJSONImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG.json", "IMG_0170.JPG", takenTime("20180101")). + addImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG/Photos from 2018/IMG_0170.JPG", 10). + addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC.json", "IMG_0170.HEIC", takenTime("20180601")). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC", 20). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.MP4", 200) +} From 7970c4aa76937b8f89bc06e8307c0ca9e28fc371 Mon Sep 17 00:00:00 2001 From: simulot Date: Thu, 18 Jul 2024 20:18:13 +0200 Subject: [PATCH 03/17] add tests for #390 --- browser/gp/testgp_test.go | 172 ++++++++++++++++++++------------------ docs/releases.md | 5 ++ 2 files changed, 95 insertions(+), 82 deletions(-) diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index 9d194e43..80b20dc1 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -19,90 +19,98 @@ func TestBrowse(t *testing.T) { gen func() *inMemFS results []fileResult // file name / title }{ + // { + // "simpleYear", simpleYear, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, + // }), + // }, + + // { + // "simpleAlbum", simpleAlbum, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + // {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, + // }), + // }, + + // { + // "albumWithoutImage", albumWithoutImage, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + // }), + // }, + // { + // "namesWithNumbers", namesWithNumbers, + // sortFileResult([]fileResult{ + // {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, + // {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, + // {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, + // }), + // }, + // { + // "namesTruncated", namesTruncated, + // sortFileResult([]fileResult{ + // {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, + // {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + // {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, + // }), + // }, + + // { + // "imagesWithoutJSON", imagesEditedJSON, + // sortFileResult([]fileResult{ + // {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + // {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + // }), + // }, + + // { + // "titlesWithForbiddenChars", titlesWithForbiddenChars, + // sortFileResult([]fileResult{ + // {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, + // {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, + // }), + // }, + // { + // "namesIssue39", namesIssue39, + // sortFileResult([]fileResult{ + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + // }), + // }, + // { + // "issue68MPFiles", issue68MPFiles, + // sortFileResult([]fileResult{ + // {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, + // }), + // }, + // { + // "issue68LongExposure", issue68LongExposure, + // sortFileResult([]fileResult{ + // {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, + // {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + // }), + // }, + + // { + // "issue68ForgottenDuplicates", issue68ForgottenDuplicates, + // sortFileResult([]fileResult{ + // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + // }), + // }, { - "simpleYear", simpleYear, + "issue390WrongCount", issue390WrongCount, sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, - }), - }, - - { - "simpleAlbum", simpleAlbum, - sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, - {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, - }), - }, - - { - "albumWithoutImage", albumWithoutImage, - sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - }), - }, - { - "namesWithNumbers", namesWithNumbers, - sortFileResult([]fileResult{ - {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, - {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, - {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, - }), - }, - { - "namesTruncated", namesTruncated, - sortFileResult([]fileResult{ - {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, - {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, - }), - }, - - { - "imagesWithoutJSON", imagesEditedJSON, - sortFileResult([]fileResult{ - {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - }), - }, - - { - "titlesWithForbiddenChars", titlesWithForbiddenChars, - sortFileResult([]fileResult{ - {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, - {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, - }), - }, - { - "namesIssue39", namesIssue39, - sortFileResult([]fileResult{ - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - }), - }, - { - "issue68MPFiles", issue68MPFiles, - sortFileResult([]fileResult{ - {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, - }), - }, - { - "issue68LongExposure", issue68LongExposure, - sortFileResult([]fileResult{ - {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, - {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - }), - }, - - { - "issue68ForgottenDuplicates", issue68ForgottenDuplicates, - sortFileResult([]fileResult{ - {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + {name: "IMG_0170.JPG", size: 10, title: "IMG_0170.JPG"}, + {name: "IMG_0170.HEIC", size: 20, title: "IMG_0170.JPG"}, + {name: "IMG_0170.MP4", size: 200, title: "IMG_0170.MP4"}, }), }, } diff --git a/docs/releases.md b/docs/releases.md index 7981daf1..881e962f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -5,6 +5,11 @@ - [Github Sponsor page](https://github.com/sponsors/simulot) - [paypal donor page](https://www.paypal.com/donate/?hosted_button_id=VGU2SQE88T2T4) +## Release next + +### fixes: +- [[#376](https://github.com/simulot/immich-go/issues/376)] errors when uploading are disturbing the the % of the progression + ## Release 0.20.1 ### changes From 19a0b628626a6cf48e80374a07231a66f81d56d1 Mon Sep 17 00:00:00 2001 From: simulot Date: Sat, 20 Jul 2024 19:00:42 +0200 Subject: [PATCH 04/17] add fakefs to test with takeout lists --- cmd/upload/takeout_test.go | 23 + internal/fakefs/TESTDATA/one.lst | 9 + internal/fakefs/TESTDATA/small.lst | 986 +++++++++++++++++++++++++++++ internal/fakefs/fakefs.go | 175 +++++ internal/fakefs/ziplist.go | 85 +++ internal/fakefs/ziplist_test.go | 92 +++ 6 files changed, 1370 insertions(+) create mode 100644 cmd/upload/takeout_test.go create mode 100644 internal/fakefs/TESTDATA/one.lst create mode 100644 internal/fakefs/TESTDATA/small.lst create mode 100644 internal/fakefs/fakefs.go create mode 100644 internal/fakefs/ziplist.go create mode 100644 internal/fakefs/ziplist_test.go diff --git a/cmd/upload/takeout_test.go b/cmd/upload/takeout_test.go new file mode 100644 index 00000000..2cb64f1a --- /dev/null +++ b/cmd/upload/takeout_test.go @@ -0,0 +1,23 @@ +package upload + +// func simulate_upload() { +// ic := &icCatchUploadsAssets{ +// albums: map[string][]string{}, +// } +// ctx := context.Background() + +// log := slog.New(slog.NewTextHandler(io.Discard, nil)) +// serv := cmd.SharedFlags{ +// Immich: ic, +// Jnl: fileevent.NewRecorder(log, false), +// Log: log, +// } + +// args := append([]string{"-no-ui"}, tc.args...) + +// err := UploadCommand(ctx, &serv, args) +// if err != nil { +// t.Errorf("can't instantiate the UploadCmd: %s", err) +// return +// } +// } diff --git a/internal/fakefs/TESTDATA/one.lst b/internal/fakefs/TESTDATA/one.lst new file mode 100644 index 00000000..bbeb319c --- /dev/null +++ b/internal/fakefs/TESTDATA/one.lst @@ -0,0 +1,9 @@ +takeout-20240525T201456Z-003.zip: +Archive: takeout-20240525T201456Z-003.zip + Length Date Time Name +--------- ---------- ----- ---- +245068101 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719203057+0200.mp4 +221047335 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719185840+0200.mp4 +229274490 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719183450+0200.mp4 +--------- ------- +695389926 3 files diff --git a/internal/fakefs/TESTDATA/small.lst b/internal/fakefs/TESTDATA/small.lst new file mode 100644 index 00000000..50ae0612 --- /dev/null +++ b/internal/fakefs/TESTDATA/small.lst @@ -0,0 +1,986 @@ +takeout-20240525T201456Z-001.zip: +Archive: takeout-20240525T201456Z-001.zip + Length Date Time Name +--------- ---------- ----- ---- + 425704 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_8213.JPG + 307 2018-07-28 10:24 Takeout/Google Photos/Guillaumes 2018/métadonnées.json + 2754273 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_20180725_102133.jpg + 164272 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_8416.JPG + 748 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/Bebop2_20180723180057+0200.m4v.json + 730 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_8416.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_8213.JPG.json + 775 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_20180725_102133.jpg.json + 139865 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/✈️ DSC04119.jpg + 736 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/✈️ DSC04119.jpg.json + 954 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json + 353 2023-08-21 19:24 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/métadonnées.json + 2021088 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1763.HEIC + 145804 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045312704.jpg.json + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045318279.jpg.json + 2548657 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1762.HEIC + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_052150006.jpg.json + 866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045311490.jpg.json + 707 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1762.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1763.HEIC.json + 2734800 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1764.HEIC + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053046647.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1764.HEIC.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053056618.jpg.json + 165685 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/IMG_8416-modifié.JPG + 2797125 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045318279.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053450706.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053252586.jpg.json + 2246976 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_052150006.jpg + 1829557 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053252586.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053257151.jpg.json + 1819407 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053046647.jpg + 2629906 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045312704.jpg + 3161101 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_045311490.jpg + 1811270 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053257151.jpg + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_054704630.jpg.json + 880 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_054809080.jpg.json + 2238741 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053056618.jpg + 850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_060922712.jpg.json + 882 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_061449612.jpg.json + 1706679 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_054704630.jpg + 868 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_061617240.jpg.json + 1099162 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_061617240.jpg + 866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065633104.jpg.json + 864 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065541326.jpg.json + 2018556 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_053450706.jpg + 1758371 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_060922712.jpg + 1506192 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_054809080.jpg + 866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065651473.jpg.json + 1605479 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_061449612.jpg + 854 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065801190.LS.mp4.json + 864 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_070314539.jpg.json + 864 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_071125792.jpg.json + 2513620 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065633104.jpg + 2689434 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065541326.jpg + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_072708786.jpg.json + 1680663 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_071125792.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1772.MOV.json + 868 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_070539132.jpg.json + 705 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1767.MOV.json + 1839801 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_070539132.jpg + 830 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074101341.LS.mp4.json + 2364569 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065651473.jpg + 2174283 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_070314539.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074326135.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074330974.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_075720250.jpg.json + 826044 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065801190.LS.mp4 + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080614621.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_075722087.jpg.json + 1475614 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074330974.jpg + 1402475 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074326135.jpg + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080729129.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080832073.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080615786.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074318267.jpg.json + 2357148 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080614621.jpg + 2258518 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_072708786.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085230735.jpg.json + 1960901 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_075722087.jpg + 2428606 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080832073.jpg + 2009846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_075720250.jpg + 1067912 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074318267.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085317917.jpg.json + 866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065344256.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085545918.jpg.json + 866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065426987.jpg.json + 2877589 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080729129.jpg + 20458330 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1772.MOV + 2764415 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085230735.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085707338.jpg.json + 2754160 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_080615786.jpg + 43921624 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1767.MOV + 1664294 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065344256.jpg + 837 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_093742072.PANO.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085824598.jpg.json + 1947640 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_065426987.jpg + 837 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095051535.PANO.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_094133465.jpg.json + 834 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095014093.jpg.json + 2951881 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085220659.jpg + 2928838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_093742072.PANO.jpg + 3054790 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085317917.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_100426120.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095241390.jpg.json + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_101130729.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_101025932.jpg.json + 3249369 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095051535.PANO.jpg + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_104957115.LS.mp4.json + 3079050 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1773.HEIC + 3136587 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085545918.jpg + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_114039025.jpg.json + 3100522 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085707338.jpg + 1396573 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1775.HEIC + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_143108095.jpg.json + 3434665 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085824598.jpg + 2852147 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_094133465.jpg + 2030183 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1776.HEIC + 2442085 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1774.HEIC + 1245550 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_100426120.jpg + 2682608 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1778.HEIC + 1905682 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1780.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1774.HEIC.json + 1183599 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1782.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1775.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1776.HEIC.json + 1768315 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1777.HEIC + 3461019 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095241390.jpg + 1546245 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_074101341.LS.mp4 + 2596682 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_143108095.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1779.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1780.HEIC.json + 2577241 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1779.HEIC + 3180012 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_101130729.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1778.HEIC.json + 699580 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1787.HEIC + 4030321 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_095014093.jpg + 3402509 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1781.HEIC + 2281196 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1786.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1777.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1783.HEIC.json + 3181783 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1783.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1784.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1781.HEIC.json + 2332185 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1791.HEIC + 2296877 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1785.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1782.HEIC.json + 3649659 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1784.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1785.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1786.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1791.HEIC.json + 3173006 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_101025932.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1788.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1787.HEIC.json + 2778043 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1788.HEIC + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_175934570.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_173903662.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_172315843.jpg.json + 5621814 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_114039025.jpg + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_175940633.jpg.json + 27203585 2024-05-25 22:15 Takeout/Google Photos/Guillaumes 2018/Bebop2_20180723180057+0200.m4v + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_172540663.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181336232.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181337872.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_180819064.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_180515752.jpg.json + 2894559 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_173903662.jpg + 1875846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_180819064.jpg + 4769063 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_104957115.LS.mp4 + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181918957.jpg.json + 852 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_202853503.NIGHT.jpg.json + 852 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_202938567.NIGHT.jpg.json + 3611141 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_172315843.jpg + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181952207.jpg.json + 953011 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1795.HEIC + 2005029 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1794.HEIC + 3031838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_180515752.jpg + 2052762 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181336232.jpg + 2239188 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181337872.jpg + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203007881.jpg.json + 868 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203016989.NIGHT.jpg.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203051232.NIGHT.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203004692.jpg.json + 2249199 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_175940633.jpg + 2971596 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181952207.jpg + 2149199 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_175934570.jpg + 1988569 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1796.HEIC + 4191014 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_172540663.jpg + 1785180 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1798.HEIC + 1685183 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203004692.jpg + 3673018 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_181918957.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1795.HEIC.json + 2372396 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_202853503.NIGHT.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1794.HEIC.json + 1688525 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203007881.jpg + 1474426 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203051232.NIGHT.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_071217029.jpg.json + 954052 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1803.HEIC + 1357647 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1802.HEIC + 2217158 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1801.HEIC + 2045173 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1797.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1796.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1798.HEIC.json + 2291074 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1799.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1797.HEIC.json + 2518704 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_202938567.NIGHT.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1799.HEIC.json + 1752476 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_203016989.NIGHT.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1800.HEIC.json + 2350286 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1800.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1801.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1802.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1803.HEIC.json + 840 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_083758386.jpg.json + 2750364 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_071217029.jpg + 878 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085041151.jpg.json + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085134035.LS.mp4.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085348060.mp4.json + 828 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085241731.mp4.json + 828 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_090912639.mp4.json + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085142027.TS.mp4.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085641282.mp4.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091023980.jpg.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085417927.mp4.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091029409.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091031920.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091035587.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091328950.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091609310.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091608239.jpg.json + 154044 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085417927.mp4 + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091959433.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092000946.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092304596.jpg.json + 2473525 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091959433.jpg + 4594317 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085041151.jpg + 1894384 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092000946.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092309141.jpg.json + 850572 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1804.HEIC + 828 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_093331250.mp4.json + 2356936 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091031920.jpg + 514406 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085641282.mp4 + 3200270 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085134035.LS.mp4 + 3232353 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091035587.jpg + 4151500 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_083758386.jpg + 850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_093746681.mp4.json + 3846315 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091608239.jpg + 3839435 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091609310.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1804.HEIC.json + 2439866 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091029409.jpg + 880 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_094846004.PORTRAIT.ORIGINAL.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_094907387.jpg.json + 4431123 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091328950.jpg + 4982318 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_091023980.jpg + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_095433860.jpg.json + 838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100609571.jpg.json + 8171047 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_090912639.mp4 + 1751462 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_095433860.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100754362.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100934441.jpg.json + 2590405 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_094907387.jpg + 4204935 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092304596.jpg + 1913930 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_094846004.PORTRAIT.ORIGINAL.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101954705.jpg.json + 4938850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085348060.mp4 + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101958829.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101956501.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103647627.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_102604015.jpg.json + 2616162 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100754362.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_102135803.jpg.json + 1676634 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101956501.jpg + 2122990 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100934441.jpg + 1704142 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101954705.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103652839.jpg.json + 838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104311200.jpg.json + 4648621 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085241731.mp4 + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103704197.jpg.json + 838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104355371.jpg.json + 9350036 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_093331250.mp4 + 1894488 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_101958829.jpg + 3945359 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_100609571.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104439655.jpg.json + 2883623 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_092309141.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104535388.jpg.json + 2177026 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_102604015.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104550263.jpg.json + 4094755 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_102135803.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104646669.jpg.json + 3365027 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103652839.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_105115882.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_105118064.jpg.json + 2824711 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103647627.jpg + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_111430712.jpg.json + 2523883 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_103704197.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_111310171.jpg.json + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112118374.PANO.jpg.json + 2306670 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_105118064.jpg + 1516138 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112118374.PANO.jpg + 872 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112941866.jpg.json + 3807836 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104550263.jpg + 872 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112926703.jpg.json + 323256 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_093746681.mp4 + 872 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112944670.jpg.json + 1387263 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1805.HEIC + 2711896 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104355371.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_113132593.jpg.json + 2940811 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112926703.jpg + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_113936027.jpg.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_114626362.jpg.json + 3446024 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104439655.jpg + 2281660 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_105115882.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_120327153.jpg.json + 3784479 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_111430712.jpg + 2502925 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_113132593.jpg + 2924684 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104311200.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_121239554.jpg.json + 1998148 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_113936027.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_120803085.jpg.json + 2280645 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_120327153.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1805.HEIC.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_122023413.jpg.json + 2833588 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1809.HEIC + 4079835 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104535388.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_122032538.jpg.json + 3783408 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1810.HEIC + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_121248049.jpg.json + 2905349 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_111310171.jpg + 1664304 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_114626362.jpg + 4787027 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_104646669.jpg + 850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_134329900.mp4.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_134755219.jpg.json + 3464559 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1811.HEIC + 2879828 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1808.HEIC + 1094337 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1812.HEIC + 3286583 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_121248049.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1808.HEIC.json + 10006046 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_085142027.TS.mp4 + 2540915 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_120803085.jpg + 2850159 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_121239554.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1810.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1809.HEIC.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_185012667.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191603424.jpg.json + 2829587 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_122023413.jpg + 2819007 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_122032538.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_185027454.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1811.HEIC.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191303726.jpg.json + 4694514 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112941866.jpg + 2706891 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1814.HEIC + 1474839 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1813.HEIC + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191605842.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1812.HEIC.json + 2264631 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_185012667.jpg + 4497231 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_112944670.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_192412458.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1813.HEIC.json + 878 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_193423423.jpg.json + 878 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_193422379.jpg.json + 2385333 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191603424.jpg + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_192636406.jpg.json + 2885377 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1817.HEIC + 1671841 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1815.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1814.HEIC.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_194519579.jpg.json + 2712576 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191605842.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1816.HEIC.json + 2917329 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1816.HEIC + 4388501 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_134755219.jpg + 3456118 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_191303726.jpg + 864 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195900654.NIGHT.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195917841.jpg.json + 705 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1818.MOV.json + 68756797 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1818.MOV + 1804560 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_192636406.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195924456.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1815.HEIC.json + 705 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1819.MOV.json + 2552809 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_193423423.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1817.HEIC.json + 27203198 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1819.MOV + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200210541.jpg.json + 2240140 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_185027454.jpg + 2500593 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_192412458.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200156149.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200308368.jpg.json + 831 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200310052.TS.mp4.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200529292.jpg.json + 2522639 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_193422379.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200532278.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201038861.jpg.json + 3251996 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195924456.jpg + 3499095 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_194519579.jpg + 3147675 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195917841.jpg + 804 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER..json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201231937.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201300341.jpg.json + 1749506 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201205358.ACTION_PAN-01.COVER.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203801941.jpg.json + 801 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201205358.ACTION_PAN-01.COVER.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203823133.jpg.json + 3247533 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_195900654.NIGHT.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203824642.jpg.json + 3727860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200308368.jpg + 3264214 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200210541.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203825515.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_204729094.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_204736848.jpg.json + 3260444 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200156149.jpg + 3721976 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg + 2583993 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200529292.jpg + 12783566 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200310052.TS.mp4 + 843 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203826359.jpg.json + 3227076 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201300341.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_213833167.jpg.json + 3538166 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201038861.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_214029337.jpg.json + 3006962 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203823133.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_214049584.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_220414717.jpg.json + 3144573 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203825515.jpg + 2859457 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_204736848.jpg + 852 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_221244885.NIGHT.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_220430652.jpg.json + 16335360 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1820.MOV + 3205962 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203801941.jpg + 3638799 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_201231937.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_073609646.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092115663.jpg.json + 705 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1820.MOV.json + 2950346 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203826359.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092244364.jpg.json + 3706201 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_213833167.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092314014.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092420813.jpg.json + 3166324 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_203824642.jpg + 2076957 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_073609646.jpg + 2450989 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_200532278.jpg + 2061705 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_220414717.jpg + 2711975 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_204729094.jpg + 2365786 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_220430652.jpg + 1356103 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_221244885.NIGHT.jpg + 2929617 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_214049584.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092454769.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092549245.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092559642.jpg.json + 15697268 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_134329900.mp4 + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092609468.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092742640.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_095719059.jpg.json + 4392195 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230814_214029337.jpg + 3267005 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092314014.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_095911791.jpg.json + 3479639 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092115663.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110610989.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110716350.jpg.json + 856 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110637215.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110627557.jpg.json + 838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110956602.jpg.json + 3795394 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092549245.jpg + 856 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110909980.jpg.json + 838 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110958888.jpg.json + 3332616 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092420813.jpg + 2555347 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092559642.jpg + 1104969 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110958888.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111355138.jpg.json + 2822809 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1821.HEIC + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111416343.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111400308.jpg.json + 1477767 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1822.HEIC + 4891737 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110610989.jpg + 3257767 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_095719059.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111624099.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111805566.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111809021.jpg.json + 5538289 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092454769.jpg + 2801493 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110956602.jpg + 4236065 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110637215.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112215507.jpg.json + 3825053 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110909980.jpg + 3882670 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_095911791.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1821.HEIC.json + 3682575 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111400308.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1822.HEIC.json + 4421482 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092609468.jpg + 3807495 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110627557.jpg + 1406035 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1826.HEIC + 5056540 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092742640.jpg + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112252009.jpg.json + 840 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112349398.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112447598.jpg.json + 3655827 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111355138.jpg + 2021117 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1824.HEIC + 2928784 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111809021.jpg + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112517602.jpg.json + 3280542 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111416343.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1823.HEIC.json + 2752273 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111805566.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_113519297.jpg.json + 3392793 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1823.HEIC + 3234964 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112215507.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114315562.jpg.json + 2672206 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112252009.jpg + 1902366 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1829.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1826.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1825.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1824.HEIC.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114438496.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1827.HEIC.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114334732.jpg.json + 4857728 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_111624099.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114451718.jpg.json + 1590243 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1828.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1828.HEIC.json + 3276096 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112517602.jpg + 2207069 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_092244364.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1829.HEIC.json + 2880413 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1827.HEIC + 4622459 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_110716350.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114603834.jpg.json + 4107927 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112349398.jpg + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114655917.jpg.json + 1858441 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1825.HEIC + 1230898 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114941122.jpg + 2329548 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114334732.jpg + 3027958 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_113519297.jpg + 852 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114708377.jpg.json + 4163914 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_112447598.jpg + 1288661 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1831.HEIC + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114849873.jpg.json + 1755648 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114438496.jpg + 3186118 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1832.HEIC + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114750722.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115247743.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114941122.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115339983.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115324982.jpg.json + 2281324 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1830.HEIC + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115341662.jpg.json + 2817633 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114603834.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1831.HEIC.json + 3084847 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114849873.jpg + 2159034 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1835.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1832.HEIC.json + 3083608 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1833.HEIC + 830653 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115247743.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1833.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1830.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1834.HEIC.json + 1571401 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1834.HEIC + 2711792 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1836.HEIC + 1995263 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114708377.jpg + 4297880 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114451718.jpg + 2672283 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114655917.jpg + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_121631092.jpg.json + 2200132 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115341662.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1835.HEIC.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_121633187.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122137232.jpg.json + 2085527 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115339983.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122826011.jpg.json + 2189884 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122137232.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1836.HEIC.json + 2463125 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1837.HEIC + 1882389 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_121631092.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122926905.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123051917.jpg.json + 2471414 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1838.HEIC + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123055666.jpg.json + 1721502 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114750722.jpg + 2282623 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_115324982.jpg + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123109118.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123314870.jpg.json + 1865198 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1839.HEIC + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124031987.TS.mp4.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123624823.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1837.HEIC.json + 2772827 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123109118.jpg + 2347070 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122926905.jpg + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124552627.PORTRAIT.ORIGINAL.jpg.json + 1953026 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_121633187.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1838.HEIC.json + 1738977 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_114315562.jpg + 867 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124817964.PORTRAIT.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124911036.jpg.json + 5106360 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124817964.PORTRAIT.jpg + 1700453 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1842.HEIC + 3246872 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_122826011.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1839.HEIC.json + 1281223 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1840.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1840.HEIC.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125508511.jpg.json + 2744968 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123051917.jpg + 1731604 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123314870.jpg + 2988456 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123055666.jpg + 1801212 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1841.HEIC + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125516847.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1841.HEIC.json + 1325046 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124031987.TS.mp4 + 2189746 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1843.HEIC + 2503191 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1844.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1842.HEIC.json + 2452627 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_123624823.jpg + 831 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125540275.TS.mp4.json + 2722453 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124552627.PORTRAIT.ORIGINAL.jpg + 836 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125637640.jpg.json + 2530601 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1845.HEIC + 836 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125646665.jpg.json + 2806033 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1848.HEIC + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125905261.PORTRAIT.ORIGINAL.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1844.HEIC.json + 2739332 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_124911036.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1843.HEIC.json + 2576854 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1846.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1845.HEIC.json + 3078212 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125516847.jpg + 2309189 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1847.HEIC + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_130155836.PORTRAIT.ORIGINAL.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1846.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1848.HEIC.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_130631866.jpg.json + 2747390 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125646665.jpg + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1847.HEIC.json + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131013972.jpg.json + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131100760.jpg.json + 874 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131020934.jpg.json + 2787837 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125508511.jpg + 2213164 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125905261.PORTRAIT.ORIGINAL.jpg + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132140303.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132154767.jpg.json + 856 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132231336.jpg.json + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_150752249.PANO.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152254524.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_151238032.jpg.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152303067.jpg.json + 2479158 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_130631866.jpg + 3184937 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132140303.jpg + 3051320 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_130155836.PORTRAIT.ORIGINAL.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152405874.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152841555.jpg.json + 5022885 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131013972.jpg + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152846037.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152846037~2.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181~3.jpg.json + 4805476 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131020934.jpg + 899 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181~2.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153110793.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153308948.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153603660.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153359013.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_155435571.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_155505084.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160052888.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1849.HEIC.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160105481.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160831688.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160934535.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_161357571.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160934535~2.jpg.json + 854 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_170918725.jpg.json + 854 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_170921221.jpg.json + 840 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_172103511.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_184541352.jpg.json + 854 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_171225430.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1850.HEIC.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_191055522.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_184608476.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_191300435.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1851.HEIC.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_193522767.jpg.json + 850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_201809279.NIGHT.jpg.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202246258.jpg.json + 882 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202327379.NIGHT.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_201655580.NIGHT.jpg.json + 884 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202818387.NIGHT.jpg.json + 884 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202820142.NIGHT.jpg.json + 850 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_065539076.NIGHT.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_060414573.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_071349105.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1852.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1853.HEIC.json + 1186801 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1856.HEIC + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1854.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1855.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1856.HEIC.json + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_113900491.LS.mp4.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1857.HEIC.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_112210934.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_115318027.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1859.HEIC.json + 123247 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1862.HEIC + 868 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_132648337.NIGHT.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_143203982.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1860.HEIC.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_143208443.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1861.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1862.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1864.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1865.HEIC.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_191312144.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1863.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1866.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1869.HEIC.json + 878 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_211707485.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1870.HEIC.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_202044782.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1871.HEIC.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_071450157.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_072539487.jpg.json + 733 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/FFE79A68-B0E6-472E-836D-EC49F351F588.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_080105375.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_081251352.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_121601920.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_075312564.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_131815116.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1875.HEIC.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1876.HEIC.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_170752393.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181209268.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1877.HEIC.json + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_175514506.PANO.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181502346.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181456746.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183021857.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183618613.jpg.json + 831 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183716695.LS.mp4.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183751525.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183811061.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183801046.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183826357.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1773.HEIC.json + 733 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/509C843D-D4B9-4F02-81B0-05BE938F12DF.jpg.json + 706 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1878.HEIC.json + 856 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_193237877.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_194439146.jpg.json + 853 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_195943901.LS.mp4.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_200858587.jpg.json + 876 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_200900799.jpg.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_201212577.jpg.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202545906.NIGHT.jpg.json + 848 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202555916.NIGHT.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230813_085220659.jpg.json + 829 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202746758.LS.mp4.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_062827230.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_072713034.jpg.json + 842 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_072728336.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073034374.jpg.json + 845 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073228970.PANO.jpg.json + 831 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073402211.TS.mp4.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073704846.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073846874.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_074307497.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080921955.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080922936.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080937939.jpg.json + 858 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_081231702.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_082348579.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_082351912.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_130000489.jpg.json + 844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_132150855.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144557167.jpg.json + 295 2018-07-21 09:08 Takeout/Google Photos/Garros 19 Juillet 2018/métadonnées.json + 860 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_134521846.jpg.json + 878 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144231600.jpg.json + 862 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144716103.jpg.json + 846 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_150420324.jpg.json + 720710 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_191506.jpg + 789 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719182934+0200.mp4.json + 812 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_161304257.jpg.json + 407330 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8151.JPG + 785 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719183450+0200.mp4.json + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719185209+0200.mp4.json + 369458 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8153.JPG + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719185840+0200.mp4.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8152.JPG.json + 810 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_191506.jpg.json + 854 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_191638.jpg.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8151.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8153.JPG.json + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719194940+0200.mp4.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8155.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8156.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8157.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8158.JPG.json + 810 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_200157.jpg.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8162.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8163.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8164.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8165.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8166.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8167.JPG.json + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719201922+0200.mp4.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8168.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8169.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8172.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8170.JPG.json + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719203057+0200.mp4.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8177.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8178.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8179.JPG.json + 731 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8181.JPG.json + 763 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719211425+0200.mp4.json + 806 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719211742+0200.jpg.json + 336456 2024-05-25 22:15 Takeout/archive_browser.html +--------- ------- +1070219299 807 files +takeout-20240525T201456Z-002.zip: +Archive: takeout-20240525T201456Z-002.zip + Length Date Time Name +--------- ---------- ----- ---- + 2730889 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152303067.jpg + 5363355 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132231336.jpg + 2422683 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181~3.jpg + 4243153 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_132154767.jpg + 2531099 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152846037.jpg + 2085622 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_131100760.jpg + 2553353 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152841555.jpg + 2919006 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181.jpg + 2148816 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152846037~2.jpg + 2936280 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152254524.jpg + 2422348 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153003181~2.jpg + 3301585 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153110793.jpg + 1781697 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125637640.jpg + 2917045 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_152405874.jpg + 2865401 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1849.HEIC + 7552407 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_150752249.PANO.jpg + 4061761 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153603660.jpg + 3637703 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153308948.jpg + 3337879 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_155435571.jpg + 4970836 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_151238032.jpg + 3267844 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_153359013.jpg + 2905051 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160052888.jpg + 3833491 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1850.HEIC + 2315157 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_155505084.jpg + 1810781 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_171225430.jpg + 3089972 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160105481.jpg + 2991863 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160934535~2.jpg + 2795499 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160831688.jpg + 1797635 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_170921221.jpg + 3035389 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1851.HEIC + 2110158 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_170918725.jpg + 4455300 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_125540275.TS.mp4 + 3174153 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_161357571.jpg + 3529803 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_160934535.jpg + 3058540 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_172103511.jpg + 2205991 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_191300435.jpg + 1808446 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1854.HEIC + 2152786 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1852.HEIC + 2078603 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_201655580.NIGHT.jpg + 2460454 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_191055522.jpg + 1695246 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1855.HEIC + 2452610 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_184608476.jpg + 1514652 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_065539076.NIGHT.jpg + 2019308 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202818387.NIGHT.jpg + 4238096 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_184541352.jpg + 2467619 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1857.HEIC + 1836534 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1853.HEIC + 1971312 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202820142.NIGHT.jpg + 2553153 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202327379.NIGHT.jpg + 2015307 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_060414573.jpg + 3543300 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_193522767.jpg + 2084436 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_201809279.NIGHT.jpg + 2875776 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230815_202246258.jpg + 1593944 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1859.HEIC + 1587732 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_112210934.jpg + 1669594 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1860.HEIC + 2611926 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1864.HEIC + 3620068 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1865.HEIC + 2891650 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1861.HEIC + 1763765 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_143203982.jpg + 1172303 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1870.HEIC + 1703266 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1863.HEIC + 1993207 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_115318027.jpg + 2813596 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_071349105.jpg + 1023438 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_113900491.LS.mp4 + 504153 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1871.HEIC + 2609777 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1866.HEIC + 1928320 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_132648337.NIGHT.jpg + 1679572 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_143208443.jpg + 2154644 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1869.HEIC + 1382021 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/FFE79A68-B0E6-472E-836D-EC49F351F588.jpg + 942104 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1875.HEIC + 1097934 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1876.HEIC + 3537613 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_191312144.jpg + 2723602 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_080105375.jpg + 3517863 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1877.HEIC + 2905752 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_202044782.jpg + 3391315 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_075312564.jpg + 2220923 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_131815116.jpg + 2520832 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230816_211707485.jpg + 2378485 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_071450157.jpg + 3485518 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_121601920.jpg + 3695668 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_072539487.jpg + 3414098 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_175514506.PANO.jpg + 3322147 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_081251352.jpg + 2424218 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/IMG_1878.HEIC + 2410890 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181502346.jpg + 2330097 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183801046.jpg + 3377082 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183618613.jpg + 2557888 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181209268.jpg + 2630534 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183021857.jpg + 2496726 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183826357.jpg + 2413675 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_181456746.jpg + 2214172 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/509C843D-D4B9-4F02-81B0-05BE938F12DF.jpg + 1985323 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183811061.jpg + 2071081 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_194439146.jpg + 1875898 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_200900799.jpg + 5670281 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_170752393.jpg + 2963787 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183751525.jpg + 2108272 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_183716695.LS.mp4 + 2131124 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202545906.NIGHT.jpg + 3579884 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_193237877.jpg + 2963124 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_200858587.jpg + 1964148 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_072713034.jpg + 1902861 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202555916.NIGHT.jpg + 1468513 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_201212577.jpg + 1959607 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_062827230.jpg + 2666859 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_195943901.LS.mp4 + 1757662 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080922936.jpg + 1661260 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080937939.jpg + 3945089 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073228970.PANO.jpg + 2307315 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230817_202746758.LS.mp4 + 2702757 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_072728336.jpg + 2015016 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_082351912.jpg + 1969639 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_082348579.jpg + 1789401 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_080921955.jpg + 3342821 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073846874.jpg + 3243628 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073034374.jpg + 4464270 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073704846.jpg + 2910174 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_074307497.jpg + 2336173 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_130000489.jpg + 1929416 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_132150855.jpg + 5934269 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_081231702.jpg + 3169095 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144716103.jpg + 3046438 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_134521846.jpg + 2138959 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_161304257.jpg + 1531803 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_191638.jpg + 2974227 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144557167.jpg + 393806 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8155.JPG + 485394 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8152.JPG + 474952 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8156.JPG + 2538601 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_144231600.jpg + 496970 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8157.JPG + 371649 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8158.JPG + 3422738 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_150420324.jpg + 723610 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_20180719_200157.jpg + 635305 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8162.JPG + 708902 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8163.JPG + 560297 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8164.JPG + 381046 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8167.JPG + 483752 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8165.JPG + 456804 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8166.JPG + 343792 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8168.JPG + 367294 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8169.JPG + 489839 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8178.JPG + 669605 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8170.JPG + 474541 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8177.JPG + 1627237 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8181.JPG + 548717 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8179.JPG + 1951988 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/IMG_8172.JPG + 1419461 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719211742+0200.jpg + 13156833 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/PXL_20230818_073402211.TS.mp4 + 1729388 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719211742+0200-modifié.jpg + 98595828 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719211425+0200.mp4 +145104161 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719185209+0200.mp4 + 89765082 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719182934+0200.mp4 + 98016626 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719194940+0200.mp4 +258577282 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719201922+0200.mp4 +--------- ------- +1065090969 158 files +takeout-20240525T201456Z-003.zip: +Archive: takeout-20240525T201456Z-003.zip + Length Date Time Name +--------- ---------- ----- ---- +245068101 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719203057+0200.mp4 +221047335 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719185840+0200.mp4 +229274490 2024-05-25 22:15 Takeout/Google Photos/Garros 19 Juillet 2018/Bebop2_20180719183450+0200.mp4 +--------- ------- +695389926 3 files diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go new file mode 100644 index 00000000..baf883c9 --- /dev/null +++ b/internal/fakefs/fakefs.go @@ -0,0 +1,175 @@ +package fakefs + +import ( + "crypto/rand" + "io" + "io/fs" + "path" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/simulot/immich-go/helpers/gen" +) + +/* + simulate a file system based on the list of files contained into a set of archive. + + +*/ + +type FakeDirEntry struct { + name string // name of the file + size int64 // length in bytes for regular files; system-dependent for others + mode fs.FileMode // file mode bits + modTime time.Time // modification time +} + +func (fi FakeDirEntry) Name() string { return filepath.Base(fi.name) } +func (fi FakeDirEntry) Size() int64 { return fi.size } +func (fi FakeDirEntry) Mode() fs.FileMode { return fi.mode } +func (fi FakeDirEntry) ModTime() time.Time { return fi.modTime } +func (fi FakeDirEntry) IsDir() bool { return fi.mode.IsDir() } +func (fi FakeDirEntry) Sys() any { return nil } +func (fi FakeDirEntry) Type() fs.FileMode { return fi.mode } +func (fi FakeDirEntry) Info() (fs.FileInfo, error) { return fi, nil } + +type FakeFile struct { + fi FakeDirEntry + pos int64 +} + +func (f FakeFile) Stat() (fs.FileInfo, error) { + return f.fi, nil +} + +func (f *FakeFile) Read(b []byte) (int, error) { + if f.pos < f.fi.size { + l := min(len(b), int(f.fi.size-f.pos)) + b = b[:l] + f.pos += int64(l) + return rand.Read(b) + } + return 0, io.EOF +} + +func (f *FakeFile) Close() error { + f.pos = 0 + return nil +} + +type FakeFS struct { + name string + files map[string]map[string]FakeDirEntry +} + +func (fsys FakeFS) Stat(name string) (fs.FileInfo, error) { + name = filepath.ToSlash(name) + dir, base := path.Split(name) + dir = strings.TrimSuffix(dir, "/") + var l map[string]FakeDirEntry + if dir == "" { + dir = "." + } + l = fsys.files[dir] + if len(l) == 0 { + return nil, fs.ErrNotExist + } + if e, ok := l[base]; ok { + return e, nil + } + return nil, fs.ErrNotExist +} + +func (fsys FakeFS) Open(name string) (fs.File, error) { + info, err := fsys.Stat(name) + if err != nil { + return nil, err + } + return &FakeFile{fi: info.(FakeDirEntry)}, nil +} + +func (fsys FakeFS) ReadDir(name string) ([]fs.DirEntry, error) { + if name != "." { + name = "./" + name + } + info, err := fsys.Stat(name) + if err != nil { + return nil, err + } + if !info.IsDir() { + return nil, fs.ErrNotExist + } + + entries := fsys.files[name] + if len(entries) == 0 { + return nil, fs.ErrNotExist + } + + keys := gen.MapKeys(entries) + sort.Strings(keys) + out := []fs.DirEntry{} + for _, k := range keys { + if k != "." { + out = append(out, entries[k]) + } + } + return out, nil +} + +func (fsys FakeFS) addFile(name string, size int64, modDate time.Time) { + dir, base := path.Split("./" + name) + dir = strings.TrimSuffix(dir, "/") + parts := strings.Split(dir, "/") + + for i, p := range parts { + // create the entry in the parent + if i == 0 { + sub := "." + if _, ok := fsys.files[sub]; !ok { + // + e := FakeDirEntry{ + name: ".", + modTime: time.Now(), + size: 0, + mode: 0o777 | fs.ModeDir, + } + + fsys.files[sub] = map[string]FakeDirEntry{ + ".": e, + } + } + } else { + // add entry in the parent + parent := strings.Join(parts[:i], "/") + dir := parent + "/" + p + if _, ok := fsys.files[parent][p]; !ok { + fsys.files[parent][p] = FakeDirEntry{ + name: dir, + modTime: time.Now(), + size: 0, + mode: 0o777 | fs.ModeDir, + } + } + // create the dir entry + if _, ok := fsys.files[dir]; !ok { + fsys.files[dir] = map[string]FakeDirEntry{ + ".": { + name: dir + "/.", + modTime: time.Now(), + size: 0, + mode: 0o777 | fs.ModeDir, + }, + } + } + } + } + l := fsys.files[dir] + l[base] = FakeDirEntry{ + name: name, + modTime: modDate, + size: size, + mode: 0o777, + } +} diff --git a/internal/fakefs/ziplist.go b/internal/fakefs/ziplist.go new file mode 100644 index 00000000..6467aa18 --- /dev/null +++ b/internal/fakefs/ziplist.go @@ -0,0 +1,85 @@ +package fakefs + +/* + for f in *.zip; do echo "$f: "; unzip -l $f; done >list.lst +*/ +import ( + "bufio" + "io/fs" + "os" + "strconv" + "strings" + "time" +) + +func readFileLine(l string, dateFormat string) (string, int64, time.Time) { + if len(l) < 30 { + return "", 0, time.Time{} + } + // ` 2104348 07-20-2023 00:00 Takeout/Google Photos/2020 - Costa Rica/IMG_3235.MP4` + s := strings.TrimSpace(l[:9]) + d := l[11:27] + name := l[30:] + size, _ := strconv.ParseInt(s, 10, 64) + modTime, _ := time.ParseInLocation(dateFormat, d, time.Local) + return name, size, modTime +} + +func ScanFileList(name string, dateFormat string) ([]fs.FS, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + fsyss := map[string]*FakeFS{} + var fsys *FakeFS + currentZip := "" + inList := false + ok := false + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + l := scanner.Text() + if strings.HasPrefix(l, "Archive: ") { + currentZip = strings.TrimPrefix(l, "Archive: ") + fsys, ok = fsyss[currentZip] + if !ok { + fsys = &FakeFS{ + name: currentZip, + files: map[string]map[string]FakeDirEntry{}, + } + + fsyss[currentZip] = fsys + } + scanner.Scan() + scanner.Scan() + inList = true + continue + } + if strings.HasPrefix(l, "--------- ") { + scanner.Scan() + inList = false + continue + } + if inList { + if name, size, modTime := readFileLine(l, dateFormat); name != "" { + fsys.addFile(name, size, modTime) + } else { + inList = false + } + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + output := make([]fs.FS, len(fsyss)) + i := 0 + for _, fs := range fsyss { + output[i] = fs + i++ + } + return output, nil +} diff --git a/internal/fakefs/ziplist_test.go b/internal/fakefs/ziplist_test.go new file mode 100644 index 00000000..7bd55128 --- /dev/null +++ b/internal/fakefs/ziplist_test.go @@ -0,0 +1,92 @@ +package fakefs + +import ( + "fmt" + "io/fs" + "testing" + "time" +) + +func Test_readFileLine(t *testing.T) { + type args struct { + l string + dateFormat string + } + tests := []struct { + name string + args args + wantName string + wantModTime time.Time + wantSize int64 + }{ + { + name: "simulot", + args: args{ + l: " 145804 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", + dateFormat: "2006-01-02 15:04", + }, + wantName: "Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", + wantSize: 145804, + wantModTime: time.Date(2024, 5, 25, 22, 15, 0, 0, time.Local), + }, + { + name: "pixil", + args: args{ + l: " 197486 07-19-2023 23:53 Takeout/Google Photos/2011 - Omaha Zoo/IMG_20110702_153447.jpg", + dateFormat: "01-02-2006 15:04", + }, + wantName: "Takeout/Google Photos/2011 - Omaha Zoo/IMG_20110702_153447.jpg", + wantSize: 197486, + wantModTime: time.Date(2023, 7, 19, 23, 53, 0, 0, time.Local), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotName, gotSize, gotModTime := readFileLine(tt.args.l, tt.args.dateFormat) + + if gotName != tt.wantName { + t.Errorf("readFileLine() got = %v, want %v", gotName, tt.wantName) + } + if gotSize != tt.wantSize { + t.Errorf("readFileLine() got = %v, want %v", gotSize, tt.wantSize) + } + if !gotModTime.Equal(tt.wantModTime) { + t.Errorf("readFileLine() got = %v, want %v", gotModTime, tt.wantModTime) + } + }) + } +} + +type NameFS interface { + Name() string +} + +func TestFakeFS(t *testing.T) { + fsyss, err := ScanFileList("TESTDATA/one.lst", "2006-01-02 15:04") + if err != nil { + t.Error(err) + return + } + + for _, fsys := range fsyss { + if fsys, ok := fsys.(NameFS); ok { + fmt.Println(fsys.Name()) + } + err := fs.WalkDir(fsys, ".", + func(name string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + f, err := d.Info() + if f != nil { + fmt.Printf("%9d %s %s\n", f.Size(), f.ModTime().Format("2006-01-02 15:04"), name) + } + return err + }) + if err != nil { + t.Error(err) + return + } + } + fmt.Println() +} From 931d4df4f9a0ba3f17f43e1a1d77afde2d970f3d Mon Sep 17 00:00:00 2001 From: simulot Date: Sat, 20 Jul 2024 22:37:31 +0200 Subject: [PATCH 05/17] wip: duplicate count --- browser/gp/testgp_bigread_test.go | 2 +- cmd/upload/takeout_test.go | 69 +++++++++++++++++++++---------- cmd/upload/upload.go | 10 +++-- docs/google-takeout.md | 2 +- helpers/fshelper/parseArgs.go | 2 +- internal/fakefs/fakefs.go | 55 +++++++++++++++++++----- internal/fakefs/metadata.go | 68 ++++++++++++++++++++++++++++++ internal/fakefs/ziplist_test.go | 2 +- 8 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 internal/fakefs/metadata.go diff --git a/browser/gp/testgp_bigread_test.go b/browser/gp/testgp_bigread_test.go index a0d3304e..d3283a6e 100644 --- a/browser/gp/testgp_bigread_test.go +++ b/browser/gp/testgp_bigread_test.go @@ -31,7 +31,7 @@ func TestReadBigTakeout(t *testing.T) { return } cnt := 0 - fsyss, err := fshelper.ParsePath(m, true) + fsyss, err := fshelper.ParsePath(m) to, err := NewTakeout(context.Background(), j, immich.DefaultSupportedMedia, fsyss...) if err != nil { t.Error(err) diff --git a/cmd/upload/takeout_test.go b/cmd/upload/takeout_test.go index 2cb64f1a..d0d9d244 100644 --- a/cmd/upload/takeout_test.go +++ b/cmd/upload/takeout_test.go @@ -1,23 +1,50 @@ package upload -// func simulate_upload() { -// ic := &icCatchUploadsAssets{ -// albums: map[string][]string{}, -// } -// ctx := context.Background() - -// log := slog.New(slog.NewTextHandler(io.Discard, nil)) -// serv := cmd.SharedFlags{ -// Immich: ic, -// Jnl: fileevent.NewRecorder(log, false), -// Log: log, -// } - -// args := append([]string{"-no-ui"}, tc.args...) - -// err := UploadCommand(ctx, &serv, args) -// if err != nil { -// t.Errorf("can't instantiate the UploadCmd: %s", err) -// return -// } -// } +import ( + "context" + "io/fs" + "path/filepath" + "testing" + + "github.com/simulot/immich-go/cmd" + "github.com/simulot/immich-go/internal/fakefs" +) + +// Simulate a takeout archive with the list of zipped files +func simulate_upload(t *testing.T, zipList string, dateFormat string) { + ic := &icCatchUploadsAssets{ + albums: map[string][]string{}, + } + ctx := context.Background() + + // log := slog.New(slog.NewTextHandler(io.Discard, nil)) + serv := cmd.SharedFlags{ + Immich: ic, + LogLevel: "INFO", + // Jnl: fileevent.NewRecorder(log, false), + // Log: log, + } + + fsOpener := func() ([]fs.FS, error) { + return fakefs.ScanFileList(zipList, dateFormat) + } + args := []string{"-google-photos", "-no-ui", "-debug-counters", "-log-file=" + filepath.Dir(zipList) + "/debug.log"} + + app, err := newCommand(ctx, &serv, args, fsOpener) + if err != nil { + t.Errorf("can't instantiate the UploadCmd: %s", err) + return + } + + err = app.run(ctx) + if err != nil { + t.Errorf("can't run the UploadCmd: %s", err) + return + } +} + +func TestPixilTakeOut(t *testing.T) { + initMyEnv(t) + + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/pixil/list.lst", "01-02-2006 15:04") +} diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index ed182ffc..07068996 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -70,7 +70,9 @@ type UpCmd struct { } func UploadCommand(ctx context.Context, common *cmd.SharedFlags, args []string) error { - app, err := newCommand(ctx, common, args) + app, err := newCommand(ctx, common, args, func() ([]fs.FS, error) { + return fshelper.ParsePath(args) + }) if err != nil { return err } @@ -80,7 +82,9 @@ func UploadCommand(ctx context.Context, common *cmd.SharedFlags, args []string) return app.run(ctx) } -func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string) (*UpCmd, error) { +type fsOpener func() ([]fs.FS, error) + +func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsOpener fsOpener) (*UpCmd, error) { var err error cmd := flag.NewFlagSet("upload", flag.ExitOnError) @@ -188,7 +192,7 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string) (*U return nil, err } - app.fsyss, err = fshelper.ParsePath(cmd.Args(), app.GooglePhotos) + app.fsyss, err = fsOpener() if err != nil { return nil, err } diff --git a/docs/google-takeout.md b/docs/google-takeout.md index d1ff5554..a5c4ce96 100644 --- a/docs/google-takeout.md +++ b/docs/google-takeout.md @@ -78,7 +78,7 @@ takeout-20240712T112341Z-010.zip: | ---------- | ------------------------- | ---------------- | | US English | Google Photos | metadata.json | | French | Google Photos | métadonnées.json | -| Italian | Google Foto | | +| Italian | Google Foto | metadati.json | | Slovak | Fotky Google | metadáta.json | diff --git a/helpers/fshelper/parseArgs.go b/helpers/fshelper/parseArgs.go index fff24059..8af89c70 100644 --- a/helpers/fshelper/parseArgs.go +++ b/helpers/fshelper/parseArgs.go @@ -16,7 +16,7 @@ import ( // // TODO: Implement a tgz reader for non google-photos archives -func ParsePath(args []string, googlePhoto bool) ([]fs.FS, error) { +func ParsePath(args []string) ([]fs.FS, error) { var errs error fsyss := []fs.FS{} diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go index baf883c9..9b718c0f 100644 --- a/internal/fakefs/fakefs.go +++ b/internal/fakefs/fakefs.go @@ -11,6 +11,7 @@ import ( "time" "github.com/simulot/immich-go/helpers/gen" + "github.com/simulot/immich-go/immich/metadata" ) /* @@ -26,7 +27,7 @@ type FakeDirEntry struct { modTime time.Time // modification time } -func (fi FakeDirEntry) Name() string { return filepath.Base(fi.name) } +func (fi FakeDirEntry) Name() string { return path.Base(fi.name) } func (fi FakeDirEntry) Size() int64 { return fi.size } func (fi FakeDirEntry) Mode() fs.FileMode { return fi.mode } func (fi FakeDirEntry) ModTime() time.Time { return fi.modTime } @@ -37,6 +38,7 @@ func (fi FakeDirEntry) Info() (fs.FileInfo, error) { return fi, nil } type FakeFile struct { fi FakeDirEntry + r io.Reader pos int64 } @@ -46,10 +48,9 @@ func (f FakeFile) Stat() (fs.FileInfo, error) { func (f *FakeFile) Read(b []byte) (int, error) { if f.pos < f.fi.size { - l := min(len(b), int(f.fi.size-f.pos)) - b = b[:l] - f.pos += int64(l) - return rand.Read(b) + n, err := f.r.Read(b) + f.pos += int64(n) + return n, err } return 0, io.EOF } @@ -64,7 +65,19 @@ type FakeFS struct { files map[string]map[string]FakeDirEntry } +func (fsys FakeFS) Name() string { + return fsys.name +} + +func normalizeName(name string) string { + if name != "." && !strings.HasPrefix(name, "./") { + return "./" + name + } + return name +} + func (fsys FakeFS) Stat(name string) (fs.FileInfo, error) { + name = normalizeName(name) name = filepath.ToSlash(name) dir, base := path.Split(name) dir = strings.TrimSuffix(dir, "/") @@ -83,17 +96,38 @@ func (fsys FakeFS) Stat(name string) (fs.FileInfo, error) { } func (fsys FakeFS) Open(name string) (fs.File, error) { + // name = normalizeName(name) info, err := fsys.Stat(name) if err != nil { return nil, err } - return &FakeFile{fi: info.(FakeDirEntry)}, nil + + fakeInfo := info.(FakeDirEntry) + var r io.Reader + + ext := path.Ext(name) + if strings.ToLower(ext) == ".json" { + base := path.Base(name) + switch base { + case "métadonnées.json", "metadata.json", "metadati.json", "metadáta.json": + album := path.Base(path.Dir(name)) + r, fakeInfo.size = fakeAlbumData(album) + default: + d := info.ModTime() + if d2 := metadata.TakeTimeFromName(name); !d2.IsZero() { + d = d2 + } + title := strings.TrimSuffix(path.Base(name), path.Ext(base)) + r, fakeInfo.size = fakePhotoData(title, d) + } + } else { + r = rand.Reader + } + return &FakeFile{fi: fakeInfo, r: r}, nil } func (fsys FakeFS) ReadDir(name string) ([]fs.DirEntry, error) { - if name != "." { - name = "./" + name - } + name = normalizeName(name) info, err := fsys.Stat(name) if err != nil { return nil, err @@ -119,7 +153,8 @@ func (fsys FakeFS) ReadDir(name string) ([]fs.DirEntry, error) { } func (fsys FakeFS) addFile(name string, size int64, modDate time.Time) { - dir, base := path.Split("./" + name) + name = normalizeName(name) + dir, base := path.Split(name) dir = strings.TrimSuffix(dir, "/") parts := strings.Split(dir, "/") diff --git a/internal/fakefs/metadata.go b/internal/fakefs/metadata.go new file mode 100644 index 00000000..0ca83ca6 --- /dev/null +++ b/internal/fakefs/metadata.go @@ -0,0 +1,68 @@ +package fakefs + +import ( + "fmt" + "io" + "strings" + "time" +) + +const albumTemplate = `{ + "title": "%s", + "description": "", + "access": "", + "date": { + "timestamp": "0", + "formatted": "1 janv. 1970, 00:00:00 UTC" + }, + "geoData": { + "latitude": 0.0, + "longitude": 0.0, + "altitude": 0.0, + "latitudeSpan": 0.0, + "longitudeSpan": 0.0 + } +}` + +func fakeAlbumData(name string) (io.Reader, int64) { + t := fmt.Sprintf(albumTemplate, name) + return strings.NewReader(t), int64(len(t)) +} + +const pictureTemplate = `{ + "title": "%[1]s", + "description": "", + "imageViews": "50", + "creationTime": { + "timestamp": "%[2]d" + }, + "photoTakenTime": { + "timestamp": "%[2]d" + }, + "geoData": { + "latitude": 48.0, + "longitude": 1.0, + "altitude": 102.86, + "latitudeSpan": 0.0, + "longitudeSpan": 0.0 + }, + "geoDataExif": { + "latitude": 48.0, + "longitude": 1.0, + "altitude": 102.86, + "latitudeSpan": 0.0, + "longitudeSpan": 0.0 + }, + "url": "https://photos.google.com/photo/AF1QipMZVTuUYj4K1jaN5vy6mkflX6yiWLQO2GDXSNKl", + "googlePhotosOrigin": { + "webUpload": { + "computerUpload": { + } + } + } +}` + +func fakePhotoData(name string, captureDate time.Time) (io.Reader, int64) { + t := fmt.Sprintf(pictureTemplate, name, captureDate.Unix()) + return strings.NewReader(t), int64(len(t)) +} diff --git a/internal/fakefs/ziplist_test.go b/internal/fakefs/ziplist_test.go index 7bd55128..09005c2d 100644 --- a/internal/fakefs/ziplist_test.go +++ b/internal/fakefs/ziplist_test.go @@ -62,7 +62,7 @@ type NameFS interface { } func TestFakeFS(t *testing.T) { - fsyss, err := ScanFileList("TESTDATA/one.lst", "2006-01-02 15:04") + fsyss, err := ScanFileList("TESTDATA/small.lst", "2006-01-02 15:04") if err != nil { t.Error(err) return From 91726a01a5cf3449b12427440c2c270dfea901c2 Mon Sep 17 00:00:00 2001 From: simulot Date: Sat, 20 Jul 2024 23:35:46 +0200 Subject: [PATCH 06/17] wip: counters --- browser/gp/googlephotos.go | 5 +++++ cmd/upload/takeout_test.go | 6 ++++++ internal/fakefs/fakefs.go | 3 +-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index c3162a29..4413c11c 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -549,6 +549,7 @@ func (to *Takeout) googleMDToAsset(md *GoogleMetaData, fsys fs.FS, name string) title := md.Title titleExt := path.Ext(title) fileExt := path.Ext(name) + if titleExt != fileExt { title = strings.TrimSuffix(title, titleExt) titleExt = path.Ext(title) @@ -561,6 +562,7 @@ func (to *Takeout) googleMDToAsset(md *GoogleMetaData, fsys fs.FS, name string) if err != nil { return nil, err } + a := browser.LocalAssetFile{ FileName: name, FileSize: int(i.Size()), @@ -574,14 +576,17 @@ func (to *Takeout) googleMDToAsset(md *GoogleMetaData, fsys fs.FS, name string) } // Prepare sidecar data to force Immich with Google metadata + sidecar := metadata.Metadata{ Description: md.Description, DateTaken: md.PhotoTakenTime.Time(), } + if md.GeoDataExif.Latitude != 0 || md.GeoDataExif.Longitude != 0 { sidecar.Latitude = md.GeoDataExif.Latitude sidecar.Longitude = md.GeoDataExif.Longitude } + if md.GeoData.Latitude != 0 || md.GeoData.Longitude != 0 { sidecar.Latitude = md.GeoData.Latitude sidecar.Longitude = md.GeoData.Longitude diff --git a/cmd/upload/takeout_test.go b/cmd/upload/takeout_test.go index d0d9d244..4d7fa682 100644 --- a/cmd/upload/takeout_test.go +++ b/cmd/upload/takeout_test.go @@ -48,3 +48,9 @@ func TestPixilTakeOut(t *testing.T) { simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/pixil/list.lst", "01-02-2006 15:04") } + +func TestPhyl404TakeOut(t *testing.T) { + initMyEnv(t) + + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/Phyl404/list.lst", "2006-01-02 15:04") +} diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go index 9b718c0f..e069de91 100644 --- a/internal/fakefs/fakefs.go +++ b/internal/fakefs/fakefs.go @@ -17,7 +17,6 @@ import ( /* simulate a file system based on the list of files contained into a set of archive. - */ type FakeDirEntry struct { @@ -96,7 +95,7 @@ func (fsys FakeFS) Stat(name string) (fs.FileInfo, error) { } func (fsys FakeFS) Open(name string) (fs.File, error) { - // name = normalizeName(name) + name = normalizeName(name) info, err := fsys.Stat(name) if err != nil { return nil, err From 8a9a26fef8608dd031856584568be49dcb618abc Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 21 Jul 2024 09:53:58 +0200 Subject: [PATCH 07/17] WIP takeout by directory --- browser/gp/googlephotos.go | 220 ++++++++++++++----------------------- 1 file changed, 81 insertions(+), 139 deletions(-) diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index 4413c11c..dfd95743 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -2,7 +2,6 @@ package gp import ( "context" - "fmt" "io/fs" "path" "path/filepath" @@ -20,27 +19,24 @@ import ( ) type Takeout struct { - fsyss []fs.FS - catalogs dirCatalog // file catalogs by directory in the set of the all takeout parts - jsonByYear map[jsonKey]*GoogleMetaData // assets by year of capture and base name - uploaded map[fileKey]any // track files already uploaded - albums map[string]browser.LocalAlbum // tack album names by folder - log *fileevent.Recorder - sm immich.SupportedMedia - banned namematcher.List // Banned files + fsyss []fs.FS + catalogs map[string]directoryCatalog // file catalogs by directory in the set of the all takeout parts + albums map[string]browser.LocalAlbum // track album names by folder + log *fileevent.Recorder + sm immich.SupportedMedia + banned namematcher.List // Banned files + totalUnmatched int // count the number of asset not matched } -// dirCatalog collects all directory catalogs -type dirCatalog map[string]directoryCatalog // by directory in the walker - // directoryCatalog captures all files in a given directory type directoryCatalog struct { - unMatchedFiles map[string]fileInfo // map of fileInfo by base name - matchedFiles map[string]fileInfo // map of fileInfo by base name + jsons map[string]*GoogleMetaData // JSONs in the catalog by base name + unMatchedFiles map[string]*assetFile // files to be matched map by base name + matchedFiles map[string]*assetFile // files matched by base name } -// fileInfo keep information collected during pass one -type fileInfo struct { +// assetFile keep information collected during pass one +type assetFile struct { fsys fs.FS // Remember in which part of the archive the the file base string // Remember the original file name length int // file length in bytes @@ -48,27 +44,19 @@ type fileInfo struct { md *GoogleMetaData // will point to the associated metadata } -// fileKey is the key of the uploaded files map -// GP can't have duplicate JSON name in the same year. +// fileKey track the duplicates based on the file name and its length type fileKey struct { base string length int - year int -} - -// jsonKey allow to map jsons by base name and year of capture -type jsonKey struct { - name string - year int } func NewTakeout(ctx context.Context, l *fileevent.Recorder, sm immich.SupportedMedia, fsyss ...fs.FS) (*Takeout, error) { to := Takeout{ - fsyss: fsyss, - jsonByYear: map[jsonKey]*GoogleMetaData{}, - albums: map[string]browser.LocalAlbum{}, - log: l, - sm: sm, + fsyss: fsyss, + catalogs: map[string]directoryCatalog{}, + albums: map[string]browser.LocalAlbum{}, + log: l, + sm: sm, } return &to, nil @@ -83,7 +71,6 @@ func (to *Takeout) SetBannedFiles(banned namematcher.List) *Takeout { // metadata files content is read and kept func (to *Takeout) Prepare(ctx context.Context) error { - to.catalogs = dirCatalog{} for _, w := range to.fsyss { err := to.passOneFsWalk(ctx, w) if err != nil { @@ -113,15 +100,15 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { dir = strings.TrimSuffix(dir, "/") ext := strings.ToLower(path.Ext(base)) - dirCatalog := to.catalogs[dir] - if dirCatalog.unMatchedFiles == nil { - dirCatalog.unMatchedFiles = map[string]fileInfo{} - } - if dirCatalog.matchedFiles == nil { - dirCatalog.matchedFiles = map[string]fileInfo{} + dirCatalog, ok := to.catalogs[dir] + if !ok { + dirCatalog.jsons = map[string]*GoogleMetaData{} + dirCatalog.unMatchedFiles = map[string]*assetFile{} + dirCatalog.matchedFiles = map[string]*assetFile{} } finfo, err := d.Info() if err != nil { + to.log.Record(ctx, fileevent.Error, nil, name, "error", err.Error()) return err } switch ext { @@ -130,7 +117,7 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { if err == nil { switch { case md.isAsset(): - to.addJSON(dir, base, md) + dirCatalog.jsons[base] = md to.log.Record(ctx, fileevent.DiscoveredSidecar, nil, name, "type", "asset metadata", "title", md.Title) case md.isAlbum(): a := to.albums[dir] @@ -172,15 +159,10 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { return nil } - fi := dirCatalog.unMatchedFiles[base] - fi.base = base - fi.fsys = w - fi.length = int(finfo.Size()) - fi.count++ - dirCatalog.unMatchedFiles[base] = fi - if fi.count > 1 { - // #380 not all GP duplicates are detected correctly, counters are wrong - to.log.Record(ctx, fileevent.AnalysisLocalDuplicate, nil, name) + dirCatalog.unMatchedFiles[base] = &assetFile{ + fsys: w, + base: base, + length: int(finfo.Size()), } } to.catalogs[dir] = dirCatalog @@ -190,36 +172,6 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { return err } -// addJSON stores metadata and all paths where the combo base+year has been found -func (to *Takeout) addJSON(dir, base string, md *GoogleMetaData) { - k := jsonKey{ - name: base, - year: md.PhotoTakenTime.Time().Year(), - } - - if mdPresent, ok := to.jsonByYear[k]; ok { - md = mdPresent - } - md.foundInPaths = append(md.foundInPaths, dir) - to.jsonByYear[k] = md -} - -type matcherFn func(jsonName string, fileName string, sm immich.SupportedMedia) bool - -// matchers is a list of matcherFn from the most likely to be used to the least one -var matchers = []struct { - name string - fn matcherFn -}{ - {name: "normalMatch", fn: normalMatch}, - {name: "livePhotoMatch", fn: livePhotoMatch}, - {name: "matchWithOneCharOmitted", fn: matchWithOneCharOmitted}, - {name: "matchVeryLongNameWithNumber", fn: matchVeryLongNameWithNumber}, - {name: "matchDuplicateInYear", fn: matchDuplicateInYear}, - {name: "matchEditedName", fn: matchEditedName}, - {name: "matchForgottenDuplicates", fn: matchForgottenDuplicates}, -} - // solvePuzzle prepares metadata with information collected during pass one for each accepted files // // JSON files give important information about the relative photos / movies: @@ -228,7 +180,9 @@ var matchers = []struct { // - The GPS coordinates (will be useful in a future release) // // Each JSON is checked. JSON is duplicated in albums folder. -// Associated files with the JSON can be found in the JSON's folder, or in the Year photos. +// --Associated files with the JSON can be found in the JSON's folder, or in the Year photos.-- +// ++JSON and files are located in the same folder +/// // Once associated and sent to the main program, files are tagged for not been associated with an other one JSON. // Association is done with the help of a set of matcher functions. Each one implement a rule // @@ -241,62 +195,58 @@ var matchers = []struct { // - of course those rules are likely to collide. They have to be applied from the most common to the least one. // - sometimes the file isn't in the same folder than the json... It can be found in Year's photos folder // -// The duplicates files (same name, same length in bytes) found in the local source are discarded before been presented to the immich server. +// --The duplicates files (same name, same length in bytes) found in the local source are discarded before been presented to the immich server. +// ++ Duplicates are presented to the next layer to allow the album handling // +// To solve the puzzle, each directory is checked with all matchers in the order of the most common to the least. -func (to *Takeout) solvePuzzle(ctx context.Context) error { - jsonKeys := gen.MapKeys(to.jsonByYear) - sort.Slice(jsonKeys, func(i, j int) bool { - yd := jsonKeys[i].year - jsonKeys[j].year - switch { - case yd < 0: - return true - case yd > 0: - return false - } - return jsonKeys[i].name < jsonKeys[j].name - }) +type matcherFn func(jsonName string, fileName string, sm immich.SupportedMedia) bool - // For the most common matcher to the least, - for _, matcher := range matchers { - // Check files that match each json files - for _, k := range jsonKeys { - md := to.jsonByYear[k] - - // list of paths where to search the assets: paths where this json has been found + year path in all of the walkers - paths := map[string]any{} - paths[path.Join(path.Dir(md.foundInPaths[0]), fmt.Sprintf("Photos from %d", md.PhotoTakenTime.Time().Year()))] = nil - for _, d := range md.foundInPaths { - paths[d] = nil - } - for d := range paths { - l := to.catalogs[d] - for f := range l.unMatchedFiles { +// matchers is a list of matcherFn from the most likely to be used to the least one +var matchers = []struct { + name string + fn matcherFn +}{ + {name: "normalMatch", fn: normalMatch}, + {name: "livePhotoMatch", fn: livePhotoMatch}, + {name: "matchWithOneCharOmitted", fn: matchWithOneCharOmitted}, + {name: "matchVeryLongNameWithNumber", fn: matchVeryLongNameWithNumber}, + {name: "matchDuplicateInYear", fn: matchDuplicateInYear}, + {name: "matchEditedName", fn: matchEditedName}, + {name: "matchForgottenDuplicates", fn: matchForgottenDuplicates}, +} + +func (to *Takeout) solvePuzzle(ctx context.Context) error { + dirs := gen.MapKeys(to.catalogs) + sort.Strings(dirs) + for _, dir := range dirs { + cat := to.catalogs[dir] + jsons := gen.MapKeys(cat.jsons) + sort.Strings(jsons) + for _, matcher := range matchers { + for _, json := range jsons { + md := cat.jsons[json] + for f := range cat.unMatchedFiles { select { case <-ctx.Done(): return ctx.Err() default: - if matcher.fn(k.name, f, to.sm) { - i := l.unMatchedFiles[f] + if matcher.fn(json, f, to.sm) { + i := cat.unMatchedFiles[f] i.md = md - l.matchedFiles[f] = i - to.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, l.unMatchedFiles[f], filepath.Join(d, f), "json", k.name, "year", k.year, "size", i.length, "matcher", matcher.name) - delete(l.unMatchedFiles, f) + cat.matchedFiles[f] = i + to.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, cat.unMatchedFiles[f], filepath.Join(dir, f), "json", json, "size", i.length, "matcher", matcher.name) + delete(cat.unMatchedFiles, f) } } } - to.catalogs[d] = l } } - } - - paths := gen.MapKeys(to.catalogs) - sort.Strings(paths) - for _, p := range paths { - files := gen.MapKeys(to.catalogs[p].unMatchedFiles) + to.catalogs[dir] = cat + files := gen.MapKeys(cat.unMatchedFiles) sort.Strings(files) for _, f := range files { - to.log.Record(ctx, fileevent.AnalysisMissingAssociatedMetadata, to.catalogs[p].unMatchedFiles[f], filepath.Join(p, f)) + to.log.Record(ctx, fileevent.AnalysisMissingAssociatedMetadata, f, filepath.Join(dir, f)) } } return nil @@ -448,12 +398,13 @@ func matchForgottenDuplicates(jsonName string, fileName string, sm immich.Suppor // each file net yet sent to immich is sent with associated metadata func (to *Takeout) Browse(ctx context.Context) chan *browser.LocalAssetFile { - to.uploaded = map[fileKey]any{} assetChan := make(chan *browser.LocalAssetFile) go func() { defer close(assetChan) - for dir := range to.catalogs { + dirs := gen.MapKeys(to.catalogs) + sort.Strings(dirs) + for _, dir := range dirs { if len(to.catalogs[dir].matchedFiles) > 0 { err := to.passTwo(ctx, dir, assetChan) if err != nil { @@ -461,6 +412,10 @@ func (to *Takeout) Browse(ctx context.Context) chan *browser.LocalAssetFile { } } } + + if to.totalUnmatched > 0 { + to.log.Record(ctx, fileevent.Error, nil, "", "error", "too many unmatched files with JSON. Have you processed all parts of the takeout in this run?") + } }() return assetChan } @@ -469,8 +424,8 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow catalog := to.catalogs[dir] linkedFiles := map[string]struct { - video fileInfo - image fileInfo + video *assetFile + image *assetFile }{} // detects couples image + video, likely been a motion picture @@ -498,13 +453,13 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow linked := linkedFiles[base] - if linked.image.md != nil { + if linked.image != nil { a, err = to.googleMDToAsset(linked.image.md, linked.image.fsys, path.Join(dir, linked.image.base)) if err != nil { to.log.Record(ctx, fileevent.Error, nil, path.Join(dir, linked.image.base), "error", err.Error()) continue } - if linked.video.md != nil { + if linked.video != nil { i, err := to.googleMDToAsset(linked.video.md, linked.video.fsys, path.Join(dir, linked.video.base)) if err != nil { to.log.Record(ctx, fileevent.Error, nil, path.Join(dir, linked.video.base), "error", err.Error()) @@ -524,20 +479,7 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow case <-ctx.Done(): return ctx.Err() default: - fk := fileKey{ - base: filepath.Base(a.FileName), - length: a.FileSize, - year: a.Metadata.DateTaken.Year(), - } - if _, found := to.uploaded[fk]; !found { - assetChan <- a - to.uploaded[fk] = nil - } else { - to.log.Record(ctx, fileevent.AnalysisLocalDuplicate, nil, a.FileName, "title", a.Title) - if a.LivePhoto != nil { - to.log.Record(ctx, fileevent.AnalysisLocalDuplicate, nil, a.LivePhoto.FileName, "title", a.LivePhoto.Title) - } - } + assetChan <- a } } return nil From 47c89eee931459cc84aa5cadb1af8b58076aa4ae Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 21 Jul 2024 15:31:41 +0200 Subject: [PATCH 08/17] fix name matchers for duplicate in year and live photo --- browser/gp/googlephotos.go | 94 +++++++++++------ browser/gp/googlephotos_test.go | 112 ++++++++++++++++++--- browser/gp/testgp_test.go | 172 ++++++++++++++++---------------- docs/releases.md | 21 ++++ immich/client.go | 3 + internal/fakefs/fakefs.go | 2 + internal/fakefs/metadata.go | 9 ++ 7 files changed, 287 insertions(+), 126 deletions(-) diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index dfd95743..31f201a0 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -5,6 +5,8 @@ import ( "io/fs" "path" "path/filepath" + "regexp" + "runtime" "sort" "strings" "unicode/utf8" @@ -26,6 +28,8 @@ type Takeout struct { sm immich.SupportedMedia banned namematcher.List // Banned files totalUnmatched int // count the number of asset not matched + totalSent int // DEBUG + files map[string]any // DEBUG } // directoryCatalog captures all files in a given directory @@ -44,12 +48,6 @@ type assetFile struct { md *GoogleMetaData // will point to the associated metadata } -// fileKey track the duplicates based on the file name and its length -type fileKey struct { - base string - length int -} - func NewTakeout(ctx context.Context, l *fileevent.Recorder, sm immich.SupportedMedia, fsyss ...fs.FS) (*Takeout, error) { to := Takeout{ fsyss: fsyss, @@ -57,6 +55,7 @@ func NewTakeout(ctx context.Context, l *fileevent.Recorder, sm immich.SupportedM albums: map[string]browser.LocalAlbum{}, log: l, sm: sm, + files: map[string]any{}, } return &to, nil @@ -159,6 +158,7 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { return nil } + to.files[name] = nil // DEBUG dirCatalog.unMatchedFiles[base] = &assetFile{ fsys: w, base: base, @@ -285,21 +285,24 @@ func livePhotoMatch(jsonName string, fileName string, sm immich.SupportedMedia) // PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGIN.json // PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg // -// 05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json +// 05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json <-- match also with LivePhoto matcher // 05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg // // 😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json // 😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg func matchWithOneCharOmitted(jsonName string, fileName string, sm immich.SupportedMedia) bool { - base := strings.TrimSuffix(jsonName, path.Ext(jsonName)) - if strings.HasPrefix(fileName, base) { - if t := sm.TypeFromExt(path.Ext(base)); t == immich.TypeImage || t == immich.TypeVideo { - // Trim only if the EXT is known extension, and not .COVER or .ORIGINAL - base = strings.TrimSuffix(base, path.Ext(base)) - } - fileName = strings.TrimSuffix(fileName, path.Ext(fileName)) - a, b := utf8.RuneCountInString(fileName), utf8.RuneCountInString(base) + baseJSON := strings.TrimSuffix(jsonName, path.Ext(jsonName)) + ext := path.Ext(baseJSON) + if sm.IsExtensionPrefix(ext) { + baseJSON = strings.TrimSuffix(baseJSON, ext) + } + fileName = strings.TrimSuffix(fileName, path.Ext(fileName)) + if fileName == baseJSON { + return true + } + if strings.HasPrefix(fileName, baseJSON) { + a, b := utf8.RuneCountInString(fileName), utf8.RuneCountInString(baseJSON) if a-b <= 1 { return true } @@ -337,22 +340,46 @@ func matchVeryLongNameWithNumber(jsonName string, fileName string, sm immich.Sup // // IMG_3479.JPG(2).json // IMG_3479(2).JPG +// + +// Fast implementation, but does't work with live photos +// func matchDuplicateInYear(jsonName string, fileName string, sm immich.SupportedMedia) bool { +// jsonName = strings.TrimSuffix(jsonName, path.Ext(jsonName)) +// p1JSON := strings.Index(jsonName, "(") +// if p1JSON < 1 { +// return false +// } +// p2JSON := strings.Index(jsonName, ")") +// if p2JSON < 0 || p2JSON != len(jsonName)-1 { +// return false +// } + +// num := jsonName[p1JSON:] +// jsonName = strings.TrimSuffix(jsonName, num) +// ext := path.Ext(jsonName) +// jsonName = strings.TrimSuffix(jsonName, ext) + num + ext +// return jsonName == fileName +// } + +// Regexp implementation, work with live photos, 10 times slower +var ( + reDupInYearJSON = regexp.MustCompile(`(.*)\.(.{2,4})\((\d+)\)\..{2,4}$`) + reDupInYearFile = regexp.MustCompile(`(.*)\((\d+)\)\..{2,4}$`) +) + func matchDuplicateInYear(jsonName string, fileName string, sm immich.SupportedMedia) bool { - jsonName = strings.TrimSuffix(jsonName, path.Ext(jsonName)) - p1JSON := strings.Index(jsonName, "(") - if p1JSON < 1 { + mFile := reDupInYearFile.FindStringSubmatch(fileName) + if len(mFile) < 3 { return false } - p2JSON := strings.Index(jsonName, ")") - if p2JSON < 0 || p2JSON != len(jsonName)-1 { + mJSON := reDupInYearJSON.FindStringSubmatch(jsonName) + if len(mJSON) < 4 { return false } - - num := jsonName[p1JSON:] - jsonName = strings.TrimSuffix(jsonName, num) - ext := path.Ext(jsonName) - jsonName = strings.TrimSuffix(jsonName, ext) + num + ext - return jsonName == fileName + if mFile[1] == mJSON[1] && mFile[2] == mJSON[3] { + return true + } + return false } // matchEditedName @@ -376,7 +403,7 @@ func matchEditedName(jsonName string, fileName string, sm immich.SupportedMedia) // TODO: This one interferes with matchVeryLongNameWithNumber // matchForgottenDuplicates -// original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json +// "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json" // original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg // original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg @@ -474,11 +501,22 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow continue } } - + if base == "image000000" { + runtime.Breakpoint() + } + if a.FileName == "Takeout/Google Photos/Photos from 2021/image000000.gif" { + runtime.Breakpoint() + } select { case <-ctx.Done(): return ctx.Err() default: + delete(to.files, a.FileName) + to.totalSent++ + if a.LivePhoto != nil { + delete(to.files, a.LivePhoto.FileName) + to.totalSent++ + } assetChan <- a } } diff --git a/browser/gp/googlephotos_test.go b/browser/gp/googlephotos_test.go index ef883290..9135e9c6 100644 --- a/browser/gp/googlephotos_test.go +++ b/browser/gp/googlephotos_test.go @@ -6,37 +6,99 @@ import ( "github.com/simulot/immich-go/immich" ) -func Test_matchEditedName(t *testing.T) { +func Test_matchers(t *testing.T) { tests := []struct { jsonName string fileName string - want bool + want string }{ + { + jsonName: "PXL_20211013_220651983.jpg.json", + fileName: "PXL_20211013_220651983.jpg", + want: "normalMatch", + }, { jsonName: "PXL_20220405_090123740.PORTRAIT.jpg.json", fileName: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", - want: true, + want: "matchEditedName", }, { jsonName: "PXL_20220405_090123740.PORTRAIT.jpg.json", fileName: "PXL_20220405_100123740.PORTRAIT-modifié.jpg", - want: false, + want: "", }, { jsonName: "DSC_0238.JPG.json", fileName: "DSC_0238.JPG", - want: true, + want: "normalMatch", + }, + { + jsonName: "DSC_0238.JPG(1).json", + fileName: "DSC_0238(1).JPG", + want: "matchDuplicateInYear", + }, + { + jsonName: "IMG_2710.HEIC(1).json", + fileName: "IMG_2710(1).HEIC", + want: "matchDuplicateInYear", + }, + { + jsonName: "IMG_2710.HEIC(1).json", + fileName: "IMG_2710(1).MP4", + want: "matchDuplicateInYear", + }, + { + jsonName: "PXL_20231118_035751175.MP.jpg.json", + fileName: "PXL_20231118_035751175.MP.jpg", + want: "normalMatch", + }, + { + jsonName: "PXL_20231118_035751175.MP.jpg.json", + fileName: "PXL_20231118_035751175.MP", + want: "livePhotoMatch", + }, + { + jsonName: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGIN.json", + fileName: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", + want: "matchWithOneCharOmitted", + }, + { + jsonName: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json", + fileName: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", + want: "livePhotoMatch", + }, + { + jsonName: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json", + fileName: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", + want: "matchWithOneCharOmitted", + }, + { + jsonName: "Backyard_ceremony_wedding_photography_xxxxxxx_(494).json", + fileName: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", + want: "matchVeryLongNameWithNumber", + }, + { + jsonName: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json", + fileName: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", + want: "matchWithOneCharOmitted", + }, + { + jsonName: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json", + fileName: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", + want: "matchForgottenDuplicates", }, - // { - // jsonName: "DSC_0238.JPG.json", - // fileName: "DSC_0238(1).JPG", - // want: false, - // }, } for _, tt := range tests { t.Run(tt.fileName, func(t *testing.T) { - if got := matchEditedName(tt.jsonName, tt.fileName, immich.DefaultSupportedMedia); got != tt.want { - t.Errorf("matchEditedName() = %v, want %v", got, tt.want) + matcher := "" + for _, m := range matchers { + if m.fn(tt.jsonName, tt.fileName, immich.DefaultSupportedMedia) { + matcher = m.name + break + } + } + if matcher != tt.want { + t.Errorf("matcher is '%s', want %v", matcher, tt.want) } }) } @@ -125,3 +187,29 @@ func Test_matchForgottenDuplicates(t *testing.T) { }) } } + +/* indexes, but false +goos: linux +goarch: amd64 +pkg: github.com/simulot/immich-go/browser/gp +cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz +Benchmark_matchDuplicateInYear-12 27067428 52.06 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/simulot/immich-go/browser/gp 1.458s + +goos: linux +goarch: amd64 +pkg: github.com/simulot/immich-go/browser/gp +cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz +Benchmark_matchDuplicateInYear-12 881652 1491 ns/op 240 B/op 4 allocs/op +PASS +ok github.com/simulot/immich-go/browser/gp 1.332s + + +*/ + +func Benchmark_matchDuplicateInYear(b *testing.B) { + for i := 0; i < b.N; i++ { + matchDuplicateInYear("IMG_3479.JPG(2).json", "IMG_3479(2).JPG", nil) + } +} diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index 80b20dc1..cb482833 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -19,92 +19,92 @@ func TestBrowse(t *testing.T) { gen func() *inMemFS results []fileResult // file name / title }{ - // { - // "simpleYear", simpleYear, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, - // }), - // }, - - // { - // "simpleAlbum", simpleAlbum, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, - // {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, - // }), - // }, - - // { - // "albumWithoutImage", albumWithoutImage, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - // }), - // }, - // { - // "namesWithNumbers", namesWithNumbers, - // sortFileResult([]fileResult{ - // {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, - // {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, - // {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, - // }), - // }, - // { - // "namesTruncated", namesTruncated, - // sortFileResult([]fileResult{ - // {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, - // {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - // {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, - // }), - // }, - - // { - // "imagesWithoutJSON", imagesEditedJSON, - // sortFileResult([]fileResult{ - // {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - // {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - // }), - // }, - - // { - // "titlesWithForbiddenChars", titlesWithForbiddenChars, - // sortFileResult([]fileResult{ - // {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, - // {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, - // }), - // }, - // { - // "namesIssue39", namesIssue39, - // sortFileResult([]fileResult{ - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - // }), - // }, - // { - // "issue68MPFiles", issue68MPFiles, - // sortFileResult([]fileResult{ - // {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, - // }), - // }, - // { - // "issue68LongExposure", issue68LongExposure, - // sortFileResult([]fileResult{ - // {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, - // {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - // }), - // }, - - // { - // "issue68ForgottenDuplicates", issue68ForgottenDuplicates, - // sortFileResult([]fileResult{ - // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - // }), - // }, + { + "simpleYear", simpleYear, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, + }), + }, + + { + "simpleAlbum", simpleAlbum, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, + }), + }, + + { + "albumWithoutImage", albumWithoutImage, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + }), + }, + { + "namesWithNumbers", namesWithNumbers, + sortFileResult([]fileResult{ + {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, + {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, + {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, + }), + }, + { + "namesTruncated", namesTruncated, + sortFileResult([]fileResult{ + {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, + {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, + }), + }, + + { + "imagesWithoutJSON", imagesEditedJSON, + sortFileResult([]fileResult{ + {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + }), + }, + + { + "titlesWithForbiddenChars", titlesWithForbiddenChars, + sortFileResult([]fileResult{ + {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, + {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, + }), + }, + { + "namesIssue39", namesIssue39, + sortFileResult([]fileResult{ + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + }), + }, + { + "issue68MPFiles", issue68MPFiles, + sortFileResult([]fileResult{ + {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, + }), + }, + { + "issue68LongExposure", issue68LongExposure, + sortFileResult([]fileResult{ + {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, + {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + }), + }, + + { + "issue68ForgottenDuplicates", issue68ForgottenDuplicates, + sortFileResult([]fileResult{ + {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + }), + }, { "issue390WrongCount", issue390WrongCount, sortFileResult([]fileResult{ diff --git a/docs/releases.md b/docs/releases.md index 881e962f..3472639f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -7,8 +7,29 @@ ## Release next +### TO DO +- [ ] Counters / missing assets + - [ ] [#390](https://github.com/simulot/immich-go/issues/390) + - [ ] [#376](https://github.com/simulot/immich-go/issues/376) +- [ ] Report connection errors + - [ ] [#395](https://github.com/simulot/immich-go/issues/395) + - [ ] [#396](https://github.com/simulot/immich-go/issues/396) + - [ ] [#393](https://github.com/simulot/immich-go/issues/393) +- [ ] Creation date incorrect + - [ ] [#392](https://github.com/simulot/immich-go/issues/392) + - [ ] [#397](https://github.com/simulot/immich-go/issues/397) + + + + +### Refactoring the Google Photos import another time +Lot of users have reported inconsistencies in upload counters. Because we are manipulating personal photos and because it's not practical to test with user's archives, +I have developed a way to simulate user cases using only the list files from the takeout archives. + + ### fixes: - [[#376](https://github.com/simulot/immich-go/issues/376)] errors when uploading are disturbing the the % of the progression +- [ ] IPhone's Live photos recognition when the name is duplicated: ex IMG_2710(1).MP4 and IMG_2710(1).HEIC ## Release 0.20.1 diff --git a/immich/client.go b/immich/client.go index aef111b5..61f15467 100644 --- a/immich/client.go +++ b/immich/client.go @@ -225,6 +225,9 @@ func (sm SupportedMedia) IsMedia(ext string) bool { func (sm SupportedMedia) IsExtensionPrefix(ext string) bool { ext = strings.ToLower(ext) + if len(ext) > 4 { + return false + } for e, t := range sm { if t == TypeVideo || t == TypeImage { if ext == e[:len(e)-1] { diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go index e069de91..ba6e9d5e 100644 --- a/internal/fakefs/fakefs.go +++ b/internal/fakefs/fakefs.go @@ -111,6 +111,8 @@ func (fsys FakeFS) Open(name string) (fs.File, error) { case "métadonnées.json", "metadata.json", "metadati.json", "metadáta.json": album := path.Base(path.Dir(name)) r, fakeInfo.size = fakeAlbumData(album) + case "print-subscriptions.json", "shared_album_comments.json", "user-generated-memory-titles.json": + r, fakeInfo.size = fakeJSON() default: d := info.ModTime() if d2 := metadata.TakeTimeFromName(name); !d2.IsZero() { diff --git a/internal/fakefs/metadata.go b/internal/fakefs/metadata.go index 0ca83ca6..1b7376fa 100644 --- a/internal/fakefs/metadata.go +++ b/internal/fakefs/metadata.go @@ -66,3 +66,12 @@ func fakePhotoData(name string, captureDate time.Time) (io.Reader, int64) { t := fmt.Sprintf(pictureTemplate, name, captureDate.Unix()) return strings.NewReader(t), int64(len(t)) } + +const fakeJSONTemplate = `{ + "Nothing": "" +}` + +func fakeJSON() (io.Reader, int64) { + t := fakeJSONTemplate + return strings.NewReader(t), int64(len(t)) +} From 042afcf2241505203c6daf2c5f0b89def87adfe7 Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 21 Jul 2024 18:40:18 +0200 Subject: [PATCH 09/17] wip fixe: missing image from the same directory but different type --- browser/gp/googlephotos.go | 21 +-- browser/gp/testgp_samples_test.go | 166 +++++++++++--------- browser/gp/testgp_test.go | 250 ++++++++++++++++-------------- docs/releases.md | 3 +- internal/fakefs/ziplist.go | 10 ++ 5 files changed, 249 insertions(+), 201 deletions(-) diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index 31f201a0..fb56f8e9 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -6,7 +6,6 @@ import ( "path" "path/filepath" "regexp" - "runtime" "sort" "strings" "unicode/utf8" @@ -458,20 +457,20 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow // detects couples image + video, likely been a motion picture for _, f := range gen.MapKeys(catalog.matchedFiles) { ext := path.Ext(f) - base := strings.TrimSuffix(f, ext) - ext2 := path.Ext(base) - if to.sm.IsMedia(ext2) { - base = strings.TrimSuffix(base, ext2) - } + // base := strings.TrimSuffix(f, ext) + // ext2 := path.Ext(base) + // if to.sm.IsMedia(ext2) { + // base = strings.TrimSuffix(base, ext2) + // } - linked := linkedFiles[base] + linked := linkedFiles[f] switch to.sm.TypeFromExt(ext) { case immich.TypeVideo: linked.video = catalog.matchedFiles[f] case immich.TypeImage: linked.image = catalog.matchedFiles[f] } - linkedFiles[base] = linked + linkedFiles[f] = linked } for _, base := range gen.MapKeys(linkedFiles) { @@ -501,12 +500,6 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow continue } } - if base == "image000000" { - runtime.Breakpoint() - } - if a.FileName == "Takeout/Google Photos/Photos from 2021/image000000.gif" { - runtime.Breakpoint() - } select { case <-ctx.Done(): return ctx.Err() diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index aa63eaa4..5c0938c8 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -115,121 +115,143 @@ func sortFileResult(s []fileResult) []fileResult { func simpleYear() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg", 10). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144956000.jpg.json", "PXL_20230922_144956000.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144956000.jpg", 20) + addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). + addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10). + addJSONImage("Photos from 2023/PXL_20230922_144956000.jpg.json", "PXL_20230922_144956000.jpg"). + addImage("Photos from 2023/PXL_20230922_144956000.jpg", 20) } func simpleAlbum() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg", takenTime("PXL_20230922_144936660")). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg", 10). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg", takenTime("PXL_20230922_144934440")). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144934440.jpg", 15). - addJSONAlbum("Takeout/Google Photos/Album/anyname.json", "Album"). - addJSONImage("Takeout/Google Photos/Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg", takenTime("PXL_20230922_144936660")). - addImage("Takeout/Google Photos/Album/PXL_20230922_144936660.jpg", 10). - addJSONImage("Takeout/Google Photos/Photos from 2023/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20230922102100")). - addImage("Takeout/Google Photos/Photos from 2023/IMG_8172.jpg", 52). - addJSONImage("Takeout/Google Photos/Album/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20230922102100")). - addImage("Takeout/Google Photos/Album/IMG_8172.jpg", 52). - addJSONImage("Takeout/Google Photos/Photos from 2020/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20200101103000")). - addImage("Takeout/Google Photos/Photos from 2020/IMG_8172.jpg", 25) + addJSONImage("Photos from 2020/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20200101103000")). + addImage("Photos from 2020/IMG_8172.jpg", 25). + addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg", takenTime("PXL_20230922_144936660")). + addJSONImage("Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg", takenTime("PXL_20230922_144934440")). + addJSONImage("Photos from 2023/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20230922102100")). + addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10). + addImage("Photos from 2023/PXL_20230922_144934440.jpg", 15). + addImage("Photos from 2023/IMG_8172.jpg", 52). + addJSONAlbum("Album/anyname.json", "Album"). + addJSONImage("Album/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20230922102100")). + addJSONImage("Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg", takenTime("PXL_20230922_144936660")). + addImage("Album/IMG_8172.jpg", 52). + addImage("Album/PXL_20230922_144936660.jpg", 10) } func albumWithoutImage() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144936660.jpg", 10). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230922_144934440.jpg", 15). - addJSONAlbum("Takeout/Google Photos/Album/anyname.json", "Album"). - addJSONImage("Takeout/Google Photos/Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). - addImage("Takeout/Google Photos/Album/PXL_20230922_144936660.jpg", 10). - addJSONImage("Takeout/Google Photos/Album/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg") + addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). + addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10). + addJSONImage("Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). + addImage("Photos from 2023/PXL_20230922_144934440.jpg", 15). + addJSONAlbum("Album/anyname.json", "Album"). + addJSONImage("Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). + addImage("Album/PXL_20230922_144936660.jpg", 10). + addJSONImage("Album/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg") } func namesWithNumbers() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2009/IMG_3479.JPG.json", "IMG_3479.JPG"). - addImage("Takeout/Google Photos/Photos from 2009/IMG_3479.JPG", 10). - addJSONImage("Takeout/Google Photos/Photos from 2009/IMG_3479.JPG(1).json", "IMG_3479.JPG"). - addImage("Takeout/Google Photos/Photos from 2009/IMG_3479(1).JPG", 12). - addJSONImage("Takeout/Google Photos/Photos from 2009/IMG_3479.JPG(2).json", "IMG_3479.JPG"). - addImage("Takeout/Google Photos/Photos from 2009/IMG_3479(2).JPG", 15) + addJSONImage("Photos from 2009/IMG_3479.JPG.json", "IMG_3479.JPG"). + addImage("Photos from 2009/IMG_3479.JPG", 10). + addJSONImage("Photos from 2009/IMG_3479.JPG(1).json", "IMG_3479.JPG"). + addImage("Photos from 2009/IMG_3479(1).JPG", 12). + addJSONImage("Photos from 2009/IMG_3479.JPG(2).json", "IMG_3479.JPG"). + addImage("Photos from 2009/IMG_3479(2).JPG", 15) } func namesTruncated() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json", "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", 10). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", 40). - addJSONImage("Takeout/Google Photos/Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json", "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", 25) + addJSONImage("Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json", "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"). + addImage("Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", 10). + addJSONImage("Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"). + addImage("Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", 40). + addJSONImage("Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json", "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"). + addImage("Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", 25) } func imagesEditedJSON() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg.json", "PXL_20220405_090123740.PORTRAIT.jpg"). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg", 41). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20220405_090123740.PORTRAIT-modifié.jpg", 21). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20220405_090200110.PORTRAIT-modifié.jpg", 12) + addJSONImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg.json", "PXL_20220405_090123740.PORTRAIT.jpg"). + addImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg", 41). + addImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT-modifié.jpg", 21). + addImage("Photos from 2023/PXL_20220405_090200110.PORTRAIT-modifié.jpg", 12) } func titlesWithForbiddenChars() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2012/27_06_12 - 1.mov.json", "27/06/12 - 1", takenTime("20120627")). - addImage("Takeout/Google Photos/Photos from 2012/27_06_12 - 1.mov", 52). - addJSONImage("Takeout/Google Photos/Photos from 2012/27_06_12 - 2.json", "27/06/12 - 2", takenTime("20120627")). - addImage("Takeout/Google Photos/Photos from 2012/27_06_12 - 2.jpg", 24) + addJSONImage("Photos from 2012/27_06_12 - 1.mov.json", "27/06/12 - 1", takenTime("20120627")). + addImage("Photos from 2012/27_06_12 - 1.mov", 52). + addJSONImage("Photos from 2012/27_06_12 - 2.json", "27/06/12 - 2", takenTime("20120627")). + addImage("Photos from 2012/27_06_12 - 2.jpg", 24) } func namesIssue39() *inMemFS { return newInMemFS(). - addJSONAlbum("Takeout/Google Photos/Album/anyname.json", "Album"). - addJSONImage("Takeout/Google Photos/Album/Backyard_ceremony_wedding_photography_xxxxxxx_.json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg", takenTime("20200101")). - addImage("Takeout/Google Photos/Album/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1). - addImage("Takeout/Google Photos/Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1). - addJSONImage("Takeout/Google Photos/Album/Backyard_ceremony_wedding_photography_xxxxxxx_(1).json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg", takenTime("20200101")). - addImage("Takeout/Google Photos/Album/Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", 181). - addImage("Takeout/Google Photos/Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", 181). - addJSONImage("Takeout/Google Photos/Album/Backyard_ceremony_wedding_photography_xxxxxxx_(494).json", "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg", takenTime("20200101")). - addImage("Takeout/Google Photos/Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", 494) + addJSONAlbum("Album/anyname.json", "Album"). + addJSONImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_.json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg", takenTime("20200101")). + addJSONImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_(1).json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg", takenTime("20200101")). + addJSONImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_(494).json", "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg", takenTime("20200101")). + addImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1). + addImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", 181). + addImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", 494). + addJSONImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_.json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg", takenTime("20200101")). + addJSONImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_(1).json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg", takenTime("20200101")). + addJSONImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_(494).json", "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg", takenTime("20200101")). + addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", 181). + addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", 494). + addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1) } func issue68MPFiles() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2022/PXL_20221228_185930354.MP.jpg.json", "PXL_20221228_185930354.MP.jpg", takenTime("20220101")). - addImage("Takeout/Google Photos/Photos from 2022/PXL_20221228_185930354.MP", 1). - addImage("Takeout/Google Photos/Photos from 2022/PXL_20221228_185930354.MP.jpg", 2) + addJSONImage("Photos from 2022/PXL_20221228_185930354.MP.jpg.json", "PXL_20221228_185930354.MP.jpg", takenTime("20220101")). + addImage("Photos from 2022/PXL_20221228_185930354.MP", 1). + addImage("Photos from 2022/PXL_20221228_185930354.MP.jpg", 2) } func issue68LongExposure() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER..json", "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", takenTime("20230101")). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", 1). - addJSONImage("Takeout/Google Photos/Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg", takenTime("20230101")). - addImage("Takeout/Google Photos/Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", 2) + addJSONImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER..json", "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", takenTime("20230101")). + addImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", 1). + addJSONImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg", takenTime("20230101")). + addImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", 2) } func issue68ForgottenDuplicates() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json", "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg", takenTime("20220101")). - addImage("Takeout/Google Photos/Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", 1). - addImage("Takeout/Google Photos/Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", 2) + addJSONImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json", "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg", takenTime("20220101")). + addImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", 1). + addImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", 2) } // #390 Question: report shows way less images uploaded than scanned func issue390WrongCount() *inMemFS { return newInMemFS(). - addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG.json", "IMG_0170.JPG", takenTime("20180101")). - addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG", 10). - addJSONAlbum("Takeout/Google Photos/2018 - Cambodia/metadata.json", "2018 - Cambodia"). - addJSONImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG.json", "IMG_0170.JPG", takenTime("20180101")). - addImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG/Photos from 2018/IMG_0170.JPG", 10). - addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC.json", "IMG_0170.HEIC", takenTime("20180601")). - addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC", 20). - addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.MP4", 200) + addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.jpg.json", "image000000.jpg"). + addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.jpg(1).json", "image000000.jpg"). + addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.gif.json", "image000000.gif.json"). + addImage("Takeout/Google Photos/Photos from 2021/image000000.gif", 10). + addImage("Takeout/Google Photos/Photos from 2021/image000000.jpg", 20) +} + +func issue390WrongCount2() *inMemFS { + return newInMemFS(). + addJSONImage("Takeout/Google Photos/2017 - Croatia/IMG_0170.jpg.json", "IMG_0170.jpg"). + addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG.json", "IMG_0170.JPG"). + addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC.json", "IMG_0170.HEIC"). + addJSONImage("Takeout/Google Photos/Photos from 2023/IMG_0170.HEIC.json", "IMG_0170.HEIC"). + addJSONImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG.json", "IMG_0170.JPG"). + addJSONImage("Takeout/Google Photos/2023 - Belize/IMG_0170.HEIC.json", "IMG_0170.HEIC"). + addJSONImage("Takeout/Google Photos/Photos from 2017/IMG_0170.jpg.json", "IMG_0170.jpg"). + addImage("Takeout/Google Photos/2017 - Croatia/IMG_0170.jpg", 514963). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.HEIC", 1332980). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG", 4570661). + addImage("Takeout/Google Photos/Photos from 2023/IMG_0170.MP4", 6024972). + addImage("Takeout/Google Photos/Photos from 2023/IMG_0170.HEIC", 4443973). + addImage("Takeout/Google Photos/Photos from 2018/IMG_0170.MP4", 2288647). + addImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG", 4570661). + addImage("Takeout/Google Photos/2023 - Belize/IMG_0170.MP4", 6024972). + addImage("Takeout/Google Photos/2023 - Belize/IMG_0170.HEIC", 4443973). + addImage("Takeout/Google Photos/Photos from 2017/IMG_0170.jpg", 514963) } diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index cb482833..d410508a 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -15,102 +15,122 @@ import ( func TestBrowse(t *testing.T) { tc := []struct { - name string - gen func() *inMemFS - results []fileResult // file name / title + name string + gen func() *inMemFS + want []fileResult // file name / title }{ - { - "simpleYear", simpleYear, - sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, - }), - }, + // { + // "simpleYear", simpleYear, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, + // }), + // }, - { - "simpleAlbum", simpleAlbum, - sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, - {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, - }), - }, + // { + // "simpleAlbum", simpleAlbum, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + // {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, + // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + // }), + // }, - { - "albumWithoutImage", albumWithoutImage, - sortFileResult([]fileResult{ - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - }), - }, - { - "namesWithNumbers", namesWithNumbers, - sortFileResult([]fileResult{ - {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, - {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, - {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, - }), - }, - { - "namesTruncated", namesTruncated, - sortFileResult([]fileResult{ - {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, - {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, - }), - }, + // { + // "albumWithoutImage", albumWithoutImage, + // sortFileResult([]fileResult{ + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + // }), + // }, + // { + // "namesWithNumbers", namesWithNumbers, + // sortFileResult([]fileResult{ + // {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, + // {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, + // {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, + // }), + // }, + // { + // "namesTruncated", namesTruncated, + // sortFileResult([]fileResult{ + // {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, + // {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + // {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, + // }), + // }, - { - "imagesWithoutJSON", imagesEditedJSON, - sortFileResult([]fileResult{ - {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - }), - }, + // { + // "imagesWithoutJSON", imagesEditedJSON, + // sortFileResult([]fileResult{ + // {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + // {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + // }), + // }, - { - "titlesWithForbiddenChars", titlesWithForbiddenChars, - sortFileResult([]fileResult{ - {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, - {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, - }), - }, - { - "namesIssue39", namesIssue39, - sortFileResult([]fileResult{ - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - }), - }, - { - "issue68MPFiles", issue68MPFiles, - sortFileResult([]fileResult{ - {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, - }), - }, - { - "issue68LongExposure", issue68LongExposure, - sortFileResult([]fileResult{ - {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, - {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - }), - }, + // { + // "titlesWithForbiddenChars", titlesWithForbiddenChars, + // sortFileResult([]fileResult{ + // {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, + // {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, + // }), + // }, + // { + // "namesIssue39", namesIssue39, + // sortFileResult([]fileResult{ + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + // }), + // }, + // { + // "issue68MPFiles", issue68MPFiles, + // sortFileResult([]fileResult{ + // {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, + // }), + // }, + // { + // "issue68LongExposure", issue68LongExposure, + // sortFileResult([]fileResult{ + // {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, + // {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + // }), + // }, + // { + // "issue68ForgottenDuplicates", issue68ForgottenDuplicates, + // sortFileResult([]fileResult{ + // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + // }), + // }, + // { + // "issue390WrongCount", issue390WrongCount, + // sortFileResult([]fileResult{ + // {name: "image000000.gif", size: 10, title: "image000000.gif"}, + // {name: "image000000.jpg", size: 20, title: "image000000.jpg"}, + // }), + // }, { - "issue68ForgottenDuplicates", issue68ForgottenDuplicates, - sortFileResult([]fileResult{ - {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - }), - }, - { - "issue390WrongCount", issue390WrongCount, + "issue390WrongCount2", issue390WrongCount2, sortFileResult([]fileResult{ - {name: "IMG_0170.JPG", size: 10, title: "IMG_0170.JPG"}, - {name: "IMG_0170.HEIC", size: 20, title: "IMG_0170.JPG"}, - {name: "IMG_0170.MP4", size: 200, title: "IMG_0170.MP4"}, + {name: "IMG_0170.jpg", size: 514963, title: "IMG_0170.jpg"}, + {name: "IMG_0170.HEIC", size: 1332980, title: "IMG_0170.HEIC"}, + {name: "IMG_0170.JPG", size: 4570661, title: "IMG_0170.JPG"}, + {name: "IMG_0170.MP4", size: 6024972, title: "IMG_0170.MP4"}, + {name: "IMG_0170.HEIC", size: 4443973, title: "IMG_0170.HEIC"}, + {name: "IMG_0170.MP4", size: 2288647, title: "IMG_0170.MP4"}, + {name: "IMG_0170.JPG", size: 4570661, title: "IMG_0170.JPG"}, + {name: "IMG_0170.MP4", size: 6024972, title: "IMG_0170.MP4"}, + {name: "IMG_0170.HEIC", size: 4443973, title: "IMG_0170.HEIC"}, + {name: "IMG_0170.jpg", size: 514963, title: "IMG_0170.jpg"}, }), }, } @@ -141,9 +161,9 @@ func TestBrowse(t *testing.T) { } results = sortFileResult(results) - if !reflect.DeepEqual(results, c.results) { + if !reflect.DeepEqual(results, c.want) { t.Errorf("difference\n") - pretty.Ldiff(t, c.results, results) + pretty.Ldiff(t, c.want, results) } }) } @@ -156,11 +176,11 @@ func TestAlbums(t *testing.T) { gen func() *inMemFS albums album }{ - { - name: "simpleYear", - gen: simpleYear, - albums: album{}, - }, + // { + // name: "simpleYear", + // gen: simpleYear, + // albums: album{}, + // }, { name: "simpleAlbum", gen: simpleAlbum, @@ -171,27 +191,29 @@ func TestAlbums(t *testing.T) { }), }, }, - { - name: "albumWithoutImage", - gen: albumWithoutImage, - albums: album{ - "Album": sortFileResult([]fileResult{ - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - }), + /* + { + name: "albumWithoutImage", + gen: albumWithoutImage, + albums: album{ + "Album": sortFileResult([]fileResult{ + {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + }), + }, }, - }, - { - name: "namesIssue39", - gen: namesIssue39, - albums: album{ - "Album": sortFileResult([]fileResult{ - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - }), + { + name: "namesIssue39", + gen: namesIssue39, + albums: album{ + "Album": sortFileResult([]fileResult{ + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + }), + }, }, - }, + */ } for _, c := range tc { diff --git a/docs/releases.md b/docs/releases.md index 3472639f..5538c495 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -8,9 +8,10 @@ ## Release next ### TO DO -- [ ] Counters / missing assets +- [ ] Counters / missing assets GooglePhotos - [ ] [#390](https://github.com/simulot/immich-go/issues/390) - [ ] [#376](https://github.com/simulot/immich-go/issues/376) + - [ ] Some files are not matched (PIXIL) - [ ] Report connection errors - [ ] [#395](https://github.com/simulot/immich-go/issues/395) - [ ] [#396](https://github.com/simulot/immich-go/issues/396) diff --git a/internal/fakefs/ziplist.go b/internal/fakefs/ziplist.go index 6467aa18..d56c183b 100644 --- a/internal/fakefs/ziplist.go +++ b/internal/fakefs/ziplist.go @@ -5,6 +5,7 @@ package fakefs */ import ( "bufio" + "io" "io/fs" "os" "strconv" @@ -25,13 +26,22 @@ func readFileLine(l string, dateFormat string) (string, int64, time.Time) { return name, size, modTime } +func ScanStringList(dateFormat string, s string) ([]fs.FS, error) { + r := strings.NewReader(s) + + return ScanFileListReader(r, dateFormat) +} + func ScanFileList(name string, dateFormat string) ([]fs.FS, error) { f, err := os.Open(name) if err != nil { return nil, err } defer f.Close() + return ScanFileListReader(f, dateFormat) +} +func ScanFileListReader(f io.Reader, dateFormat string) ([]fs.FS, error) { fsyss := map[string]*FakeFS{} var fsys *FakeFS currentZip := "" From 9505b8d0b766b54af2022d7b360013a25adececc Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 21 Jul 2024 18:44:29 +0200 Subject: [PATCH 10/17] edit release.md --- docs/releases.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 5538c495..b073e863 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -11,7 +11,10 @@ - [ ] Counters / missing assets GooglePhotos - [ ] [#390](https://github.com/simulot/immich-go/issues/390) - [ ] [#376](https://github.com/simulot/immich-go/issues/376) - - [ ] Some files are not matched (PIXIL) + - [X] Some files are not matched (PIXIL) + - [ ] Check .MP.JPG + - [ ] run tests + - [ ] remove break points - [ ] Report connection errors - [ ] [#395](https://github.com/simulot/immich-go/issues/395) - [ ] [#396](https://github.com/simulot/immich-go/issues/396) @@ -31,6 +34,7 @@ I have developed a way to simulate user cases using only the list files from the ### fixes: - [[#376](https://github.com/simulot/immich-go/issues/376)] errors when uploading are disturbing the the % of the progression - [ ] IPhone's Live photos recognition when the name is duplicated: ex IMG_2710(1).MP4 and IMG_2710(1).HEIC +- [ ] Missing a file when a directory contain several file withe the same name, but of a different type. Ex: IMG_0170.HEIC, IMG_0170.JPG ## Release 0.20.1 From ee2fe642e6b4e325ad3910f496a78c968f5db79e Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 21 Jul 2024 20:08:43 +0200 Subject: [PATCH 11/17] fix .MP.jpg --- browser/gp/googlephotos.go | 64 +++++-- browser/gp/testgp_samples_test.go | 20 +- browser/gp/testgp_test.go | 300 +++++++++++++++++------------- docs/google-takeout.md | 2 +- docs/releases.md | 2 +- 5 files changed, 241 insertions(+), 147 deletions(-) diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index fb56f8e9..878e3cb1 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -43,7 +43,6 @@ type assetFile struct { fsys fs.FS // Remember in which part of the archive the the file base string // Remember the original file name length int // file length in bytes - count int // Track duplicates md *GoogleMetaData // will point to the associated metadata } @@ -115,6 +114,7 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { if err == nil { switch { case md.isAsset(): + md.foundInPaths = append(md.foundInPaths, dir) dirCatalog.jsons[base] = md to.log.Record(ctx, fileevent.DiscoveredSidecar, nil, name, "type", "asset metadata", "title", md.Title) case md.isAlbum(): @@ -446,6 +446,9 @@ func (to *Takeout) Browse(ctx context.Context) chan *browser.LocalAssetFile { return assetChan } +// detect livephotos and motion pictures +// 1. get all pictures +// 2. scan vidoes, if a picture matches, this is a live photo func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *browser.LocalAssetFile) error { catalog := to.catalogs[dir] @@ -454,25 +457,58 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow image *assetFile }{} - // detects couples image + video, likely been a motion picture + // Scan pictures for _, f := range gen.MapKeys(catalog.matchedFiles) { ext := path.Ext(f) - // base := strings.TrimSuffix(f, ext) - // ext2 := path.Ext(base) - // if to.sm.IsMedia(ext2) { - // base = strings.TrimSuffix(base, ext2) - // } - - linked := linkedFiles[f] - switch to.sm.TypeFromExt(ext) { - case immich.TypeVideo: - linked.video = catalog.matchedFiles[f] - case immich.TypeImage: + if to.sm.TypeFromExt(ext) == immich.TypeImage { + linked := linkedFiles[f] linked.image = catalog.matchedFiles[f] + linkedFiles[f] = linked } - linkedFiles[f] = linked } + // Scan videos +nextVideo: + for _, f := range gen.MapKeys(catalog.matchedFiles) { + ext := path.Ext(f) + if to.sm.TypeFromExt(ext) == immich.TypeVideo { + name := strings.TrimSuffix(f, ext) + for i, linked := range linkedFiles { + p := linked.image.base + ext := path.Ext(p) + p = strings.TrimSuffix(p, ext) + ext = path.Ext(p) + if strings.ToUpper(ext) == ".MP" { + p = strings.TrimSuffix(p, ext) + } + if p == name { + linked.video = catalog.matchedFiles[f] + linkedFiles[i] = linked + continue nextVideo + } + } + linked := linkedFiles[f] + linked.video = catalog.matchedFiles[f] + linkedFiles[f] = linked + } + } + + // base := strings.TrimSuffix(f, ext) + // ext2 := path.Ext(base) + // if to.sm.IsMedia(ext2) { + // base = strings.TrimSuffix(base, ext2) + // } + + // linked := linkedFiles[f] + // switch to.sm.TypeFromExt(ext) { + // case immich.TypeVideo: + // linked.video = catalog.matchedFiles[f] + // case immich.TypeImage: + // linked.image = catalog.matchedFiles[f] + // } + // linkedFiles[f] = linked + // } + for _, base := range gen.MapKeys(linkedFiles) { var a *browser.LocalAssetFile var err error diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index 5c0938c8..c0e0d6cc 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -140,14 +140,14 @@ func simpleAlbum() *inMemFS { func albumWithoutImage() *inMemFS { return newInMemFS(). - addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). - addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10). - addJSONImage("Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). - addImage("Photos from 2023/PXL_20230922_144934440.jpg", 15). addJSONAlbum("Album/anyname.json", "Album"). addJSONImage("Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). + addJSONImage("Album/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). addImage("Album/PXL_20230922_144936660.jpg", 10). - addJSONImage("Album/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg") + addJSONImage("Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). + addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). + addImage("Photos from 2023/PXL_20230922_144934440.jpg", 15). + addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10) } func namesWithNumbers() *inMemFS { @@ -255,3 +255,13 @@ func issue390WrongCount2() *inMemFS { addImage("Takeout/Google Photos/2023 - Belize/IMG_0170.HEIC", 4443973). addImage("Takeout/Google Photos/Photos from 2017/IMG_0170.jpg", 514963) } + +func checkLivePhoto() *inMemFS { + return newInMemFS(). + addJSONImage("Motion test/20231227_152817.jpg.json", "20231227_152817.jpg"). + addImage("Motion test/20231227_152817.jpg", 7426453). + addImage("Motion test/20231227_152817.MP4", 5192477). + addJSONImage("Motion Test/PXL_20231118_035751175.MP.jpg.json", "20231118_035751175.MP.jpg"). + addImage("Motion Test/PXL_20231118_035751175.MP", 3478685). + addImage("Motion Test/PXL_20231118_035751175.MP.jpg", 8025699) +} diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index d410508a..a4b8fa83 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -19,105 +19,106 @@ func TestBrowse(t *testing.T) { gen func() *inMemFS want []fileResult // file name / title }{ - // { - // "simpleYear", simpleYear, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, - // }), - // }, + { + "simpleYear", simpleYear, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144956000.jpg", size: 20, title: "PXL_20230922_144956000.jpg"}, + }), + }, - // { - // "simpleAlbum", simpleAlbum, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - // {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, - // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, - // {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, - // }), - // }, + { + "simpleAlbum", simpleAlbum, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + {name: "IMG_8172.jpg", size: 25, title: "IMG_8172.jpg"}, + {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, + }), + }, - // { - // "albumWithoutImage", albumWithoutImage, - // sortFileResult([]fileResult{ - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - // {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - // }), - // }, - // { - // "namesWithNumbers", namesWithNumbers, - // sortFileResult([]fileResult{ - // {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, - // {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, - // {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, - // }), - // }, - // { - // "namesTruncated", namesTruncated, - // sortFileResult([]fileResult{ - // {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, - // {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - // {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, - // }), - // }, + { + "albumWithoutImage", albumWithoutImage, + sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, + }), + }, + { + "namesWithNumbers", namesWithNumbers, + sortFileResult([]fileResult{ + {name: "IMG_3479.JPG", size: 10, title: "IMG_3479.JPG"}, + {name: "IMG_3479(1).JPG", size: 12, title: "IMG_3479.JPG"}, + {name: "IMG_3479(2).JPG", size: 15, title: "IMG_3479.JPG"}, + }), + }, + { + "namesTruncated", namesTruncated, + sortFileResult([]fileResult{ + {name: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", size: 10, title: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"}, + {name: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", size: 40, title: "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + {name: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", size: 25, title: "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"}, + }), + }, - // { - // "imagesWithoutJSON", imagesEditedJSON, - // sortFileResult([]fileResult{ - // {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - // {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, - // }), - // }, + { + "imagesWithoutJSON", imagesEditedJSON, + sortFileResult([]fileResult{ + {name: "PXL_20220405_090123740.PORTRAIT.jpg", size: 41, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + {name: "PXL_20220405_090123740.PORTRAIT-modifié.jpg", size: 21, title: "PXL_20220405_090123740.PORTRAIT.jpg"}, + }), + }, - // { - // "titlesWithForbiddenChars", titlesWithForbiddenChars, - // sortFileResult([]fileResult{ - // {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, - // {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, - // }), - // }, - // { - // "namesIssue39", namesIssue39, - // sortFileResult([]fileResult{ - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - // {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - // }), - // }, - // { - // "issue68MPFiles", issue68MPFiles, - // sortFileResult([]fileResult{ - // {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, - // }), - // }, - // { - // "issue68LongExposure", issue68LongExposure, - // sortFileResult([]fileResult{ - // {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, - // {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, - // }), - // }, + { + "titlesWithForbiddenChars", titlesWithForbiddenChars, + sortFileResult([]fileResult{ + {name: "27_06_12 - 1.mov", size: 52, title: "27/06/12 - 1.mov"}, + {name: "27_06_12 - 2.jpg", size: 24, title: "27/06/12 - 2.jpg"}, + }), + }, + { + "namesIssue39", namesIssue39, + sortFileResult([]fileResult{ + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + }), + }, + { + "issue68MPFiles", issue68MPFiles, + sortFileResult([]fileResult{ + {name: "PXL_20221228_185930354.MP", size: 1, title: "PXL_20221228_185930354.MP"}, + {name: "PXL_20221228_185930354.MP.jpg", size: 2, title: "PXL_20221228_185930354.MP.jpg"}, + }), + }, + { + "issue68LongExposure", issue68LongExposure, + sortFileResult([]fileResult{ + {name: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", size: 1, title: "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg"}, + {name: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", size: 2, title: "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg"}, + }), + }, - // { - // "issue68ForgottenDuplicates", issue68ForgottenDuplicates, - // sortFileResult([]fileResult{ - // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - // {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, - // }), - // }, - // { - // "issue390WrongCount", issue390WrongCount, - // sortFileResult([]fileResult{ - // {name: "image000000.gif", size: 10, title: "image000000.gif"}, - // {name: "image000000.jpg", size: 20, title: "image000000.jpg"}, - // }), - // }, + { + "issue68ForgottenDuplicates", issue68ForgottenDuplicates, + sortFileResult([]fileResult{ + {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", size: 1, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + {name: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", size: 2, title: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg"}, + }), + }, + { + "issue390WrongCount", issue390WrongCount, + sortFileResult([]fileResult{ + {name: "image000000.gif", size: 10, title: "image000000.gif"}, + {name: "image000000.jpg", size: 20, title: "image000000.jpg"}, + }), + }, { "issue390WrongCount2", issue390WrongCount2, sortFileResult([]fileResult{ @@ -172,48 +173,45 @@ func TestBrowse(t *testing.T) { func TestAlbums(t *testing.T) { type album map[string][]fileResult tc := []struct { - name string - gen func() *inMemFS - albums album + name string + gen func() *inMemFS + want album }{ - // { - // name: "simpleYear", - // gen: simpleYear, - // albums: album{}, - // }, + { + name: "simpleYear", + gen: simpleYear, + want: album{}, + }, { name: "simpleAlbum", gen: simpleAlbum, - albums: album{ + want: album{ "Album": sortFileResult([]fileResult{ {name: "IMG_8172.jpg", size: 52, title: "IMG_8172.jpg"}, {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, }), }, }, - /* - { - name: "albumWithoutImage", - gen: albumWithoutImage, - albums: album{ - "Album": sortFileResult([]fileResult{ - {name: "PXL_20230922_144934440.jpg", size: 15, title: "PXL_20230922_144934440.jpg"}, - {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, - }), - }, + { + name: "albumWithoutImage", + gen: albumWithoutImage, + want: album{ + "Album": sortFileResult([]fileResult{ + {name: "PXL_20230922_144936660.jpg", size: 10, title: "PXL_20230922_144936660.jpg"}, + }), }, - { - name: "namesIssue39", - gen: namesIssue39, - albums: album{ - "Album": sortFileResult([]fileResult{ - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, - {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, - }), - }, + }, + { + name: "namesIssue39", + gen: namesIssue39, + want: album{ + "Album": sortFileResult([]fileResult{ + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", size: 1, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", size: 181, title: "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-181.jpg"}, + {name: "Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", size: 494, title: "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg"}, + }), }, - */ + }, } for _, c := range tc { @@ -248,9 +246,59 @@ func TestAlbums(t *testing.T) { albums[k] = sortFileResult(al) } - if !reflect.DeepEqual(albums, c.albums) { + if !reflect.DeepEqual(albums, c.want) { + t.Errorf("difference\n") + pretty.Ldiff(t, c.want, albums) + } + }) + } +} + +func TestLivePhotos(t *testing.T) { + type livephoto map[string]string + tc := []struct { + name string + gen func() *inMemFS + want livephoto + }{ + { + name: "checkLivePhoto", + gen: checkLivePhoto, + want: livephoto{ + "20231227_152817.jpg": "20231227_152817.MP4", + "PXL_20231118_035751175.MP.jpg": "PXL_20231118_035751175.MP", + }, + }, + } + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + ctx := context.Background() + fsys := c.gen() + if fsys.err != nil { + t.Error(fsys.err) + return + } + b, err := NewTakeout(ctx, fileevent.NewRecorder(nil, false), immich.DefaultSupportedMedia, fsys) + if err != nil { + t.Error(err) + } + err = b.Prepare(ctx) + if err != nil { + t.Error(err) + } + + livePhotos := livephoto{} + for a := range b.Browse(ctx) { + if a.LivePhoto != nil { + photo := path.Base(a.FileName) + video := path.Base(a.LivePhoto.FileName) + livePhotos[photo] = video + } + } + + if !reflect.DeepEqual(livePhotos, c.want) { t.Errorf("difference\n") - pretty.Ldiff(t, c.albums, albums) + pretty.Ldiff(t, c.want, livePhotos) } }) } diff --git a/docs/google-takeout.md b/docs/google-takeout.md index a5c4ce96..65e48ab4 100644 --- a/docs/google-takeout.md +++ b/docs/google-takeout.md @@ -83,6 +83,6 @@ takeout-20240712T112341Z-010.zip: # What if you have problems with a takeout archive? -Please open an issue with details. You cna share your files using Discord DM @`simulot`. +Please open an issue with details. You cna share your files using Discord DM `@simulot`. I'll check if I can improve the program. Sometime a manual import is the best option. diff --git a/docs/releases.md b/docs/releases.md index b073e863..3a494cb9 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -12,7 +12,7 @@ - [ ] [#390](https://github.com/simulot/immich-go/issues/390) - [ ] [#376](https://github.com/simulot/immich-go/issues/376) - [X] Some files are not matched (PIXIL) - - [ ] Check .MP.JPG + - [X] Check .MP.JPG - [ ] run tests - [ ] remove break points - [ ] Report connection errors From f2835138250ed9d87ad9cb6d75136f1517f42eaa Mon Sep 17 00:00:00 2001 From: simulot Date: Thu, 25 Jul 2024 23:22:17 +0200 Subject: [PATCH 12/17] fix: Problem with images with same name #402 --- browser/gp/googlephotos.go | 228 ++++++++++-------- browser/gp/googlephotos_test.go | 8 + browser/gp/testgp_samples_test.go | 116 ++++++--- browser/gp/testgp_test.go | 169 +++++++++---- .../{takeout_test.go => e2e_takeout_test.go} | 11 + cmd/upload/noui.go | 21 +- cmd/upload/upload.go | 6 + docs/how-to-send-debug-data.md | 87 +++++++ docs/releases.md | 26 +- helpers/fshelper/globwalkfs.go | 3 +- immich/client.go | 30 ++- immich/client_test.go | 30 +++ internal/fakefs/fakefs.go | 3 +- internal/fakefs/ziplist.go | 49 ++-- internal/fakefs/ziplist_test.go | 26 ++ 15 files changed, 591 insertions(+), 222 deletions(-) rename cmd/upload/{takeout_test.go => e2e_takeout_test.go} (84%) create mode 100644 docs/how-to-send-debug-data.md create mode 100644 immich/client_test.go diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index 878e3cb1..cd8ed3ee 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -2,10 +2,10 @@ package gp import ( "context" + "fmt" "io/fs" "path" "path/filepath" - "regexp" "sort" "strings" "unicode/utf8" @@ -20,15 +20,18 @@ import ( ) type Takeout struct { - fsyss []fs.FS - catalogs map[string]directoryCatalog // file catalogs by directory in the set of the all takeout parts - albums map[string]browser.LocalAlbum // track album names by folder - log *fileevent.Recorder - sm immich.SupportedMedia - banned namematcher.List // Banned files - totalUnmatched int // count the number of asset not matched - totalSent int // DEBUG - files map[string]any // DEBUG + fsyss []fs.FS + catalogs map[string]directoryCatalog // file catalogs by directory in the set of the all takeout parts + albums map[string]browser.LocalAlbum // track album names by folder + log *fileevent.Recorder + sm immich.SupportedMedia + + banned namematcher.List // Banned files + acceptMissingJSON bool + totalUnmatched int // count the number of asset not matched + totalSent int // DEBUG + totalMotionPicture int // DEBUG + files map[string]int // DEBUG } // directoryCatalog captures all files in a given directory @@ -53,7 +56,7 @@ func NewTakeout(ctx context.Context, l *fileevent.Recorder, sm immich.SupportedM albums: map[string]browser.LocalAlbum{}, log: l, sm: sm, - files: map[string]any{}, + files: map[string]int{}, } return &to, nil @@ -64,6 +67,11 @@ func (to *Takeout) SetBannedFiles(banned namematcher.List) *Takeout { return to } +func (to *Takeout) SetAcceptMissingJSON(flag bool) *Takeout { + to.acceptMissingJSON = flag + return to +} + // Prepare scans all files in all walker to build the file catalog of the archive // metadata files content is read and kept @@ -103,6 +111,11 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { dirCatalog.unMatchedFiles = map[string]*assetFile{} dirCatalog.matchedFiles = map[string]*assetFile{} } + if _, ok := dirCatalog.unMatchedFiles[base]; ok { + // to.log.Record(ctx, fileevent.AnalysisLocalDuplicate, nil, name) + return nil + } + finfo, err := d.Info() if err != nil { to.log.Record(ctx, fileevent.Error, nil, name, "error", err.Error()) @@ -157,7 +170,7 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { return nil } - to.files[name] = nil // DEBUG + to.files[name] = to.files[name] + 1 // DEBUG dirCatalog.unMatchedFiles[base] = &assetFile{ fsys: w, base: base, @@ -246,6 +259,10 @@ func (to *Takeout) solvePuzzle(ctx context.Context) error { sort.Strings(files) for _, f := range files { to.log.Record(ctx, fileevent.AnalysisMissingAssociatedMetadata, f, filepath.Join(dir, f)) + if to.acceptMissingJSON { + cat.matchedFiles[f] = cat.unMatchedFiles[f] + delete(cat.unMatchedFiles, f) + } } } return nil @@ -342,24 +359,48 @@ func matchVeryLongNameWithNumber(jsonName string, fileName string, sm immich.Sup // // Fast implementation, but does't work with live photos -// func matchDuplicateInYear(jsonName string, fileName string, sm immich.SupportedMedia) bool { -// jsonName = strings.TrimSuffix(jsonName, path.Ext(jsonName)) -// p1JSON := strings.Index(jsonName, "(") -// if p1JSON < 1 { -// return false -// } -// p2JSON := strings.Index(jsonName, ")") -// if p2JSON < 0 || p2JSON != len(jsonName)-1 { -// return false -// } - -// num := jsonName[p1JSON:] -// jsonName = strings.TrimSuffix(jsonName, num) -// ext := path.Ext(jsonName) -// jsonName = strings.TrimSuffix(jsonName, ext) + num + ext -// return jsonName == fileName -// } +func matchDuplicateInYear(jsonName string, fileName string, sm immich.SupportedMedia) bool { + jsonName = strings.TrimSuffix(jsonName, path.Ext(jsonName)) + p1JSON := strings.Index(jsonName, "(") + if p1JSON < 1 { + return false + } + p1File := strings.Index(fileName, "(") + if p1File < 0 { + return false + } + jsonExt := path.Ext(jsonName[:p1JSON]) + + p2JSON := strings.Index(jsonName, ")") + if p2JSON < 0 || p2JSON != len(jsonName)-1 { + return false + } + + p2File := strings.Index(fileName, ")") + if p2File < 0 || p2File < p1File { + return false + } + + fileExt := path.Ext(fileName) + + if fileExt != jsonExt { + return false + } + + jsonBase := strings.TrimSuffix(jsonName[:p1JSON], path.Ext(jsonName[:p1JSON])) + if jsonBase != fileName[:p1File] { + return false + } + + if fileName[p1File+1:p2File] != jsonName[p1JSON+1:p2JSON] { + return false + } + + return true +} + +/* // Regexp implementation, work with live photos, 10 times slower var ( reDupInYearJSON = regexp.MustCompile(`(.*)\.(.{2,4})\((\d+)\)\..{2,4}$`) @@ -380,6 +421,7 @@ func matchDuplicateInYear(jsonName string, fileName string, sm immich.SupportedM } return false } +*/ // matchEditedName // PXL_20220405_090123740.PORTRAIT.jpg.json @@ -442,6 +484,9 @@ func (to *Takeout) Browse(ctx context.Context) chan *browser.LocalAssetFile { if to.totalUnmatched > 0 { to.log.Record(ctx, fileevent.Error, nil, "", "error", "too many unmatched files with JSON. Have you processed all parts of the takeout in this run?") } + if len(to.files) > 0 { + to.log.Record(ctx, fileevent.Error, nil, "", "error", fmt.Sprintf("%d scanned images/video, but not processed. Contact the developer.\n", len(to.files))) + } }() return assetChan } @@ -474,6 +519,12 @@ nextVideo: if to.sm.TypeFromExt(ext) == immich.TypeVideo { name := strings.TrimSuffix(f, ext) for i, linked := range linkedFiles { + if linked.image == nil { + continue + } + if linked.image != nil && linked.video != nil { + continue + } p := linked.image.base ext := path.Ext(p) p = strings.TrimSuffix(p, ext) @@ -484,6 +535,7 @@ nextVideo: if p == name { linked.video = catalog.matchedFiles[f] linkedFiles[i] = linked + to.totalMotionPicture++ continue nextVideo } } @@ -493,22 +545,6 @@ nextVideo: } } - // base := strings.TrimSuffix(f, ext) - // ext2 := path.Ext(base) - // if to.sm.IsMedia(ext2) { - // base = strings.TrimSuffix(base, ext2) - // } - - // linked := linkedFiles[f] - // switch to.sm.TypeFromExt(ext) { - // case immich.TypeVideo: - // linked.video = catalog.matchedFiles[f] - // case immich.TypeImage: - // linked.image = catalog.matchedFiles[f] - // } - // linkedFiles[f] = linked - // } - for _, base := range gen.MapKeys(linkedFiles) { var a *browser.LocalAssetFile var err error @@ -516,13 +552,13 @@ nextVideo: linked := linkedFiles[base] if linked.image != nil { - a, err = to.googleMDToAsset(linked.image.md, linked.image.fsys, path.Join(dir, linked.image.base)) + a, err = to.makeAsset(linked.image.md, linked.image.fsys, path.Join(dir, linked.image.base)) if err != nil { to.log.Record(ctx, fileevent.Error, nil, path.Join(dir, linked.image.base), "error", err.Error()) continue } if linked.video != nil { - i, err := to.googleMDToAsset(linked.video.md, linked.video.fsys, path.Join(dir, linked.video.base)) + i, err := to.makeAsset(linked.video.md, linked.video.fsys, path.Join(dir, linked.video.base)) if err != nil { to.log.Record(ctx, fileevent.Error, nil, path.Join(dir, linked.video.base), "error", err.Error()) } else { @@ -530,7 +566,7 @@ nextVideo: } } } else { - a, err = to.googleMDToAsset(linked.video.md, linked.video.fsys, path.Join(dir, linked.video.base)) + a, err = to.makeAsset(linked.video.md, linked.video.fsys, path.Join(dir, linked.video.base)) if err != nil { to.log.Record(ctx, fileevent.Error, nil, path.Join(dir, linked.video.base), "error", err.Error()) continue @@ -540,10 +576,10 @@ nextVideo: case <-ctx.Done(): return ctx.Err() default: - delete(to.files, a.FileName) + to.files[a.FileName] = to.files[a.FileName] - 1 to.totalSent++ if a.LivePhoto != nil { - delete(to.files, a.LivePhoto.FileName) + to.files[a.LivePhoto.FileName] = to.files[a.LivePhoto.FileName] - 1 to.totalSent++ } assetChan <- a @@ -552,65 +588,69 @@ nextVideo: return nil } -// googleMDToAsset makes a localAssetFile based on the google metadata -func (to *Takeout) googleMDToAsset(md *GoogleMetaData, fsys fs.FS, name string) (*browser.LocalAssetFile, error) { - // Change file's title with the asset's title and the actual file's extension - title := md.Title - titleExt := path.Ext(title) - fileExt := path.Ext(name) - - if titleExt != fileExt { - title = strings.TrimSuffix(title, titleExt) - titleExt = path.Ext(title) - if titleExt != fileExt { - title = strings.TrimSuffix(title, titleExt) + fileExt - } - } - +// makeAsset makes a localAssetFile based on the google metadata +func (to *Takeout) makeAsset(md *GoogleMetaData, fsys fs.FS, name string) (*browser.LocalAssetFile, error) { i, err := fs.Stat(fsys, name) if err != nil { return nil, err } a := browser.LocalAssetFile{ - FileName: name, - FileSize: int(i.Size()), - Title: title, - Archived: md.Archived, - FromPartner: md.isPartner(), - Trashed: md.Trashed, - Favorite: md.Favorited, - - FSys: fsys, + FileName: name, + FileSize: int(i.Size()), + Title: path.Base(name), + FSys: fsys, } - // Prepare sidecar data to force Immich with Google metadata - - sidecar := metadata.Metadata{ - Description: md.Description, - DateTaken: md.PhotoTakenTime.Time(), + if album, ok := to.albums[path.Dir(name)]; ok { + a.Albums = append(a.Albums, album) } - if md.GeoDataExif.Latitude != 0 || md.GeoDataExif.Longitude != 0 { - sidecar.Latitude = md.GeoDataExif.Latitude - sidecar.Longitude = md.GeoDataExif.Longitude - } + if md != nil { + // Change file's title with the asset's title and the actual file's extension + title := md.Title + titleExt := path.Ext(title) + fileExt := path.Ext(name) - if md.GeoData.Latitude != 0 || md.GeoData.Longitude != 0 { - sidecar.Latitude = md.GeoData.Latitude - sidecar.Longitude = md.GeoData.Longitude - } + if titleExt != fileExt { + title = strings.TrimSuffix(title, titleExt) + titleExt = path.Ext(title) + if titleExt != fileExt { + title = strings.TrimSuffix(title, titleExt) + fileExt + } + } + a.Title = title + a.Archived = md.Archived + a.FromPartner = md.isPartner() + a.Trashed = md.Trashed + a.Favorite = md.Favorited - for _, p := range md.foundInPaths { - if album, exists := to.albums[p]; exists { - if (album.Latitude != 0 || album.Longitude != 0) && (sidecar.Latitude == 0 && sidecar.Longitude == 0) { - sidecar.Latitude = album.Latitude - sidecar.Longitude = album.Longitude + // Prepare sidecar data to force Immich with Google metadata + + sidecar := metadata.Metadata{ + Description: md.Description, + DateTaken: md.PhotoTakenTime.Time(), + } + + if md.GeoDataExif.Latitude != 0 || md.GeoDataExif.Longitude != 0 { + sidecar.Latitude = md.GeoDataExif.Latitude + sidecar.Longitude = md.GeoDataExif.Longitude + } + + if md.GeoData.Latitude != 0 || md.GeoData.Longitude != 0 { + sidecar.Latitude = md.GeoData.Latitude + sidecar.Longitude = md.GeoData.Longitude + } + for _, p := range md.foundInPaths { + if album, exists := to.albums[p]; exists { + if (album.Latitude != 0 || album.Longitude != 0) && (sidecar.Latitude == 0 && sidecar.Longitude == 0) { + sidecar.Latitude = album.Latitude + sidecar.Longitude = album.Longitude + } } - a.Albums = append(a.Albums, album) } + a.Metadata = sidecar } - a.Metadata = sidecar return &a, nil } diff --git a/browser/gp/googlephotos_test.go b/browser/gp/googlephotos_test.go index 9135e9c6..e2da29d3 100644 --- a/browser/gp/googlephotos_test.go +++ b/browser/gp/googlephotos_test.go @@ -206,6 +206,14 @@ PASS ok github.com/simulot/immich-go/browser/gp 1.332s +goos: linux +goarch: amd64 +pkg: github.com/simulot/immich-go/browser/gp +cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz +Benchmark_matchDuplicateInYear-12 25737067 43.88 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/simulot/immich-go/browser/gp 1.180s + */ func Benchmark_matchDuplicateInYear(b *testing.B) { diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index c0e0d6cc..29ce39ef 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "io/fs" "path" "sort" "strconv" @@ -12,6 +13,7 @@ import ( "github.com/psanford/memfs" "github.com/simulot/immich-go/immich/metadata" + "github.com/simulot/immich-go/internal/fakefs" ) type inMemFS struct { @@ -25,6 +27,10 @@ func newInMemFS() *inMemFS { } } +func (mfs *inMemFS) FSs() []fs.FS { + return []fs.FS{mfs} +} + func (mfs *inMemFS) addFile(name string, content []byte) *inMemFS { if mfs.err != nil { return mfs @@ -113,15 +119,15 @@ func sortFileResult(s []fileResult) []fileResult { return s } -func simpleYear() *inMemFS { +func simpleYear() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10). addJSONImage("Photos from 2023/PXL_20230922_144956000.jpg.json", "PXL_20230922_144956000.jpg"). - addImage("Photos from 2023/PXL_20230922_144956000.jpg", 20) + addImage("Photos from 2023/PXL_20230922_144956000.jpg", 20).FSs() } -func simpleAlbum() *inMemFS { +func simpleAlbum() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2020/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20200101103000")). addImage("Photos from 2020/IMG_8172.jpg", 25). @@ -135,10 +141,10 @@ func simpleAlbum() *inMemFS { addJSONImage("Album/IMG_8172.jpg.json", "IMG_8172.jpg", takenTime("20230922102100")). addJSONImage("Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg", takenTime("PXL_20230922_144936660")). addImage("Album/IMG_8172.jpg", 52). - addImage("Album/PXL_20230922_144936660.jpg", 10) + addImage("Album/PXL_20230922_144936660.jpg", 10).FSs() } -func albumWithoutImage() *inMemFS { +func albumWithoutImage() []fs.FS { return newInMemFS(). addJSONAlbum("Album/anyname.json", "Album"). addJSONImage("Album/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). @@ -147,46 +153,46 @@ func albumWithoutImage() *inMemFS { addJSONImage("Photos from 2023/PXL_20230922_144934440.jpg.json", "PXL_20230922_144934440.jpg"). addJSONImage("Photos from 2023/PXL_20230922_144936660.jpg.json", "PXL_20230922_144936660.jpg"). addImage("Photos from 2023/PXL_20230922_144934440.jpg", 15). - addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10) + addImage("Photos from 2023/PXL_20230922_144936660.jpg", 10).FSs() } -func namesWithNumbers() *inMemFS { +func namesWithNumbers() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2009/IMG_3479.JPG.json", "IMG_3479.JPG"). addImage("Photos from 2009/IMG_3479.JPG", 10). addJSONImage("Photos from 2009/IMG_3479.JPG(1).json", "IMG_3479.JPG"). addImage("Photos from 2009/IMG_3479(1).JPG", 12). addJSONImage("Photos from 2009/IMG_3479.JPG(2).json", "IMG_3479.JPG"). - addImage("Photos from 2009/IMG_3479(2).JPG", 15) + addImage("Photos from 2009/IMG_3479(2).JPG", 15).FSs() } -func namesTruncated() *inMemFS { +func namesTruncated() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋.json", "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶.jpg"). addImage("Photos from 2023/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", 10). addJSONImage("Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINAL.jpg"). addImage("Photos from 2023/PXL_20230809_203449253.LONG_EXPOSURE-02.ORIGINA.jpg", 40). addJSONImage("Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jp.json", "05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg"). - addImage("Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", 25) + addImage("Photos from 2023/05yqt21kruxwwlhhgrwrdyb6chhwszi9bqmzu16w0 2.jpg", 25).FSs() } -func imagesEditedJSON() *inMemFS { +func imagesEditedJSON() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg.json", "PXL_20220405_090123740.PORTRAIT.jpg"). addImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT.jpg", 41). addImage("Photos from 2023/PXL_20220405_090123740.PORTRAIT-modifié.jpg", 21). - addImage("Photos from 2023/PXL_20220405_090200110.PORTRAIT-modifié.jpg", 12) + addImage("Photos from 2023/PXL_20220405_090200110.PORTRAIT-modifié.jpg", 12).FSs() } -func titlesWithForbiddenChars() *inMemFS { +func titlesWithForbiddenChars() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2012/27_06_12 - 1.mov.json", "27/06/12 - 1", takenTime("20120627")). addImage("Photos from 2012/27_06_12 - 1.mov", 52). addJSONImage("Photos from 2012/27_06_12 - 2.json", "27/06/12 - 2", takenTime("20120627")). - addImage("Photos from 2012/27_06_12 - 2.jpg", 24) + addImage("Photos from 2012/27_06_12 - 2.jpg", 24).FSs() } -func namesIssue39() *inMemFS { +func namesIssue39() []fs.FS { return newInMemFS(). addJSONAlbum("Album/anyname.json", "Album"). addJSONImage("Album/Backyard_ceremony_wedding_photography_xxxxxxx_.json", "Backyard_ceremony_wedding_photography_xxxxxxx_magnoliastudios-371.jpg", takenTime("20200101")). @@ -200,42 +206,42 @@ func namesIssue39() *inMemFS { addJSONImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_(494).json", "Backyard_ceremony_wedding_photography_markham_magnoliastudios-19.jpg", takenTime("20200101")). addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(1).jpg", 181). addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m(494).jpg", 494). - addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1) + addImage("Photos from 2020/Backyard_ceremony_wedding_photography_xxxxxxx_m.jpg", 1).FSs() } -func issue68MPFiles() *inMemFS { +func issue68MPFiles() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2022/PXL_20221228_185930354.MP.jpg.json", "PXL_20221228_185930354.MP.jpg", takenTime("20220101")). addImage("Photos from 2022/PXL_20221228_185930354.MP", 1). - addImage("Photos from 2022/PXL_20221228_185930354.MP.jpg", 2) + addImage("Photos from 2022/PXL_20221228_185930354.MP.jpg", 2).FSs() } -func issue68LongExposure() *inMemFS { +func issue68LongExposure() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER..json", "PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", takenTime("20230101")). addImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-01.COVER.jpg", 1). addJSONImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGIN.json", "PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINAL.jpg", takenTime("20230101")). - addImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", 2) + addImage("Photos from 2023/PXL_20230814_201154491.LONG_EXPOSURE-02.ORIGINA.jpg", 2).FSs() } -func issue68ForgottenDuplicates() *inMemFS { +func issue68ForgottenDuplicates() []fs.FS { return newInMemFS(). addJSONImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_.json", "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_PXL_20220516_164814158.jpg", takenTime("20220101")). addImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P.jpg", 1). - addImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", 2) + addImage("Photos from 2022/original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", 2).FSs() } // #390 Question: report shows way less images uploaded than scanned -func issue390WrongCount() *inMemFS { +func issue390WrongCount() []fs.FS { return newInMemFS(). addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.jpg.json", "image000000.jpg"). addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.jpg(1).json", "image000000.jpg"). addJSONImage("Takeout/Google Photos/Photos from 2021/image000000.gif.json", "image000000.gif.json"). addImage("Takeout/Google Photos/Photos from 2021/image000000.gif", 10). - addImage("Takeout/Google Photos/Photos from 2021/image000000.jpg", 20) + addImage("Takeout/Google Photos/Photos from 2021/image000000.jpg", 20).FSs() } -func issue390WrongCount2() *inMemFS { +func issue390WrongCount2() []fs.FS { return newInMemFS(). addJSONImage("Takeout/Google Photos/2017 - Croatia/IMG_0170.jpg.json", "IMG_0170.jpg"). addJSONImage("Takeout/Google Photos/Photos from 2018/IMG_0170.JPG.json", "IMG_0170.JPG"). @@ -253,15 +259,69 @@ func issue390WrongCount2() *inMemFS { addImage("Takeout/Google Photos/2018 - Cambodia/IMG_0170.JPG", 4570661). addImage("Takeout/Google Photos/2023 - Belize/IMG_0170.MP4", 6024972). addImage("Takeout/Google Photos/2023 - Belize/IMG_0170.HEIC", 4443973). - addImage("Takeout/Google Photos/Photos from 2017/IMG_0170.jpg", 514963) + addImage("Takeout/Google Photos/Photos from 2017/IMG_0170.jpg", 514963).FSs() } -func checkLivePhoto() *inMemFS { +func checkLivePhoto() []fs.FS { return newInMemFS(). addJSONImage("Motion test/20231227_152817.jpg.json", "20231227_152817.jpg"). addImage("Motion test/20231227_152817.jpg", 7426453). addImage("Motion test/20231227_152817.MP4", 5192477). addJSONImage("Motion Test/PXL_20231118_035751175.MP.jpg.json", "20231118_035751175.MP.jpg"). addImage("Motion Test/PXL_20231118_035751175.MP", 3478685). - addImage("Motion Test/PXL_20231118_035751175.MP.jpg", 8025699) + addImage("Motion Test/PXL_20231118_035751175.MP.jpg", 8025699).FSs() +} + +func loadFromString(dateFormat string, s string) []fs.FS { + fss, err := fakefs.ScanStringList(dateFormat, s) + if err != nil { + panic(err.Error()) + } + return fss +} + +func checkLivePhotoPixil() []fs.FS { + return loadFromString("01-02-2006 15:04", `Archive: takeout-20230720T065335Z-001.zip + Length Date Time Name +--------- ---------- ----- ---- + 309 03-05-2023 10:10 Takeout/Google Photos/2022 - Germany/metadata.json + 801 07-19-2023 23:59 Takeout/Google Photos/2022 - Germany/IMG_4573.HEIC.json + 2232086 07-19-2023 23:59 Takeout/Google Photos/2022 - Germany/IMG_4573.MP4 + 3530351 07-20-2023 00:00 Takeout/Google Photos/2022 - Germany/IMG_4573.HEIC + 319 03-05-2023 10:10 Takeout/Google Photos/2022 - Germany - Private/metadata.json + 802 07-20-2023 00:03 Takeout/Google Photos/2022 - Germany - Private/IMG_4573.HEIC.json + 3530351 07-19-2023 23:56 Takeout/Google Photos/2022 - Germany - Private/IMG_4573.HEIC + 2232086 07-19-2023 23:56 Takeout/Google Photos/2022 - Germany - Private/IMG_4573.MP4 + 803 07-19-2023 23:58 Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC.json + 3530351 07-19-2023 23:59 Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC + 2232086 07-19-2023 23:59 Takeout/Google Photos/Photos from 2022/IMG_4573.MP4 +`) +} + +func checkMissingJSON() []fs.FS { + return loadFromString("01-02-2006 15:04", `Archive: takeout-20230720T065335Z-001.zip + Length Date Time Name +--------- ---------- ----- ---- + 803 07-19-2023 23:58 Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC.json + 3530351 07-19-2023 23:59 Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC + 1352455 2024-07-12 15:18 Takeout/Google Foto/Photos from 2016/IMG-20161201-WA0035.jpeg + 3530351 07-19-2023 23:56 Takeout/Google Photos/2022 - Germany - Private/IMG_4553.HEIC + 309 03-05-2023 10:10 Takeout/Google Photos/2022 - Germany/metadata.json + 2232086 07-19-2023 23:59 Takeout/Google Photos/2022 - Germany/IMG_1234.MP4 + 3530351 07-20-2023 00:00 Takeout/Google Photos/2022 - Germany/IMG_1234.HEIC +`) +} + +func checkDuplicates() []fs.FS { + return loadFromString("01-02-2006 15:04", `Archive: takeout-20230720T065335Z-001.zip +-rw-r--r-- 0/0 365022 2024-07-19 01:19 Takeout/Google Foto/[E&S] 2016-01-05 - Castello De Albertis e Mostra d/20160105_121621_LLS.jpg +-rw-r--r-- 0/0 708 2024-07-19 01:19 Takeout/Google Foto/[E&S] 2016-01-05 - Castello De Albertis e Mostra d/20160105_121621_LLS.jpg.json +-rw-r--r-- 0/0 364041 2024-07-19 01:51 Takeout/Google Foto/Photos from 2016/20160105_121621_LLS.jpg +-rw-r--r-- 0/0 709 2024-07-19 01:51 Takeout/Google Foto/Photos from 2016/20160105_121621_LLS.jpg.json +-rw-r--r-- 0/0 708 2024-07-19 02:13 Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg.json +-rw-r--r-- 0/0 364041 2024-07-19 02:20 Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg +Archive: takeout-20230720T065335Z-002.zip +-rw-r--r-- 0/0 364041 2024-07-19 06:14 Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg +-rw-r--r-- 0/0 708 2024-07-19 02:13 Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg.json +`) } diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index a4b8fa83..26acd24e 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -3,6 +3,7 @@ package gp import ( "context" "io" + "io/fs" "log/slog" "path" "reflect" @@ -16,7 +17,7 @@ import ( func TestBrowse(t *testing.T) { tc := []struct { name string - gen func() *inMemFS + gen func() []fs.FS want []fileResult // file name / title }{ { @@ -138,15 +139,12 @@ func TestBrowse(t *testing.T) { for _, c := range tc { t.Run(c.name, func(t *testing.T) { fsys := c.gen() - if fsys.err != nil { - t.Error(fsys.err) - return - } + ctx := context.Background() log := slog.New(slog.NewTextHandler(io.Discard, nil)) - b, err := NewTakeout(ctx, fileevent.NewRecorder(log, false), immich.DefaultSupportedMedia, fsys) + b, err := NewTakeout(ctx, fileevent.NewRecorder(log, false), immich.DefaultSupportedMedia, fsys...) if err != nil { t.Error(err) } @@ -159,6 +157,9 @@ func TestBrowse(t *testing.T) { results := []fileResult{} for a := range b.Browse(ctx) { results = append(results, fileResult{name: path.Base(a.FileName), size: a.FileSize, title: a.Title}) + if a.LivePhoto != nil { + results = append(results, fileResult{name: path.Base(a.LivePhoto.FileName), size: a.LivePhoto.FileSize, title: a.LivePhoto.Title}) + } } results = sortFileResult(results) @@ -174,7 +175,7 @@ func TestAlbums(t *testing.T) { type album map[string][]fileResult tc := []struct { name string - gen func() *inMemFS + gen func() []fs.FS want album }{ { @@ -218,11 +219,8 @@ func TestAlbums(t *testing.T) { t.Run(c.name, func(t *testing.T) { ctx := context.Background() fsys := c.gen() - if fsys.err != nil { - t.Error(fsys.err) - return - } - b, err := NewTakeout(ctx, fileevent.NewRecorder(nil, false), immich.DefaultSupportedMedia, fsys) + + b, err := NewTakeout(ctx, fileevent.NewRecorder(nil, false), immich.DefaultSupportedMedia, fsys...) if err != nil { t.Error(err) } @@ -254,52 +252,125 @@ func TestAlbums(t *testing.T) { } } -func TestLivePhotos(t *testing.T) { - type livephoto map[string]string +func TestArchives(t *testing.T) { + type photo map[string]string + type album map[string][]string tc := []struct { - name string - gen func() *inMemFS - want livephoto + name string + gen func() []fs.FS + acceptMissingJSON bool + wantLivePhotos photo + wantAlbum album + wantAsset photo }{ { - name: "checkLivePhoto", - gen: checkLivePhoto, - want: livephoto{ - "20231227_152817.jpg": "20231227_152817.MP4", - "PXL_20231118_035751175.MP.jpg": "PXL_20231118_035751175.MP", + name: "checkLivePhoto", + gen: checkLivePhoto, + wantAsset: photo{}, + wantLivePhotos: photo{ + "Motion Test/PXL_20231118_035751175.MP.jpg": "Motion Test/PXL_20231118_035751175.MP", + "Motion test/20231227_152817.jpg": "Motion test/20231227_152817.MP4", }, + wantAlbum: album{}, + }, + { + name: "checkLivePhotoPixil", + gen: checkLivePhotoPixil, + wantAsset: photo{}, + wantLivePhotos: photo{ + "Takeout/Google Photos/2022 - Germany - Private/IMG_4573.HEIC": "Takeout/Google Photos/2022 - Germany - Private/IMG_4573.MP4", + "Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC": "Takeout/Google Photos/Photos from 2022/IMG_4573.MP4", + "Takeout/Google Photos/2022 - Germany/IMG_4573.HEIC": "Takeout/Google Photos/2022 - Germany/IMG_4573.MP4", + }, + wantAlbum: album{ + "2022 - Germany - Private": []string{"IMG_4573.HEIC"}, + "2022 - Germany": []string{"IMG_4573.HEIC"}, + }, + }, + { + name: "checkMissingJSON-No", + gen: checkMissingJSON, + wantAsset: photo{ + "Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC": "", + }, + wantLivePhotos: photo{}, + wantAlbum: album{}, + }, + { + name: "checkMissingJSON-Yes", + gen: checkMissingJSON, + acceptMissingJSON: true, + wantAsset: photo{ + "Takeout/Google Photos/Photos from 2022/IMG_4573.HEIC": "", + "Takeout/Google Foto/Photos from 2016/IMG-20161201-WA0035.jpeg": "", + "Takeout/Google Photos/2022 - Germany - Private/IMG_4553.HEIC": "", + }, + wantLivePhotos: photo{ + "Takeout/Google Photos/2022 - Germany/IMG_1234.HEIC": "Takeout/Google Photos/2022 - Germany/IMG_1234.MP4", + }, + wantAlbum: album{ + "2022 - Germany": []string{"IMG_1234.HEIC"}, + }, + }, + { + name: "checkDuplicates", + gen: checkDuplicates, + wantAsset: photo{ + "Takeout/Google Foto/[E&S] 2016-01-05 - Castello De Albertis e Mostra d/20160105_121621_LLS.jpg": "", + "Takeout/Google Foto/Photos from 2016/20160105_121621_LLS.jpg": "", + "Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg": "", + }, + wantLivePhotos: photo{}, + wantAlbum: album{}, }, } for _, c := range tc { - t.Run(c.name, func(t *testing.T) { - ctx := context.Background() - fsys := c.gen() - if fsys.err != nil { - t.Error(fsys.err) - return - } - b, err := NewTakeout(ctx, fileevent.NewRecorder(nil, false), immich.DefaultSupportedMedia, fsys) - if err != nil { - t.Error(err) - } - err = b.Prepare(ctx) - if err != nil { - t.Error(err) - } + t.Run( + c.name, + func(t *testing.T) { + ctx := context.Background() + fsys := c.gen() - livePhotos := livephoto{} - for a := range b.Browse(ctx) { - if a.LivePhoto != nil { - photo := path.Base(a.FileName) - video := path.Base(a.LivePhoto.FileName) - livePhotos[photo] = video + b, err := NewTakeout(ctx, fileevent.NewRecorder(nil, false), immich.DefaultSupportedMedia, fsys...) + if err != nil { + t.Error(err) + } + b.SetAcceptMissingJSON(c.acceptMissingJSON) + err = b.Prepare(ctx) + if err != nil { + t.Error(err) } - } - if !reflect.DeepEqual(livePhotos, c.want) { - t.Errorf("difference\n") - pretty.Ldiff(t, c.want, livePhotos) - } - }) + livePhotos := photo{} + assets := photo{} + albums := album{} + for a := range b.Browse(ctx) { + if a.LivePhoto != nil { + photo := a.FileName + video := a.LivePhoto.FileName + livePhotos[photo] = video + } else { + assets[a.FileName] = "" + } + for _, al := range a.Albums { + l := albums[al.Title] + l = append(l, path.Base(a.FileName)) + albums[al.Title] = l + } + } + if !reflect.DeepEqual(assets, c.wantAsset) { + t.Errorf("difference assets\n") + pretty.Ldiff(t, c.wantAsset, assets) + } + if !reflect.DeepEqual(livePhotos, c.wantLivePhotos) { + t.Errorf("difference LivePhotos\n") + pretty.Ldiff(t, c.wantLivePhotos, livePhotos) + } + if !reflect.DeepEqual(albums, c.wantAlbum) { + t.Errorf("difference Album\n") + pretty.Ldiff(t, c.wantAlbum, albums) + } + }, + ) } } diff --git a/cmd/upload/takeout_test.go b/cmd/upload/e2e_takeout_test.go similarity index 84% rename from cmd/upload/takeout_test.go rename to cmd/upload/e2e_takeout_test.go index 4d7fa682..8b21366d 100644 --- a/cmd/upload/takeout_test.go +++ b/cmd/upload/e2e_takeout_test.go @@ -1,8 +1,12 @@ +//go:build e2e +// +build e2e + package upload import ( "context" "io/fs" + "os" "path/filepath" "testing" @@ -28,6 +32,7 @@ func simulate_upload(t *testing.T, zipList string, dateFormat string) { fsOpener := func() ([]fs.FS, error) { return fakefs.ScanFileList(zipList, dateFormat) } + os.Remove(filepath.Dir(zipList) + "/debug.log") args := []string{"-google-photos", "-no-ui", "-debug-counters", "-log-file=" + filepath.Dir(zipList) + "/debug.log"} app, err := newCommand(ctx, &serv, args, fsOpener) @@ -54,3 +59,9 @@ func TestPhyl404TakeOut(t *testing.T) { simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/Phyl404/list.lst", "2006-01-02 15:04") } + +func TestSteve81TakeOut(t *testing.T) { + initMyEnv(t) + + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/Steve81/list.list", "2006-01-02 15:04") +} diff --git a/cmd/upload/noui.go b/cmd/upload/noui.go index 4344cdf2..85ad593c 100644 --- a/cmd/upload/noui.go +++ b/cmd/upload/noui.go @@ -28,25 +28,30 @@ func (app *UpCmd) runNoUI(ctx context.Context) error { immichPct := 0 if maxImmich > 0 { immichPct = 100 * currImmich / maxImmich + } else { + immichPct = 100 } - ScannedAssets := counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo] - counts[fileevent.DiscoveredDiscarded] + AssetsToUpload := counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo] - counts[fileevent.AnalysisLocalDuplicate] + ProcessedAssets := counts[fileevent.Uploaded] + counts[fileevent.UploadServerError] + counts[fileevent.UploadNotSelected] + counts[fileevent.UploadUpgraded] + counts[fileevent.UploadServerDuplicate] + counts[fileevent.UploadServerBetter] + - counts[fileevent.DiscoveredDiscarded] + - counts[fileevent.AnalysisLocalDuplicate] + counts[fileevent.DiscoveredDiscarded] + if !app.ForceUploadWhenNoJSON { + ProcessedAssets += counts[fileevent.AnalysisMissingAssociatedMetadata] + } if app.GooglePhotos { gpPct := 0 upPct := 0 - if ScannedAssets > 0 { - gpPct = int(100 * counts[fileevent.AnalysisAssociatedMetadata] / ScannedAssets) - } - if counts[fileevent.AnalysisAssociatedMetadata] > 0 { - upPct = int(100 * ProcessedAssets / counts[fileevent.AnalysisAssociatedMetadata]) + + if AssetsToUpload > 0 { + gpPct = int(100 * (counts[fileevent.AnalysisAssociatedMetadata] + counts[fileevent.AnalysisMissingAssociatedMetadata]) / (counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo])) + + upPct = int(100 * ProcessedAssets / AssetsToUpload) } s = fmt.Sprintf("\rImmich read %d%%, Google Photos Analysis: %d%%, Upload errors: %d, Uploaded %d%% %s", immichPct, gpPct, counts[fileevent.UploadServerError], upPct, string(spinner[spinIdx])) diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index 07068996..b4114abe 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -55,6 +55,7 @@ type UpCmd struct { StackBurst bool // Stack burst (Default: TRUE) DiscardArchived bool // Don't import archived assets (Default: FALSE) WhenNoDate string // When the date can't be determined use the FILE's date or NOW (default: FILE) + ForceUploadWhenNoJSON bool // Some takeout don't supplies all JSON. When true, files are uploaded without any additional metadata BannedFiles namematcher.List // List of banned file name patterns BrowserConfig Configuration @@ -174,6 +175,8 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsO cmd.Var(&app.BannedFiles, "exclude-files", "Ignore files based on a pattern. Case insensitive. Add one option for each pattern do you need.") + cmd.BoolVar(&app.ForceUploadWhenNoJSON, "upload-when-missing-JSON", app.ForceUploadWhenNoJSON, "when true, photos are upload even without associated JSON file.") + err = cmd.Parse(args) if err != nil { return nil, err @@ -463,6 +466,9 @@ func (app *UpCmd) handleAsset(ctx context.Context, a *browser.LocalAssetFile) er app.Jnl.Record(ctx, fileevent.UploadServerDuplicate, a, a.FileName) } else { app.Jnl.Record(ctx, fileevent.AnalysisLocalDuplicate, a, a.FileName) + if a.LivePhoto != nil { + app.Jnl.Record(ctx, fileevent.AnalysisLocalDuplicate, a, a.LivePhoto.FileName) + } } app.manageAssetAlbum(ctx, advice.ServerAsset.ID, a, advice) diff --git a/docs/how-to-send-debug-data.md b/docs/how-to-send-debug-data.md new file mode 100644 index 00000000..992a2e33 --- /dev/null +++ b/docs/how-to-send-debug-data.md @@ -0,0 +1,87 @@ +# How to share data with the developer? + +The structure of the takeout archive can be weird enough to get `Immich-go` confused. + + +In most of the cases, the list of files is sufficient for trouble-shooting the problem. +This size of the list is much smaller than the full archive and contains enough information for simulating the import process. + +This list reveals only the files's name and size, and the albums' name. + +If you agree, you can share it with me via a DM on discord @simulot. + + + +## Get the file list from a zip takeout + +```sh +for f in *.zip; do echo "$f:"; unzip -l $f; done >list.lst +``` + +This produces a file like this: +``` +takeout-20240523T170453Z-001.zip: +Archive: takeout-20240523T170453Z-001.zip + Length Date Time Name +--------- ---------- ----- ---- + 800432 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8135.JPG + 1166223 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8133.JPG + 17132148 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/VID_20180819_191954.mp4 + 604784 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8131.JPG + 645224 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8130.JPG + 188804 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8132.JPG + 375981 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8129.JPG + 478073 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8128.JPG + 2047609 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8125.JPG + 2250833 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8124.JPG + 429040 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8120.JPG + 908856 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8117.JPG + 699546 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8118.JPG + 625635 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8115.JPG + 1006873 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8116.JPG + 499507 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/IMG_8114.JPG + 43189565 2024-05-23 19:31 Takeout/Google Photos/Photos from 2018/VID_20180819_192245.mp4 + 541875 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_8112.JPG + 503405 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_8113.JPG + 1070437 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_8111.JPG + 583809 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_8110.JPG + 808994 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_20180718_163816.jpg + 798787 2024-05-23 19:32 Takeout/Google Photos/Photos from 2018/IMG_20180718_163817.jpg +... +``` + + + +## Get the file list from a tgz takeout + +```sh +for f in *.tgz; do echo "$f:"; tar -tzvf $f; done >list.lst +``` + +This produces a file like this: +``` +takeout-20231209T153001Z-001.tgz: +-rw-r--r-- 0/0 3987330 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192201288.PORTRAIT.jpg +-rw-r--r-- 0/0 3825143 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192200378.PORTRAIT.jpg +-rw-r--r-- 0/0 838 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_202525504.jpg.json +-rw-r--r-- 0/0 4136113 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192157945.PORTRAIT.jpg +-rw-r--r-- 0/0 2817334 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192146687.PORTRAIT.jpg +-rw-r--r-- 0/0 838 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_202513366.jpg.json +-rw-r--r-- 0/0 827 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231209_074450784.LS.mp4.json +-rw-r--r-- 0/0 1453060 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_202525504.jpg +-rw-r--r-- 0/0 819 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192200378.PORTRAIT.jpg.json +-rw-r--r-- 0/0 849 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192157945.PORTRAIT.jpg.json +-rw-r--r-- 0/0 2852580 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192125032.PORTRAIT.jpg +-rw-r--r-- 0/0 827 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231209_073951854.LS.mp4.json +-rw-r--r-- 0/0 3046592 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192127213.PORTRAIT.jpg +-rw-r--r-- 0/0 684979 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231209_074450784.LS.mp4 +-rw-r--r-- 0/0 2638469 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192128472.PORTRAIT.jpg +-rw-r--r-- 0/0 819 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192201288.PORTRAIT.jpg.json +-rw-r--r-- 0/0 1046367 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_202513366.jpg +-rw-r--r-- 0/0 867 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192128472.PORTRAIT.jpg.json +-rw-r--r-- 0/0 602708 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_191742033.jpg +-rw-r--r-- 0/0 867 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192127213.PORTRAIT.jpg.json +-rw-r--r-- 0/0 867 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192146687.PORTRAIT.jpg.json +-rw-r--r-- 0/0 867 2023-12-09 16:30 Takeout/Google Photos/Photos from 2023/PXL_20231207_192125032.PORTRAIT.jpg.json +... +``` \ No newline at end of file diff --git a/docs/releases.md b/docs/releases.md index 3a494cb9..9c67e019 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -11,10 +11,25 @@ - [ ] Counters / missing assets GooglePhotos - [ ] [#390](https://github.com/simulot/immich-go/issues/390) - [ ] [#376](https://github.com/simulot/immich-go/issues/376) + - [ ] [#401](https://github.com/simulot/immich-go/issues/401) + - [ ] [#402](https://github.com/simulot/immich-go/issues/402) - [X] Some files are not matched (PIXIL) - [X] Check .MP.JPG - - [ ] run tests + - [X] Check asset in multiple albums + - [ ] Phyl404 case + - [X] Ziplist reader broken (Phyl404) + - [X] Some file aren't matched (Phyl404) + - [ ] fix gp solving percent (Phyl404) + - [ ] [#402](https://github.com/simulot/immich-go/issues/402) Steve81 issue + - [X] wrong album + - [ ] Add message about missing JSON UI and NOUI + - [ ] Display errors on UI + - [ ] Deactivate stack option + - [ ] Performances + - [X] matchDuplicateInYear 27% + - [X] SupportedMedia.IsExtensionPrefix - [ ] remove break points + - [ ] run tests - [ ] Report connection errors - [ ] [#395](https://github.com/simulot/immich-go/issues/395) - [ ] [#396](https://github.com/simulot/immich-go/issues/396) @@ -32,9 +47,12 @@ I have developed a way to simulate user cases using only the list files from the ### fixes: -- [[#376](https://github.com/simulot/immich-go/issues/376)] errors when uploading are disturbing the the % of the progression -- [ ] IPhone's Live photos recognition when the name is duplicated: ex IMG_2710(1).MP4 and IMG_2710(1).HEIC -- [ ] Missing a file when a directory contain several file withe the same name, but of a different type. Ex: IMG_0170.HEIC, IMG_0170.JPG +- [#376](https://github.com/simulot/immich-go/issues/376) errors when uploading are disturbing the the % of the progression +- files with same path and name, but in different part of the takeout file set was forgotten in duplicate counters +- iPhone's Live photos recognition when the name is duplicated: ex IMG_2710(1).MP4 and IMG_2710(1).HEIC +- Missing a file when a directory contain several files with the same name, but of a different type. Ex: IMG_0170.HEIC, IMG_0170.JPG +- Live videos attached to duplicated photos are now counted as duplicate as well, making the final report more relevant +- [#402](https://github.com/simulot/immich-go/issues/402) Wrong album assignment for images with the same name ## Release 0.20.1 diff --git a/helpers/fshelper/globwalkfs.go b/helpers/fshelper/globwalkfs.go index e595995d..1a841fde 100644 --- a/helpers/fshelper/globwalkfs.go +++ b/helpers/fshelper/globwalkfs.go @@ -1,6 +1,7 @@ package fshelper import ( + "fmt" "io/fs" "os" "path" @@ -108,7 +109,7 @@ func (gw GlobWalkFS) ReadDir(name string) ([]fs.DirEntry, error) { } entries, err := fs.ReadDir(gw.rootFS, name) if err != nil { - return nil, err + return nil, fmt.Errorf("ReadDir %s: %w", name, err) } returned := []fs.DirEntry{} diff --git a/immich/client.go b/immich/client.go index 61f15467..97b107ac 100644 --- a/immich/client.go +++ b/immich/client.go @@ -7,7 +7,10 @@ import ( "io" "net/http" "os" + "slices" + "sort" "strings" + "sync" "time" ) @@ -223,19 +226,24 @@ func (sm SupportedMedia) IsMedia(ext string) bool { return t == TypeVideo || t == TypeImage } +var ( + _supportedExtension []string + initSupportedExtion sync.Once +) + func (sm SupportedMedia) IsExtensionPrefix(ext string) bool { - ext = strings.ToLower(ext) - if len(ext) > 4 { - return false - } - for e, t := range sm { - if t == TypeVideo || t == TypeImage { - if ext == e[:len(e)-1] { - return true - } + initSupportedExtion.Do(func() { + _supportedExtension = make([]string, len(sm)) + i := 0 + for k := range sm { + _supportedExtension[i] = k[:len(k)-2] + i++ } - } - return false + sort.Strings(_supportedExtension) + }) + ext = strings.ToLower(ext) + _, b := slices.BinarySearch(_supportedExtension, ext) + return b } func (sm SupportedMedia) IsIgnoredExt(ext string) bool { diff --git a/immich/client_test.go b/immich/client_test.go new file mode 100644 index 00000000..0e20511f --- /dev/null +++ b/immich/client_test.go @@ -0,0 +1,30 @@ +package immich + +import "testing" + +/* +baseline + +goos: linux +goarch: amd64 +pkg: github.com/simulot/immich-go/immich +cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz +Benchmark_IsExtensionPrefix-12 4096238 297.3 ns/op 3 B/op 1 allocs/op +PASS +ok github.com/simulot/immich-go/immich 1.518s + +goos: linux +goarch: amd64 +pkg: github.com/simulot/immich-go/immich +cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz +Benchmark_IsExtensionPrefix-12 16536936 72.85 ns/op 3 B/op 1 allocs/op +PASS +ok github.com/simulot/immich-go/immich 1.283s +*/ +func Benchmark_IsExtensionPrefix(b *testing.B) { + sm := DefaultSupportedMedia + sm.IsExtensionPrefix(".JP") + for i := 0; i < b.N; i++ { + sm.IsExtensionPrefix(".JP") + } +} diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go index ba6e9d5e..add9ce94 100644 --- a/internal/fakefs/fakefs.go +++ b/internal/fakefs/fakefs.go @@ -2,6 +2,7 @@ package fakefs import ( "crypto/rand" + "fmt" "io" "io/fs" "path" @@ -86,7 +87,7 @@ func (fsys FakeFS) Stat(name string) (fs.FileInfo, error) { } l = fsys.files[dir] if len(l) == 0 { - return nil, fs.ErrNotExist + return nil, fmt.Errorf("%s:%s: %w", fsys.name, name, fs.ErrNotExist) } if e, ok := l[base]; ok { return e, nil diff --git a/internal/fakefs/ziplist.go b/internal/fakefs/ziplist.go index d56c183b..a8698736 100644 --- a/internal/fakefs/ziplist.go +++ b/internal/fakefs/ziplist.go @@ -8,22 +8,30 @@ import ( "io" "io/fs" "os" + "regexp" + "sort" "strconv" "strings" "time" + + "github.com/simulot/immich-go/helpers/gen" ) +// ` 2104348 07-20-2023 00:00 Takeout/Google Photos/2020 - Costa Rica/IMG_3235.MP4` + +var reZipList = regexp.MustCompile(`^(-..-..-..-\s\d+/\d+)?\s+(\d+)\s+(.{16})\s+(.*)$`) + func readFileLine(l string, dateFormat string) (string, int64, time.Time) { if len(l) < 30 { return "", 0, time.Time{} } - // ` 2104348 07-20-2023 00:00 Takeout/Google Photos/2020 - Costa Rica/IMG_3235.MP4` - s := strings.TrimSpace(l[:9]) - d := l[11:27] - name := l[30:] - size, _ := strconv.ParseInt(s, 10, 64) - modTime, _ := time.ParseInLocation(dateFormat, d, time.Local) - return name, size, modTime + m := reZipList.FindStringSubmatch(l) + if len(m) < 5 { + return "", 0, time.Time{} + } + size, _ := strconv.ParseInt(m[2], 10, 64) + modTime, _ := time.ParseInLocation(dateFormat, m[3], time.Local) + return m[4], size, modTime } func ScanStringList(dateFormat string, s string) ([]fs.FS, error) { @@ -45,14 +53,13 @@ func ScanFileListReader(f io.Reader, dateFormat string) ([]fs.FS, error) { fsyss := map[string]*FakeFS{} var fsys *FakeFS currentZip := "" - inList := false ok := false scanner := bufio.NewScanner(f) for scanner.Scan() { l := scanner.Text() - if strings.HasPrefix(l, "Archive: ") { - currentZip = strings.TrimPrefix(l, "Archive: ") + if strings.HasPrefix(l, "Archive:") { + currentZip = strings.TrimSpace(strings.TrimPrefix(l, "Archive:")) fsys, ok = fsyss[currentZip] if !ok { fsys = &FakeFS{ @@ -62,22 +69,10 @@ func ScanFileListReader(f io.Reader, dateFormat string) ([]fs.FS, error) { fsyss[currentZip] = fsys } - scanner.Scan() - scanner.Scan() - inList = true - continue - } - if strings.HasPrefix(l, "--------- ") { - scanner.Scan() - inList = false continue } - if inList { - if name, size, modTime := readFileLine(l, dateFormat); name != "" { - fsys.addFile(name, size, modTime) - } else { - inList = false - } + if name, size, modTime := readFileLine(l, dateFormat); name != "" { + fsys.addFile(name, size, modTime) } } @@ -85,10 +80,12 @@ func ScanFileListReader(f io.Reader, dateFormat string) ([]fs.FS, error) { return nil, err } + names := gen.MapKeys(fsyss) + sort.Strings(names) output := make([]fs.FS, len(fsyss)) i := 0 - for _, fs := range fsyss { - output[i] = fs + for _, name := range names { + output[i] = fsyss[name] i++ } return output, nil diff --git a/internal/fakefs/ziplist_test.go b/internal/fakefs/ziplist_test.go index 09005c2d..91964f45 100644 --- a/internal/fakefs/ziplist_test.go +++ b/internal/fakefs/ziplist_test.go @@ -39,6 +39,26 @@ func Test_readFileLine(t *testing.T) { wantSize: 197486, wantModTime: time.Date(2023, 7, 19, 23, 53, 0, 0, time.Local), }, + { + name: "Phyl404", + args: args{ + l: "2555584471 2024-07-12 13:41 Takeout/Google Foto/Vultures 1/IMG_2002_580.MOV", + dateFormat: "2006-01-02 15:04", + }, + wantName: "Takeout/Google Foto/Vultures 1/IMG_2002_580.MOV", + wantSize: 2555584471, + wantModTime: time.Date(2024, 7, 12, 13, 41, 0, 0, time.Local), + }, + { + name: "tgz", + args: args{ + l: "-rw-r--r-- 0/0 717454980 2023-12-09 17:23 Takeout/Google Photos/Photos from 2019/VID_20190621_193014.mp4", + dateFormat: "2006-01-02 15:04", + }, + wantName: "Takeout/Google Photos/Photos from 2019/VID_20190621_193014.mp4", + wantModTime: time.Date(2023, 12, 9, 17, 23, 0, 0, time.Local), + wantSize: 717454980, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -57,6 +77,12 @@ func Test_readFileLine(t *testing.T) { } } +func BenchmarkReadFileLine(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _, _ = readFileLine(" 145804 2024-05-25 22:15 Takeout/Google Photos/🇵🇹 Lisbonne ❤️ en famille 👨‍👩‍👦‍👦/😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛.jpg", "2006-01-02 15:04") + } +} + type NameFS interface { Name() string } From 2fbc421bf0b5b9ef82e1c8564031d86619ae2619 Mon Sep 17 00:00:00 2001 From: simulot Date: Sat, 27 Jul 2024 10:12:25 +0200 Subject: [PATCH 13/17] fix Wrong creation date results in false album assignment #392 --- cmd/upload/e2e_takeout_test.go | 12 +++++++++--- docs/google-takeout.md | 1 + docs/releases.md | 2 +- internal/fakefs/fakefs.go | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cmd/upload/e2e_takeout_test.go b/cmd/upload/e2e_takeout_test.go index 8b21366d..26765c9d 100644 --- a/cmd/upload/e2e_takeout_test.go +++ b/cmd/upload/e2e_takeout_test.go @@ -51,17 +51,23 @@ func simulate_upload(t *testing.T, zipList string, dateFormat string) { func TestPixilTakeOut(t *testing.T) { initMyEnv(t) - simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/pixil/list.lst", "01-02-2006 15:04") + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/pixil/list.lst", "01-02-2006 15:04") } func TestPhyl404TakeOut(t *testing.T) { initMyEnv(t) - simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/Phyl404/list.lst", "2006-01-02 15:04") + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/Phyl404/list.lst", "2006-01-02 15:04") } func TestSteve81TakeOut(t *testing.T) { initMyEnv(t) - simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/Counters/Steve81/list.list", "2006-01-02 15:04") + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/Steve81/list.list", "2006-01-02 15:04") +} + +func TestMuetyTakeOut(t *testing.T) { + initMyEnv(t) + + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/muety/list.lst", "01-02-2006 15:04") } diff --git a/docs/google-takeout.md b/docs/google-takeout.md index 65e48ab4..b98815e7 100644 --- a/docs/google-takeout.md +++ b/docs/google-takeout.md @@ -80,6 +80,7 @@ takeout-20240712T112341Z-010.zip: | French | Google Photos | métadonnées.json | | Italian | Google Foto | metadati.json | | Slovak | Fotky Google | metadáta.json | +| German | Google Fotos | Metadaten.json | # What if you have problems with a takeout archive? diff --git a/docs/releases.md b/docs/releases.md index 9c67e019..f1a8b391 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -35,7 +35,7 @@ - [ ] [#396](https://github.com/simulot/immich-go/issues/396) - [ ] [#393](https://github.com/simulot/immich-go/issues/393) - [ ] Creation date incorrect - - [ ] [#392](https://github.com/simulot/immich-go/issues/392) + - [X] [#392](https://github.com/simulot/immich-go/issues/392) - [ ] [#397](https://github.com/simulot/immich-go/issues/397) diff --git a/internal/fakefs/fakefs.go b/internal/fakefs/fakefs.go index add9ce94..186b7a03 100644 --- a/internal/fakefs/fakefs.go +++ b/internal/fakefs/fakefs.go @@ -109,7 +109,7 @@ func (fsys FakeFS) Open(name string) (fs.File, error) { if strings.ToLower(ext) == ".json" { base := path.Base(name) switch base { - case "métadonnées.json", "metadata.json", "metadati.json", "metadáta.json": + case "métadonnées.json", "metadata.json", "metadati.json", "metadáta.json", "Metadaten.json": album := path.Base(path.Dir(name)) r, fakeInfo.size = fakeAlbumData(album) case "print-subscriptions.json", "shared_album_comments.json", "user-generated-memory-titles.json": From cb0af18186e4fd02bf712c1380b71c58763e2e4d Mon Sep 17 00:00:00 2001 From: simulot Date: Sun, 28 Jul 2024 21:07:30 +0200 Subject: [PATCH 14/17] fix Problem with images with same name #402 , #390, #376, #401 --- cmd/shared.go | 8 +++ cmd/upload/e2e_takeout_test.go | 6 ++ cmd/upload/noui.go | 49 ++++++++--------- cmd/upload/ui.go | 16 +++--- cmd/upload/upload.go | 16 ++++++ docs/google-takeout.md | 16 +++--- docs/releases.md | 13 +++-- helpers/fileevent/fileevents.go | 22 ++++++++ internal/fakeImmich/immich.go | 97 +++++++++++++++++++++++++++++++++ 9 files changed, 194 insertions(+), 49 deletions(-) create mode 100644 internal/fakeImmich/immich.go diff --git a/cmd/shared.go b/cmd/shared.go index 390ebbac..b9c4512d 100644 --- a/cmd/shared.go +++ b/cmd/shared.go @@ -17,6 +17,7 @@ import ( "github.com/simulot/immich-go/helpers/myflag" "github.com/simulot/immich-go/helpers/tzone" "github.com/simulot/immich-go/immich" + fakeimmich "github.com/simulot/immich-go/internal/fakeImmich" "github.com/simulot/immich-go/ui" "github.com/telemachus/humane" ) @@ -38,6 +39,7 @@ type SharedFlags struct { NoUI bool // Disable user interface JSONLog bool // Enable JSON structured log DebugCounters bool // Enable CSV action counters per file + DebugFileList bool // When true, the file argument is a file wile the list of Takeout files Immich immich.ImmichInterface // Immich client Log *slog.Logger // Logger @@ -94,6 +96,12 @@ func (app *SharedFlags) Start(ctx context.Context) error { app.Jnl = fileevent.NewRecorder(nil, app.DebugCounters) } + if app.DebugFileList { + app.Immich = &fakeimmich.MockedCLient{} + _ = os.Remove(app.LogFile) + + } + if app.LogFile != "" { if app.LogWriterCloser == nil { err := configuration.MakeDirForFile(app.LogFile) diff --git a/cmd/upload/e2e_takeout_test.go b/cmd/upload/e2e_takeout_test.go index 26765c9d..9d077989 100644 --- a/cmd/upload/e2e_takeout_test.go +++ b/cmd/upload/e2e_takeout_test.go @@ -60,6 +60,12 @@ func TestPhyl404TakeOut(t *testing.T) { simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/Phyl404/list.lst", "2006-01-02 15:04") } +func TestPhyl404_2TakeOut(t *testing.T) { + initMyEnv(t) + + simulate_upload(t, myEnv["IMMICH_TESTFILES"]+"/User Files/Phy404#2/list.lst", "2006-01-02 15:04") +} + func TestSteve81TakeOut(t *testing.T) { initMyEnv(t) diff --git a/cmd/upload/noui.go b/cmd/upload/noui.go index 85ad593c..7e2c288d 100644 --- a/cmd/upload/noui.go +++ b/cmd/upload/noui.go @@ -3,6 +3,7 @@ package upload import ( "context" "fmt" + "sync/atomic" "time" "github.com/simulot/immich-go/helpers/fileevent" @@ -13,6 +14,8 @@ func (app *UpCmd) runNoUI(ctx context.Context) error { ctx, cancel := context.WithCancelCause(ctx) defer cancel(nil) + var preparationDone atomic.Bool + stopProgress := make(chan any) var maxImmich, currImmich int spinner := []rune{' ', ' ', '.', ' ', ' '} @@ -23,46 +26,37 @@ func (app *UpCmd) runNoUI(ctx context.Context) error { } progressString := func() string { - var s string counts := app.Jnl.GetCounts() + defer func() { + spinIdx++ + if spinIdx == len(spinner) { + spinIdx = 0 + } + }() immichPct := 0 if maxImmich > 0 { immichPct = 100 * currImmich / maxImmich } else { immichPct = 100 } - AssetsToUpload := counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo] - counts[fileevent.AnalysisLocalDuplicate] - - ProcessedAssets := counts[fileevent.Uploaded] + - counts[fileevent.UploadServerError] + - counts[fileevent.UploadNotSelected] + - counts[fileevent.UploadUpgraded] + - counts[fileevent.UploadServerDuplicate] + - counts[fileevent.UploadServerBetter] + - counts[fileevent.DiscoveredDiscarded] - if !app.ForceUploadWhenNoJSON { - ProcessedAssets += counts[fileevent.AnalysisMissingAssociatedMetadata] - } if app.GooglePhotos { - gpPct := 0 - upPct := 0 - - if AssetsToUpload > 0 { - gpPct = int(100 * (counts[fileevent.AnalysisAssociatedMetadata] + counts[fileevent.AnalysisMissingAssociatedMetadata]) / (counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo])) + gpTotal := app.Jnl.TotalAssets() + gpProcessed := app.Jnl.TotalProcessedGP() - upPct = int(100 * ProcessedAssets / AssetsToUpload) + gpPercent := int(100 * gpProcessed / gpTotal) + upProcessed := int64(0) + if preparationDone.Load() { + upProcessed = app.Jnl.TotalProcessed() } + upTotal := app.Jnl.TotalAssets() + upPercent := 100 * upProcessed / upTotal - s = fmt.Sprintf("\rImmich read %d%%, Google Photos Analysis: %d%%, Upload errors: %d, Uploaded %d%% %s", immichPct, gpPct, counts[fileevent.UploadServerError], upPct, string(spinner[spinIdx])) - } else { - s = fmt.Sprintf("\rImmich read %d%%, Processed %d, Upload errors: %d, Uploaded %d %s", immichPct, ProcessedAssets, counts[fileevent.UploadServerError], counts[fileevent.Uploaded], string(spinner[spinIdx])) + return fmt.Sprintf("\rImmich read %d%%, Google Photos Analysis: %d%%, Upload errors: %d, Uploaded %d%% %s", + immichPct, gpPercent, counts[fileevent.UploadServerError], upPercent, string(spinner[spinIdx])) } - spinIdx++ - if spinIdx == len(spinner) { - spinIdx = 0 - } - return s + + return fmt.Sprintf("\rImmich read %d%%, Upload errors: %d, Uploaded %d %s", immichPct, counts[fileevent.UploadServerError], counts[fileevent.Uploaded], string(spinner[spinIdx])) } uiGrp := errgroup.Group{} @@ -116,6 +110,7 @@ func (app *UpCmd) runNoUI(ctx context.Context) error { return err } } + preparationDone.Store(true) err = app.uploadLoop(ctx) if err != nil { cancel(err) diff --git a/cmd/upload/ui.go b/cmd/upload/ui.go index c516fd02..df30fdb2 100644 --- a/cmd/upload/ui.go +++ b/cmd/upload/ui.go @@ -128,15 +128,13 @@ func (app *UpCmd) runUI(ctx context.Context) error { ui.getCountView(c, counts[c]) } if app.GooglePhotos { - ui.immichPrepare.SetMaxValue(int(counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo])) - ui.immichPrepare.SetValue(int(counts[fileevent.AnalysisAssociatedMetadata])) - - ui.immichUpload.SetMaxValue(int(counts[fileevent.DiscoveredImage] + counts[fileevent.DiscoveredVideo] - counts[fileevent.UploadServerError])) - ui.immichUpload.SetValue(int(counts[fileevent.UploadNotSelected] + - counts[fileevent.UploadUpgraded] + - counts[fileevent.UploadServerDuplicate] + - counts[fileevent.UploadServerBetter] + - counts[fileevent.Uploaded])) + ui.immichPrepare.SetMaxValue(int(app.Jnl.TotalAssets())) + ui.immichPrepare.SetValue(int(app.Jnl.TotalProcessedGP())) + + if preparationDone.Load() { + ui.immichUpload.SetMaxValue(int(app.Jnl.TotalAssets())) + } + ui.immichUpload.SetValue(int(app.Jnl.TotalProcessed())) } }) } diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index b4114abe..bccd0f94 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "strings" + "sync/atomic" "time" "github.com/gdamore/tcell/v2" @@ -27,6 +28,7 @@ import ( "github.com/simulot/immich-go/helpers/namematcher" "github.com/simulot/immich-go/helpers/stacking" "github.com/simulot/immich-go/immich" + "github.com/simulot/immich-go/internal/fakefs" ) type UpCmd struct { @@ -65,6 +67,7 @@ type UpCmd struct { AssetIndex *AssetIndex // List of assets present on the server deleteServerList []*immich.Asset // List of server assets to remove deleteLocalList []*browser.LocalAssetFile // List of local assets to remove + gpProcessDone atomic.Bool // True when terminated // updateAlbums map[string]map[string]any // track immich albums changes stacks *stacking.StackBuilder browser browser.Browser @@ -176,12 +179,25 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsO cmd.Var(&app.BannedFiles, "exclude-files", "Ignore files based on a pattern. Case insensitive. Add one option for each pattern do you need.") cmd.BoolVar(&app.ForceUploadWhenNoJSON, "upload-when-missing-JSON", app.ForceUploadWhenNoJSON, "when true, photos are upload even without associated JSON file.") + cmd.BoolVar(&app.DebugFileList, "debug-file-list", app.DebugFileList, "Check how the your file list would be processed") err = cmd.Parse(args) if err != nil { return nil, err } + if app.DebugFileList { + if len(cmd.Args()) < 2 { + return nil, fmt.Errorf("the option -debug-file-list requires a file name and a date format") + } + app.LogFile = strings.TrimSuffix(cmd.Arg(0), filepath.Ext(cmd.Arg(0))) + ".log" + _ = os.Remove(app.LogFile) + + fsOpener = func() ([]fs.FS, error) { + return fakefs.ScanFileList(cmd.Arg(0), cmd.Arg(1)) + } + } + app.WhenNoDate = strings.ToUpper(app.WhenNoDate) switch app.WhenNoDate { case "FILE", "NOW": diff --git a/docs/google-takeout.md b/docs/google-takeout.md index b98815e7..d662b7e8 100644 --- a/docs/google-takeout.md +++ b/docs/google-takeout.md @@ -72,15 +72,15 @@ takeout-20240712T112341Z-010.zip: ``` -## Some key names are spelled in the user language +## Some key file names are spelled in the user language -| Language | Google Photos folder name | Album's metadata | -| ---------- | ------------------------- | ---------------- | -| US English | Google Photos | metadata.json | -| French | Google Photos | métadonnées.json | -| Italian | Google Foto | metadati.json | -| Slovak | Fotky Google | metadáta.json | -| German | Google Fotos | Metadaten.json | +| Language | Google Photos folder | Album's metadata | Edited photo name | +| ---------- | -------------------- | ---------------- | ----------------- | +| US English | Google Photos | metadata.json | | +| French | Google Photos | métadonnées.json | \*-modifié.\* | +| Italian | Google Foto | metadati.json | \*-modificato.\* | +| Slovak | Fotky Google | metadáta.json | | +| German | Google Fotos | Metadaten.json | \*-bearbeitet.\* | # What if you have problems with a takeout archive? diff --git a/docs/releases.md b/docs/releases.md index f1a8b391..67737223 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -9,10 +9,11 @@ ### TO DO - [ ] Counters / missing assets GooglePhotos - - [ ] [#390](https://github.com/simulot/immich-go/issues/390) - - [ ] [#376](https://github.com/simulot/immich-go/issues/376) - - [ ] [#401](https://github.com/simulot/immich-go/issues/401) - - [ ] [#402](https://github.com/simulot/immich-go/issues/402) + - [X] [#390](https://github.com/simulot/immich-go/issues/390) + - [X] [#376](https://github.com/simulot/immich-go/issues/376) + - [X] [#401](https://github.com/simulot/immich-go/issues/401) + - [X] [#402](https://github.com/simulot/immich-go/issues/402) Problem with images with same name + - [ ] [#405](https://github.com/simulot/immich-go/issues/405) MP~2 files - [X] Some files are not matched (PIXIL) - [X] Check .MP.JPG - [X] Check asset in multiple albums @@ -53,7 +54,9 @@ I have developed a way to simulate user cases using only the list files from the - Missing a file when a directory contain several files with the same name, but of a different type. Ex: IMG_0170.HEIC, IMG_0170.JPG - Live videos attached to duplicated photos are now counted as duplicate as well, making the final report more relevant - [#402](https://github.com/simulot/immich-go/issues/402) Wrong album assignment for images with the same name - +- [#390](https://github.com/simulot/immich-go/issues/390) Question: report shows way less images uploaded than scanned +- [#376](https://github.com/simulot/immich-go/issues/376) errors when uploading are disturbing the the % of the progression +- [#401](https://github.com/simulot/immich-go/issues/401) Add an option to import images/movies even if there is no JSON file in the takeout ## Release 0.20.1 ### changes diff --git a/helpers/fileevent/fileevents.go b/helpers/fileevent/fileevents.go index 2026a31e..aab3c8c9 100644 --- a/helpers/fileevent/fileevents.go +++ b/helpers/fileevent/fileevents.go @@ -221,3 +221,25 @@ func (r *Recorder) WriteFileCounts(w io.Writer) error { } return nil } + +func (r *Recorder) TotalAssets() int64 { + return atomic.LoadInt64(&r.counts[DiscoveredImage]) + atomic.LoadInt64(&r.counts[DiscoveredVideo]) +} + +func (r *Recorder) TotalProcessedGP() int64 { + return atomic.LoadInt64(&r.counts[AnalysisAssociatedMetadata]) + + atomic.LoadInt64(&r.counts[AnalysisMissingAssociatedMetadata]) + + atomic.LoadInt64(&r.counts[DiscoveredDiscarded]) +} + +func (r *Recorder) TotalProcessed() int64 { + return atomic.LoadInt64(&r.counts[Uploaded]) + + atomic.LoadInt64(&r.counts[UploadServerError]) + + atomic.LoadInt64(&r.counts[UploadNotSelected]) + + atomic.LoadInt64(&r.counts[UploadUpgraded]) + + atomic.LoadInt64(&r.counts[UploadServerDuplicate]) + + atomic.LoadInt64(&r.counts[UploadServerBetter]) + + atomic.LoadInt64(&r.counts[DiscoveredDiscarded]) + + atomic.LoadInt64(&r.counts[AnalysisLocalDuplicate]) + + atomic.LoadInt64(&r.counts[AnalysisMissingAssociatedMetadata]) +} diff --git a/internal/fakeImmich/immich.go b/internal/fakeImmich/immich.go new file mode 100644 index 00000000..dd06a72f --- /dev/null +++ b/internal/fakeImmich/immich.go @@ -0,0 +1,97 @@ +package fakeimmich + +import ( + "context" + "io" + + "github.com/simulot/immich-go/browser" + "github.com/simulot/immich-go/immich" +) + +type MockedCLient struct{} + +func (c *MockedCLient) GetAllAssetsWithFilter(context.Context, func(*immich.Asset) error) error { + return nil +} + +func (c *MockedCLient) AssetUpload(context.Context, *browser.LocalAssetFile) (immich.AssetResponse, error) { + return immich.AssetResponse{}, nil +} + +func (c *MockedCLient) DeleteAssets(context.Context, []string, bool) error { + return nil +} + +func (c *MockedCLient) GetAllAlbums(context.Context) ([]immich.AlbumSimplified, error) { + return nil, nil +} + +func (c *MockedCLient) AddAssetToAlbum(context.Context, string, []string) ([]immich.UpdateAlbumResult, error) { + return nil, nil +} + +func (c *MockedCLient) CreateAlbum(context.Context, string, string, []string) (immich.AlbumSimplified, error) { + return immich.AlbumSimplified{}, nil +} + +func (c *MockedCLient) UpdateAssets(ctx context.Context, ids []string, isArchived bool, isFavorite bool, latitude float64, longitude float64, removeParent bool, stackParentID string) error { + return nil +} + +func (c *MockedCLient) StackAssets(ctx context.Context, cover string, ids []string) error { + return nil +} + +func (c *MockedCLient) UpdateAsset(ctx context.Context, id string, a *browser.LocalAssetFile) (*immich.Asset, error) { + return nil, nil +} + +func (c *MockedCLient) EnableAppTrace(w io.Writer) {} + +func (c *MockedCLient) GetServerStatistics(ctx context.Context) (immich.ServerStatistics, error) { + return immich.ServerStatistics{}, nil +} + +func (c *MockedCLient) PingServer(ctx context.Context) error { + return nil +} + +func (c *MockedCLient) SetDeviceUUID(string) {} + +func (c *MockedCLient) SetEndPoint(string) {} + +func (c *MockedCLient) ValidateConnection(ctx context.Context) (immich.User, error) { + return immich.User{}, nil +} + +func (c *MockedCLient) GetAssetAlbums(ctx context.Context, id string) ([]immich.AlbumSimplified, error) { + return nil, nil +} + +func (c *MockedCLient) GetAllAssets(ctx context.Context) ([]*immich.Asset, error) { + return nil, nil +} + +func (c *MockedCLient) DeleteAlbum(ctx context.Context, id string) error { + return nil +} + +func (c *MockedCLient) SupportedMedia() immich.SupportedMedia { + return immich.DefaultSupportedMedia +} + +func (c *MockedCLient) GetAssetStatistics(ctx context.Context) (immich.UserStatistics, error) { + return immich.UserStatistics{ + Images: 1, + Videos: 1, + Total: 1, + }, nil +} + +func (c *MockedCLient) GetJobs(ctx context.Context) (map[string]immich.Job, error) { + return nil, nil +} + +func (c *MockedCLient) GetAlbumInfo(context.Context, string, bool) (immich.AlbumContent, error) { + return immich.AlbumContent{}, nil +} From c9457961e745b0fda814988a95ef6fff47daa6bd Mon Sep 17 00:00:00 2001 From: simulot Date: Mon, 29 Jul 2024 19:16:13 +0200 Subject: [PATCH 15/17] fix tests --- browser/gp/googlephotos.go | 25 ++---------------------- browser/gp/googlephotos_test.go | 5 ----- browser/gp/testgp_samples_test.go | 28 +++++++++++++++++++++++++++ browser/gp/testgp_test.go | 4 ++++ cmd/upload/noui.go | 16 +++++++++++++++ cmd/upload/ui.go | 27 +++++++++++++++++++++----- cmd/upload/upload.go | 18 +++++++++-------- cpu.prof | Bin 0 -> 46819 bytes docs/releases.md | 26 ++++++++++++++++++++----- internal/fakefs/ziplist.go | 2 +- readme.md | 31 +++++++++++++++--------------- 11 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 cpu.prof diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index cd8ed3ee..9a936ed4 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -2,7 +2,6 @@ package gp import ( "context" - "fmt" "io/fs" "path" "path/filepath" @@ -26,12 +25,8 @@ type Takeout struct { log *fileevent.Recorder sm immich.SupportedMedia - banned namematcher.List // Banned files - acceptMissingJSON bool - totalUnmatched int // count the number of asset not matched - totalSent int // DEBUG - totalMotionPicture int // DEBUG - files map[string]int // DEBUG + banned namematcher.List // Banned files + acceptMissingJSON bool } // directoryCatalog captures all files in a given directory @@ -56,7 +51,6 @@ func NewTakeout(ctx context.Context, l *fileevent.Recorder, sm immich.SupportedM albums: map[string]browser.LocalAlbum{}, log: l, sm: sm, - files: map[string]int{}, } return &to, nil @@ -170,7 +164,6 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { return nil } - to.files[name] = to.files[name] + 1 // DEBUG dirCatalog.unMatchedFiles[base] = &assetFile{ fsys: w, base: base, @@ -480,13 +473,6 @@ func (to *Takeout) Browse(ctx context.Context) chan *browser.LocalAssetFile { } } } - - if to.totalUnmatched > 0 { - to.log.Record(ctx, fileevent.Error, nil, "", "error", "too many unmatched files with JSON. Have you processed all parts of the takeout in this run?") - } - if len(to.files) > 0 { - to.log.Record(ctx, fileevent.Error, nil, "", "error", fmt.Sprintf("%d scanned images/video, but not processed. Contact the developer.\n", len(to.files))) - } }() return assetChan } @@ -535,7 +521,6 @@ nextVideo: if p == name { linked.video = catalog.matchedFiles[f] linkedFiles[i] = linked - to.totalMotionPicture++ continue nextVideo } } @@ -576,12 +561,6 @@ nextVideo: case <-ctx.Done(): return ctx.Err() default: - to.files[a.FileName] = to.files[a.FileName] - 1 - to.totalSent++ - if a.LivePhoto != nil { - to.files[a.LivePhoto.FileName] = to.files[a.LivePhoto.FileName] - 1 - to.totalSent++ - } assetChan <- a } } diff --git a/browser/gp/googlephotos_test.go b/browser/gp/googlephotos_test.go index e2da29d3..3c467a82 100644 --- a/browser/gp/googlephotos_test.go +++ b/browser/gp/googlephotos_test.go @@ -42,11 +42,6 @@ func Test_matchers(t *testing.T) { fileName: "IMG_2710(1).HEIC", want: "matchDuplicateInYear", }, - { - jsonName: "IMG_2710.HEIC(1).json", - fileName: "IMG_2710(1).MP4", - want: "matchDuplicateInYear", - }, { jsonName: "PXL_20231118_035751175.MP.jpg.json", fileName: "PXL_20231118_035751175.MP.jpg", diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index 29ce39ef..b00ba88f 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -325,3 +325,31 @@ Archive: takeout-20230720T065335Z-002.zip -rw-r--r-- 0/0 708 2024-07-19 02:13 Takeout/Google Foto/2016-01-05 - _3/20160105_121621_LLS.jpg.json `) } + +func checkMP_405() []fs.FS { + return loadFromString("2006-01-02 15:04", `Archive: takeout-20230720T065335Z-001.zip + 893 2024-01-21 16:33 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP.jpg.json + 896 2024-01-21 16:33 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP~2.jpg.json + 895 2024-01-21 16:52 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP~2.jpg.json + 893 2024-01-21 16:52 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP.jpg.json + 893 2024-01-21 17:46 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP.jpg.json + 895 2024-01-21 17:46 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP~2.jpg.json + 3242290 2024-01-21 16:58 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP + 1214365 2024-01-21 16:58 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP~2 + 4028710 2024-01-21 16:59 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP~2.jpg + 6486725 2024-01-21 16:59 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP.jpg + 1214365 2024-01-21 15:40 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP~2 + 3242290 2024-01-21 15:40 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP + 6486725 2024-01-21 15:41 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP.jpg + 4028710 2024-01-21 15:41 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP~2.jpg + 3242290 2024-01-21 17:53 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP + 1214365 2024-01-21 17:53 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP~2 + 6486725 2024-01-21 17:53 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP.jpg + 4028710 2024-01-21 17:53 Takeout/Google Photos/Photos from 2021/PXL_20210102_221126856.MP~2.jpg + 1214365 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP~2 + 3242290 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP + 892 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP.jpg.json + 895 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP~2.jpg.json + 4028710 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP~2.jpg + 6486725 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP.jpg`) +} diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index 26acd24e..677cfa68 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -323,6 +323,10 @@ func TestArchives(t *testing.T) { wantLivePhotos: photo{}, wantAlbum: album{}, }, + // { // #405 + // name: "checkMP_405", + // gen: checkMP_405, + // }, } for _, c := range tc { t.Run( diff --git a/cmd/upload/noui.go b/cmd/upload/noui.go index 7e2c288d..3518dafb 100644 --- a/cmd/upload/noui.go +++ b/cmd/upload/noui.go @@ -2,7 +2,9 @@ package upload import ( "context" + "errors" "fmt" + "strings" "sync/atomic" "time" @@ -115,6 +117,20 @@ func (app *UpCmd) runNoUI(ctx context.Context) error { if err != nil { cancel(err) } + + counts := app.Jnl.GetCounts() + messages := strings.Builder{} + if counts[fileevent.Error]+counts[fileevent.UploadServerError] > 0 { + messages.WriteString("Some errors have occurred. Look at the log file for details\n") + } + if app.GooglePhotos && counts[fileevent.AnalysisMissingAssociatedMetadata] > 0 { + messages.WriteString(fmt.Sprintf("\n%d JSON files are missing.\n", counts[fileevent.AnalysisMissingAssociatedMetadata])) + messages.WriteString("- have you processed all takeout parts together?\n") + messages.WriteString("- ask for another takeout, or ask one year at a time.\n") + } + if messages.Len() > 0 { + cancel(errors.New(messages.String())) + } close(stopProgress) return err }) diff --git a/cmd/upload/ui.go b/cmd/upload/ui.go index df30fdb2..18fb09bb 100644 --- a/cmd/upload/ui.go +++ b/cmd/upload/ui.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "strings" "sync/atomic" "time" @@ -51,6 +52,7 @@ func (app *UpCmd) runUI(ctx context.Context) error { var preparationDone atomic.Bool var uploadDone atomic.Bool var uiGroup errgroup.Group + var messages strings.Builder uiApp.SetRoot(pages, true) @@ -61,9 +63,7 @@ func (app *UpCmd) runUI(ctx context.Context) error { } } - modal := newModal() pages.AddPage("ui", ui.screen, true, true) - pages.AddPage("modal", modal, true, false) // handle Ctrl+C and Ctrl+Q uiApp.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { @@ -193,9 +193,21 @@ func (app *UpCmd) runUI(ctx context.Context) error { return context.Cause(ctx) } uploadDone.Store(true) + counts := app.Jnl.GetCounts() + if counts[fileevent.Error]+counts[fileevent.UploadServerError] > 0 { + messages.WriteString("Some errors have occurred. Look at the log file for details\n") + } + if app.GooglePhotos && counts[fileevent.AnalysisMissingAssociatedMetadata] > 0 { + messages.WriteString(fmt.Sprintf("\n%d JSON files are missing.\n", counts[fileevent.AnalysisMissingAssociatedMetadata])) + messages.WriteString("- have you processed all takeout parts together?\n") + messages.WriteString("- ask for another takeout, or ask one year at a time.\n") + } + modal := newModal(messages.String()) + pages.AddPage("modal", modal, true, false) // upload is done! pages.ShowPage("modal") + return err }) @@ -207,10 +219,15 @@ func (app *UpCmd) runUI(ctx context.Context) error { // Time to leave app.Jnl.Report() + if messages.Len() > 0 { + return (errors.New(messages.String())) + } return err } -func newModal() tview.Primitive { +func newModal(message string) tview.Primitive { + message += "\nYou can quit the program safely.\n\nPress the [enter] key to exit." + lines := strings.Count(message, "\n") // Returns a new primitive which puts the provided primitive in the center and // sets its size to the given width and height. modal := func(p tview.Primitive, width, height int) tview.Primitive { @@ -222,12 +239,12 @@ func newModal() tview.Primitive { AddItem(nil, 0, 1, false), width, 1, true). AddItem(nil, 0, 1, false) } - text := tview.NewTextView().SetText("\nYou can quit the program safely.\n\nPress the [enter] key to exit.").SetTextAlign(tview.AlignCenter) + text := tview.NewTextView().SetText(message) box := tview.NewBox(). SetBorder(true). SetTitle("Upload completed") text.Box = box - return modal(text, 40, 7) + return modal(text, 80, 2+lines) } func newUI(ctx context.Context, app *UpCmd) *uiPage { diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index bccd0f94..906555cf 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -12,7 +12,6 @@ import ( "path" "path/filepath" "strings" - "sync/atomic" "time" "github.com/gdamore/tcell/v2" @@ -67,16 +66,13 @@ type UpCmd struct { AssetIndex *AssetIndex // List of assets present on the server deleteServerList []*immich.Asset // List of server assets to remove deleteLocalList []*browser.LocalAssetFile // List of local assets to remove - gpProcessDone atomic.Bool // True when terminated // updateAlbums map[string]map[string]any // track immich albums changes stacks *stacking.StackBuilder browser browser.Browser } func UploadCommand(ctx context.Context, common *cmd.SharedFlags, args []string) error { - app, err := newCommand(ctx, common, args, func() ([]fs.FS, error) { - return fshelper.ParsePath(args) - }) + app, err := newCommand(ctx, common, args, nil) if err != nil { return err } @@ -157,14 +153,14 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsO cmd.BoolFunc( "create-stacks", - "Stack jpg/raw or bursts (default TRUE)", myflag.BoolFlagFn(&app.CreateStacks, true)) + "Stack jpg/raw or bursts (default FALSE)", myflag.BoolFlagFn(&app.CreateStacks, false)) cmd.BoolFunc( "stack-jpg-raw", - "Control the stacking of jpg/raw photos (default TRUE)", myflag.BoolFlagFn(&app.StackJpgRaws, true)) + "Control the stacking of jpg/raw photos (default TRUE)", myflag.BoolFlagFn(&app.StackJpgRaws, false)) cmd.BoolFunc( "stack-burst", - "Control the stacking bursts (default TRUE)", myflag.BoolFlagFn(&app.StackBurst, true)) + "Control the stacking bursts (default TRUE)", myflag.BoolFlagFn(&app.StackBurst, false)) // cmd.BoolVar(&app.Delete, "delete", false, "Delete local assets after upload") @@ -196,6 +192,7 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsO fsOpener = func() ([]fs.FS, error) { return fakefs.ScanFileList(cmd.Arg(0), cmd.Arg(1)) } + } else { } app.WhenNoDate = strings.ToUpper(app.WhenNoDate) @@ -211,6 +208,11 @@ func newCommand(ctx context.Context, common *cmd.SharedFlags, args []string, fsO return nil, err } + if fsOpener == nil { + fsOpener = func() ([]fs.FS, error) { + return fshelper.ParsePath(cmd.Args()) + } + } app.fsyss, err = fsOpener() if err != nil { return nil, err diff --git a/cpu.prof b/cpu.prof new file mode 100644 index 0000000000000000000000000000000000000000..829e26eed034a50db7e1ef776509cb0b9eb52fea GIT binary patch literal 46819 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 literal 0 HcmV?d00001 diff --git a/docs/releases.md b/docs/releases.md index 67737223..7ce60380 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -20,11 +20,10 @@ - [ ] Phyl404 case - [X] Ziplist reader broken (Phyl404) - [X] Some file aren't matched (Phyl404) - - [ ] fix gp solving percent (Phyl404) + - [X] fix gp solving percent (Phyl404) - [ ] [#402](https://github.com/simulot/immich-go/issues/402) Steve81 issue - [X] wrong album - - [ ] Add message about missing JSON UI and NOUI - - [ ] Display errors on UI + - [X] Add message about missing JSON UI and NOUI - [ ] Deactivate stack option - [ ] Performances - [X] matchDuplicateInYear 27% @@ -43,8 +42,23 @@ ### Refactoring the Google Photos import another time -Lot of users have reported inconsistencies in upload counters. Because we are manipulating personal photos and because it's not practical to test with user's archives, -I have developed a way to simulate user cases using only the list files from the takeout archives. +Lot of users have reported inconsistencies in upload counters. Each user case a different, and the takeout structure varies a bit. +In order to debug those cases, I have developed a way to simulate the takeout import using only the the file list. Read [how to send debug data](/docs/how-to-send-debug-data.md) without sharing photos. + + +### Option to force the upload of images despite the lack of JSON +Each image in a takeout is supposed to come with A JSON file giving the date of capture and the GPS coordinate. There a few reason for this: +1. The original file is copied, modified... and sometime there ins't a JSON for all versions +2. JSON aren't in the same ZIP file than the image, and only one part of the takeout is processed +3. The takeout misses a bunch of JSON + +When asking another takeout isn't an option, it's possible to force the upload of photos with no JSON. Use the option `-upload-when-missing-JSON` + +### The stack function is disabled +The stack function need to be improved [#399](https://github.com/simulot/immich-go/issues/399), [#345](https://github.com/simulot/immich-go/issues/345), [#235](https://github.com/simulot/immich-go/issues/235) +Meanwhile, it is disabled by default. You can enable it using the option `-create-stacks=TRUE`. + + ### fixes: @@ -57,6 +71,8 @@ I have developed a way to simulate user cases using only the list files from the - [#390](https://github.com/simulot/immich-go/issues/390) Question: report shows way less images uploaded than scanned - [#376](https://github.com/simulot/immich-go/issues/376) errors when uploading are disturbing the the % of the progression - [#401](https://github.com/simulot/immich-go/issues/401) Add an option to import images/movies even if there is no JSON file in the takeout + + ## Release 0.20.1 ### changes diff --git a/internal/fakefs/ziplist.go b/internal/fakefs/ziplist.go index a8698736..40094ff2 100644 --- a/internal/fakefs/ziplist.go +++ b/internal/fakefs/ziplist.go @@ -19,7 +19,7 @@ import ( // ` 2104348 07-20-2023 00:00 Takeout/Google Photos/2020 - Costa Rica/IMG_3235.MP4` -var reZipList = regexp.MustCompile(`^(-..-..-..-\s\d+/\d+)?\s+(\d+)\s+(.{16})\s+(.*)$`) +var reZipList = regexp.MustCompile(`(-rw-r--r-- 0/0\s+)?(\d+)\s+(.{16})\s+(.*)$`) func readFileLine(l string, dateFormat string) (string, int64, time.Time) { if len(l) < 30 { diff --git a/readme.md b/readme.md index e14d5172..569d9bb6 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,7 @@ Example: Immich-go check the server's SSL certificate. you can disable this beha | `-server=URL` | URL of the Immich service, example http://:2283 or https://your-domain | | | `-api=URL` | URL of the Immich api endpoint (http://container_ip:3301) | | | `-device-uuid=VALUE` | Force the device identification | `$HOSTNAME` | -| `-client-timeout=duration` | Set the timeout for server calls. The duration is a decimal number with a unit suffix, such as "300ms", "1.5m" or "45m". Valid time units are "ms", "s", "m", "h". | `5m` | +| `-client-timeout=duration` | Set the timeout for server calls. The duration is a decimal number with a unit suffix, such as "300ms", "1.5m" or "45m". Valid time units are "ms", "s", "m", "h". | `5m` | | `-skip-verify-ssl` | Skip SSL verification for use with self-signed certificates | `false` | | `-key=KEY` | A key generated by the user. Uploaded photos will belong to the key's owner. | | | `-log-level=LEVEL` | Adjust the log verbosity as follows:
- `ERROR`: Display only errors
- `WARNING`: Same as previous one plus non-blocking error
- `INFO`: Information messages | `INFO` | @@ -82,8 +82,8 @@ Example: Immich-go check the server's SSL certificate. you can disable this beha | `-log-json` | Output the log as line-delimited JSON file | `false` | | `-time-zone=time_zone_name` | Set the time zone for dates without time zone information | the system's time zone | | `-no-ui` | Disable the user interface | `false` | -| `-debug-counters` | Enable the generation a CSV beside the log file | `false` | -| `-api-trace` | Enable trace of API calls | `false` | +| `-debug-counters` | Enable the generation a CSV beside the log file | `false` | +| `-api-trace` | Enable trace of API calls | `false` | ## Command `upload` @@ -91,18 +91,18 @@ Use this command for uploading photos and videos from a local directory, a zippe ### Switches and options: -| **Parameter** | **Description** | **Default value** | -| ------------------------------------ | --------------------------------------------------------------------------------------- | ----------------- | -| `-album="ALBUM NAME"` | Import assets into the Immich album `ALBUM NAME`. | | -| `-dry-run` | Preview all actions as they would be done. | `FALSE` | -| `-create-album-folder` | Generate immich albums after folder names. | `FALSE` | -| `-create-stacks` | Stack jpg/raw or bursts. | `TRUE` | -| `-stack-jpg-raw` | Control the stacking of jpg/raw photos. | `TRUE` | -| `-stack-burst` | Control the stacking bursts. | `TRUE` | -| `-select-types=".ext,.ext,.ext..."` | List of accepted extensions. | | -| `-exclude-types=".ext,.ext,.ext..."` | List of excluded extensions. | | -| `-when-no-date=FILE\|NOW` | When the date of take can't be determined, use the FILE's date or the current time NOW. | `FILE` | -| `-exclude-files=pattern` | Ignore files based on a pattern. Case insensitive. Repeat the option for each pattern do you need. | `@eaDir/`
`@__thumb/`
`SYNOFILE_THUMB_*.*`
`Lightroom Catalog/`
`thumbnails/`| +| **Parameter** | **Description** | **Default value** | +| ------------------------------------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `-album="ALBUM NAME"` | Import assets into the Immich album `ALBUM NAME`. | | +| `-dry-run` | Preview all actions as they would be done. | `FALSE` | +| `-create-album-folder` | Generate immich albums after folder names. | `FALSE` | +| `-create-stacks` | Stack jpg/raw or bursts. | `FALSE` | +| `-stack-jpg-raw` | Control the stacking of jpg/raw photos. | `FALSE` | +| `-stack-burst` | Control the stacking bursts. | `FALS` | +| `-select-types=".ext,.ext,.ext..."` | List of accepted extensions. | | +| `-exclude-types=".ext,.ext,.ext..."` | List of excluded extensions. | | +| `-when-no-date=FILE\|NOW` | When the date of take can't be determined, use the FILE's date or the current time NOW. | `FILE` | +| `-exclude-files=pattern` | Ignore files based on a pattern. Case insensitive. Repeat the option for each pattern do you need. | `@eaDir/`
`@__thumb/`
`SYNOFILE_THUMB_*.*`
`Lightroom Catalog/`
`thumbnails/` | ### Date selection: Fine-tune import based on specific dates: @@ -142,6 +142,7 @@ Specialized options for Google Photos management: | `-keep-partner` | Specifies inclusion or exclusion of partner-taken photos. | `TRUE` | | `-partner-album="partner's album"` | import assets from partner into given album. | | | `-discard-archived` | don't import archived assets. | `FALSE` | +| `-upload-when-missing-JSON` | Upload photos not associated with a JSON metadata file | `FALSE` | Read [here](docs/google-takeout.md) to understand how Google Photos takeout isn't easy to handle. From db1926336e40943b7e7a7d8bdbf4ec07fa57b3ff Mon Sep 17 00:00:00 2001 From: simulot Date: Mon, 29 Jul 2024 19:50:12 +0200 Subject: [PATCH 16/17] linter fix --- browser/gp/testgp_samples_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index b00ba88f..85b4d88c 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -326,6 +326,7 @@ Archive: takeout-20230720T065335Z-002.zip `) } +/* func checkMP_405() []fs.FS { return loadFromString("2006-01-02 15:04", `Archive: takeout-20230720T065335Z-001.zip 893 2024-01-21 16:33 Takeout/Google Photos/Family _ friends/PXL_20210102_221126856.MP.jpg.json @@ -353,3 +354,4 @@ func checkMP_405() []fs.FS { 4028710 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP~2.jpg 6486725 2024-01-21 16:14 Takeout/Google Photos/pi_info/PXL_20210102_221126856.MP.jpg`) } +*/ From cd98781298678fb8ca6412ed22afb0574c62c86c Mon Sep 17 00:00:00 2001 From: simulot Date: Mon, 29 Jul 2024 19:50:29 +0200 Subject: [PATCH 17/17] linter fix --- cmd/shared.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/shared.go b/cmd/shared.go index b9c4512d..46c862e1 100644 --- a/cmd/shared.go +++ b/cmd/shared.go @@ -99,7 +99,6 @@ func (app *SharedFlags) Start(ctx context.Context) error { if app.DebugFileList { app.Immich = &fakeimmich.MockedCLient{} _ = os.Remove(app.LogFile) - } if app.LogFile != "" {