diff --git a/browser/files/localassets.go b/browser/files/localassets.go index cfff5b4f..5caf630a 100644 --- a/browser/files/localassets.go +++ b/browser/files/localassets.go @@ -27,7 +27,7 @@ type fileLinks struct { type LocalAssetBrowser struct { fsyss []fs.FS albums map[string]string - catalogs map[fs.FS]map[string]map[string]fileLinks // per FS, DIR and base name + catalogs map[fs.FS]map[string][]string log *fileevent.Recorder sm immich.SupportedMedia bannedFiles namematcher.List // list of file pattern to be exclude @@ -38,9 +38,10 @@ func NewLocalFiles(ctx context.Context, l *fileevent.Recorder, fsyss ...fs.FS) ( return &LocalAssetBrowser{ fsyss: fsyss, albums: map[string]string{}, - catalogs: map[fs.FS]map[string]map[string]fileLinks{}, + catalogs: map[fs.FS]map[string][]string{}, log: l, whenNoDate: "FILE", + sm: immich.DefaultSupportedMedia, }, nil } @@ -71,17 +72,16 @@ func (la *LocalAssetBrowser) Prepare(ctx context.Context) error { } func (la *LocalAssetBrowser) passOneFsWalk(ctx context.Context, fsys fs.FS) error { - fsCatalog := map[string]map[string]fileLinks{} + la.catalogs[fsys] = map[string][]string{} err := fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error { if err != nil { return err } + if d.IsDir() { - fsCatalog[name] = map[string]fileLinks{} - return nil + la.catalogs[fsys][name] = []string{} } - select { case <-ctx.Done(): // If the context has been cancelled, return immediately @@ -100,27 +100,14 @@ func (la *LocalAssetBrowser) passOneFsWalk(ctx context.Context, fsys fs.FS) erro return nil } - linkBase := strings.TrimSuffix(base, ext) - for { - e := path.Ext(linkBase) - if la.sm.IsMedia(e) { - linkBase = strings.TrimSuffix(linkBase, e) - continue - } - break - } - dirLinks := fsCatalog[dir] - links := dirLinks[linkBase] + cat := la.catalogs[fsys][dir] switch mediaType { case immich.TypeImage: - links.image = name la.log.Record(ctx, fileevent.DiscoveredImage, nil, name) case immich.TypeVideo: - links.video = name la.log.Record(ctx, fileevent.DiscoveredVideo, nil, name) case immich.TypeSidecar: - links.sidecar = name la.log.Record(ctx, fileevent.DiscoveredSidecar, nil, name) } @@ -128,12 +115,10 @@ func (la *LocalAssetBrowser) passOneFsWalk(ctx context.Context, fsys fs.FS) erro la.log.Record(ctx, fileevent.DiscoveredDiscarded, nil, name, "reason", "banned file") return nil } - dirLinks[linkBase] = links - fsCatalog[dir] = dirLinks + la.catalogs[fsys][dir] = append(cat, name) } return nil }) - la.catalogs[fsys] = fsCatalog return err } @@ -150,46 +135,117 @@ func (la *LocalAssetBrowser) Browse(ctx context.Context) chan *browser.LocalAsse } } for _, fsys := range la.fsyss { - dirLinks := la.catalogs[fsys] - dirKeys := gen.MapKeys(dirLinks) - sort.Strings(dirKeys) - for _, d := range dirKeys { - linksList := la.catalogs[fsys][d] - linksKeys := gen.MapKeys(linksList) - sort.Strings(linksKeys) - for _, l := range linksKeys { + dirs := gen.MapKeys(la.catalogs[fsys]) + sort.Strings(dirs) + for _, dir := range dirs { + links := map[string]fileLinks{} + files := la.catalogs[fsys][dir] + + if len(files) == 0 { + continue + } + + // Scan images first + for _, file := range files { + ext := path.Ext(file) + if la.sm.TypeFromExt(ext) == immich.TypeImage { + linked := links[file] + linked.image = file + links[file] = linked + } + } + + next: + for _, file := range files { + ext := path.Ext(file) + t := la.sm.TypeFromExt(ext) + if t == immich.TypeImage { + continue next + } + + base := strings.TrimSuffix(file, ext) + switch t { + case immich.TypeSidecar: + if image, ok := links[base]; ok { + // file.ext.XMP -> file.ext + image.sidecar = file + links[base] = image + continue next + } + for f := range links { + if strings.TrimSuffix(f, path.Ext(f)) == base { + if image, ok := links[f]; ok { + // base.XMP -> base.ext + image.sidecar = file + links[f] = image + continue next + } + } + } + case immich.TypeVideo: + if image, ok := links[base]; ok { + // file.MP.ext -> file.ext + image.sidecar = file + links[base] = image + continue next + } + for f := range links { + if strings.TrimSuffix(f, path.Ext(f)) == base { + if image, ok := links[f]; ok { + // base.MP4 -> base.ext + image.video = file + links[f] = image + continue next + } + } + if strings.TrimSuffix(f, path.Ext(f)) == file { + if image, ok := links[f]; ok { + // base.MP4 -> base.ext + image.video = file + links[f] = image + continue next + } + } + } + // Unlinked video + links[file] = fileLinks{video: file} + } + } + + files = gen.MapKeys(links) + sort.Strings(files) + for _, file := range files { var a *browser.LocalAssetFile - links := linksList[l] + linked := links[file] - if links.image != "" { - a, err = la.assetFromFile(fsys, links.image) + if linked.image != "" { + a, err = la.assetFromFile(fsys, linked.image) if err != nil { - errFn(links.image, err) + errFn(linked.image, err) return } - if links.video != "" { - a.LivePhoto, err = la.assetFromFile(fsys, links.video) + if linked.video != "" { + a.LivePhoto, err = la.assetFromFile(fsys, linked.video) if err != nil { - errFn(links.video, err) + errFn(linked.video, err) return } } - } else if links.video != "" { - a, err = la.assetFromFile(fsys, links.video) + } else if linked.video != "" { + a, err = la.assetFromFile(fsys, linked.video) if err != nil { - errFn(links.video, err) + errFn(linked.video, err) return } } - if a != nil && links.sidecar != "" { + if a != nil && linked.sidecar != "" { a.SideCar = metadata.SideCarFile{ FSys: fsys, - FileName: links.sidecar, + FileName: linked.sidecar, } - la.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, nil, links.sidecar, "main", a.FileName) + la.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, nil, linked.sidecar, "main", a.FileName) } - select { case <-ctx.Done(): return diff --git a/browser/files/localassets_test.go b/browser/files/localassets_test.go index 93bc8fe2..1313b4ec 100644 --- a/browser/files/localassets_test.go +++ b/browser/files/localassets_test.go @@ -1,16 +1,15 @@ -package files_test +package files import ( "context" "errors" + "io/fs" "path" "reflect" - "sort" "testing" "github.com/kr/pretty" "github.com/psanford/memfs" - "github.com/simulot/immich-go/browser/files" "github.com/simulot/immich-go/helpers/fileevent" "github.com/simulot/immich-go/helpers/namematcher" "github.com/simulot/immich-go/immich" @@ -37,49 +36,77 @@ func (mfs *inMemFS) addFile(name string) *inMemFS { return mfs } -func generateFS() *inMemFS { - return newInMemFS(). - addFile("root_01.jpg"). - addFile("photos/photo_01.jpg"). - addFile("photos/photo_02.cr3"). - addFile("photos/photo_03.jpg"). - addFile("photos/summer 2023/20230801-001.jpg"). - addFile("photos/summer 2023/20230801-002.jpg"). - addFile("photos/summer 2023/20230801-003.cr3"). - addFile("@eaDir/thb1.jpg"). - addFile("photos/SYNOFILE_THUMB_0001.jpg"). - addFile("photos/summer 2023/.@__thumb/thb2.jpg") -} - func TestLocalAssets(t *testing.T) { tc := []struct { name string - expected []string + fsys fs.FS + expected map[string]fileLinks }{ { - name: "all", - expected: []string{ - "root_01.jpg", - "photos/photo_01.jpg", - "photos/photo_02.cr3", - "photos/photo_03.jpg", - "photos/summer 2023/20230801-001.jpg", - "photos/summer 2023/20230801-002.jpg", - "photos/summer 2023/20230801-003.cr3", + name: "simple", + fsys: newInMemFS(). + addFile("root_01.jpg"). + addFile("photos/photo_01.jpg"). + addFile("photos/photo_02.cr3"). + addFile("photos/photo_03.jpg"). + addFile("photos/summer 2023/20230801-001.jpg"). + addFile("photos/summer 2023/20230801-002.jpg"). + addFile("photos/summer 2023/20230801-003.cr3"). + addFile("@eaDir/thb1.jpg"). + addFile("photos/SYNOFILE_THUMB_0001.jpg"). + addFile("photos/summer 2023/.@__thumb/thb2.jpg"), + expected: map[string]fileLinks{ + "root_01.jpg": {image: "root_01.jpg"}, + "photos/photo_01.jpg": {image: "photos/photo_01.jpg"}, + "photos/photo_02.cr3": {image: "photos/photo_02.cr3"}, + "photos/photo_03.jpg": {image: "photos/photo_03.jpg"}, + "photos/summer 2023/20230801-001.jpg": {image: "photos/summer 2023/20230801-001.jpg"}, + "photos/summer 2023/20230801-002.jpg": {image: "photos/summer 2023/20230801-002.jpg"}, + "photos/summer 2023/20230801-003.cr3": {image: "photos/summer 2023/20230801-003.cr3"}, + }, + }, + { + name: "motion picture", + fsys: newInMemFS(). + addFile("motion/PXL_20210102_221126856.MP~2"). + addFile("motion/PXL_20210102_221126856.MP~2.jpg"). + addFile("motion/PXL_20210102_221126856.MP.jpg"). + addFile("motion/PXL_20210102_221126856.MP"). + addFile("motion/20231227_152817.jpg"). + addFile("motion/20231227_152817.MP4"), + expected: map[string]fileLinks{ + "motion/PXL_20210102_221126856.MP.jpg": {image: "motion/PXL_20210102_221126856.MP.jpg", video: "motion/PXL_20210102_221126856.MP"}, + "motion/PXL_20210102_221126856.MP~2.jpg": {image: "motion/PXL_20210102_221126856.MP~2.jpg", video: "motion/PXL_20210102_221126856.MP~2"}, + "motion/20231227_152817.jpg": {image: "motion/20231227_152817.jpg", video: "motion/20231227_152817.MP4"}, + }, + }, + { + name: "sidecar", + fsys: newInMemFS(). + addFile("root_01.jpg"). + addFile("root_01.XMP"). + addFile("root_02.jpg"). + addFile("root_02.jpg.XMP"). + addFile("video_01.mp4"). + addFile("video_01.mp4.XMP"). + addFile("root_03.MP.jpg"). + addFile("root_03.MP.jpg.XMP"). + addFile("root_03.MP"), + expected: map[string]fileLinks{ + "root_01.jpg": {image: "root_01.jpg", sidecar: "root_01.XMP"}, + "root_02.jpg": {image: "root_02.jpg", sidecar: "root_02.jpg.XMP"}, + "root_03.MP.jpg": {image: "root_03.MP.jpg", sidecar: "root_03.MP.jpg.XMP", video: "root_03.MP"}, + "video_01.mp4": {video: "video_01.mp4", sidecar: "video_01.mp4.XMP"}, }, }, } for _, c := range tc { t.Run(c.name, func(t *testing.T) { - fsys := generateFS() - if fsys.err != nil { - t.Error(fsys.err) - return - } + fsys := c.fsys ctx := context.Background() - b, err := files.NewLocalFiles(ctx, fileevent.NewRecorder(nil, false), fsys) + b, err := NewLocalFiles(ctx, fileevent.NewRecorder(nil, false), fsys) if err != nil { t.Error(err) } @@ -96,12 +123,23 @@ func TestLocalAssets(t *testing.T) { t.Error(err) } - results := []string{} + results := map[string]fileLinks{} for a := range b.Browse(ctx) { - results = append(results, a.FileName) + links := fileLinks{} + ext := path.Ext(a.FileName) + if b.sm.TypeFromExt(ext) == immich.TypeImage { + links.image = a.FileName + if a.LivePhoto != nil { + links.video = a.LivePhoto.FileName + } + } else { + links.video = a.FileName + } + if a.SideCar.FileName != "" { + links.sidecar = a.SideCar.FileName + } + results[a.FileName] = links } - sort.Strings(c.expected) - sort.Strings(results) if !reflect.DeepEqual(results, c.expected) { t.Errorf("difference\n") diff --git a/browser/gp/googlephotos.go b/browser/gp/googlephotos.go index 09e0239d..14705a9c 100644 --- a/browser/gp/googlephotos.go +++ b/browser/gp/googlephotos.go @@ -502,9 +502,9 @@ func (to *Takeout) passTwo(ctx context.Context, dir string, assetChan chan *brow // 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) + fExt := path.Ext(f) + if to.sm.TypeFromExt(fExt) == immich.TypeVideo { + name := strings.TrimSuffix(f, fExt) for i, linked := range linkedFiles { if linked.image == nil { continue @@ -516,7 +516,10 @@ nextVideo: ext := path.Ext(p) p = strings.TrimSuffix(p, ext) ext = path.Ext(p) - if strings.ToUpper(ext) == ".MP" { + if strings.ToUpper(ext) == ".MP" || strings.HasPrefix(strings.ToUpper(ext), ".MP~") { + if fExt != ext { + continue + } p = strings.TrimSuffix(p, ext) } if p == name { diff --git a/browser/gp/googlephotos_test.go b/browser/gp/googlephotos_test.go index 3c467a82..4e469830 100644 --- a/browser/gp/googlephotos_test.go +++ b/browser/gp/googlephotos_test.go @@ -82,6 +82,11 @@ func Test_matchers(t *testing.T) { fileName: "original_1d4caa6f-16c6-4c3d-901b-9387de10e528_P(1).jpg", want: "matchForgottenDuplicates", }, + { // #405 + jsonName: "PXL_20210102_221126856.MP~2.jpg.json", + fileName: "PXL_20210102_221126856.MP~2", + want: "livePhotoMatch", + }, } for _, tt := range tests { t.Run(tt.fileName, func(t *testing.T) { diff --git a/browser/gp/testgp_samples_test.go b/browser/gp/testgp_samples_test.go index 15ac396a..e6a2624a 100644 --- a/browser/gp/testgp_samples_test.go +++ b/browser/gp/testgp_samples_test.go @@ -328,32 +328,12 @@ Part: takeout-20230720T065335Z-002.tgz `) } -/* -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 +func checkMPissue405() []fs.FS { + return loadFromString("2006-01-02 15:04", `Part: takeout-20230720T065335Z-001.zip 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`) + 6486725 2024-01-21 16:59 Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP.jpg`) } -*/ diff --git a/browser/gp/testgp_test.go b/browser/gp/testgp_test.go index 7fa93e76..96230c42 100644 --- a/browser/gp/testgp_test.go +++ b/browser/gp/testgp_test.go @@ -323,10 +323,16 @@ func TestArchives(t *testing.T) { wantLivePhotos: photo{}, wantAlbum: album{}, }, - // // { // #405 - // // name: "checkMP_405", - // // gen: checkMP_405, - // // }, + { // #405 + name: "checkMP_405", + gen: checkMPissue405, + wantLivePhotos: photo{ + "Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP.jpg": "Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP", + "Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP~2.jpg": "Takeout/Google Photos/Untitled(1)/PXL_20210102_221126856.MP~2", + }, + wantAlbum: album{}, + wantAsset: photo{}, + }, } for _, c := range tc { t.Run( diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index e4c89e91..8df140e2 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -619,19 +619,21 @@ func (app *UpCmd) UploadAsset(ctx context.Context, a *browser.LocalAssetFile) (s if liveResp.Status == immich.UploadDuplicate { app.Jnl.Record(ctx, fileevent.UploadServerDuplicate, a.LivePhoto, a.LivePhoto.FileName, "info", "the server has this file") } else { - a.LivePhotoID = liveResp.ID app.Jnl.Record(ctx, fileevent.Uploaded, a.LivePhoto, a.LivePhoto.FileName) } + a.LivePhotoID = liveResp.ID } else { app.Jnl.Record(ctx, fileevent.UploadServerError, a.LivePhoto, a.LivePhoto.FileName, "error", err.Error()) } } + b := *a // Keep a copy of the asset to log errors specifically on the image resp, err = app.Immich.AssetUpload(ctx, a) if err == nil { if resp.Status == immich.UploadDuplicate { app.Jnl.Record(ctx, fileevent.UploadServerDuplicate, a, a.FileName, "info", "the server has this file") } else { - app.Jnl.Record(ctx, fileevent.Uploaded, a, a.FileName, "capture date", a.Metadata.DateTaken.String()) + b.LivePhoto = nil + app.Jnl.Record(ctx, fileevent.Uploaded, &b, b.FileName, "capture date", b.Metadata.DateTaken.String()) } } else { app.Jnl.Record(ctx, fileevent.UploadServerError, a, a.FileName, "error", err.Error()) @@ -641,13 +643,12 @@ func (app *UpCmd) UploadAsset(ctx context.Context, a *browser.LocalAssetFile) (s // dry-run mode if a.LivePhoto != nil { liveResp.ID = uuid.NewString() - app.Jnl.Record(ctx, fileevent.Uploaded, a.LivePhoto, a.LivePhoto.FileName) } resp.ID = uuid.NewString() app.Jnl.Record(ctx, fileevent.Uploaded, a, a.FileName, "capture date", a.Metadata.DateTaken.String()) } if resp.Status != immich.UploadDuplicate { - if a.LivePhoto != nil { + if a.LivePhoto != nil && liveResp.ID != "" { app.AssetIndex.AddLocalAsset(a, liveResp.ID) } app.AssetIndex.AddLocalAsset(a, resp.ID) diff --git a/docs/todo.md b/docs/todo.md index 7c8a93ce..b5299067 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,9 @@ # TO DO list - Motion pictures - - [ ] [#405](https://github.com/simulot/immich-go/issues/405) MP~2 files + - [ ] [#405](https://github.com/simulot/immich-go/issues/405) MP~2 files + - Test with GP photo + - Test with folder import + - test upload with clean installation - Report connection errors - [ ] [#395](https://github.com/simulot/immich-go/issues/395) - [ ] [#396](https://github.com/simulot/immich-go/issues/396) diff --git a/helpers/fileevent/fileevents.go b/helpers/fileevent/fileevents.go index b0bbb8b8..f4da5cab 100644 --- a/helpers/fileevent/fileevents.go +++ b/helpers/fileevent/fileevents.go @@ -126,7 +126,15 @@ func (r *Recorder) Record(ctx context.Context, code Code, object any, file strin r.log.Log(ctx, level, code.String(), args...) } if a, ok := object.(*browser.LocalAssetFile); ok && a.LivePhoto != nil { - r.Record(ctx, code, a.LivePhoto, a.LivePhoto.FileName, args...) + arg2 := []any{} + for i := 0; i < len(args); i++ { + if args[i] == "file" { + i += 1 + continue + } + arg2 = append(arg2, args[i]) + } + r.Record(ctx, code, a.LivePhoto, a.LivePhoto.FileName, arg2...) } } diff --git a/immich/asset.go b/immich/asset.go index b9471f88..311ea0ba 100644 --- a/immich/asset.go +++ b/immich/asset.go @@ -45,7 +45,16 @@ func formatDuration(duration time.Duration) string { func (ic *ImmichClient) AssetUpload(ctx context.Context, la *browser.LocalAssetFile) (AssetResponse, error) { var ar AssetResponse - mtype := ic.TypeFromExt(path.Ext(la.FileName)) + ext := path.Ext(la.FileName) + if strings.TrimSuffix(la.Title, ext) == "" { + la.Title = "No Name" + ext // fix #88, #128 + } + + if strings.HasPrefix(strings.ToUpper(ext), ".MP") { + ext = ".MP4" // #405 + la.Title = la.Title + ".MP4" + } + mtype := ic.TypeFromExt(ext) switch mtype { case "video", "image": default: @@ -70,17 +79,6 @@ func (ic *ImmichClient) AssetUpload(ctx context.Context, la *browser.LocalAssetF if err != nil { return } - assetType := strings.ToUpper(mtype) - ext := path.Ext(la.Title) - if strings.TrimSuffix(la.Title, ext) == "" { - la.Title = "No Name" + ext // fix #88, #128 - } - - // disguise MP files as MP4 - if strings.ToLower(ext) == ".mp" { - la.Title = strings.TrimSuffix(la.Title, ext) + ".MP4" - ext = ".MP4" - } err = m.WriteField("deviceAssetId", fmt.Sprintf("%s-%d", path.Base(la.Title), s.Size())) if err != nil { @@ -90,7 +88,7 @@ func (ic *ImmichClient) AssetUpload(ctx context.Context, la *browser.LocalAssetF if err != nil { return } - err = m.WriteField("assetType", assetType) + err = m.WriteField("assetType", mtype) if err != nil { return } diff --git a/immich/client.go b/immich/client.go index 97b107ac..2c7083e1 100644 --- a/immich/client.go +++ b/immich/client.go @@ -218,6 +218,10 @@ func (ic *ImmichClient) GetSupportedMediaTypes(ctx context.Context) (SupportedMe func (sm SupportedMedia) TypeFromExt(ext string) string { ext = strings.ToLower(ext) + if strings.HasPrefix(ext, ".mp~") { + // #405 + ext = ".mp4" + } return sm[ext] }