From 09f8a584049ce0388c99f68cb4eb9928212f37eb Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 21 Oct 2023 15:02:16 +0200 Subject: [PATCH] Library: Stack sidecar files with vendor specific naming schemes #2983 Signed-off-by: Michael Mayer --- internal/photoprism/mediafile_related.go | 46 ++++++-- internal/photoprism/mediafile_related_test.go | 100 ++++++++++-------- ...G_1234_HEVC (3).JPEG => IMG_1234 (2).JPEG} | Bin .../testdata/related/IMG_1234_HEVC.JPEG | Bin 0 -> 36433 bytes .../testdata/related/IMG_E1234 (2).JPEG | Bin 0 -> 36433 bytes pkg/fs/filepath.go | 15 --- pkg/fs/filepath_test.go | 49 ++------- pkg/list/join.go | 18 ++++ pkg/list/join_test.go | 23 ++++ 9 files changed, 138 insertions(+), 113 deletions(-) rename internal/photoprism/testdata/related/{IMG_1234_HEVC (3).JPEG => IMG_1234 (2).JPEG} (100%) create mode 100644 internal/photoprism/testdata/related/IMG_1234_HEVC.JPEG create mode 100644 internal/photoprism/testdata/related/IMG_E1234 (2).JPEG create mode 100644 pkg/list/join.go create mode 100644 pkg/list/join_test.go diff --git a/internal/photoprism/mediafile_related.go b/internal/photoprism/mediafile_related.go index 33e78597e..7b585cbe9 100644 --- a/internal/photoprism/mediafile_related.go +++ b/internal/photoprism/mediafile_related.go @@ -9,18 +9,13 @@ import ( "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" + "github.com/photoprism/photoprism/pkg/list" ) -// RelatedFilePathPrefix returns the absolute file path and name prefix without file extensions -// and suffixes to be ignored. -func (m *MediaFile) RelatedFilePathPrefix(stripSequence bool) (s string) { - return fs.RelatedFilePathPrefix(m.FileName(), stripSequence) -} - // RelatedFiles returns files which are related to this file. func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) { // Related file path prefix without ignored file name extensions and suffixes. - filePathPrefix := m.RelatedFilePathPrefix(stripSequence) + filePathPrefix := m.AbsPrefix(stripSequence) // Storage folder path prefixes. sidecarPrefix := Config().SidecarPath() + "/" @@ -58,9 +53,10 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e return result, err } - // Additionally include edited version in the file matches, if exists. - if name := m.EditedName(); name != "" { - matches = append(matches, name) + // Find additional sidecar files with naming schemes not matching the glob pattern, + // see https://github.com/photoprism/photoprism/issues/2983 for further information. + if files, _ := m.RelatedSidecarFiles(stripSequence); len(files) > 0 { + matches = list.Join(matches, files) } isHEIC := false @@ -138,3 +134,33 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e return result, nil } + +// RelatedSidecarFiles finds additional sidecar files with naming schemes not matching the default glob pattern +// for related files. see https://github.com/photoprism/photoprism/issues/2983 for further information. +func (m *MediaFile) RelatedSidecarFiles(stripSequence bool) (files []string, err error) { + baseName := filepath.Base(m.fileName) + files = make([]string, 0, 2) + + // Find edited file versions with a naming scheme as used by Apple, for example "IMG_E12345.JPG". + if strings.ToUpper(baseName[:4]) == "IMG_" && strings.ToUpper(baseName[:5]) != "IMG_E" { + if fileName := filepath.Join(filepath.Dir(m.fileName), baseName[:4]+"E"+baseName[4:]); fs.FileExists(fileName) { + files = append(files, fileName) + } + } + + // Related file path prefix without ignored file name extensions and suffixes. + filePathPrefix := m.AbsPrefix(stripSequence) + + // Find additional sidecar files that match the default glob pattern for related files. + globPattern := regexp.QuoteMeta(filePathPrefix) + "_????\\.*" + matches, err := filepath.Glob(globPattern) + + if err != nil { + return files, err + } + + // Add glob file matches to results. + files = append(files, matches...) + + return files, nil +} diff --git a/internal/photoprism/mediafile_related_test.go b/internal/photoprism/mediafile_related_test.go index a261f03fb..eb1a05afb 100644 --- a/internal/photoprism/mediafile_related_test.go +++ b/internal/photoprism/mediafile_related_test.go @@ -8,35 +8,6 @@ import ( "github.com/photoprism/photoprism/internal/config" ) -func TestMediaFile_RelatedFilePathPrefix(t *testing.T) { - t.Run("IMG_1234_HEVC.JPEG", func(t *testing.T) { - fileName := "testdata/related/IMG_1234_HEVC (3).JPEG" - f, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, fileName, f.FileName()) - assert.Equal(t, "testdata/related/IMG_1234_HEVC", f.AbsPrefix(true)) - assert.Equal(t, "testdata/related/IMG_1234_HEVC (3)", f.AbsPrefix(false)) - assert.Equal(t, "testdata/related/IMG_1234", f.RelatedFilePathPrefix(true)) - assert.Equal(t, "testdata/related/IMG_1234_HEVC (3)", f.RelatedFilePathPrefix(false)) - }) - t.Run("fern_green.jpg", func(t *testing.T) { - f, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg") - - if err != nil { - t.Fatal(err) - } - - expected := conf.ExamplesPath() + "/fern_green" - - assert.Equal(t, expected, f.RelatedFilePathPrefix(true)) - assert.Equal(t, expected, f.RelatedFilePathPrefix(false)) - }) -} - func TestMediaFile_RelatedFiles(t *testing.T) { c := config.TestConfig() @@ -227,30 +198,67 @@ func TestMediaFile_RelatedFiles(t *testing.T) { assert.Equal(t, "2015-02-04.jpg.json", related.Files[2].BaseName()) assert.Equal(t, "2015-02-04.jpg(1).json", related.Files[3].BaseName()) }) + + t.Run("Ordering", func(t *testing.T) { + mediaFile, err := NewMediaFile(c.ExamplesPath() + "/IMG_4120.JPG") + + if err != nil { + t.Fatal(err) + } + + related, err := mediaFile.RelatedFiles(true) + + if err != nil { + t.Fatal(err) + } + + assert.Len(t, related.Files, 5) + + assert.Equal(t, c.ExamplesPath()+"/IMG_4120.AAE", related.Files[0].FileName()) + assert.Equal(t, c.ExamplesPath()+"/IMG_4120.JPG", related.Files[1].FileName()) + + for _, result := range related.Files { + filename := result.FileName() + t.Logf("FileName: %s", filename) + } + }) } -func TestMediaFile_RelatedFiles_Ordering(t *testing.T) { - c := config.TestConfig() +func TestMediaFile_RelatedSidecarFiles(t *testing.T) { + t.Run("FindEdited", func(t *testing.T) { + file, err := NewMediaFile("testdata/related/IMG_1234 (2).JPEG") - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/IMG_4120.JPG") + if err != nil { + t.Fatal(err) + } - if err != nil { - t.Fatal(err) - } + files, err := file.RelatedSidecarFiles(false) - related, err := mediaFile.RelatedFiles(true) + if err != nil { + t.Fatal(err) + } - if err != nil { - t.Fatal(err) - } + expected := []string{"testdata/related/IMG_E1234 (2).JPEG"} - assert.Len(t, related.Files, 5) + assert.Len(t, files, len(expected)) + assert.Equal(t, expected, files) + }) + t.Run("StripSequence", func(t *testing.T) { + file, err := NewMediaFile("testdata/related/IMG_1234 (2).JPEG") - assert.Equal(t, c.ExamplesPath()+"/IMG_4120.AAE", related.Files[0].FileName()) - assert.Equal(t, c.ExamplesPath()+"/IMG_4120.JPG", related.Files[1].FileName()) + if err != nil { + t.Fatal(err) + } - for _, result := range related.Files { - filename := result.FileName() - t.Logf("FileName: %s", filename) - } + files, err := file.RelatedSidecarFiles(true) + + if err != nil { + t.Fatal(err) + } + + expected := []string{"testdata/related/IMG_E1234 (2).JPEG", "testdata/related/IMG_1234_HEVC.JPEG"} + + assert.Len(t, files, len(expected)) + assert.Equal(t, expected, files) + }) } diff --git a/internal/photoprism/testdata/related/IMG_1234_HEVC (3).JPEG b/internal/photoprism/testdata/related/IMG_1234 (2).JPEG similarity index 100% rename from internal/photoprism/testdata/related/IMG_1234_HEVC (3).JPEG rename to internal/photoprism/testdata/related/IMG_1234 (2).JPEG diff --git a/internal/photoprism/testdata/related/IMG_1234_HEVC.JPEG b/internal/photoprism/testdata/related/IMG_1234_HEVC.JPEG new file mode 100644 index 0000000000000000000000000000000000000000..19331d0837777ed0d9bb46abfb894adbea19fef9 GIT binary patch literal 36433 zcmeFacU)CHvoN|gz4zX$fJpBky@T}L%VyI%8xW8pA}B=!1*M6AfOL_jARq{WH0iwy zNbd+z@7h?N=Xu}vob%mtfA_D?@LS1bW|B;@lI$ciD_o3T%mM^zN~%f#1OfmM@DI3{ zBd<`r>0}Q88XCY=001xm91x8RLJ$mu2l)Y`Az-9S7(sgmLHmIRkU%`qPZ*IY=}#Es zf=~gdh-BdN0z&;8E{3rGEiE4+2Y?ZLDk1!rFbN0`ML7W|V2DV&4hAfsT!=rc(=Y&F zBls1{+IiT*7_=Qd;2u7X9-a*ReBAsD>>4&uCwByf1K{W7=N0D_5$EG$;1d%U6cQH@ z0doWZl!pL-jtF`A1^Ax-tRted=SaWGg#gcyf5E_eL?Y0kL6CpS4mQYN^@Di-RX<4J z-|L42`IQGG9O-x7WE?-T1605eQOC>hV)SAIz`3lGmAnQ3x!45oE@OaQ7=rI&0*C-` zFfcGMF>o+3aR{-oun9@=ad7ZSDTs+liHRu)aW37@;78Cr-?N&s581VhF zr^NR7#ofxJmcfPn+^W{0#REfozsTgg>W{-q-$ay+9Q>o6(+d1#>M0q_@sE^mfDN3(II^RWCkI~Y&KUEGJ4_h#OQE+nzl4s=eP zd+Ev%O|SGUeSasXOo(RZbah;M$Xbb)cg~ujX{aEFV%wGeN=a;E;oE~Vi<~kOmT~ps zLdoure#5qlb<1ZGB3l^AMZ|r`*LI{>(A?fOD90t+T)d$xf$z2)D!q$+!=p{DMJAdRKYV!O7 z7(%5M_t2_X`O?1?=Vs`aK$PIRYvYrx;Ht9g*N*Pby?LiEvxk{_>66EqdNRFE%g2?s z#o1vd21!v(67?-_Bqq0}U5qT2Q+negN*&fh#PG8&fOpMo-K7`6VDt!QqHa4QNBe!t zh{U$U@&}?V=0SLI+HlNx-?XU5OYitUZ0wjqnx>ah(BBxbA`^`*?zLTWb(&;C8xx*HloJfJoE%~%$2EH%n-hnfl@)v-d zpZ9SHGxh8A$T6sCzYMj>=CPTu$1zzjkLgU+d1Ben`M|emzTL7mUnPa)x@FPI8o#uL zm`1V2IqGv1Zkc5gez9{%q^iGxqWVD1Z>tc{etp|G zTjUaKR(t`V>mE!$>ffnV*38fULfiFHeIqV!g7);A{KN%-vGsgrTmRS~#5O>jlg5|j z0$?hxHSwXDpcpCNxE5>|q^>f$8YA6z9GWwB64Tc3dY^4y^ToOscWbq4>#IY%3qbg5 zXl~X_PR02Ym|K0;ydnCqWt6hfdq1JXk@D$WGW-`KaX4B_!>n*Hd6hT4qBhqMW0!WGR?L`HuBqsIpkKO(szX)SIC}W=h4z1Tn%;f>>u7( zr-5GpT}F~0CXAd10u5+o?yN`16`)aJZ_wTQMFDc&sYE^IQ?gVo!iK811Gk18lJ>*je4_ayH zi`RSBamV}uxIvEg>Vu@2bl=J3=g{6Ww*37S*__o2z^t9Qx2bCO5DEQz&v@%=oGXx5 zfpO-W*Wbjx;bwCpkYErQvyRFT3uE9A8oy;Y%rhumOpaNxKH9ib<1W24FT&{ZfteTcD8#(Fmpl4q4q`(9@3+ht&e-*!vUYPRd91%UpN-l-zbkrD~(TSM?u;&c?`< z6*L-9t&V$k$4kprFPLvEHhei2aC5Er9y&V!-!ZD;o+~;ae=dTK;%gpxUXx+G50UQv zA~D%5Jz#(0UUR;7Od6VJAvwQ85SRdlSS-hOHzbnI$#Q9uhr8mrl0y0XQ!7utCsW+#$O6;VliQs3mYTX z?y^UteVc3m8|t0<79GazfW;HpW>mAs z-d6#!Z*$XAb(!jxZ8PyuNh`&J1*0|Sc>+>HLGT3tYp*K%(CYgYpZnpJN!Q+`bhX`G z>V~3PzaR?>v$ev)P?NR;u6^%NWql(*VGUXyvT7?ukw%f{-A#3IOdr4QU)`l%6|Nat zo$d%3xB#3&jcUGU>fTYSvhQ_z^|n#&L$RO#R_KWI-u#R2BQm#cnsL=ozqG|Jm~_5X z)m@$&4~afdBCeUUTZWw+6 zT=XXdI1YzE&rgZb?_JL%=*PHXK!TBQiJrY75KvwbCC2d$ehN+=1{OH zlOT#=S;oIjQL~1|wM^!7o)=Ni*R?6jLcoB1qXq6~-Y##YH{YmTcdG(t01 zt2_Dl`5Hf*-|4MRTRXerpt?#a470a(a65tFiIF776;}{$?z{LJeA+*63wCeqxBwz9 z08Eb+H{I^`j`EuQHCKu!cB5u}wfFk-t<1(!qV7D|KkrW8(yR_jTlS^#SPIoTrq%gWY+l}|~1CksbnJF!4Ap> znhrMz*Q%BzT@8g!LSile)%c`i@nCv&w&4u!XuDnz8A7lxl?}rY9_@n zRIhVio@z-tznX5OYDRewYO!KFW>vBOtkh6>`2rBw?=U%P$YBY-vWhjptblF8QF--2 zgG=nECj>~+QzXcr#bc}~GJz?5Y#JdZDORWP0rdxA4oUKk{O?QvLXnf8<$MUkXFEIT34?|Fy zq${OK7P(&6e7kdApTwQ306mTfTYa#a|Mr;sW$4FplZMro#SI^TV6sEDrlQm}3%7$> zw3>&(;@?Rt&x0Bt=Y7EJkiToMs-Q5@I}5{hacO$icG9=YM{#178Sm88{gPn4W1P0O zM#e|L$-jn&tiIT1^a5B~J@@$DsNtPyB%a06=W@Ti_S0flrcYyW>j-qB4ZVF8W6xCX zxRM0|^<4GqUH@DmLSF)R$q0s*w+=4of5WfQiF3JAGyiqO;&W+Zzg1k~XT=L3x5_HS zNUaf_S%UW!+v7h9dt+BLV z9G}&esmf^deZQ~fXWa+I9{Gy;{hupg=TT$FT9waj;UPuC>8Fh)@%V)srA2ug!%NUMHP?#?_G9n0M~Nyg~1H;8-4~ zVf^$wnG*`Nsk_fQpsVswsGS#pCu?DXq#hHOu;{Pd(Y)Fq?QzXYav z?i*LL-t#&}TK!mSSQ6(+@BNgN_<@%NTP`N5<&W<%RU7ux?4C>WCDrw}<;J&nHV$NG zew9?X%D2n4U-mh4rO?N%Fm=*&e41*BKBva-c%wFLXKK~RHzc!c&BUn4j(?+aVa~44 ztW3}$-E_?@{H@RGr`Gm%m*O+}mtFBgYd+vSVgR@J7W(x9P$8PiVT!Fy@e^nrGRAaC zV-8=E@b$p0&KLe@VwU1Uakj4QHaJPYJUVjh?(4xNP_=%gj^YAXEU^ zfz1@urB`}=-M@x1`8at{ahF=E;ly1PToPPAvb67nOECcmc~t7avS|G-(upAY?YeFMS@Zkt!7J({U?sKmIh7APZEUQ z`O-H%AvuE0`uk%~!x`wKLiZkPgtaXl5$DfGOsoaF1({nIgh?j*1%+;vX*I|&M7h(W zK9l#l7%bL&R9fhUw+`RJkTDjLth;knaTc)0+`LXkDnXVs;@0aeQFwS@WVBw8wMFx8 z6>`@1{j<*NZv*OE0e#OmGL&1r-e#z+N0AMGv5-s^Wc#kAH{0~_7BZdrJLK;1)p}P$ zoHrK93PKf(nH?cBITrxg+nV~VfS^gsLrR}`mY1hQMmzJdsA%a+cD|~L-=LG7tGvfu z@yPQ7r%$Ty#}EF3R@ z8k5t?6}%$Z6{RMk?KJ+PAvPIm0)pJhD5CZ^USAotpE2YHGc7BqXu%i1j-4@`>mR%e8B* z1^Mg?Pp~Jrrq^S$9&*+|FMxqiM^C4Q0x3@FwM-kUjzhBo z*)`%vcD7axptg}SijT3^-ib?^(ZBBCbMZK03&j0Y!8YtraLlu9nehqBbTvw=(_Zme zq~b7N68h`n!u8jkeC`$E<;Pc-%Q!YVT2C~{&vN$7_@x!q{1*D+8u3Z+E{sT+oopBvN}TBeS^CTp)0I-Bh+^B@yEznuYzq zwF>^G)2+Jc4(j82>CTG%BGl5;fhjo~K90dU>jNu;z=pAvL(#B3L8+Ue$I`3db}{pb z+D})@Vuki=m=-m6|1Vf#(V$qGJ7X%Q4W<%lHip%exg2}hN?H=nXs@@O zkn!Giws60aIZWLhEVK~JY0gO}Q>p`RG_@SKD=#}Z!5a`Z)Kzr)vN*)9O6<<2WxLRAwKE`@hFzxR`$C~!5{*ax?NciVk^ zLvs$uQAa}F`bxbTfeIX4LwEEl^y0pG0n`Q1war`rq{r7ij!5so;>$N*w~>$TnjgM- zBG$5Mepom8m0ivA9%ku!EJ}Z>9^2KC#}`0?B0~^Kf@|tB?@e*ipgdR|)hu4gQ_mgj z>jmX)a#D{%2_mvtICPSzLobGx6zVq_7i&K^+-M7DuN*?{7K&$ zgN&61xZf`pv)y~}exoU?C-XrJlL0&_U1%eE!m7J0`KWtyRMkjTrTkT5X`-4hpLo!& zmq#U%dDAh2v}kp0x*@G5z1lWVV7Cw=F!=BOytURp(%2NhEp^k*KE8a~~OfHat-! z^kVE{gP#AdLvI0YUO-e%RRaZaU=AK@(|`x+h%@y|9P1^HV0p)UkEe+uD?7-cq}6Vy z_L3%kNm~Sc0`Nf`LjwS|r#EZ z8440{0Byh<@Bshp0R})1fB|s87w|-61z>sp<_2Sz*~q!V-G54iW9#eW3U_k9%o3zy zUUJK8n5h3~4GBaRNL--zSB;7OR%0zsxRZyw4@f6RaLRka-G5aAtfZ~??})C0kH+ta zytljjUlDisUy&QOu0FphnhtQk-w{POSB2jZupR%(S`O;s@UtO)v=X4JEGG|2gNzVK z*N%a~&coMM#$y%{y>!an?*E39bNzQdId40C19!L*v#u-R-TroRu67Ln39swp3cti_ z-*lDH{mqW$2ZO>rycKNVHi#Nr3aRa&?enWwAYcSD7$U?5Zouq+Wb-e@>VpcK|5U6V z)b$5m+Z!rlg2>_05kS2?JdGS-;L8PjsFSwm2cZZ)zaDGt` zBK!@H`wOq@fL@NyhoxuoI!EEEi+!+wrF+)Er{ zT+&@~QvNfJymuMLy!1g2a@k8k^%lZm{Tw7@Yvgf6j#tk=;RQIeqgdv zQh#IkFY%_J|BFZH5`*aZKQWhi9)WQHF|scHqeEUskpCm;;{OZnvMn$WdH))RNHSnG z!RKeY(IKEq#o3Q>ONEFctAgGS=@C%?c>i+(zykOIUVsk}0=U4C|I!x(=|6`Lg7vri z8}1T(|C{m?_#4A-(qEGW;O^^+n9~sRE1Io`ue+Vkzl>EVoDZa3PQ{20{<{}}$badD zfZSib^*u-ibboioOCivFT%DjWA4At0h~WhJM>(jMG!RV$KIoSbRfWHmh3??(;p_Pi z6q<*(lY`UmnNm>?k-OF<4$K4B#ux6P40DHh+rVLVAOg@2@ccE=WBp)8U=S2lHwOkV z`2V~9$iCjL|MG<+QX`ICZX;|kZearg%p9u(&4st>yfSM^cR*SPU((!%(K z{s%Lrqlb5(tgDm5ua?69QF)bLXt3t!b})Mz-^)o9(+}ni{}1ejztI0=$F_Bl_i**_ z{#)Y`{>WcW`8NSf0%&@;Bib4h?%@gk82iBfK1*S_g45Z*VR39Pr@w#0V}q~Q@n6|5 zM<6?*An?}~?9S5Lm++6#Msf)wx-jID55WPDDI((F^BjC`g5WFw%z+@N%BBES(|=Ug ze^l3hRM&r0*MC&ke^l3hRM&r0*MC&ke^l3hRM&r0*MC&ke^l3hRM&r0*MC&ke^l3h zRM&r0*MC&k|97jdf1EHgf(OR{U<5vh577X8Ko+nA)xEZ$s+R#&{yKs%9DIB(Ay7R9 zko*rE43{b(gc=BONPT%~{XkBm&R`1z^(2x^TXl_03{G~EOh!T)yc(YJFh?hqKyR2{pr$@F z&;=@H$0RMqaQ&wEO*c8rkks~kN8bVrpwI5K^TGNVPd#sfxAdDU4G2PV636d zAn)M~V-V)%<%06^@-v8taq|fa3JUXHV?cbc#=|Sb!^h9XCoIk@D9$Is@Y6AYrFq-g zi|Z;V{VWTNNizMcl)t|}x4!_lhqnU{pO}~!4=+CtKR*}9!Q~U+4!60<vigH3CN+Pmy z{34>fykfu7sJi>WZQP+SL>e#`ZYL)@aj2c3sF0w&Jr`8O&X!A17;48Q3KJIL60oxs z6B6LH=d~Au{Yw8s|7PR*|E_F1sJOj{x0?;v>`rbr4lo{PPnZJ}!|(cwU#gNcJnUeS zOagz$tIErRrV;i|t{}k&G%H|GRg@Rz6B8EZ;^*eOlu2CP8wP3sLFGQk!q3Ym#s&67 zUNL<>QE?$LaRGi#UOsVN-XE-B0Z#S-zgYQsxp+mm`1tgHeQ3$a%l{WEI1=nYgMt5v z88M(PUxtd44`^c$@UzS7!MuJ#u1*Y>9ZuW^iZImxt6}2@vt#;+boyVvjvtx&gXRT@ zYX6sY`N8JnVGsAW@rKDdfZgTSu>Q*+=U=}<{UsgL(Z<~Y22LnEOgvz9co0MHN6mQt zwbGZgzYWu0?BLi(fWIdlsb7=BpYTr${AqzdE%2uW{23LAk+PYdwsy7rb_gFY8H=Nu(kU&c|H_+G=vGg-GF=asS0WH}uL8Cg*X3Ylb zsPXvA&gh^~&5HJ$L^oYb86cB_d5U|4!_}V4U4_F2P+qt=ewrWTSnLX{?>=1AZ z2;cMbMcA+*-vHrAKPQ+!2oHiVi>t4j69^;B9!cC_HlSS{3c|RF1rCEcA}#{~VH|IL zUC?Y5!! z?ZBO$o2QL?008``Ga?m0^lxkLOY{%#za{>S@4o{P@Ae0MPv`QTLCCuP!u^%lKQjF0 z{I7w(mB)=J@AtliLE(or9s{CRLqY30UvCBWv8DIyEZTSHaKmw2f6ah6r3(yBl080Q0p4_;BM*RN3E#Ni~ z3B&*ofg~Um$OK*h1wbkA8mIx@0ZqV1pcCi?hJXoR7FYzRgAT!k5JM;- z^bl4ECxjm&3Xy^+LT*5GAtn%Ohy%nO;tRP2xdVxTBtlXlS&#xqIpi&*3GxZj2N{FR zK~^BUkYgldBpf6XBwC~^NIXcQNU})kNcuBHJT-BZnYIBPSzgBbOrABDWzAAkQMNBY#Ih zLm@_CK;c4>Kv6+4K!Kuop@g8sqNJj{M5#t;K^Z`qL)k_-N5w^@LFGghM^!~NMs+~- zM~y^HM$JR5LTyDILS01thK7blhQ@{_f~JCIg651CgcggIfmVk09<2}U3)%rXIyxmf zC%PoM7P<{O96b^}1-%%(5xpOM5&Z}Q8-pH$A43Vl6vG`O93vT{5TgO3ALA>=2_^w1 zE2cQ67N#9$0A@Vq3(Q)~&zOstCs>47Y*><5`dH3bx3QjLm14DGO=9g~V`DR6i(%_v zJ7I@mKgE89-Httn{T+u0=PHgojyaAm&I6o$ocB0mID5FbxL0sxa7}UHxDRj(a6jNq z;~wD=<8kAu;@RPa;-%qL+S&;s@d<<5%E+#$PAEB)CE#Phdk3LXbxA zmSBY7fRLDwk5G%yjWCw5knj`XG7&n_6(U6eIFrPXl#uk0Y?Bg^3X&R<-Xu*UttXu&LngaIrb^~Y7Ekt?Y?$nroR(ac z97cYhyp+73{E&j0LWTlH5lvA>F+_1p$v~+{=|Y)6SwlHRg+g_eN{7my>KRol)jBl^ zwFEVkI-2@5^*9X@%~cv*njo4Snl73HS~^-~T2I;(+7GnrbYyfgbk1~-=^E%(=!xm2 z=$+^v(>KzuGLSOJGPp84WoTj8W~5dVisk#XMW7w z%)HG)&!WL{i=~KVoE4K*l+}^-32Phc;T5(kMpvS))LdC+qhM2I3uG%|n`FmjmuB~3 z&t@OwK;sbQaN)?{=)H<`Rp_eY)wHXhIgvPpIh{B&IQy=lUK6|Kel7dj2p0~QESDcw z5!W0y8TSqDaPAuJO&%5=Q=SB#HlA}{AzoMB7rYaEM0{#|;e54xyZl%Aq5Nt5g95k$ zN&=w*Zv=J(IR#;Y&jd$>h=nwSqJ)}-&V)sTy@gAKS43DutVPmAMnp+OwMAn^+r?1D zu8%8Sbf%fDAZR!~-mR_IYAQZ!IZRh(90 zQgTo#QrcD)QVvpnuY#(gu9BcKqDrd@RV`56RufSRR%=nmQP)#XRiD3c^@jJ2S`8!( zHH}9albWoW?wU1PfR?J(BdsZIHf=BMIvq3}ZJjioC0#z$vdSw?KrDAXF4CcXu0IOT(}y!mbqcLS-ZV; zCvtajZ}Fh>xara7$>|yCIqN0vmE^VUt?r%Y1M#u&c>^Z_7gJrn?7nw>=lrDn()^D6 z4gD)_65e#b*%fd#;C{f@K&8OkAe10zQ1dOuTeok`1`a^@4PC%#W+pWb--CWR&CVajQ$W9m?vTv}N=P5S-x!wkEO{%11JN;7FQ zV=|AQJ3SxGQqHQ*zLK4sgO=l$v-HC7MO&_DZb2SZUQFIuzI*=cOWl_(1tJ9ng*1in zMMy=yMJvVT#l0m8C2vZ(O0&wy%3@vtuY6yvmRpw(zgBt+S}$@iQ1||Ma#)w4`5sJp61xTpHF z$mch`V!gF}*ZUg!rTd!(6b3#Hst$GzX%6)b8w`(*n2*ek+Kn!cxsL6O`;DJWgiWGO zMo$q;B~Q~#XU}lVl+6my*3T)-bQBnSc4+iqcB& zs@dw|n&;ZddgKP-M*1fEX62U5R@b)i_TrBB&c$x*9_3#CzTkfIf%d`lH@9ymhtWrr zM=!sNes4QAI{td%e~Nzk^o--I_FVmZ^1==L5NHCB2T+g^4)np_Kkz|EML|JD$3R0v zM?=HF!oUO_1{M|$!u{|s!M}NkOdu$zs2J!N_*hu@|I1tGni03qy$5gZgMinz{kVm$ z1)u@fG>9waeympzJ`w==amCz43qXb7{s$GjoDM<#b@3VjG67;IvR;QNLXp^Iy?JXU zr!3Rlz^SXUp4>{L=k)<+9v8-a0wYPfch^P)_wQ$$ppwQE#41-ZUg$^QVjI2M z7ZyEhaqhXBS73lM=zL!=FVV)!_=J{;GHTK zKA&gAQx8l(53|0h<`QCbTuU5opvAp@6cheEyI51?m|p6c-;;1RoGWO=v5TXf4rsP0 zbf0$V9h0E)^k5zRAlvp1JPR-z|`)++u$i2Nqaac$w>&>{@1sk-)+pA z>rK$5ir0S!KMaJT$H5w$1*&?UrF?pps2D@}0_n5U(FJhpX}GxRJMNxPo?|OKte{D0 zo$FghIUmaU_6#ff7Y1M2XXPwAt`9&_WR1m6g}ZlxZxTmv_UHI0tCZ-NE|s)}&K;}W zUr01TA(s+4u%z=>66lWGR=W|=5knZr`@G=b<<{1O%_|>vzkTGiy}C6|zxTWfasUC! z?Of7O>QqWzV?nQm#*F6Ub626;VvJ5Xfi8Z6W2ya|wKzu9Yic$RV}*jLpQ<(ONZxzh zOGpvHNDQFGUr8{HY;qc4h%#BKZ)EzGP?r}hF4!#dlm#FcFafVTLRF#F>mGL=-Q$~J zZaYv!!nsYrpy+_B7|#%Ujev@IW1(U#AhidMG@%#K#B2A4?@Lu)8mcu-pjeNV&-3-2 zsx7>rJGJ@fcGm5Xdw0pd3q{_0iI#f*h2O_ykBUXt10QFsH8v7F4>rY~om=Xkb$E^1 zG~hlK$qzY(aTfHp&m7D9(d6EfSew_yF}k6IQ^{LbNatV%$Md*n8;Q%dT3U3ibQ%T| z73rC$7Joe1^=CnGVWr85Eim)R)T|*xu20%}K#z_(fmglIf|M0fC(= zeyzP%m!iK!t%N@{(tC%A!i%+BcwG~3`Q2vAqbmtkxOz00(T{kIZS+*XoZ(5`X^i*V zd!_Nd%cp&O%wXsfuM-YYcE@w5-M58O5(Ar?(*<8_!?4~ur(A!nW^IpqRlJRU=2PqO zB(0M8;n1@j6PDfmdJZb5EDJh4GmcpyU&`WT*sC?RCBLIh_e_mliO1E6vCdry#O+;I zO==f!k9_`cd`d9c9)F`vGpXB1lEFmHUZ3oFuMwHL5aDxvMcJe3_^1*#y_o>oGY_b&0->Dy0fD9N(f#_SWNO^r51MHWjg{Vf@3=0n{J2u{ zUPq77swG=)5OppU0fDlMgzLMtiQUx|oPHEMym+}b&B_dj8Kxq#PGq=O^ym#~wxNKM zevTntAv;w#jbw&DzGhkG;D%8j!~B^go>AYt%xxqTJSyEYfvaOzR=Kd5zIvPAdrfoq znS4bj2T4T4`yQ1P+v6FDnpe9L^Zn3D@6)dnySss&GVgCUA)(_+YH5ma>^$WcdSBR)=<-@74?*|$R)g}!`T)zN} znk~WvRFa7?ZzB=k;Tuoptlv3PhD{1qkS?BYCE#`tBT=DX5D@tC`ccjZ6?+YT;fm-{ zP*G8hjljI8@U%aPw(-%5akTxs&xgQgK72@WD1t2GN{1qfW-z))2d#%(_fk4Bfy&v4 z+sH`h=qR7TN%tD!VoxM+5(aMqMZKJu!Ow)iNtcj_fS3`Dgn^HgiI&AFOdK;|sm66Tb>w~cbzv5I%GcmeAl+`u3o9<#ixw4Y#Jr&F@1&+Fs6gU=d-N%y43@rwg;g zpBb)@Cy?lwrO_*0bJM&l%BkZ-Flb7g8tcX}H1F+8p(^@D&F?CUmva8P_Av?%ma&PT zA2XlIY~6$Ta15?ErDf44=M7Ij`Z28etsii8JDZZ(*(PMvBxT3w*qE!$FPK)Q9%k{H zk81<@NJF2RU`0YbK{ubJr)K5*`I6^Xc(d2DCV0p6vn1*7?WndgRs z9-Gx^rG~Hfj8%M?>WjKXI}=s-jJzMJd<=48%}C}n5wJ64i5HHCCG@77c<3B6ZYY7n_1CRpkQb)1SmB{GgieO&1%|Ek!7 z@DWU`29`j%fwiOWtFkHf zHMw(#=9vniFr6WDn{N4|#)Qt1)??>-B@N$&Rt4m1J2H%^%ALuN+mu@AT8k)!o5w#Y z+hMV|E4c|f&J1kjdq&VVq{-yu^atU_npKVAC-h5DyEwD$;&>k#tVrzmdyhEw6~y1) z;8(`slNrL5;&Ig5oaV0dmJ)_ar$2c_{!d?RoOh` zH=*Zz4BqOTYppUYrv9MFzEceqDPMq6IA)BZ%vmC-NZjS7T9s|#@Y^!1fVmPfhe01J z`R4Ht77K&`4;6+J5pg_OC$FxZaVtw%(v(-Vs1&7`&;t#2(wW2{k9+A6hF`#{?3CYSwAr!ZA-rtDi$h*~ZbSF2y zY6?n|s?AYi?djyFw(nN0T$x92t9;!k>T!j<<+ZZH#T1XwYrz%RGr_U`{iEz<--_R5 z*7Q6+OR;|BbXV}X(R@*@3S;Cu?^c?f@~k4(RW-Q5+P7W))7u)?E!0BP)q@!J+v}1V z=i;im6Y2$@YqOIEv1!pvK6&Xrb8cmeDf&dcYfCj>#j&VyxAAU39ntU;o4bMq8zXt0 z(|OMnRK3n+gP9p7%=Ikais;roZhY@r42wtG2;8VzrPNzUbO<#}sxM`K+ofwl`x;!s z;oOqYRdV_K4ZNsP9-OTO3BmO;1QiV#1A_Wv5qoWp9Ad(U67kc8b>pM9{R zg18yO0wZFLm#3{OxVKZcO890vQgGq@*+oa-r2Bh;ZmwDqyM6Pkk`B{yA>(-%E~mu{ zZM(fAWxZpXfoGOpv>Goqn)B;$-!a25^arX`k{ag=82s$kXH19phWbFEm-%+sP z9QA&5(?7Fcs<$%KMB`i1^ILjfQH@HEds?>k(On8ENZU9x21-x4QFSYY+G&y}te;Pp zUPoW}M(-P2KbrCG@ZiG>9&-HJh(LDv!0j50@6$?h1#uGElc~Ey(6Z=50+b}jVwdah z_U1;tF^dxS_6gS03;0nMVVMLEbzZ-v7C2^BJtL-4%E)S1^}O-YtjPq^2clK@@#Z}` z=P8f8Rah*)R=?QR8-|TeA{tWRFOqKyI#rMGzjsD(r5UysS(7!d=mijw%A&1SKOu{} z1Hiw2P1G;&@D+H&Fv-Lp_?_Fbp~j@}mb8Y6Zpn?MXb~GT{%Uo*p$Z2DfChE6%*m3T z+bUr#>3wtFsjYANq#SWUWX77mB4x`Rc+Qaj!BUj`1%Pig18ZTwtCojcX7Q*|j_ja? z0S1KUDif|uNhLk&Vc3$M+FdR(9H%ZmkfP74x0mg}nf&54sqZ0U5xan@7s6O#kVr^Q zJL1V+is7y!q8X^w?7PO#bI)APDJ^UuOE0-719OvRAj0l}V^$s(v>aDNoOlK!MUQD) zC_L||kCi;XmN<5b_rV)-wHM!e^wQ%T6$cV(n^4@LSwaJ;InUXoe0I8gKALJNJ*RX- zmx0B;%3i6}d7H~O=0YE&;Bc4fhIe|>EoD;za3BR080+UQu-|h14%Px(C z=+c!oI{k|jWbpJ)Y3X@;Iyjapvt99yi&MJq(}i9B$v6k3dS|)Y3+7oj{i&84S1pviR!U3i1y^ zLaW43uX;w{TqPC3`Dn&hq7H2w%5^t;oOM$-Z{MmiD=NtUONmwxbo*Y6oG_MWj2fzT z0W)6v?y6I;RhnF^16j2JStcFvjd^i@g?W;fA4?=lKRCyZRy~ zIS=Yia#70RKIXWp-x=*9uV8$Cp0kIsn$g)w**qMxcwb}S(Uj2boIHHGn-WZ(xJeZH zc6W8s)b0u+4LWUd#NzfaGkD?l)Qid8mV9h%rZRIKkD>>X81?`i(}0>RLa}n5vU%pham{N-dd|cJ8ie6_E{9z)fB)5 zFSx)(oQI$yA^yhzA_@>Pq7d=P5)d=->)4<&Fw5zBk??whvuHVsKvUn`HmRVzo-H(7 z#^+9A_Uq=K3t=>Aq`r8a+tUJGApx>%#PK@93ma=hI3L*!L!irngYmnB_hOYl@*gcX z^WM2pZ>wb78dwSsNKEk&h`v=jp`@dzxIYqKRv6)ND~JBEC0(hPz=Qi-0u1# zad*|;HXNGQR2~kV%r20o7}Je#t>1}Dxtw?!B)YoY>4{67zOFZb5Fxn(OAm<;&e5msD0~XSUwmOqDwe0*f=|(-R z=tD;egUy^5l%fw{73PC%co7BaI*HVBtD!qtr;JDgtQH!BwZ-l=Q_9X(IGqh)^ifC=`Q+Vp-8p#?jZw#>)HG7iQZ*0e>4u7tESZWI&Cn>mbpctm{2%X z6th!2AIfcEVclXYdhhA`m6!KPFiy%T)Yx;CIx}g7`m5WgiNYV<a;bNZ~S^^=poMv zZB-?B`=D#+zBM_0z~CKy*SuQ=s5f#<_3k(_*gWh&Dhp0BGBtA{C;QOmcLA{4G<$!^ zn$jM5*Pc4&8Z&JkYh~J0ZmPqjt=Uf|ka=J1UKN~8Rl~5F*q4a>@qAFU7T#>%dGN$y^c(&7o-KOO6}O&T;Yb zMjvlKC**!BYeHrxR_4;upD)I;{mgt3FRx_`wx74hx^S1(&L1ZEsM)job?X>B2JM9H zT~yz@J6|DF#-y9qfKn_9@mXWev+`ih?C_N)d*2hVuMumlI$#F)Fe*ck&WlPU#^ zHr|V*deUN>yri`bPRPPw4!whBZEsP@15#-WY$qExQDMW>=w`NxzIs+ojfT{a?{wPL zZWs+iJUQ*n$09F2=k7~n#Q1%P(c6^Zrli;x(Rwk~cbc0;l;S!|B4($^=mF2}Ph(~2 zD9lJ5X$inms>s%-R;vL{7BQ!^k(oFYdJ;-k3`meIL?=7 zdB$<y*#knAV%OHd z#I5a!q-T9f2|STut0==$uvW47&U9*Z8I5gB#nduvL6(=_Wl~o_t&MR&h?I&lGrpa+ znd(|xE-a#VdxAtUZCN8sd=*TI9i>CqWA1CKea>(q8%#=j={fCsCy7A!raI%(K7UDU z41?|Q(n1r8V~SF~X5Ik{l0KHLCf&mMSA+rf_CgM~4EQAJqUy2YWJ+xh!n+I7Y!UB z;u zossLCE8a?abN)O0z~FBlZKOApmsD|kPrB-0?oz>sNqEWdMrQ~G zvQ1%+EsfszzI|_alV^OD!y2gSIIiGt_9Q`De5Ty(9`dj?iLJ_QX~yJy+)SFulfeB2 zKdp`qX1B?Ebxc9#8S>Jv0nE2WPp#h_e?YWNI5sV%u<848{Ate44+r;`yd=$hg??}mE`=P87o(%=h z?ara9v8su_9LzKktwqUBFiyX=cOV$2(@Nd&d4)K@d*Nybfeuuu>`ZaUaxN_VOChJ{ ze6IfSic;^`gRW0H0y}rT4irCZhR3q$f0qUSj{vyfWG4XkGT?tMLV=)w|2^n(!3i!m z5qot4FK~s)P~JDk+qC^-pAMd`N;AG87wC9qZ^q4s?R|Jpim6BTTMX{$tFgJ{l!V(b zuGmOXYqfiAy-~>nURzpcZ>yhWeNrpKY(%@BFtRyD{JGA%?%Q*UekMyJ*Lu%z97YT@7|emJGED$PUTEXVDD7^)mzU z5ej+KF%oXXV;HH%>Uy~Pw$BJ#cO`<}^% z+zmSJbL*^f_OLk9OyB&UiO=Y|7G3pWKzXOm7~&PEGor$t3h51dAK8TOGUDH218Y(Li!1O*xJs3#%@Sv2!WS zU$l+tJN!kgqW1Amz=qt$iJx?Wq>6-2NV<8bD+gD;w>TkyeW6){l2c&kw~>LUZW8K( zDp;+gtPXrZVRLw!HtfX#*5-Yd$TG}t27DgnxVyX{Mv=bE?i!IA81Kww6D-_MGZ)-c z9*Cg9Ot*M1Q3C3&9YrIS!cJ?!q6I*_^Nad0`6&n*G|Y z7|8p+pnWA~0a|$*b(334A*_ZH$>y8_=MI#s74tTnF9lK>X;EQ9=*oD34q$d2cPqfg>DxZnCp z98=<1fIjnOyL?PG*}*7Q{pKzYWL?q^OeyPXI5qjU_>W_c)o?nqW~D)>OEn+KHAMt$ zjgGs-6*Ui%7=HwXd#YA^d5eD-AtCq5>baL^BQ_S<#bV_{gfUm2c(!R5+Ss5TMYaVl z7U}`i%0Aoo+u8(|GFrSG(c4a;3iQYZ3SHOUSFsn^e9tK`!k&zYyxTfC;|3t69Q10B9*!@?F0EPQh9R4y+P_y+S>eneovL=7^s zBy1E`HDks)yzj)iZS+HlrT7mv<}#D=+b86CZLGjoIAIG2Zrf$6I70?Ki51By{9p|a zMWNsz^sl33)OmwS(Ow5o2NKgYULuP!d*&hidL4)gkl4hw%tvxx^h9_fd0zgvF;{h` zvx#(bhzu59gmxAcXe;r^ zi?`OBt1ipnQM|IW{KKo>;@K9;LJu3)||w~ zN-IlZJVQI4?Dgh0tSG8tB*Doo9~Wif(`C;GqhBTK7Jo5Vl@)#h!eWlOj%*48u(H@q5DB4dqw`8BPTm z7yjI-aXSIkntVnh25*g2xQ$uzV$n&{hzbQT8qd)p-;ve-Rxf-PkqB*7xQjy7Iwb+$2J0QuYFlh&j04%&3X}0GJS8`a5DLg7p&f(E+ot0At zr<>Rj>CI$Q8f0DF*8YxoOc_q2$5W*M0h`=OAUKr8Tq#X%Yh{34T;lh=m$UbCE;uI? zyOkcr?GQlDE-Eb_3`(quwKA+%3_P<6+Sfz}fy)-VKhzmEit{%q=;{i-XB%82=l)Z& z2$oifvr|SS*xI1Vi(O0QF5%6C7}oTQ93e<$S*bged;b7YhfsqBm3eJ>mG1>I2GH<$ z{-xPhSIM|;F~6>;OOZ^$u9ScVDGLA?36@?fbX{K}`Ee_K@q@LhhC|>BmZ2Af8lkMo zhH~>RYN{1g<#xMXT*hR|L-ou(xV<}VIeYl_f=B8sB^sTTlAd7JajJ?g8kzSXx8?&E z_Y!Y&$u59(0;gWvmW$*I6{WAoqODJ4Sn%C-$n+O0qd~q+brO`eps9YK)t~DziD|TzZJWY&0LhcL)ZlR8Y*}qE1Po!jWqT?$r;zu z;e7_x;4=XOvgo|eqw-?o#Ms%g2|U`)VYH<5>6n1~GDG(Y#4n3Q!BNiK1eQ`*Fg^_Cb>EF|Gf*%IzWcQIg=fP0p5%gn5_0q0d%#)!b;{qe!^w?^m1eP3Vh z+sg>g;y_75^vB_>q7r zomtHqiQydsfk(*Lbr2?7MScmk??l0BsmYaD_iG&zohL3aZu!Mm*-c6oY)H*otIuJ@ z&F}nC;O*!$GMwgdla-H!mxNV7A zXPY|3)c^1ThVVxiC+w(Cs z<6!aa{=}BT8t}S2AHQ#{%k`=7#Kp9rC*1v@0^hW+i^LY&IuISYIi5E`@%|c}1G)bI zkibH}PX-)Of6=F!5SHsWm)QFi1sInv()~B;KEf)8B-`Qmi50sFbsKX0YGPN~D{nvW zzp&ok{Kf zlv2H_RY%A%3T@NHd_?EKsbYJ5E??@t8#EXev1L1KjyGgCo=h-mIR5|`#O4i8|Jncy z0|5X600RI301&S7XP#kBI12T3L!Z#5$N>10Lt0B#RE;FabXp((0FcD@LwE?52*{p_ z8K%*iara5|M$JQFmr@dA%90j|0kcDJ(l%4Uw&_)aT8(_iAbuKNY7*zFuk169%U_0u!w+?E7BG}5)rc>(9@-0vwR zZOFeSC9kCI^W=PV8FfC34F-G|y+GvZ1GXB5mJv^Mm7*0U{HtbJUEN)IG(Qu`o-)$Z zXP!sBd-j>-)*ar^Epdf70mmW$08{||OqFHP^uyz|;Q%I>4rF&mgi1%wu?EC~nR|hg zG+P2|cs`lPn>q#v`94p^oFCh5qLk~uI|_v$2199$ktaW(B<%zL!~iG}00RI40|WsC z0|5a500000009vp5HUd@QE~8rVUeN1FtO3`;s4qI2mu2D0Y4BBvAIf3T=V2+#)jh` z1^tx&0P^kqwHhoMq*WdmF=6Sl#>I(b4a6Y`LWEjP+9l3{GpBVLpKwu;}sfiatnMox|e z$qA&>=o`XgJ2r%HI4C~1tsh4t8`ByWV?M{wW7NL3%lR979wPl2f3XU_xfI7>f@2%# zx6vx-X2Eho5QOQYlo*SV4QSB}O?oe3ur{xw%k_@HYr*Q7rm;kS!bw1b@y|hJ)0{+fn^q^6yO_bDv6AI4zvzd(X}Hr^o2YFvc=#bS5qN^Q9h?WY!reMKQKyh0(dfk=v;{VmCvU zK1^OrF#iCkZ->w8{SKzZ^nXEG7c1H0k+$Uw%h~QGQvU!2(V`Q^<61&=N>hQh+XvqL zkbOuxg=($|3Ap9BM&(84?H3`*KK}qbeYB#lUAox_O=!|;4K|0P?jb50OL7_0b-_xJ z69`DyZMR_|?(}8S3YP7ISPo|X!M`TLYHS5tcRCghv^gUBg zVQqma(LpF+$R@ZcN;FhP>{iT|?A-5$P?03;Cs0RaF40R#dA0RaF20000101+WE z5FkNOVKDG0-z*%Jf09D2aAecVK{{STB$ncgtHqWT>;>C+H zJT12V4Mq-TA%+*k$!Cp|Zm8JDQchM`Wu6(#^=-D>ZHqA8i}=|Sn8%VLUGzH01|1$V zEe-Vh-EHx<`1jRx$^Jok25*1;&FvS%eMw{y9vyK5A6&PzE*=d1&bAMvmloS;X>7;p z2q6#UJR(O>_}BH|0)3IG0c5Jq4GB_&hI#%NPxkBo1$FEVu_qx+pWZDMP(1Kol+P=5EoBj*;_5)%!`N5P0v8$yj=ESqDn z=s(l{04#mR*GKyFZ#fKh#ga4aT>ekd{aUX=VzZ>D<&l}+_ zI3KTj2f>MuVRr^~Yu*ut-LREQ)NwIHcs1aNrq32z;k!q&J5NQp+aQ^32VUNHV0%tx zIXpEZnR|6I!>A0MS5N(q3-bUaJ@{W6;NdRLTsrf&$p+r?f3ov#qU@4OoSh2}TLg7x z9spw+aQEQ!8RRE($0>Ht0K5?$_jhPoGJiA0wky>0V+I1`&jR>`keTQM=*~5cnFP`S z7M7{cjgkO3n3KV9p3|#1?C&C^ipo5PY>(7gnjw1YL zjJomv!~iD{0RRF50s;a80s{d7000000RRypF+ovbae*%mfr^i#PC)TIgrZ%$Th`h?evTk1%voF9SE!5n`qUbJS0iEvl*cf^e-` zwSB`yf>MKn<_!%?CyVB}iD;QO+yW6YvXQ6ar8PWCxkGG2!aM$Ecp-tnJxw1G6*LXy zVKr#sIEK_kglV@)S3`ZpTpa0dIgHd^HbnZ2DhL6V7Hfv3lwNAg{ZAyvy6LHSv6{?V z!xdS$zAwzRS?CBWo0g__z@@G~3rrxIv^;ToA8fO|H^|&F8OLFR7nOacC3f~bgDnz(hj8Fzh6H(X#x8iprup|Q6 zz%U>7SV3=IT68_XL|6smJJ8|j69CgR;b`;^NIFzTP*R6T;fWK!$9<(o%PM>eJX3;y zu^3yKK#+KwsC-mQOFc}XPXjCgW$fk~db`33V-vB44m1Rb%hpCzG|_`hcj-LSM*g!FF7Zt4|ep|vX~`G|nD;FP67m4%_# z!`dTE0I17Aht>jvS|CPg8L=xvaFs5B65>?iC~-34TU0=?RGO|}qAYN6FjAkHyuVCH zl%vU#VoUhV$ow%OV6p2ehbd!@CV9N|HdFxcGQJ|7uTJP{2AY`#LCa1{LB_-?A*oO_ zPiK<`4C>j&>#E*yATA|rD4C;N?4lrTYrIro(8R^6uhefi>UUWuP3yuaLmm!RjHT$Tq$uFCsol{$ei#Lz<}A zjD(;rY+HY*<)wBKh!&g4bKdzX2xbX&{;Ws#6Xo1CyBI&rN1c_+08%-q{cdYW>@;%B z%S=5?(veqE+ijfgpyaqTmu7MUE1V*b3OSLuhhwc$16_z(%7udXBWYe7gA2in4q#vu z$xeeU@TD9?(lp;PTJk%Me4=TJu|@}}lsM;3?riU=Gke&rJ^ z0REAL7?uyXoM&B7Ju=U1?)tC^-`rFW5#YPNLGgoF33C-fKa3IAKImmfShwe z{{Ruxa>uyT8?Cc#Q-g_fZcN)R+dj$9G78>reae6>b;A{6EK#cXnRK)UA+!dz^d8{F zY+AK>g@8toaI;x}*3qNO?V_n%F91C7oG@FdcN{}uSw*b%MX)Pk3VvxHxHl)y{{Yb9 zUHjfoiQaDgp!t1b*tSf%p3NjCx6pqUMWmlTE(pHLygFX>=>1c7!qgnRu0JhXOHxl7)!J$if0;Y|O zt7Sid0|D3A*{V9YO%kXWY#o?x;@6mYIVHemykwaxE7R1q%fLq#nR%$PlOD`#1a@lX zZFcJ{N*QIF{{R^3!;BfkRS{ScrscrIx&w`(;{1l6)F&d0Ia8+YH2a_9-VE?X*&l6}VJ2TIN=If_mb&SDl_);vT>F0;&cuslZ_Ey5F12YHxD zJSyPDqNf;(SPk6B_9? zs{s|(hPVM#cl1GZKyZb8MiTGXqQ?k{7R2-n!5K^YQs9rVFifiz)5#ONOmuSAZ|=#? zVX_e(czcvgk|Al-n*o8;NCz z96-3v{ZB1T5q`{uRxZZ)To_+tkC@ysmR{viz1Q|ZqU~@ClH#irXzYE$?Syh|ei#@W z=maY;)%tu98HPcgUmnL34&rh~){I^U!1pWwd()8F5}(IRJGwNKiFKAvvNH%nih(6G zL3v_Z3<(u;kxNRaP-*Q#YYnFdXiPr5qfNo_OO};Q4RU}@Kd6k4+3n@Q7E~4FSmp#l zkO*6#aqo2UQUS8%+bFHDR9scs!--}KN>yV3 zGk1!cpPL=9av`fZ7$I(Ff$;Reh!!hh@c=6;Rgv>Cju`_nL?Ps3z&Kj%k7zpva=wq{ z4!~`UU&I)K$2ra={5Kq)Y|I&0%J=Av>$c!JJxac6E~oMtnB*Z56(FnR&BVK6s5Kz8 zt-0ijbdqpH42hTsr7#0&xh19Qt_VRb4CUSd!y-oj$_qF}tD1jMT(XLA3&oyg;6bHB zlc1Qak-$tjzzqOkI3>nWqJW;o>kzgyX(EHb(X7=Nm3iKhQPJjzQ7EOr==gDKtl4)e(5fqX;ZWCUIU;bmvLVr;H0v-^tC8#PSHypz;x zaPT$r4!<*=AQO>$#aRRd%}u;ixGX08x{McM_t|_!KKu@cM7xxNcRWAoHB?@fJ~m_< z#2!j3W6{bOr(0qZAcUG_00%_ETuvZ76f)Dl9Nm}#kfWiHI<`Eg15JTw7V+GuEXYNL z*ALKP00NyGVM%_h6e7BmDmrAL*)K^3-oVB2H*_iB8u(_YvK>=j+{@XYLS7s?8m^^; zu=zYj%Ett9SmQi-n4)ooo>OxyHLEpwvNA%PgZMBR72x3}J)BNUfB*q^#mkr_&GtT3|a9cQhZU?Mp+h;mldZWQhX1M2bjf z6a=bE$pH1yN}Q^T*7-#3xCo^J4e(G95APJ9)PU9mNYIul%Nd;t*g2xG3avb_Vi?qv z)l_sC#vnk_V(Ea{@?;KH+!CK{4=&_wn5DMe<@t}eX2y}tp5`1YDsaI%{!)-+$nBOW z-7@T%aP%8rnVw5}ewk%`T`xpvSZZ?d_QhkKEgquZ<1*oQU$}HrcT_zl-b z2}q3BpNY+QW(Mpl1qUjvq9CTgf|qwl>M}}%Qf%lydzTgA{N}%5Lq!2@$gm25V@(AC zH{ChXaiBYp6deN_hmI(&u^CPpx`Y&@0Sr5>Jf_x^%DU8AtIHK(+*-*S62oTq7`h|b z5eRNMc6@At9lglwcld#b9&P+QLemWGtVgj81HXucoya&mOR^{%qq%C!@BaV?)Ylg> zlg-n=@*Yu*^V8&()@H7_gWuj~2Y1_00O-Md32IhqUA7d=V$LH0-pwQWSz8U-t#X$~ zX^cA6^d9|DK`(6!Z0R36LZ#Q>T+CYa7nzKD8FEyYT`&x)HdSGtmr})4-gt`zSZH~A zs6ydXaB}?2fV_x*h>YkRQg8nN2bsUYRg%XE*u_k3OaA~Edx(s#V>2(T{w@Pu%?=S5 z7^@a{dm=WlgR50r{X+-e%%VDR%x%EHjZo%X>mE$- z_uND@Y3MxaJmn^IXO9eLw*HUB{r+v2EtmHgR*?&z<+OT)Sg0c4RvygDX}FbS5Q1AU z^H-nIUSR1P2yXISI!wVR-nUc*jPbQ-Nw-ow*$RpdxfD3^C05WWnMGlR`3kG&0UTm*uWVuymgj(Oahl`$v zwUfZIM{nqDlO7CBBM4%$<)4MzZVwKn82WS&Be}T X(-Qvx7cO7no1ANz!1;)T*+2i;o82RZ literal 0 HcmV?d00001 diff --git a/internal/photoprism/testdata/related/IMG_E1234 (2).JPEG b/internal/photoprism/testdata/related/IMG_E1234 (2).JPEG new file mode 100644 index 0000000000000000000000000000000000000000..19331d0837777ed0d9bb46abfb894adbea19fef9 GIT binary patch literal 36433 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 X(-Qvx7cO7no1ANz!1;)T*+2i;o82RZ literal 0 HcmV?d00001 diff --git a/pkg/fs/filepath.go b/pkg/fs/filepath.go index 8471b5abb..f518523dd 100644 --- a/pkg/fs/filepath.go +++ b/pkg/fs/filepath.go @@ -2,15 +2,10 @@ package fs import ( "path/filepath" - "regexp" "strconv" "strings" ) -// RelatedMediaFileSuffix is a regular expression that matches suffixes of related media files, -// see https://github.com/photoprism/photoprism/issues/2983 (Support Live Photos downloaded with "iCloudPD"). -var RelatedMediaFileSuffix = regexp.MustCompile(`(?i)_(jpg|jpeg|hevc)$`) - // StripSequence removes common sequence patterns at the end of file names. func StripSequence(fileName string) string { if fileName == "" { @@ -66,13 +61,3 @@ func AbsPrefix(fileName string, stripSequence bool) string { return filepath.Join(filepath.Dir(fileName), BasePrefix(fileName, stripSequence)) } - -// RelatedFilePathPrefix returns the absolute file path and name prefix without file extensions and media file -// suffixes to be ignored for comparison, see https://github.com/photoprism/photoprism/issues/2983. -func RelatedFilePathPrefix(fileName string, stripSequence bool) string { - if fileName == "" { - return "" - } - - return RelatedMediaFileSuffix.ReplaceAllString(AbsPrefix(fileName, stripSequence), "") -} diff --git a/pkg/fs/filepath_test.go b/pkg/fs/filepath_test.go index f101062bb..39e8aeeae 100644 --- a/pkg/fs/filepath_test.go +++ b/pkg/fs/filepath_test.go @@ -130,6 +130,10 @@ func TestAbsPrefix(t *testing.T) { assert.Equal(t, "", AbsPrefix("", true)) assert.Equal(t, "", AbsPrefix("", false)) }) + t.Run("IMG_4120", func(t *testing.T) { + assert.Equal(t, "/foo/bar/IMG_4120", AbsPrefix("/foo/bar/IMG_4120.JPG", false)) + assert.Equal(t, "/foo/bar/IMG_E4120", AbsPrefix("/foo/bar/IMG_E4120.JPG", false)) + }) t.Run("Test copy 3.jpg", func(t *testing.T) { result := AbsPrefix("/testdata/Test (4).jpg", true) @@ -140,50 +144,11 @@ func TestAbsPrefix(t *testing.T) { assert.Equal(t, "/testdata/Test (4)", result) }) -} - -func TestRelatedFilePathPrefix(t *testing.T) { - t.Run("Empty", func(t *testing.T) { - assert.Equal(t, "", RelatedFilePathPrefix("", true)) - assert.Equal(t, "", RelatedFilePathPrefix("", false)) - }) - t.Run("IMG_4120", func(t *testing.T) { - assert.Equal(t, "/foo/bar/IMG_4120", RelatedFilePathPrefix("/foo/bar/IMG_4120.JPG", false)) - assert.Equal(t, "/foo/bar/IMG_E4120", RelatedFilePathPrefix("/foo/bar/IMG_E4120.JPG", false)) - }) - t.Run("LivePhoto", func(t *testing.T) { - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC.MOV", false)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC.MOV", true)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_HevC", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_HEVC.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_HEVC.MOV", true)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_hevc.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722_hevc_", RelatedFilePathPrefix("/foo/bar/IMG_1722_hevc_.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_HEVC.AVC", true)) - assert.Equal(t, "/foo/bar/IMG_1722_MOV", RelatedFilePathPrefix("/foo/bar/IMG_1722_MOV.MOV", true)) - assert.Equal(t, "/foo/bar/IMG_1722_AVC", RelatedFilePathPrefix("/foo/bar/IMG_1722_AVC.MOV", true)) - - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC.JPEG", false)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC.JPEG", true)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC (1).JPEG", true)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_HEVC (2).JPEG", true)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_JPEG (1).JPEG", true)) - assert.Equal(t, "IMG_1722", RelatedFilePathPrefix("IMG_1722_JPG (2).JPEG", true)) - assert.Equal(t, "IMG_1722_JPG (2)", RelatedFilePathPrefix("IMG_1722_JPG (2).JPEG", false)) - assert.Equal(t, "IMG_1722_AVC", RelatedFilePathPrefix("IMG_1722_AVC (3).JPEG", true)) - assert.Equal(t, "IMG_1722_AVC (3)", RelatedFilePathPrefix("IMG_1722_AVC (3).JPEG", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_Jpeg", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_JPEG.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_JPEG.MOV", true)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_jpeg.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722_jpeg_", RelatedFilePathPrefix("/foo/bar/IMG_1722_jpeg_.MOV", false)) - assert.Equal(t, "/foo/bar/IMG_1722", RelatedFilePathPrefix("/foo/bar/IMG_1722_JPEG.JPEG", false)) - }) t.Run("Sequence", func(t *testing.T) { - assert.Equal(t, "/foo/bar/Test", RelatedFilePathPrefix("/foo/bar/Test (4).jpg", true)) - assert.Equal(t, "/foo/bar/Test (4)", RelatedFilePathPrefix("/foo/bar/Test (4).jpg", false)) + assert.Equal(t, "/foo/bar/Test", AbsPrefix("/foo/bar/Test (4).jpg", true)) + assert.Equal(t, "/foo/bar/Test (4)", AbsPrefix("/foo/bar/Test (4).jpg", false)) }) t.Run("LowerCase", func(t *testing.T) { - assert.Equal(t, "/foo/bar/IMG_E4120", RelatedFilePathPrefix("/foo/bar/IMG_E4120.JPG", false)) + assert.Equal(t, "/foo/bar/IMG_E4120", AbsPrefix("/foo/bar/IMG_E4120.JPG", false)) }) } diff --git a/pkg/list/join.go b/pkg/list/join.go new file mode 100644 index 000000000..b5216ec2e --- /dev/null +++ b/pkg/list/join.go @@ -0,0 +1,18 @@ +package list + +// Join combines two lists without adding duplicates. +func Join(list []string, join []string) []string { + if len(join) == 0 { + return list + } else if len(list) == 0 { + return join + } + + for j := range join { + if Excludes(list, join[j]) { + list = append(list, join[j]) + } + } + + return list +} diff --git a/pkg/list/join_test.go b/pkg/list/join_test.go new file mode 100644 index 000000000..394675587 --- /dev/null +++ b/pkg/list/join_test.go @@ -0,0 +1,23 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJoin(t *testing.T) { + assert.Equal(t, []string{""}, Join([]string{}, []string{""})) + assert.Equal(t, []string{"bar"}, Join([]string{}, []string{"bar"})) + assert.Equal(t, []string{""}, Join([]string{""}, []string{})) + assert.Equal(t, []string{"bar"}, Join([]string{"bar"}, []string{})) + assert.Equal(t, []string{"foo", "bar"}, Join([]string{"foo", "bar"}, []string{""})) + assert.Equal(t, []string{"foo", "bar"}, Join([]string{"foo", "bar"}, []string{"foo"})) + assert.Equal(t, []string{"foo", "bar", "zzz"}, Join([]string{"foo", "bar"}, []string{"zzz"})) + assert.Equal(t, []string{"foo", "bar", " "}, Join([]string{"foo", "bar"}, []string{" "})) + assert.Equal(t, []string{"foo", "bar", "645656"}, Join([]string{"foo", "bar"}, []string{"645656"})) + assert.Equal(t, []string{"foo", "bar ", "foo ", "baz", "bar"}, Join([]string{"foo", "bar ", "foo ", "baz"}, []string{"bar"})) + assert.Equal(t, []string{"foo", "bar", "foo ", "baz", "bar "}, Join([]string{"foo", "bar", "foo ", "baz"}, []string{"bar "})) + assert.Equal(t, []string{"bar", "baz", "foo", "bar ", "foo "}, Join([]string{"bar", "baz"}, []string{"foo", "bar ", "foo ", "baz"})) + assert.Equal(t, []string{"bar", "foo", "foo ", "baz"}, Join([]string{"bar"}, []string{"foo", "bar", "foo ", "baz"})) +}