diff --git a/docker/.env.example b/docker/.env.example index e958baabd4..e9f12364ab 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -58,5 +58,6 @@ MAPBOX_KEY= # know where can it make the request to. # For example: If your server IP address is 10.1.11.50, the environment variable will # be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283 +# !CAUTION! THERE IS NO FORWARD SLASH AT THE END VITE_SERVER_ENDPOINT= diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index 9f3ace1f22..c542efaa32 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -73,7 +73,7 @@ class BackupService { }); // Build thumbnail multipart data - var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280)); + var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560)); if (thumbnailData != null) { thumbnailUploadData = MultipartFile.fromBytes( List.from(thumbnailData), diff --git a/server/Dockerfile b/server/Dockerfile index 95f50dfcb0..e7fa33d2ef 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /usr/src/app COPY package.json package-lock.json ./ -RUN apk add --update-cache build-base python3 libheif vips-dev vips +RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg RUN npm install diff --git a/server/package-lock.json b/server/package-lock.json index 11a7244eb3..663d6183d8 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -32,6 +32,7 @@ "diskusage": "^1.1.3", "dotenv": "^14.2.0", "exifr": "^7.1.3", + "fluent-ffmpeg": "^2.1.2", "joi": "^17.5.0", "lodash": "^4.17.21", "passport": "^0.5.2", @@ -41,7 +42,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "sharp": "^0.30.4", + "sharp": "^0.28.0", "socket.io-redis": "^6.1.1", "systeminformation": "^5.11.0", "typeorm": "^0.2.41" @@ -3141,6 +3142,11 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", "dev": true }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3860,15 +3866,12 @@ "dev": true }, "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, "node_modules/color-convert": { @@ -3896,6 +3899,19 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -4231,28 +4247,14 @@ "dev": true }, "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "dependencies": { - "mimic-response": "^3.1.0" + "mimic-response": "^2.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/dedent": { @@ -5418,6 +5420,18 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/follow-redirects": { "version": "1.14.8", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", @@ -6329,8 +6343,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -7959,14 +7972,19 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" + "semver": "^5.4.1" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" } }, "node_modules/node-addon-api": { @@ -8668,21 +8686,21 @@ } }, "node_modules/prebuild-install": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz", - "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", + "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "dependencies": { - "detect-libc": "^2.0.0", + "detect-libc": "^1.0.3", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", + "node-abi": "^2.21.0", "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^4.0.0", + "simple-get": "^3.0.3", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -8690,15 +8708,7 @@ "prebuild-install": "bin.js" }, "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/prelude-ls": { @@ -9460,40 +9470,27 @@ } }, "node_modules/sharp": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz", - "integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==", + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.28.3.tgz", + "integrity": "sha512-21GEP45Rmr7q2qcmdnjDkNP04Ooh5v0laGS5FDpojOO84D1DJwUijLiSq8XNNM6e8aGXYtoYRh3sVNdm8NodMA==", "hasInstallScript": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1", - "semver": "^7.3.7", - "simple-get": "^4.0.1", + "color": "^3.1.3", + "detect-libc": "^1.0.3", + "node-addon-api": "^3.2.0", + "prebuild-install": "^6.1.2", + "semver": "^7.3.5", + "simple-get": "^3.1.0", "tar-fs": "^2.1.1", "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=10" }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sharp/node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9571,25 +9568,11 @@ ] }, "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "dependencies": { - "decompress-response": "^6.0.0", + "decompress-response": "^4.2.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } @@ -11026,6 +11009,17 @@ "node": ">=10" } }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -13602,6 +13596,11 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", "dev": true }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -14145,12 +14144,27 @@ "dev": true }, "color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } } }, "color-convert": { @@ -14460,18 +14474,11 @@ "dev": true }, "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } + "mimic-response": "^2.0.0" } }, "dedent": { @@ -15394,6 +15401,15 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=", + "requires": { + "async": ">=0.2.9", + "which": "^1.1.1" + } + }, "follow-redirects": { "version": "1.14.8", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", @@ -16043,8 +16059,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -17335,11 +17350,18 @@ "dev": true }, "node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "requires": { - "semver": "^7.3.5" + "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "node-addon-api": { @@ -17856,30 +17878,23 @@ } }, "prebuild-install": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz", - "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", + "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "requires": { - "detect-libc": "^2.0.0", + "detect-libc": "^1.0.3", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", + "node-abi": "^2.21.0", "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^4.0.0", + "simple-get": "^3.0.3", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - } } }, "prelude-ls": { @@ -18437,30 +18452,18 @@ } }, "sharp": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz", - "integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==", + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.28.3.tgz", + "integrity": "sha512-21GEP45Rmr7q2qcmdnjDkNP04Ooh5v0laGS5FDpojOO84D1DJwUijLiSq8XNNM6e8aGXYtoYRh3sVNdm8NodMA==", "requires": { - "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1", - "semver": "^7.3.7", - "simple-get": "^4.0.1", + "color": "^3.1.3", + "detect-libc": "^1.0.3", + "node-addon-api": "^3.2.0", + "prebuild-install": "^6.1.2", + "semver": "^7.3.5", + "simple-get": "^3.1.0", "tar-fs": "^2.1.1", "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" - } } }, "shebang-command": { @@ -18511,11 +18514,11 @@ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" }, "simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "requires": { - "decompress-response": "^6.0.0", + "decompress-response": "^4.2.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } @@ -19516,6 +19519,14 @@ "webidl-conversions": "^6.1.0" } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/server/package.json b/server/package.json index 6a1e2f9137..c5749fe66b 100644 --- a/server/package.json +++ b/server/package.json @@ -45,6 +45,7 @@ "diskusage": "^1.1.3", "dotenv": "^14.2.0", "exifr": "^7.1.3", + "fluent-ffmpeg": "^2.1.2", "joi": "^17.5.0", "lodash": "^4.17.21", "passport": "^0.5.2", diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 6d7ec40b8f..068729e43f 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -39,7 +39,7 @@ export class AssetController { private wsCommunicateionGateway: CommunicationGateway, private assetService: AssetService, private backgroundTaskService: BackgroundTaskService, - ) {} + ) { } @Post('upload') @UseInterceptors( diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts index 7e2d974605..6237e4aa28 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/src/api-v1/asset/asset.service.ts @@ -1,19 +1,16 @@ import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { MoreThan, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { CreateAssetDto } from './dto/create-asset.dto'; import { AssetEntity, AssetType } from './entities/asset.entity'; import _ from 'lodash'; -import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; -import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto'; import { createReadStream, stat } from 'fs'; import { ServeFileDto } from './dto/serve-file.dto'; import { Response as Res } from 'express'; import { promisify } from 'util'; import { DeleteAssetDto } from './dto/delete-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto'; -import path from 'path'; const fileInfo = promisify(stat); @@ -124,21 +121,43 @@ export class AssetService { public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) { let file = null; const asset = await this.findOne(query.did, query.aid); + if (!asset) { throw new BadRequestException('Asset does not exist'); } + + // Handle Sending Images if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { - res.set({ - 'Content-Type': asset.mimeType, - }); + /** + * Serve file viewer on the web + */ + if (query.isWeb) { + res.set({ + 'Content-Type': 'image/jpeg', + }); + return new StreamableFile(createReadStream(asset.resizePath)); + } + + /** + * Serve thumbnail image for both web and mobile app + */ if (query.isThumb === 'false' || !query.isThumb) { + res.set({ + 'Content-Type': asset.mimeType, + }); file = createReadStream(asset.originalPath); } else { if (asset.webpPath != '') { + res.set({ + 'Content-Type': 'image/webp', + }); file = createReadStream(asset.webpPath); } else { + res.set({ + 'Content-Type': 'image/jpeg', + }); file = createReadStream(asset.resizePath); } } @@ -147,9 +166,11 @@ export class AssetService { Logger.log(`Cannot create read stream ${error}`); return new BadRequestException('Cannot Create Read Stream'); }); + return new StreamableFile(file); + } else if (asset.type == AssetType.VIDEO) { - // Handle Handling Video + // Handle Video const { size } = await fileInfo(asset.originalPath); const range = headers.range; @@ -191,6 +212,8 @@ export class AssetService { const videoStream = createReadStream(asset.originalPath, { start: start, end: end }); return new StreamableFile(videoStream); + + } else { res.set({ 'Content-Type': asset.mimeType, diff --git a/server/src/api-v1/asset/dto/serve-file.dto.ts b/server/src/api-v1/asset/dto/serve-file.dto.ts index 42f87d485b..a4244faee4 100644 --- a/server/src/api-v1/asset/dto/serve-file.dto.ts +++ b/server/src/api-v1/asset/dto/serve-file.dto.ts @@ -13,4 +13,8 @@ export class ServeFileDto { @IsOptional() @IsBooleanString() isThumb: string; + + @IsOptional() + @IsBooleanString() + isWeb: string; } diff --git a/web/src/hooks.ts b/web/src/hooks.ts index 56977df1a3..b9efecb3ae 100644 --- a/web/src/hooks.ts +++ b/web/src/hooks.ts @@ -11,29 +11,35 @@ export const handle: Handle = async ({ event, resolve, }) => { return await resolve(event) } - const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session); + try { + const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session); - const res = await fetch(`${serverEndpoint}/auth/validateToken`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${accessToken}` + const res = await fetch(`${serverEndpoint}/auth/validateToken`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }) + + if (res.status === 201) { + event.locals.user = { + id, + accessToken, + firstName, + lastName, + isAdmin, + email + }; } - }) - if (res.status === 201) { - event.locals.user = { - id, - accessToken, - firstName, - lastName, - isAdmin, - email - }; + const response = await resolve(event); + + return response; + } catch (error) { + console.log('Error parsing session', error); + return await resolve(event); } - const response = await resolve(event); - - return response; }; export const getSession: GetSession = async ({ locals }) => { diff --git a/web/src/lib/components/photos/immich-thumbnail.svelte b/web/src/lib/components/photos/immich-thumbnail.svelte index 23e730fa53..9cdfd0a633 100644 --- a/web/src/lib/components/photos/immich-thumbnail.svelte +++ b/web/src/lib/components/photos/immich-thumbnail.svelte @@ -1,13 +1,25 @@ -
+
(mouseOver = true)} + on:mouseleave={() => (mouseOver = false)} + on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })} + > + {#if mouseOver} +
+
(mouseOverIcon = true)} + on:mouseleave={() => (mouseOverIcon = false)} + class="inline-block" + > + +
+
+ {/if} + + {#if asset.type === AssetType.VIDEO} +
+ {parseVideoDuration(asset.duration)} + +
+ {/if} + {#if intersecting} {#await loadImageData()}
...
@@ -37,10 +110,18 @@ in:fade={{ duration: 200 }} src={imageData} alt={asset.id} - class="object-cover h-[200px] w-[200px] transition-all duration-100" + class="object-cover h-[200px] w-[200px] transition-all duration-100 z-0" loading="lazy" /> {/await} {/if} + +
diff --git a/web/src/lib/components/photos/photo_viewer.svelte b/web/src/lib/components/photos/photo_viewer.svelte new file mode 100644 index 0000000000..b4a7d03985 --- /dev/null +++ b/web/src/lib/components/photos/photo_viewer.svelte @@ -0,0 +1,62 @@ + + +
dispatch('close')} class="h-screen"> + {#if assetInfo} + {#await loadAssetData()} +
+ +
+ {:then assetData} +
+ {assetId} +
+ {/await} + {/if} +
diff --git a/web/src/lib/components/shared/loading-spinner.svelte b/web/src/lib/components/shared/loading-spinner.svelte new file mode 100644 index 0000000000..d5be9eb872 --- /dev/null +++ b/web/src/lib/components/shared/loading-spinner.svelte @@ -0,0 +1,18 @@ +
+ + + + +
diff --git a/web/src/lib/stores/assets.ts b/web/src/lib/stores/assets.ts index a9d922d8b9..f43295f60d 100644 --- a/web/src/lib/stores/assets.ts +++ b/web/src/lib/stores/assets.ts @@ -4,9 +4,9 @@ import type { ImmichAsset } from '$lib/models/immich-asset' import lodash from 'lodash-es'; import moment from 'moment'; -const assets = writable([]); +export const assets = writable([]); -const assetsGroupByDate = derived(assets, ($assets) => { +export const assetsGroupByDate = derived(assets, ($assets) => { try { return lodash.chain($assets) @@ -20,14 +20,14 @@ const assetsGroupByDate = derived(assets, ($assets) => { }) -const getAssetsInfo = async (accessToken: string) => { +export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => { + return $assetsGroupByDate.flat(); +}) + +export const getAssetsInfo = async (accessToken: string) => { const res = await getRequest('asset', accessToken); assets.set(res); + } -export default { - assets, - assetsGroupByDate, - getAssetsInfo, -} \ No newline at end of file diff --git a/web/src/routes/__layout.svelte b/web/src/routes/__layout.svelte index c66405cb88..13c5700990 100644 --- a/web/src/routes/__layout.svelte +++ b/web/src/routes/__layout.svelte @@ -11,7 +11,7 @@ const response = await getRequest('server-info/ping', ''); if (response.res === 'pong') isServerOk = true; - if (response.statusCode === 404) isServerOk = false; + else isServerOk = false; }, 10000); onDestroy(() => clearInterval(pingServerInterval)); diff --git a/web/src/routes/photos/index.svelte b/web/src/routes/photos/index.svelte index 4cc585cb37..327a29c6c7 100644 --- a/web/src/routes/photos/index.svelte +++ b/web/src/routes/photos/index.svelte @@ -2,8 +2,9 @@ export const prerender = false; import type { Load } from '@sveltejs/kit'; + import { getAssetsInfo } from '$lib/stores/assets'; - export const load: Load = ({ session }) => { + export const load: Load = async ({ session }) => { if (!session.user) { return { status: 302, @@ -25,24 +26,36 @@ import NavigationBar from '../../lib/components/shared/navigation-bar.svelte'; import SideBarButton from '$lib/components/shared/side-bar-button.svelte'; - import Magnify from 'svelte-material-icons/Magnify.svelte'; + import CheckCircle from 'svelte-material-icons/CheckCircle.svelte'; + import ChevronRight from 'svelte-material-icons/ChevronRight.svelte'; + import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection'; - import { onDestroy, onMount } from 'svelte'; + import { onMount } from 'svelte'; + import { fade, fly } from 'svelte/transition'; import { session } from '$app/stores'; - import assetStore from '$lib/stores/assets'; - import type { ImmichAsset } from '../../lib/models/immich-asset'; + import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets'; import ImmichThumbnail from '../../lib/components/photos/immich-thumbnail.svelte'; import moment from 'moment'; + import PhotoViewer from '../../lib/components/photos/photo_viewer.svelte'; + import type { ImmichAsset } from '../../lib/models/immich-asset'; + import { AssetType } from '../../lib/models/immich-asset'; + import LoadingSpinner from '../../lib/components/shared/loading-spinner.svelte'; export let user: ImmichUser; let selectedAction: AppSideBarSelection; - let assets: ImmichAsset[] = []; - let assetsGroupByDate: ImmichAsset[][]; - // Subscribe to store values - const assetsSub = assetStore.assets.subscribe((newAssets) => (assets = newAssets)); - const assetsGroupByDateSub = assetStore.assetsGroupByDate.subscribe((value) => (assetsGroupByDate = value)); + let selectedGroupThumbnail: number | null; + let isMouseOverGroup: boolean; + $: if (isMouseOverGroup == false) { + selectedGroupThumbnail = null; + } + + let isShowAsset = false; + let viewDeviceId: string = ''; + let viewAssetId: string = ''; + let currentViewAssetIndex = 0; + let currentSelectedAsset: ImmichAsset; const onButtonClicked = (buttonType: CustomEvent) => { selectedAction = buttonType.detail['actionType'] as AppSideBarSelection; @@ -50,15 +63,46 @@ onMount(async () => { selectedAction = AppSideBarSelection.PHOTOS; + if ($session.user) { - await assetStore.getAssetsInfo($session.user.accessToken); + await getAssetsInfo($session.user.accessToken); } }); - onDestroy(() => { - assetsSub(); - assetsGroupByDateSub(); - }); + const thumbnailMouseEventHandler = (event: CustomEvent) => { + const { selectedGroupIndex }: { selectedGroupIndex: number } = event.detail; + + selectedGroupThumbnail = selectedGroupIndex; + }; + + const viewAssetHandler = (event: CustomEvent) => { + const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail; + + viewDeviceId = deviceId; + viewAssetId = assetId; + + currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId); + currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex]; + isShowAsset = true; + }; + + const navigateAssetForward = () => { + const nextAsset = $flattenAssetGroupByDate[currentViewAssetIndex + 1]; + viewDeviceId = nextAsset.deviceId; + viewAssetId = nextAsset.id; + + currentViewAssetIndex = currentViewAssetIndex + 1; + currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex]; + }; + + const navigateAssetBackward = () => { + const lastAsset = $flattenAssetGroupByDate[currentViewAssetIndex - 1]; + viewDeviceId = lastAsset.deviceId; + viewAssetId = lastAsset.id; + + currentViewAssetIndex = currentViewAssetIndex - 1; + currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex]; + }; @@ -70,6 +114,7 @@
+
- -
+
-
-
- {#each assetsGroupByDate as assetsInDateGroup} -
-

+

+
+ {#each $assetsGroupByDate as assetsInDateGroup, groupIndex} + +
(isMouseOverGroup = true)} + on:mouseleave={() => (isMouseOverGroup = false)} + > + +

+ {#if selectedGroupThumbnail === groupIndex && isMouseOverGroup} +

+ +
+ {/if} + {moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')}

-
+ + +
{#each assetsInDateGroup as asset} - + {/each}
@@ -107,3 +168,37 @@
+ + +{#if isShowAsset} +
+ + + {#key currentViewAssetIndex} + {#if currentSelectedAsset.type == AssetType.IMAGE} + (isShowAsset = false)} /> + {:else} +
(isShowAsset = false)} + > +

Video viewer is under construction

+
+ {/if} + {/key} + + +
+{/if}