diff --git a/.dockerignore b/.dockerignore index 603449cd1b..87d63b5942 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,5 +24,7 @@ cli/.reverse-geocoding-dump/ cli/upload/ cli/dist/ +e2e/ + open-api/typescript-sdk/node_modules/ open-api/typescript-sdk/build/ diff --git a/.gitattributes b/.gitattributes index 260c9e9dae..b0a7f5ccc4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,9 +8,9 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true mobile/lib/**/*.g.dart -diff -merge mobile/lib/**/*.g.dart linguist-generated=true -open-api/typescript-sdk/client/**/*.md -diff -merge -open-api/typescript-sdk/client/**/*.md linguist-generated=true -open-api/typescript-sdk/client/**/*.ts -diff -merge -open-api/typescript-sdk/client/**/*.ts linguist-generated=true +open-api/typescript-sdk/axios-client/**/* -diff -merge +open-api/typescript-sdk/axios-client/**/* linguist-generated=true +open-api/typescript-sdk/fetch-client.ts -diff -merge +open-api/typescript-sdk/fetch-client.ts linguist-generated=true *.sh text eol=lf diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml deleted file mode 100644 index c66dcfb3cf..0000000000 --- a/.github/workflows/cli-release.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: CLI Release -on: - workflow_dispatch: - -jobs: - publish: - name: Publish - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./cli - - steps: - - uses: actions/checkout@v4 - # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v4 - with: - node-version: "20.x" - registry-url: "https://registry.npmjs.org" - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml new file mode 100644 index 0000000000..87170e5d59 --- /dev/null +++ b/.github/workflows/cli.yml @@ -0,0 +1,98 @@ +name: CLI Build +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "cli/**" + - ".github/workflows/cli.yml" + pull_request: + branches: [main] + paths: + - "cli/**" + - ".github/workflows/cli.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + packages: write + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./cli + + steps: + - uses: actions/checkout@v4 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + - name: Prepare SDK + run: npm ci --prefix ../open-api/typescript-sdk/ + - name: Build SDK + run: npm run build --prefix ../open-api/typescript-sdk/ + - run: npm ci + - run: npm run build + - run: npm publish + if: ${{ github.event_name == 'workflow_dispatch' }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + docker: + name: Docker + runs-on: ubuntu-latest + needs: publish + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get package version + id: package-version + run: | + version=$(jq -r '.version' cli/package.json) + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Generate docker image tags + id: metadata + uses: docker/metadata-action@v5 + with: + flavor: | + latest=false + images: | + name=ghcr.io/${{ github.repository_owner }}/immich-cli + tags: | + type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Build and push image + uses: docker/build-push-action@v5.1.0 + with: + file: cli/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name == 'workflow_dispatch' }} + cache-from: type=gha + cache-to: type=gha,mode=max + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index a49ba55912..fd404e84ef 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Clean temporary images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0 + uses: stumpylog/image-cleaner-action/ephemeral@v0.5.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" @@ -64,7 +64,7 @@ jobs: steps: - name: Clean untagged images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/untagged@v0.4.0 + uses: stumpylog/image-cleaner-action/untagged@v0.5.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 10ed06510f..a6a6835882 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -44,7 +44,7 @@ jobs: platforms: linux/amd64 device: openvino suffix: -openvino - + - image: immich-machine-learning context: machine-learning file: machine-learning/Dockerfile @@ -57,6 +57,7 @@ jobs: file: server/Dockerfile platforms: linux/amd64,linux/arm64 device: cpu + steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bff3f484a9..1a23bd351d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,6 +103,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk @@ -143,6 +148,11 @@ jobs: with: submodules: "recursive" + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk @@ -195,6 +205,34 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} + web-e2e-tests: + name: Web (e2e) + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./e2e + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Docker build + run: docker compose -f docker/docker-compose.e2e.yml build + working-directory: ./ + + - name: Run e2e tests + run: npx playwright test + mobile-unit-tests: name: Mobile runs-on: ubuntu-latest @@ -237,7 +275,7 @@ jobs: poetry run mypy --install-types --non-interactive --strict app/ - name: Run tests and coverage run: | - poetry run pytest --cov app + poetry run pytest app --cov=app --cov-report term-missing generated-api-up-to-date: name: OpenAPI Clients @@ -265,7 +303,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/LICENSE b/LICENSE index a72f398805..94ae15a0fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,620 @@ -MIT License + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2022 Hau Tran + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile index e66714a20f..e28232bef8 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,10 @@ server-e2e-jobs: server-e2e-api: npm run e2e:api --prefix server +.PHONY: e2e +e2e: + docker compose -f ./docker/docker-compose.e2e.yml up --build -V --remove-orphans + prod: docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans diff --git a/README.md b/README.md index 28fe635646..3cbb545fa4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 Discord @@ -69,6 +69,9 @@ password: demo Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM ``` +## Activities +![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image") + ## Features @@ -117,7 +120,7 @@ If you feel like this is the right cause and the app is something you are seeing - [One-time donation](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Liberapay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz ## Contributors diff --git a/README_ca_ES.md b/README_ca_ES.md index b2daa83b25..ebf1514cd5 100644 --- a/README_ca_ES.md +++ b/README_ca_ES.md @@ -1,6 +1,6 @@


- Llicència: MIT + Llicència: MIT @@ -110,5 +110,5 @@ Si creieu que aquesta és una causa justa i l'aplicació és alguna cosa que us - [Donació única](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_de_DE.md b/README_de_DE.md index 9515fff902..64f8b75a9f 100644 --- a/README_de_DE.md +++ b/README_de_DE.md @@ -1,6 +1,6 @@


- Lizenz: MIT + Lizenz: MIT Discord @@ -113,7 +113,7 @@ Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für ei - [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz ## Mitwirkende diff --git a/README_es_ES.md b/README_es_ES.md index 7be171df89..6ab6617a6b 100644 --- a/README_es_ES.md +++ b/README_es_ES.md @@ -1,6 +1,6 @@


- Licencia: MIT + Licencia: MIT @@ -111,5 +111,5 @@ Si consideras que esta es una causa justa y la aplicación es algo que te gustar - [Donación única](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_fr_FR.md b/README_fr_FR.md index aa5a670944..66c403c81c 100644 --- a/README_fr_FR.md +++ b/README_fr_FR.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -112,5 +112,5 @@ Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l - [Donation occasionnelle](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_it_IT.md b/README_it_IT.md index 130c540c50..b588e1951f 100644 --- a/README_it_IT.md +++ b/README_it_IT.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -113,5 +113,5 @@ Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti ne - [Donazione una tantum](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) tramite GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_ja_JP.md b/README_ja_JP.md index cfe970d4f9..999237a2e2 100644 --- a/README_ja_JP.md +++ b/README_ja_JP.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -112,4 +112,4 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM - GitHub スポンサー経由の[一回寄付](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 diff --git a/README_ko_KR.md b/README_ko_KR.md index f99cb801f8..db3bb7a07b 100644 --- a/README_ko_KR.md +++ b/README_ko_KR.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 Discord @@ -113,5 +113,5 @@ password: demo - GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_nl_NL.md b/README_nl_NL.md index 0e917080b6..7d853c2199 100644 --- a/README_nl_NL.md +++ b/README_nl_NL.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -112,5 +112,5 @@ Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel - [Eenmalige donatie](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_ru_RU.md b/README_ru_RU.md index 81a61c772f..690870596f 100644 --- a/README_ru_RU.md +++ b/README_ru_RU.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 Discord @@ -115,7 +115,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM - [Одноразовое пожертвование](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz ## Авторы diff --git a/README_tr_TR.md b/README_tr_TR.md index d4bf3b9179..f04293f88d 100644 --- a/README_tr_TR.md +++ b/README_tr_TR.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -109,5 +109,5 @@ Eğer bu size doğru bir amaç gibi geliyorsa ve uygulamanın uzun bir süre boy - [Bir seferlik bağış](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_zh_CN.md b/README_zh_CN.md index 58e5a48c64..0b9bd07061 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -1,6 +1,6 @@


- License: MIT + License: AGPLv3 @@ -119,7 +119,7 @@ - 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=alextran1502) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- 比特币: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz ## 贡献者 diff --git a/cli/Dockerfile b/cli/Dockerfile index 0af3c3cef6..02fffca397 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,19 +1,19 @@ -FROM ghcr.io/immich-app/base-server-dev:20240130@sha256:a11ac5c56f0ccce1f218954c07c43caadf489557252ba5b9ca1c5977aaa25999 as test +FROM node:20-alpine3.19 as core -WORKDIR /usr/src/app/server -COPY server/package.json server/package-lock.json ./ +WORKDIR /usr/src/open-api/typescript-sdk +COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ RUN npm ci -COPY ./server/ . +COPY open-api/typescript-sdk/ ./ +RUN npm run build + +WORKDIR /usr/src/app -WORKDIR /usr/src/app/cli COPY cli/package.json cli/package-lock.json ./ RUN npm ci -COPY ./cli/ . -FROM ghcr.io/immich-app/base-server-prod:20240130@sha256:ce23a32154540b906df3c971766bcd991561c60331794e0ebb780947ac48113f +COPY cli . +RUN npm run build -VOLUME /usr/src/app/upload +WORKDIR /import -EXPOSE 3001 - -ENTRYPOINT ["tini", "--", "/bin/sh"] +ENTRYPOINT ["node", "/usr/src/app/dist"] \ No newline at end of file diff --git a/cli/LICENSE b/cli/LICENSE index a72f398805..94ae15a0fc 100644 --- a/cli/LICENSE +++ b/cli/LICENSE @@ -1,21 +1,620 @@ -MIT License + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2022 Hau Tran + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/cli/package-lock.json b/cli/package-lock.json index 9c218f78b9..39425c37d9 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,67 +1,72 @@ { "name": "@immich/cli", - "version": "2.0.6", + "version": "2.0.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.0.6", - "license": "MIT", - "dependencies": { - "@immich/sdk": "file:../open-api/typescript-sdk", - "axios": "^1.6.7", - "byte-size": "^8.1.1", - "cli-progress": "^3.12.0", - "commander": "^11.0.0", - "form-data": "^4.0.0", - "glob": "^10.3.1", - "yaml": "^2.3.1" - }, + "version": "2.0.8", + "license": "GNU Affero General Public License version 3", "bin": { - "immich": "dist/src/index.js" + "immich": "dist/index.js" }, "devDependencies": { - "@testcontainers/postgresql": "^10.4.0", + "@immich/sdk": "file:../open-api/typescript-sdk", + "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^1.2.2", + "byte-size": "^8.1.1", + "cli-progress": "^3.12.0", + "commander": "^12.0.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", + "glob": "^10.3.1", "immich": "file:../server", "mock-fs": "^5.2.0", - "ts-node": "^10.9.1", - "tslib": "^2.5.3", "typescript": "^5.3.3", - "vitest": "^1.2.1" + "vite": "^5.0.12", + "vitest": "^1.2.2", + "yaml": "^2.3.1" + }, + "engines": { + "node": ">=20.0.0" } }, "../open-api/typescript-sdk": { "name": "@immich/sdk", "version": "1.92.1", - "license": "MIT", - "dependencies": { - "axios": "^1.6.7" - }, + "dev": true, + "license": "GNU Affero General Public License version 3", "devDependencies": { + "@oazapfts/runtime": "^1.0.0", "@types/node": "^20.11.0", "typescript": "^5.3.3" + }, + "peerDependencies": { + "axios": "^1.6.7" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } } }, "../server": { "name": "immich", - "version": "1.93.3", + "version": "1.94.1", "dev": true, - "license": "UNLICENSED", + "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.3", + "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -76,9 +81,9 @@ "@types/picomatch": "^2.3.3", "archiver": "^6.0.0", "async-lock": "^1.4.0", - "axios": "^1.5.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", + "chokidar": "^3.5.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", @@ -97,7 +102,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^3.0.1", + "picomatch": "^4.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -118,7 +123,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.11", + "@types/jest": "29.5.12", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -129,12 +134,11 @@ "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", - "chokidar": "^3.5.3", "dotenv": "^16.3.1", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -149,7 +153,7 @@ "ts-loader": "^9.4.4", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.2.2", + "typescript": "^5.3.3", "utimes": "^5.2.1" } }, @@ -400,28 +404,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -887,6 +869,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -903,6 +886,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -913,12 +897,14 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -935,6 +921,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1053,6 +1040,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1245,265 +1233,15 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@swc/core": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", - "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "peer": true, - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.107", - "@swc/core-darwin-x64": "1.3.107", - "@swc/core-linux-arm-gnueabihf": "1.3.107", - "@swc/core-linux-arm64-gnu": "1.3.107", - "@swc/core-linux-arm64-musl": "1.3.107", - "@swc/core-linux-x64-gnu": "1.3.107", - "@swc/core-linux-x64-musl": "1.3.107", - "@swc/core-win32-arm64-msvc": "1.3.107", - "@swc/core-win32-ia32-msvc": "1.3.107", - "@swc/core-win32-x64-msvc": "1.3.107" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz", - "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz", - "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz", - "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz", - "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz", - "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz", - "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz", - "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz", - "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz", - "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz", - "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@testcontainers/postgresql": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.6.0.tgz", - "integrity": "sha512-gHYpsXkVLpCkJ0jg7xE5n8pogTNvw2LyhkXeJm04raH1yz020jcHAB2a1tRW1J2D8mM8WhFH+xH6pzH2ZggFdg==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", + "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", "dev": true, "dependencies": { - "testcontainers": "^10.6.0" + "testcontainers": "^10.7.1" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, "node_modules/@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -1552,9 +1290,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/mock-fs": { @@ -1567,9 +1305,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1582,9 +1320,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/ssh2": { @@ -1615,16 +1353,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1649,140 +1387,16 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -1801,14 +1415,14 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1818,96 +1432,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1927,27 +1459,10 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1957,14 +1472,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1985,18 +1500,42 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -2010,13 +1549,13 @@ "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2027,30 +1566,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -2239,6 +1754,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2247,6 +1763,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2346,12 +1863,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2397,21 +1908,6 @@ "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==", "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -2421,7 +1917,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -2585,6 +2082,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", + "dev": true, "engines": { "node": ">=12.17" } @@ -2679,6 +2177,21 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, + "node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -2704,6 +2217,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, "dependencies": { "string-width": "^4.2.3" }, @@ -2715,6 +2229,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2725,25 +2240,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/compress-commons": { @@ -2832,16 +2338,11 @@ "node": ">= 10" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2886,23 +2387,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2992,7 +2476,8 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/electron-to-chromium": { "version": "1.4.616", @@ -3003,7 +2488,8 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -3180,9 +2666,9 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -3212,21 +2698,6 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/ci-info": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -3351,9 +2822,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -3459,29 +2930,11 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -3493,19 +2946,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3563,6 +3003,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -3596,6 +3037,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3604,6 +3046,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3823,6 +3266,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -3866,7 +3310,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -3922,6 +3367,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -4141,6 +3587,15 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -4179,12 +3634,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4213,25 +3662,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4257,6 +3687,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -4510,6 +3941,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -4524,6 +3956,7 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -4535,14 +3968,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4606,9 +4031,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -4735,11 +4160,6 @@ "url": "https://github.com/steveukx/properties?sponsor=1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -5165,6 +4585,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5176,6 +4597,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -5190,6 +4612,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -5335,6 +4758,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5349,6 +4773,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5362,6 +4787,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5374,6 +4800,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5530,9 +4957,9 @@ } }, "node_modules/testcontainers": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.6.0.tgz", - "integrity": "sha512-FDJ3o3J8IMu1V7Uc6lNZ2MAD8+BV4HdpR/Vf5mHtgYHKdn6k1EbGFwtnvVNOxanJ99FCjf/EU8eA5ZQ4yjlsGA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", + "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", @@ -5633,49 +5060,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -5791,12 +5175,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -5822,13 +5200,13 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz", + "integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" }, "bin": { @@ -6106,6 +5484,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6136,6 +5515,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -6153,6 +5533,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6169,6 +5550,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -6180,6 +5562,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -6190,12 +5573,14 @@ "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -6212,6 +5597,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6232,17 +5618,9 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "engines": { - "node": ">=6" + "node": ">= 14" } }, "node_modules/yocto-queue": { @@ -6508,27 +5886,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, "@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -6754,8 +6111,8 @@ "@immich/sdk": { "version": "file:../open-api/typescript-sdk", "requires": { + "@oazapfts/runtime": "^1.0.0", "@types/node": "^20.11.0", - "axios": "^1.6.7", "typescript": "^5.3.3" } }, @@ -6763,6 +6120,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "requires": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -6775,17 +6133,20 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -6796,6 +6157,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -6886,6 +6248,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true }, "@pkgr/core": { @@ -6991,157 +6354,15 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "@swc/core": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", - "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@swc/core-darwin-arm64": "1.3.107", - "@swc/core-darwin-x64": "1.3.107", - "@swc/core-linux-arm-gnueabihf": "1.3.107", - "@swc/core-linux-arm64-gnu": "1.3.107", - "@swc/core-linux-arm64-musl": "1.3.107", - "@swc/core-linux-x64-gnu": "1.3.107", - "@swc/core-linux-x64-musl": "1.3.107", - "@swc/core-win32-arm64-msvc": "1.3.107", - "@swc/core-win32-ia32-msvc": "1.3.107", - "@swc/core-win32-x64-msvc": "1.3.107", - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - } - }, - "@swc/core-darwin-arm64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz", - "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-darwin-x64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz", - "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-linux-arm-gnueabihf": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz", - "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-linux-arm64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz", - "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-linux-arm64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz", - "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-linux-x64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz", - "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-linux-x64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz", - "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-win32-arm64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz", - "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-win32-ia32-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz", - "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/core-win32-x64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz", - "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true, - "optional": true, - "peer": true - }, - "@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true, - "optional": true, - "peer": true - }, "@testcontainers/postgresql": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.6.0.tgz", - "integrity": "sha512-gHYpsXkVLpCkJ0jg7xE5n8pogTNvw2LyhkXeJm04raH1yz020jcHAB2a1tRW1J2D8mM8WhFH+xH6pzH2ZggFdg==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", + "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", "dev": true, "requires": { - "testcontainers": "^10.6.0" + "testcontainers": "^10.7.1" } }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, "@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -7190,9 +6411,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/mock-fs": { @@ -7205,9 +6426,9 @@ } }, "@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -7220,9 +6441,9 @@ "dev": true }, "@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "@types/ssh2": { @@ -7255,156 +6476,81 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - } - }, - "@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - } - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } } }, "@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - } - }, - "@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - } - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -7425,93 +6571,29 @@ } } }, - "@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - } - }, - "@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - } - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" } }, "@ungap/structured-clone": { @@ -7657,12 +6739,14 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -7746,12 +6830,6 @@ } } }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7791,21 +6869,6 @@ "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==", "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", - "requires": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -7815,7 +6878,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -7912,7 +6976,8 @@ "byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==" + "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", + "dev": true }, "cac": { "version": "6.7.14", @@ -7972,6 +7037,12 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, + "ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true + }, "clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -7993,6 +7064,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, "requires": { "string-width": "^4.2.3" } @@ -8001,6 +7073,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -8008,20 +7081,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true }, "compress-commons": { "version": "4.1.2", @@ -8089,16 +7156,11 @@ "readable-stream": "^3.4.0" } }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8129,17 +7191,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -8213,7 +7264,8 @@ "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "electron-to-chromium": { "version": "1.4.616", @@ -8224,7 +7276,8 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "end-of-stream": { "version": "1.4.4", @@ -8369,9 +7422,9 @@ } }, "eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.22.20", @@ -8390,14 +7443,6 @@ "regjsparser": "^0.10.0", "semver": "^7.5.4", "strip-indent": "^3.0.0" - }, - "dependencies": { - "ci-info": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", - "dev": true - } } }, "eslint-visitor-keys": { @@ -8476,9 +7521,9 @@ "dev": true }, "fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -8565,30 +7610,16 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" - }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, "requires": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8630,6 +7661,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "requires": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -8642,6 +7674,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -8650,6 +7683,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "requires": { "brace-expansion": "^2.0.1" } @@ -8743,7 +7777,7 @@ "version": "file:../server", "requires": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.3", + "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/cli": "^10.1.16", "@nestjs/common": "^10.2.2", @@ -8766,7 +7800,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.11", + "@types/jest": "29.5.12", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -8780,7 +7814,6 @@ "@typescript-eslint/parser": "^6.4.1", "archiver": "^6.0.0", "async-lock": "^1.4.0", - "axios": "^1.5.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", "chokidar": "^3.5.3", @@ -8791,7 +7824,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", "exiftool-vendored": "~24.4.0", "exiftool-vendored.pl": "12.73", "fluent-ffmpeg": "^2.1.2", @@ -8810,7 +7843,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^3.0.1", + "picomatch": "^4.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^3.2.3", "reflect-metadata": "^0.1.13", @@ -8828,7 +7861,7 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typeorm": "^0.3.17", - "typescript": "^5.2.2", + "typescript": "^5.3.3", "ua-parser-js": "^1.0.35", "utimes": "^5.2.1" } @@ -8904,7 +7937,8 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-glob": { "version": "4.0.3", @@ -8936,7 +7970,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "istanbul-lib-coverage": { "version": "3.2.2", @@ -8980,6 +8015,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -9166,6 +8202,12 @@ "get-func-name": "^2.0.1" } }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + }, "magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -9195,12 +8237,6 @@ "semver": "^7.5.3" } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9223,19 +8259,6 @@ "picomatch": "^2.3.1" } }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9254,7 +8277,8 @@ "minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true }, "mkdirp": { "version": "1.0.4", @@ -9435,7 +8459,8 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "path-parse": { "version": "1.0.7", @@ -9447,16 +8472,10 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, "requires": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==" - } } }, "path-type": { @@ -9507,9 +8526,9 @@ "dev": true }, "postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "requires": { "nanoid": "^3.3.7", @@ -9592,11 +8611,6 @@ "mkdirp": "^1.0.4" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9898,6 +8912,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -9905,7 +8920,8 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "siginfo": { "version": "2.0.0", @@ -9916,7 +8932,8 @@ "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true }, "slash": { "version": "3.0.0", @@ -10043,6 +9060,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10053,6 +9071,7 @@ "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10063,6 +9082,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -10071,6 +9091,7 @@ "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -10189,9 +9210,9 @@ } }, "testcontainers": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.6.0.tgz", - "integrity": "sha512-FDJ3o3J8IMu1V7Uc6lNZ2MAD8+BV4HdpR/Vf5mHtgYHKdn6k1EbGFwtnvVNOxanJ99FCjf/EU8eA5ZQ4yjlsGA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", + "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", @@ -10272,27 +9293,6 @@ "dev": true, "requires": {} }, - "ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10369,12 +9369,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -10397,14 +9391,14 @@ } }, "vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz", + "integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==", "dev": true, "requires": { "esbuild": "^0.19.3", "fsevents": "~2.3.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" } }, @@ -10543,6 +9537,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -10561,6 +9556,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "requires": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -10570,22 +9566,26 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -10596,6 +9596,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -10606,6 +9607,7 @@ "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -10621,12 +9623,7 @@ "yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true }, "yocto-queue": { diff --git a/cli/package.json b/cli/package.json index 9e32061e3d..c5a90057c0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,49 +1,44 @@ { "name": "@immich/cli", - "version": "2.0.6", + "version": "2.0.8", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", "bin": { - "immich": "./dist/src/index.js" + "immich": "dist/index.js" }, - "license": "MIT", + "license": "GNU Affero General Public License version 3", "keywords": [ "immich", "cli" ], - "dependencies": { - "@immich/sdk": "file:../open-api/typescript-sdk", - "axios": "^1.6.7", - "byte-size": "^8.1.1", - "cli-progress": "^3.12.0", - "commander": "^11.0.0", - "form-data": "^4.0.0", - "glob": "^10.3.1", - "yaml": "^2.3.1" - }, "devDependencies": { - "@testcontainers/postgresql": "^10.4.0", + "@immich/sdk": "file:../open-api/typescript-sdk", + "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^6.4.1", - "@typescript-eslint/parser": "^6.4.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^1.2.2", + "byte-size": "^8.1.1", + "cli-progress": "^3.12.0", + "commander": "^12.0.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", + "glob": "^10.3.1", "immich": "file:../server", "mock-fs": "^5.2.0", - "ts-node": "^10.9.1", - "tslib": "^2.5.3", "typescript": "^5.3.3", - "vitest": "^1.2.1" + "vite": "^5.0.12", + "vitest": "^1.2.2", + "yaml": "^2.3.1" }, "scripts": { - "build": "tsc --project tsconfig.build.json", + "build": "vite build", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0", "lint:fix": "npm run lint -- --fix", "prepack": "npm run build", @@ -56,7 +51,10 @@ }, "repository": { "type": "git", - "url": "github:immich-app/immich", + "url": "git+https://github.com/immich-app/immich.git", "directory": "cli" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/cli/src/commands/base-command.ts b/cli/src/commands/base-command.ts index 9218da6026..cc76f4a7ea 100644 --- a/cli/src/commands/base-command.ts +++ b/cli/src/commands/base-command.ts @@ -1,21 +1,20 @@ import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk'; -import { ImmichApi } from '../services/api.service'; import { SessionService } from '../services/session.service'; +import { ImmichApi } from 'src/services/api.service'; export abstract class BaseCommand { protected sessionService!: SessionService; - protected immichApi!: ImmichApi; protected user!: UserResponseDto; protected serverVersion!: ServerVersionResponseDto; - constructor(options: { config?: string }) { - if (!options.config) { + constructor(options: { configDirectory?: string }) { + if (!options.configDirectory) { throw new Error('Config directory is required'); } - this.sessionService = new SessionService(options.config); + this.sessionService = new SessionService(options.configDirectory); } - public async connect(): Promise { - this.immichApi = await this.sessionService.connect(); + public async connect(): Promise { + return await this.sessionService.connect(); } } diff --git a/cli/src/commands/login.ts b/cli/src/commands/login.command.ts similarity index 100% rename from cli/src/commands/login.ts rename to cli/src/commands/login.command.ts diff --git a/cli/src/commands/server-info.command.ts b/cli/src/commands/server-info.command.ts index c9d454d59e..c6029b1306 100644 --- a/cli/src/commands/server-info.command.ts +++ b/cli/src/commands/server-info.command.ts @@ -2,10 +2,10 @@ import { BaseCommand } from './base-command'; export class ServerInfoCommand extends BaseCommand { public async run() { - await this.connect(); - const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion(); - const { data: mediaTypes } = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); - const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics(); + const api = await this.connect(); + const versionInfo = await api.getServerVersion(); + const mediaTypes = await api.getSupportedMediaTypes(); + const statistics = await api.getAssetStatistics(); console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`); diff --git a/cli/src/commands/upload.command.ts b/cli/src/commands/upload.command.ts index f026e374df..f26f297a29 100644 --- a/cli/src/commands/upload.command.ts +++ b/cli/src/commands/upload.command.ts @@ -1,22 +1,21 @@ -import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import byteSize from 'byte-size'; import cliProgress from 'cli-progress'; -import FormData from 'form-data'; -import fs, { ReadStream, createReadStream } from 'node:fs'; +import fs, { createReadStream } from 'node:fs'; import { CrawlService } from '../services/crawl.service'; import { BaseCommand } from './base-command'; import { basename } from 'node:path'; import { access, constants, stat, unlink } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import os from 'node:os'; +import { ImmichApi } from 'src/services/api.service'; class Asset { readonly path: string; readonly deviceId!: string; deviceAssetId?: string; - fileCreatedAt?: string; - fileModifiedAt?: string; + fileCreatedAt?: Date; + fileModifiedAt?: Date; sidecarPath?: string; fileSize!: number; albumName?: string; @@ -28,8 +27,8 @@ class Asset { async prepare() { const stats = await stat(this.path); this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, ''); - this.fileCreatedAt = stats.mtime.toISOString(); - this.fileModifiedAt = stats.mtime.toISOString(); + this.fileCreatedAt = stats.mtime; + this.fileModifiedAt = stats.mtime; this.fileSize = stats.size; this.albumName = this.extractAlbumName(); } @@ -47,14 +46,14 @@ class Asset { // TODO: doesn't xmp replace the file extension? Will need investigation const sideCarPath = `${this.path}.xmp`; - let sidecarData: ReadStream | undefined = undefined; + let sidecarData: Blob | undefined = undefined; try { await access(sideCarPath, constants.R_OK); - sidecarData = createReadStream(sideCarPath); + sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath)); } catch {} const data: any = { - assetData: createReadStream(this.path), + assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)), deviceAssetId: this.deviceAssetId, deviceId: 'CLI', fileCreatedAt: this.fileCreatedAt, @@ -112,10 +111,10 @@ export class UploadCommand extends BaseCommand { uploadLength!: number; public async run(paths: string[], options: UploadOptionsDto): Promise { - await this.connect(); + const api = await this.connect(); - const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); - const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video); + const formatResponse = await api.getSupportedMediaTypes(); + const crawlService = new CrawlService(formatResponse.image, formatResponse.video); const inputFiles: string[] = []; for (const pathArgument of paths) { @@ -164,7 +163,7 @@ export class UploadCommand extends BaseCommand { } } - const { data: existingAlbums } = await this.immichApi.albumApi.getAllAlbums(); + const existingAlbums = await api.getAllAlbums(); uploadProgress.start(totalSize, 0); uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); @@ -183,15 +182,13 @@ export class UploadCommand extends BaseCommand { if (!options.skipHash) { const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] }; - const checkResponse = await this.immichApi.assetApi.checkBulkUpload({ - assetBulkUploadCheckDto, - }); + const checkResponse = await api.checkBulkUpload(assetBulkUploadCheckDto); - skipUpload = checkResponse.data.results[0].action === 'reject'; + skipUpload = checkResponse.results[0].action === 'reject'; - const isDuplicate = checkResponse.data.results[0].reason === 'duplicate'; + const isDuplicate = checkResponse.results[0].reason === 'duplicate'; if (isDuplicate) { - existingAssetId = checkResponse.data.results[0].assetId; + existingAssetId = checkResponse.results[0].assetId; } skipAsset = skipUpload && !isDuplicate; @@ -200,8 +197,9 @@ export class UploadCommand extends BaseCommand { if (!skipAsset && !options.dryRun) { if (!skipUpload) { const formData = await asset.getUploadFormData(); - const { data } = await this.uploadAsset(formData); - existingAssetId = data.id; + const response = await this.uploadAsset(api, formData); + const json = await response.json(); + existingAssetId = json.id; uploadCounter++; totalSizeUploaded += asset.fileSize; } @@ -209,17 +207,14 @@ export class UploadCommand extends BaseCommand { if ((options.album || options.albumName) && asset.albumName !== undefined) { let album = existingAlbums.find((album) => album.albumName === asset.albumName); if (!album) { - const { data } = await this.immichApi.albumApi.createAlbum({ - createAlbumDto: { albumName: asset.albumName }, - }); - album = data; + const response = await api.createAlbum({ albumName: asset.albumName }); + album = response; existingAlbums.push(album); } if (existingAssetId) { - await this.immichApi.albumApi.addAssetsToAlbum({ - id: album.id, - bulkIdsDto: { ids: [existingAssetId] }, + await api.addAssetsToAlbum(album.id, { + ids: [existingAssetId], }); } } @@ -260,22 +255,20 @@ export class UploadCommand extends BaseCommand { } } - private async uploadAsset(data: FormData): Promise { - const url = this.immichApi.instanceUrl + '/asset/upload'; + private async uploadAsset(api: ImmichApi, data: FormData): Promise { + const url = api.instanceUrl + '/asset/upload'; - const config: AxiosRequestConfig = { + const response = await fetch(url, { method: 'post', - maxRedirects: 0, - url, + redirect: 'error', headers: { - 'x-api-key': this.immichApi.apiKey, - ...data.getHeaders(), + 'x-api-key': api.apiKey, }, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY, - data, - }; - - return axios(config); + body: data, + }); + if (response.status !== 200 && response.status !== 201) { + throw new Error(await response.text()); + } + return response; } } diff --git a/cli/src/index.ts b/cli/src/index.ts index 6582b37956..eead2b0272 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -3,19 +3,22 @@ import { Command, Option } from 'commander'; import path from 'node:path'; import os from 'node:os'; import { version } from '../package.json'; -import { LoginCommand } from './commands/login'; +import { LoginCommand } from './commands/login.command'; import { LogoutCommand } from './commands/logout.command'; import { ServerInfoCommand } from './commands/server-info.command'; import { UploadCommand } from './commands/upload.command'; -const homeDirectory = os.homedir(); -const configDirectory = path.join(homeDirectory, '.config/immich/'); +const defaultConfigDirectory = path.join(os.homedir(), '.config/immich/'); const program = new Command() .name('immich') .version(version) .description('Command line interface for Immich') - .addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDirectory)); + .addOption( + new Option('-d, --config-directory', 'Configuration directory where auth.yml will be stored') + .env('IMMICH_CONFIG_DIR') + .default(defaultConfigDirectory), + ); program .command('upload') @@ -24,7 +27,7 @@ program .addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false)) .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS')) .addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false)) - .addOption(new Option('-i, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false)) + .addOption(new Option('-H, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false)) .addOption( new Option('-a, --album', 'Automatically create albums based on folder name') .env('IMMICH_AUTO_CREATE_ALBUM') diff --git a/cli/src/services/api.service.ts b/cli/src/services/api.service.ts index 8110d92d29..ef76a75441 100644 --- a/cli/src/services/api.service.ts +++ b/cli/src/services/api.service.ts @@ -1,57 +1,106 @@ import { - AlbumApi, - APIKeyApi, - AssetApi, - AuthenticationApi, - Configuration, - JobApi, - OAuthApi, - ServerInfoApi, - SystemConfigApi, - UserApi, + addAssetsToAlbum, + checkBulkUpload, + createAlbum, + createApiKey, + getAllAlbums, + getAllAssets, + getAssetStatistics, + getMyUserInfo, + getServerVersion, + getSupportedMediaTypes, + login, + pingServer, + signUpAdmin, + uploadFile, + ApiKeyCreateDto, + AssetBulkUploadCheckDto, + BulkIdsDto, + CreateAlbumDto, + CreateAssetDto, + LoginCredentialDto, + SignUpDto, } from '@immich/sdk'; -import FormData from 'form-data'; +/** + * Wraps the underlying API to abstract away the options and make API calls mockable for testing. + */ export class ImmichApi { - public userApi: UserApi; - public albumApi: AlbumApi; - public assetApi: AssetApi; - public authenticationApi: AuthenticationApi; - public oauthApi: OAuthApi; - public serverInfoApi: ServerInfoApi; - public jobApi: JobApi; - public keyApi: APIKeyApi; - public systemConfigApi: SystemConfigApi; - - private readonly config; + private readonly options; constructor( public instanceUrl: string, public apiKey: string, ) { - this.config = new Configuration({ - basePath: instanceUrl, - baseOptions: { - headers: { - 'x-api-key': apiKey, - }, + this.options = { + baseUrl: instanceUrl, + headers: { + 'x-api-key': apiKey, }, - formDataCtor: FormData, - }); - - this.userApi = new UserApi(this.config); - this.albumApi = new AlbumApi(this.config); - this.assetApi = new AssetApi(this.config); - this.authenticationApi = new AuthenticationApi(this.config); - this.oauthApi = new OAuthApi(this.config); - this.serverInfoApi = new ServerInfoApi(this.config); - this.jobApi = new JobApi(this.config); - this.keyApi = new APIKeyApi(this.config); - this.systemConfigApi = new SystemConfigApi(this.config); + }; } setApiKey(apiKey: string) { this.apiKey = apiKey; - this.config.baseOptions.headers['x-api-key'] = apiKey; + if (!this.options.headers) { + throw new Error('missing headers'); + } + this.options.headers['x-api-key'] = apiKey; + } + + addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto) { + return addAssetsToAlbum({ id, bulkIdsDto }, this.options); + } + + checkBulkUpload(assetBulkUploadCheckDto: AssetBulkUploadCheckDto) { + return checkBulkUpload({ assetBulkUploadCheckDto }, this.options); + } + + createAlbum(createAlbumDto: CreateAlbumDto) { + return createAlbum({ createAlbumDto }, this.options); + } + + createApiKey(apiKeyCreateDto: ApiKeyCreateDto, options: { headers: { Authorization: string } }) { + return createApiKey({ apiKeyCreateDto }, { ...this.options, ...options }); + } + + getAllAlbums() { + return getAllAlbums({}, this.options); + } + + getAllAssets() { + return getAllAssets({}, this.options); + } + + getAssetStatistics() { + return getAssetStatistics({}, this.options); + } + + getMyUserInfo() { + return getMyUserInfo(this.options); + } + + getServerVersion() { + return getServerVersion(this.options); + } + + getSupportedMediaTypes() { + return getSupportedMediaTypes(this.options); + } + + login(loginCredentialDto: LoginCredentialDto) { + return login({ loginCredentialDto }, this.options); + } + + pingServer() { + return pingServer(this.options); + } + + signUpAdmin(signUpDto: SignUpDto) { + return signUpAdmin({ signUpDto }, this.options); + } + + uploadFile(createAssetDto: CreateAssetDto) { + return uploadFile({ createAssetDto }, this.options); } } diff --git a/cli/src/services/session.service.spec.ts b/cli/src/services/session.service.spec.ts index dbb3d7201b..f6ef709bfc 100644 --- a/cli/src/services/session.service.spec.ts +++ b/cli/src/services/session.service.spec.ts @@ -12,18 +12,20 @@ import { spyOnConsole, } from '../../test/cli-test-utils'; -const mockPingServer = vi.fn(() => Promise.resolve({ data: { res: 'pong' } })); -const mockUserInfo = vi.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } })); +const mocks = vi.hoisted(() => { + return { + getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })), + pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })), + }; +}); -vi.mock('@immich/sdk', async () => ({ - ...(await vi.importActual('@immich/sdk')), - UserApi: vi.fn().mockImplementation(() => { - return { getMyUserInfo: mockUserInfo }; - }), - ServerInfoApi: vi.fn().mockImplementation(() => { - return { pingServer: mockPingServer }; - }), -})); +vi.mock('./api.service', async (importOriginal) => { + const module = await importOriginal(); + // @ts-expect-error this is only a partial implementation of the return value + module.ImmichApi.prototype.getMyUserInfo = mocks.getMyUserInfo; + module.ImmichApi.prototype.pingServer = mocks.pingServer; + return module; +}); describe('SessionService', () => { let sessionService: SessionService; @@ -46,7 +48,7 @@ describe('SessionService', () => { ); await sessionService.connect(); - expect(mockPingServer).toHaveBeenCalledTimes(1); + expect(mocks.pingServer).toHaveBeenCalledTimes(1); }); it('should error if no auth file exists', async () => { diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts index 9276a47210..c9eae671fd 100644 --- a/cli/src/services/session.service.ts +++ b/cli/src/services/session.service.ts @@ -3,7 +3,6 @@ import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/p import path from 'node:path'; import yaml from 'yaml'; import { ImmichApi } from './api.service'; - class LoginError extends Error { constructor(message: string) { super(message); @@ -51,12 +50,12 @@ export class SessionService { const api = new ImmichApi(instanceUrl, apiKey); - const { data: pingResponse } = await api.serverInfoApi.pingServer().catch((error) => { - throw new Error(`Failed to connect to server ${api.instanceUrl}: ${error.message}`); + const pingResponse = await api.pingServer().catch((error) => { + throw new Error(`Failed to connect to server ${instanceUrl}: ${error.message}`, error); }); if (pingResponse.res !== 'pong') { - throw new Error(`Could not parse response. Is Immich listening on ${api.instanceUrl}?`); + throw new Error(`Could not parse response. Is Immich listening on ${instanceUrl}?`); } return api; @@ -68,7 +67,7 @@ export class SessionService { const api = new ImmichApi(instanceUrl, apiKey); // Check if server and api key are valid - const { data: userInfo } = await api.userApi.getMyUserInfo().catch((error) => { + const userInfo = await api.getMyUserInfo().catch((error) => { throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`); }); @@ -82,7 +81,7 @@ export class SessionService { } } - await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey })); + await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 }); console.log('Wrote auth info to ' + this.authPath); diff --git a/cli/test/cli-test-utils.ts b/cli/test/cli-test-utils.ts index 3063e490f4..cc3e29d27d 100644 --- a/cli/test/cli-test-utils.ts +++ b/cli/test/cli-test-utils.ts @@ -1,24 +1,20 @@ import fs from 'node:fs'; import path from 'node:path'; -import { ImmichApi } from '../src/services/api.service'; +import { ImmichApi } from 'src/services/api.service'; export const TEST_CONFIG_DIR = '/tmp/immich/'; export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml'); export const TEST_IMMICH_INSTANCE_URL = 'https://test/api'; export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg'; -export const CLI_BASE_OPTIONS = { config: TEST_CONFIG_DIR }; +export const CLI_BASE_OPTIONS = { configDirectory: TEST_CONFIG_DIR }; export const setup = async () => { const api = new ImmichApi(process.env.IMMICH_INSTANCE_URL as string, ''); - await api.authenticationApi.signUpAdmin({ - signUpDto: { email: 'cli@immich.app', password: 'password', name: 'Administrator' }, - }); - const { data: admin } = await api.authenticationApi.login({ - loginCredentialDto: { email: 'cli@immich.app', password: 'password' }, - }); - const { data: apiKey } = await api.keyApi.createApiKey( - { aPIKeyCreateDto: { name: 'CLI Test' } }, + await api.signUpAdmin({ email: 'cli@immich.app', password: 'password', name: 'Administrator' }); + const admin = await api.login({ email: 'cli@immich.app', password: 'password' }); + const apiKey = await api.createApiKey( + { name: 'CLI Test' }, { headers: { Authorization: `Bearer ${admin.accessToken}` } }, ); diff --git a/cli/test/e2e/login-key.e2e-spec.ts b/cli/test/e2e/login-key.e2e-spec.ts index d1e7f780e3..acbce5c244 100644 --- a/cli/test/e2e/login-key.e2e-spec.ts +++ b/cli/test/e2e/login-key.e2e-spec.ts @@ -1,6 +1,8 @@ import { restoreTempFolder, testApp } from '@test-utils'; -import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils'; -import { LoginCommand } from '../../src/commands/login'; +import { CLI_BASE_OPTIONS, TEST_AUTH_FILE, deleteAuthFile, setup, spyOnConsole } from 'test/cli-test-utils'; +import { readFile, stat } from 'node:fs/promises'; +import { LoginCommand } from '../../src/commands/login.command'; +import yaml from 'yaml'; describe(`login-key (e2e)`, () => { let apiKey: string; @@ -20,6 +22,7 @@ describe(`login-key (e2e)`, () => { afterAll(async () => { await testApp.teardown(); await restoreTempFolder(); + deleteAuthFile(); }); beforeEach(async () => { @@ -28,15 +31,35 @@ describe(`login-key (e2e)`, () => { const api = await setup(); apiKey = api.apiKey; + + deleteAuthFile(); }); it('should error when providing an invalid API key', async () => { await expect(new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow( - `Failed to connect to server ${instanceUrl}: Request failed with status code 401`, + `Failed to connect to server ${instanceUrl}: Error: 401`, ); }); it('should log in when providing the correct API key', async () => { await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey); }); + + it('should create an auth file when logging in', async () => { + await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey); + + const data: string = await readFile(TEST_AUTH_FILE, 'utf8'); + const parsedConfig = yaml.parse(data); + + expect(parsedConfig).toEqual(expect.objectContaining({ instanceUrl, apiKey })); + }); + + it('should create an auth file with chmod 600', async () => { + await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey); + + const stats = await stat(TEST_AUTH_FILE); + const mode = (stats.mode & 0o777).toString(8); + + expect(mode).toEqual('600'); + }); }); diff --git a/cli/test/e2e/setup.ts b/cli/test/e2e/setup.ts index 52b2ae082c..fb1d939eba 100644 --- a/cli/test/e2e/setup.ts +++ b/cli/test/e2e/setup.ts @@ -25,7 +25,7 @@ export default async () => { if (process.env.DB_HOSTNAME === undefined) { // DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container. - const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.1.11') + const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') .withExposedPorts(5432) .withDatabase('immich') .withUsername('postgres') diff --git a/cli/test/e2e/upload.e2e-spec.ts b/cli/test/e2e/upload.e2e-spec.ts index e2e38b9d85..81b20ad749 100644 --- a/cli/test/e2e/upload.e2e-spec.ts +++ b/cli/test/e2e/upload.e2e-spec.ts @@ -1,7 +1,7 @@ import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test-utils'; import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils'; import { UploadCommand } from '../../src/commands/upload.command'; -import { ImmichApi } from '../../src/services/api.service'; +import { ImmichApi } from 'src/services/api.service'; describe(`upload (e2e)`, () => { let api: ImmichApi; @@ -26,13 +26,13 @@ describe(`upload (e2e)`, () => { it('should upload a folder recursively', async () => { await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true }); - const { data: assets } = await api.assetApi.getAllAssets({}, { headers: { 'x-api-key': api.apiKey } }); + const assets = await api.getAllAssets(); expect(assets.length).toBeGreaterThan(4); }); it('should not create a new album', async () => { await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true }); - const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); + const albums = await api.getAllAlbums(); expect(albums.length).toEqual(0); }); @@ -42,7 +42,7 @@ describe(`upload (e2e)`, () => { album: true, }); - const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); + const albums = await api.getAllAlbums(); expect(albums.length).toEqual(1); const natureAlbum = albums[0]; expect(natureAlbum.albumName).toEqual('nature'); @@ -59,7 +59,7 @@ describe(`upload (e2e)`, () => { album: true, }); - const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); + const albums = await api.getAllAlbums(); expect(albums.length).toEqual(1); const natureAlbum = albums[0]; expect(natureAlbum.albumName).toEqual('nature'); @@ -71,7 +71,7 @@ describe(`upload (e2e)`, () => { albumName: 'testAlbum', }); - const { data: albums } = await api.albumApi.getAllAlbums({}, { headers: { 'x-api-key': api.apiKey } }); + const albums = await api.getAllAlbums(); expect(albums.length).toEqual(1); const testAlbum = albums[0]; expect(testAlbum.albumName).toEqual('testAlbum'); diff --git a/cli/tsconfig.build.json b/cli/tsconfig.build.json deleted file mode 100644 index 0d7cd0873c..0000000000 --- a/cli/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["dist", "node_modules", "upload", "test", "**/*spec.ts"] -} diff --git a/cli/vite.config.ts b/cli/vite.config.ts new file mode 100644 index 0000000000..89ee3a3d3e --- /dev/null +++ b/cli/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + rollupOptions: { + input: 'src/index.ts', + output: { + dir: 'dist', + }, + }, + ssr: true, + }, + ssr: { + // bundle everything except for Node built-ins + noExternal: /^(?!node:).*$/, + }, +}); diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 5290e990e2..188b8c1234 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -103,7 +103,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 env_file: - .env environment: diff --git a/docker/docker-compose.e2e.yml b/docker/docker-compose.e2e.yml new file mode 100644 index 0000000000..5584335966 --- /dev/null +++ b/docker/docker-compose.e2e.yml @@ -0,0 +1,49 @@ +version: "3.8" + +name: immich-e2e + +x-server-build: &server-common + image: immich-server:latest + build: + context: ../ + dockerfile: server/Dockerfile + environment: + - DB_HOSTNAME=database + - DB_USERNAME=postgres + - DB_PASSWORD=postgres + - DB_DATABASE_NAME=immich + - REDIS_HOSTNAME=redis + volumes: + - upload:/usr/src/app/upload + depends_on: + - redis + - database + +services: + immich-server: + command: [ "./start.sh", "immich" ] + <<: *server-common + ports: + - 2283:3001 + + immich-microservices: + command: [ "./start.sh", "microservices" ] + <<: *server-common + + + redis: + image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + restart: always + + database: + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: immich + ports: + - 5432:5432 + +volumes: + model-cache: + upload: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 04215b757b..48a526c4c1 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -61,7 +61,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 env_file: - .env environment: @@ -70,7 +70,8 @@ services: POSTGRES_DB: ${DB_DATABASE_NAME} volumes: - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - restart: always + ports: + - 5432:5432 volumes: model-cache: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a6e6aa26ff..f1d16f7e6b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -31,7 +31,7 @@ services: container_name: immich_microservices image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding - # file: hwaccel.transcoding.yml + # file: hwaccel.transcoding.yml # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding command: [ "start.sh", "microservices" ] volumes: @@ -65,9 +65,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee - env_file: - - .env + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} diff --git a/docs/blog/2023/06-24/update.mdx b/docs/blog/2023/06-24/update.mdx index d4c7149439..cbfdd883f8 100644 --- a/docs/blog/2023/06-24/update.mdx +++ b/docs/blog/2023/06-24/update.mdx @@ -91,7 +91,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full - One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) - [Liberapay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything diff --git a/docs/blog/2023/07-29/update.mdx b/docs/blog/2023/07-29/update.mdx index 9a9934f3f2..2cb84e18c6 100644 --- a/docs/blog/2023/07-29/update.mdx +++ b/docs/blog/2023/07-29/update.mdx @@ -139,7 +139,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full - One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) - [Liberapay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 - Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky. Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 72aaee72d4..1465cc9fd5 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -16,18 +16,18 @@ You can see the list of all users by running [list-users](/docs/administration/s ### What is the difference between the cloud icons on the mobile app? -| Icon | Description | -| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| ![cloud](/img/cloud.svg) | Asset is only available in the cloud and was uploaded from some other device (like the web client) or was deleted from this device after upload | -| ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up | -| ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up to the server; the original file is still on the device | +| Icon | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| ![cloud](/img/cloud.svg) | Asset is only available on the server and was uploaded from some other device (like the web client) or was deleted from this device after upload | +| ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up | +| ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up to the server; the original file is still on the device | ### I cannot log into the application after an update. What can I do? First, verify that the mobile app and server are both running the same version (major and minor). :::note -App store updates sometimes take longer because the stores (play store; Google and app store; Apple) +App store updates sometimes take longer because the stores (Google play store and Apple app store) need to approve the update first which may take some time. ::: @@ -54,7 +54,7 @@ This often happens when using a reverse proxy (such as nginx or Cloudflare tunne ### Why are some photos stored in the file system with the wrong date? -There are a few different scenarios that can lead to this situation. The solution is to simply run the storage migration job again. The job is only _automatically_ run once per asset, after upload. If metadata extraction originally failed, the jobs were cleared/cancelled, etc. the job may not have run automatically the first time. +There are a few different scenarios that can lead to this situation. The solution is to run the storage migration job again. The job is only _automatically_ run once per asset, after upload. If metadata extraction originally failed, the jobs were cleared/cancelled, etc. the job may not have run automatically the first time. ### How can I hide photos from the timeline? @@ -66,11 +66,11 @@ See [Backup and Restore](/docs/administration/backup-and-restore.md). ### Does Immich support reading existing face tag metadata? -No, it currently does not. +No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348). ### Does Immich support filtering of NSFW images? -No, it currently does not, but there is an [open discussion about it On Github](https://github.com/immich-app/immich/discussions/2451). You can submit a pull request or vote for the discussion. +No, it currently does not. There is an [open feature request on Github](https://github.com/immich-app/immich/discussions/2451). ### Why are there so many thumbnail generation jobs? @@ -90,9 +90,13 @@ There are no requirements for assets to be unique across users. If multiple user Immich uses a player with known HDR color display issues. We are experimenting with a different player that provides better color profiles for HDR content for future improvements. +### Why does Immich transcode my videos to a lower quality? + +Immich always keeps your original files. Alongside that, it generates a transcoded version for compatibility and performance reasons. + ### How can I delete transcoded videos without deleting the original? -The transcode of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for an asset (with the _Refresh encoded videos_ button in the asset viewer options. Or, for all assets by running transcoding jobs for all assets. +The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options, or for all assets by running transcoding jobs for all assets from the administration page. To update the transcode policy, navigate to Administration > Video Transcoding Settings > Transcoding Policy and select a policy from the drop-down. This policy will determine whether an existing transcode will be deleted or overwritten in the transcoding job. If a video should be transcoded according to this policy, an existing transcode is overwritten. If not, then it is deleted. @@ -143,19 +147,19 @@ UPDATE assets SET "ownerId" = '' WHERE "ownerId" = '' ### Can I keep my existing album structure while importing assets into Immich? -Yes. You can by use [Immich CLI](/docs/features/command-line-interface) along with the `--album` flag. +Yes, by using the [Immich CLI](/docs/features/command-line-interface) along with the `--album` flag. ### Is there a way to reorder photos within an album? -No, not yet. For updates on this planned feature, follow the [GitHub discussion](https://github.com/immich-app/immich/discussions/1689), +No, not yet. For updates on this planned feature, follow the [GitHub discussion](https://github.com/immich-app/immich/discussions/1689). --- ## External Library -### Can I add an external library while keeping the existing albums structure? +### Can I add an external library while keeping the existing album structure? -We haven't put in an official mechanism to create albums from external libraries at the moment., but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) which you can find here to help you achieve that. +We haven't put in an official mechanism to create albums from external libraries at the moment, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that. ### What happens to duplicates in external libraries? @@ -185,7 +189,7 @@ However, disabling all jobs will not disable the machine learning service itself ### I'm getting errors about models being corrupt or failing to download. What do I do? -You can delete the model cache volume, which is where models are downloaded to. This will give the service a clean environment to download the model again. +You can delete the model cache volume, which is where models are downloaded to. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface](https://huggingface.co/immich-app) and place them in the cache folder. ### Why did Immich decide to remove object detection? @@ -194,7 +198,7 @@ For more info see [here](https://github.com/immich-app/immich/pull/5903) ### Can I use a custom CLIP model? -No, this is not supported. Only models listed in the [Huggingface](https://huggingface.co/immich-app) are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added. +No, this is not supported. Only models listed in the [Huggingface](https://huggingface.co/immich-app) page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added. ### I want to be able to search in other languages besides English. How can I do that? @@ -206,9 +210,8 @@ Feel free to make a feature request if there's a model you want to use that isn' ### Does Immich support Facial Recognition for videos ? -This is not currently implemented, but may be in the future. - -On the other hand, Immich does scan video thumbnails for faces, so it can perform recognition if the face is clear in the video thumbnail. +Immich's machine learning feature operate on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition. +Scanning the entire video for faces may be implemented in the future. ### Does Immich have animal recognition? @@ -216,7 +219,7 @@ No. ### I'm getting a lot of "faces" that aren't faces, what can I do? -You can increase the MIN DETECTION SCORE to 0.8 to help prevent bad thumbnails. However, a score of 0.9 might filter out too many real faces depending on the library used. If you just want to hide specific faces, you can adjust the 'MIN FACES DETECTED' setting in the administration panel +You can increase the MIN DETECTION SCORE to 0.8 to help prevent bad thumbnails. Setting the score too high (above 0.9) might filter out too many real faces depending on the library used. If you just want to hide specific faces, you can adjust the 'MIN FACES DETECTED' setting in the administration panel to increase the bar for what the algorithm considers a "core face" for that person, reducing the chance of bad thumbnails being chosen. ### The immich_model-cache volume takes up a lot of space, what could be the problem? @@ -236,7 +239,7 @@ To do this you can run: ### Why is Immich slow on low-memory systems like the Raspberry Pi? -Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or transfer to host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning) ,or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely. +Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely. ### Can I lower CPU and RAM usage? @@ -251,9 +254,9 @@ The initial backup is the most intensive due to the number of jobs running. The ### Can I limit the amount of CPU and RAM usage? By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. -You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to do this. +You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to limit this. -### How an I boost machine learning speed? +### How can I boost machine learning speed? :::note This advice improves throughput, not latency. This is to say that it will make Smart Search jobs process more quickly, but it won't make searching faster. @@ -262,17 +265,17 @@ This advice improves throughput, not latency. This is to say that it will make S You can increase throughput by increasing the job concurrency for machine learning jobs (Smart Search, Face Detection). With higher concurrency, the host will work on more assets in parallel. You can do this by navigating to Administration > Settings > Job Settings and increasing concurrency as needed. :::danger -On a normal machine, 2 or 3 concurrent jobs can probably max the CPU, so if you're not hitting those maximums with, say, 30 jobs. -Note that storage speed and latency may quickly become the limiting factor; particularly when using HDDs. +On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond this, note that storage speed and latency may quickly become the limiting factor; particularly when using HDDs. Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server. -more info [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708) +More detail can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708) ::: ### Why is Immich using so much of my CPU? -When a large amount of assets are uploaded to Immich it makes sense that the CPU and RAM will be heavily used due to machine learning work and creating image thumbnails after that, the percentage of CPU usage will drop to around 3-5% usage +When a large amount of assets are uploaded to Immich it makes sense that the CPU and RAM will be heavily used due to machine learning work and creating image thumbnails. +Once this process completes, the percentage of CPU usage will drop to around 3-5% usage --- @@ -280,13 +283,12 @@ When a large amount of assets are uploaded to Immich it makes sense that the CPU ### How can I see Immich logs? -Most Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md). +Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md). ### How can I run Immich as a non-root user? -1. Set the `PUID`/`PGID` environment variables (in `.env`). -2. Set the corresponding `user` argument in `docker-compose` for each service. -3. Add an additional volume to `immich-microservices` that mounts internally to `/usr/src/app/.reverse-geocoding-dump`. +You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. +You may need to add an additional volume to `immich-microservices` that mounts internally to `/usr/src/app/.reverse-geocoding-dump`. The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`. @@ -299,6 +301,10 @@ Data for Immich comes in two forms: To remove the **Metadata** you can stop Immich and delete the volume. +:::warning +This will destroy your database and reset your instance, meaning that you start from scratch. +::: + ```bash title="Remove Immich (containers and volumes)" docker compose down -v ``` @@ -308,7 +314,7 @@ If you use portainer, bring down the stack in portainer. Go into the volumes sec and remove all the volumes related to immcih then restart the stack. ::: -After removing the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting any unwanted files or folders. +After removing the containers and volumes, the **Files** should be removed from the `UPLOAD_LOCATION` to provide a clean start. ### Why does the machine learning service report workers crashing? @@ -324,6 +330,6 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job. -### Why do I get the error "duplicate key value violates unique constraint" in the log files? +### Why does Immich log migration errors on startup? -Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`. +Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`. diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 082d5396eb..66f4d7b9f2 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -10,7 +10,6 @@ Below is an example config for nginx. Make sure to include `client_max_body_size server { server_name - # https://github.com/immich-app/immich/blob/main/nginx/templates/default.conf.template#L28 client_max_body_size 50000M; location / { diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 7a271d980c..dc61c37d42 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -58,12 +58,13 @@ Please refer to the [Flutter's official documentation](https://flutter.dev/docs/ If you only want to do web development connected to an existing, remote backend, follow these steps: -1. Enter the web directory - `cd web/` -2. Install web dependencies - `npm i` -3. Start the web development server +1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -` +2. Enter the web directory - `cd web/` +3. Install web dependencies - `npm i` +4. Start the web development server -``` -IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev +```bash +IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev ``` ## IDE setup diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index ee7ca1c22c..360b3c3728 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -15,10 +15,12 @@ If you are looking to import your Google Photos takeout, we recommend this commu ## Requirements -- Node.js 20.0 or above +- Node.js 20 or above - Npm -## Installation +If you can't install node/npm, there is also a Docker version available below. + +## Installation (NPM) ```bash npm i -g @immich/cli @@ -30,6 +32,16 @@ NOTE: if you previously installed the legacy CLI, you will need to uninstall it npm uninstall -g immich ``` +## Installation (Docker) + +If npm is not available on your system you can try the Docker version + +```bash +docker run -it -v "$(pwd)":/import:ro -e IMMICH_INSTANCE_URL=https://your-immich-instance/api -e IMMICH_API_KEY=your-api-key ghcr.io/immich-app/immich-cli:latest +``` + +Please modify the `IMMICH_INSTANCE_URL` and `IMMICH_API_KEY` environment variables as suitable. You can also use a Docker env file to store your sensitive API key. + ## Usage ``` @@ -39,10 +51,11 @@ immich ``` Usage: immich [options] [command] -Immich command line interface +Command line interface for Immich Options: -V, --version output the version number + -d, --config Configuration directory (env: IMMICH_CONFIG_DIR) -h, --help display help for command Commands: @@ -69,7 +82,9 @@ Options: -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) -i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS) -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) + -H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) + -A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) --help display help for command @@ -91,7 +106,7 @@ For instance, immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG ``` -This will store your credentials in a file in your home directory. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually. +This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually. Once you are authenticated, you can upload assets to your Immich server. @@ -123,6 +138,12 @@ You can automatically create albums based on the folder name by passing the `--a immich upload --album --recursive directory/ ``` +You can also choose to upload all assets to a specific album with the `--album-name` option. + +```bash +immich upload --album-name "My summer holiday" --recursive directory/ +``` + It is possible to skip assets matching a glob pattern by passing the `--ignore` option. See [the library documentation](docs/features/libraries.md) on how to use glob patterns. You can add several exclusion patterns if needed. ```bash @@ -133,6 +154,12 @@ immich upload --ignore **/Raw/** --recursive directory/ immich upload --ignore **/Raw/** **/*.tif --recursive directory/ ``` +By default, hidden files are skipped. If you want to include hidden files, use the `--include-hidden` option: + +```bash +immich upload --include-hidden --recursive directory/ +``` + ### Obtain the API Key The API key can be obtained in the user setting panel on the web interface. diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md index 0bc5a2f19c..db3d1ba7d6 100644 --- a/docs/docs/features/hardware-transcoding.md +++ b/docs/docs/features/hardware-transcoding.md @@ -4,6 +4,10 @@ This feature allows you to use a GPU to accelerate transcoding and reduce CPU lo Note that hardware transcoding is much less efficient for file sizes. As this is a new feature, it is still experimental and may not work on all systems. +:::info +You do not need to redo any transcoding jobs after enabling hardware acceleration. The acceleration device will be used for any jobs that run after enabling it. +::: + ## Supported APIs - NVENC (NVIDIA) @@ -50,6 +54,40 @@ As this is a new feature, it is still experimental and may not work on all syste 3. Redeploy the `immich-microservices` container with these updated settings. 4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save. +#### Single Compose File + +Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly. + +For example, the `qsv` section in this file is: + +```yaml +devices: + - /dev/dri:/dev/dri +``` + +You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`: + +```yaml +immich-microservices: + container_name: immich_microservices + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # Note the lack of an `extends` section + devices: + - /dev/dri:/dev/dri + command: ['start.sh', 'microservices'] + volumes: + - ${UPLOAD_LOCATION}:/usr/src/app/upload + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + depends_on: + - redis + - database + restart: always +``` + +Once this is done, you can continue to step 3 of "Basic Setup". + #### All-In-One - Unraid Setup ##### NVENC - NVIDIA GPUs @@ -59,20 +97,6 @@ As this is a new feature, it is still experimental and may not work on all syste 3. Restart the container app. 4. Continue to step 4 of "Basic Setup". -##### Other APIs - -Unraid does not currently support multiple Compose files. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly. - -For example, the `qsv` section in this file is: - -``` -devices: - - /dev/dri:/dev/dri -``` - -You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`. -Once this is done, you can continue to step 3 of "Basic Setup". - ## Tips - You may want to choose a slower preset than for software transcoding to maintain quality and efficiency diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index 2323b468cb..8f5a889775 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -3,7 +3,11 @@ This feature allows you to use a GPU to accelerate machine learning tasks, such as Smart Search and Facial Recognition, while reducing CPU load. As this is a new feature, it is still experimental and may not work on all systems. -## Supported APIs +:::info +You do not need to redo any machine learning jobs after enabling hardware acceleration. The acceleration device will be used for any jobs that run after enabling it. +::: + +## Supported Backends - ARM NN (Mali) - CUDA (NVIDIA) @@ -14,7 +18,8 @@ As this is a new feature, it is still experimental and may not work on all syste - The instructions and configurations here are specific to Docker Compose. Other container engines may require different configuration. - Only Linux and Windows (through WSL2) servers are supported. - ARM NN is only supported on devices with Mali GPUs. Other Arm devices are not supported. -- The OpenVINO backend has only been tested on an iGPU. ARC GPUs may not work without other changes. +- There is currently an upstream issue with OpenVINO, so whether it will work is device-dependent. +- Some models may not be compatible with certain backends. CUDA is the most reliable. ## Prerequisites @@ -40,10 +45,60 @@ As this is a new feature, it is still experimental and may not work on all syste 2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend. 3. Redeploy the `immich-machine-learning` container with these updated settings. +#### Single Compose File + +Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.ml.yml`][hw-file] file into the `immich-machine-learning` service directly. + +For example, the `cuda` section in this file is: + +```yaml +deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: + - gpu + - compute + - video +``` + +You can add this to the `immich-machine-learning` service instead of extending from `hwaccel.ml.yml`: + +```yaml +immich-machine-learning: + container_name: immich_machine_learning + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda + # Note the lack of an `extends` section + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: + - gpu + - compute + - video + volumes: + - model-cache:/cache + env_file: + - .env + restart: always +``` + +Once this is done, you can redeploy the `immich-machine-learning` container. + +:::info +You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully. +::: + [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml [nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/ ## Tips +- If you encounter an error when a model is running, try a different model to see if the issue is model-specific. - You may want to increase concurrency past the default for higher utilization. However, keep in mind that this will also increase VRAM consumption. - Larger models benefit more from hardware acceleration, if you have the VRAM for them. diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md index a6c2e7d806..94e896d82a 100644 --- a/docs/docs/overview/support-the-project.md +++ b/docs/docs/overview/support-the-project.md @@ -16,7 +16,7 @@ If you feel like this is the right cause and the app is something you see yourse - One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 ## Contributing diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index e4ba774eae..053e89fa4a 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -161,7 +161,7 @@ const config = { ], }, ], - copyright: `Immich is available as open source under the terms of the MIT License.`, + copyright: `Immich is available as open source under the terms of the GNU AGPL v3 License.`, }, prism: { theme: prism.themes.github, diff --git a/docs/package-lock.json b/docs/package-lock.json index a6cc1fadb6..b6912ac7e8 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -12691,9 +12691,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -13488,9 +13488,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 1e4c3b760a..3fc2e6b873 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -71,7 +71,7 @@ export default function Home(): JSX.Element { >

-

This project is available under MIT license.

+

This project is available under GNU AGPL v3 license.

Privacy should not be a luxury

diff --git a/docs/tsconfig.json b/docs/tsconfig.json index bd85154b4d..aa1c63a6c5 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -4,6 +4,6 @@ "compilerOptions": { "baseUrl": ".", - "module": "Node16", - }, + "module": "Node16" + } } diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000000..68c5d18f00 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000000..ab84dc995b --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,383 @@ +{ + "name": "immich-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "immich-e2e", + "version": "1.0.0", + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@immich/sdk": "file:../open-api/typescript-sdk", + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.17", + "@types/pg": "^8.11.0", + "pg": "^8.11.3", + "typescript": "^5.3.3" + } + }, + "../open-api/typescript-sdk": { + "name": "@immich/sdk", + "version": "1.92.1", + "dev": true, + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@oazapfts/runtime": "^1.0.0", + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "axios": "^1.6.7" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/@immich/sdk": { + "resolved": "../open-api/typescript-sdk", + "link": true + }, + "node_modules/@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "dependencies": { + "playwright": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/pg": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.0.tgz", + "integrity": "sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "dev": true + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000000..122dde73e1 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,22 @@ +{ + "name": "immich-e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "npx playwright test", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@immich/sdk": "file:../open-api/typescript-sdk", + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.17", + "@types/pg": "^8.11.0", + "pg": "^8.11.3", + "typescript": "^5.3.3" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000000..2ff2d92acf --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,61 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './specs/', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + baseURL: 'http://127.0.0.1:2283', + trace: 'on-first-retry', + }, + + testMatch: /.*\.e2e-spec\.ts/, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: + 'docker compose -f ../docker/docker-compose.e2e.yml up --build -V --remove-orphans', + url: 'http://127.0.0.1:2283', + reuseExistingServer: true, + }, +}); diff --git a/e2e/specs/auth.e2e-spec.ts b/e2e/specs/auth.e2e-spec.ts new file mode 100644 index 0000000000..4c55d67ac1 --- /dev/null +++ b/e2e/specs/auth.e2e-spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { app } from '../test-utils'; + +test.describe('Registration', () => { + test.beforeAll(async () => { + await app.reset(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('admin registration', async ({ page }) => { + // welcome + await page.goto('/'); + await page.getByRole('button', { name: 'Getting Started' }).click(); + + // register + await expect(page).toHaveTitle(/Admin Registration/); + await page.getByLabel('Admin Email').fill('admin@immich.app'); + await page.getByLabel('Admin Password', { exact: true }).fill('password'); + await page.getByLabel('Confirm Admin Password').fill('password'); + await page.getByLabel('Name').fill('Immich Admin'); + await page.getByRole('button', { name: 'Sign up' }).click(); + + // login + await expect(page).toHaveTitle(/Login/); + await page.goto('/auth/login'); + await page.getByLabel('Email').fill('admin@immich.app'); + await page.getByLabel('Password').fill('password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // onboarding + await expect(page).toHaveURL('/auth/onboarding'); + await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Storage Template' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + + // success + await expect(page).toHaveURL('/photos'); + }); + + test('user registration', async ({ context, page }) => { + await app.adminSetup(context); + + // create user + await page.goto('/admin/user-management'); + await expect(page).toHaveTitle(/User Management/); + await page.getByRole('button', { name: 'Create user' }).click(); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password', { exact: true }).fill('password'); + await page.getByLabel('Confirm Password').fill('password'); + await page.getByLabel('Name').fill('Immich User'); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // logout + await context.clearCookies(); + + // login + await page.goto('/auth/login'); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password').fill('password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // change password + expect(page.getByRole('heading')).toHaveText('Change Password'); + await expect(page).toHaveURL('/auth/change-password'); + await page.getByLabel('New Password').fill('new-password'); + await page.getByLabel('Confirm Password').fill('new-password'); + await page.getByRole('button', { name: 'Change password' }).click(); + + // login with new password + await expect(page).toHaveURL('/auth/login'); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password').fill('new-password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // success + await expect(page).toHaveURL(/\/photos/); + }); +}); diff --git a/e2e/test-utils.ts b/e2e/test-utils.ts new file mode 100644 index 0000000000..f0d13be816 --- /dev/null +++ b/e2e/test-utils.ts @@ -0,0 +1,79 @@ +import pg from 'pg'; +import { defaults, login, setAdminOnboarding, signUpAdmin } from '@immich/sdk'; +import { BrowserContext } from '@playwright/test'; + +const client = new pg.Client( + 'postgres://postgres:postgres@localhost:5432/immich' +); +let connected = false; + +const loginCredentialDto = { + email: 'admin@immich.cloud', + password: 'password', +}; +const signUpDto = { ...loginCredentialDto, name: 'Immich Admin' }; + +const setBaseUrl = () => (defaults.baseUrl = 'http://127.0.0.1:2283/api'); +const asAuthHeader = (accessToken: string) => ({ + Authorization: `Bearer ${accessToken}`, +}); + +export const app = { + adminSetup: async (context: BrowserContext) => { + setBaseUrl(); + await signUpAdmin({ signUpDto }); + + const response = await login({ loginCredentialDto }); + + await context.addCookies([ + { + name: 'immich_access_token', + value: response.accessToken, + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: true, + secure: false, + sameSite: 'Lax', + }, + { + name: 'immich_auth_type', + value: 'password', + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: true, + secure: false, + sameSite: 'Lax', + }, + { + name: 'immich_is_authenticated', + value: 'true', + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: false, + secure: false, + sameSite: 'Lax', + }, + ]); + + await setAdminOnboarding({ headers: asAuthHeader(response.accessToken) }); + + return response; + }, + reset: async () => { + if (!connected) { + await client.connect(); + } + + for (const table of ['users', 'system_metadata']) { + await client.query(`DELETE FROM ${table} CASCADE;`); + } + }, + teardown: async () => { + if (connected) { + await client.end(); + } + }, +}; diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000000..c91b03d9db --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "target": "es2022", + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "esModuleInterop": true, + "rootDirs": ["src"], + "baseUrl": "./", + "types": ["vitest/globals"] + }, + "exclude": ["dist", "node_modules"] +} diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 5b6cc9b6d8..d317a5298e 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:8d773321add21be41f1f891b43177a41aaae0d1b83f3638872ec84636179594a as builder-cpu +FROM python:3.11-bookworm@sha256:8e697181d24bd77cc4251fdd37e4cdd6d725c5de2ed63b9bc8db77357400c5e2 as builder-cpu FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as builder-openvino USER root @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:d11b9bd5e49ea7401753d78f4d3b56f3aec952b85b49bcae88981f0452818e0b as prod-cpu +FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c24a3615b01f081d0c0583a as prod-cpu FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino USER root @@ -52,7 +52,8 @@ ENV LD_LIBRARY_PATH=/opt/armnn RUN apt-get update && apt-get install -y --no-install-recommends ocl-icd-libopencl1 mesa-opencl-icd && \ rm -rf /var/lib/apt/lists/* && \ mkdir --parents /etc/OpenCL/vendors && \ - echo "/usr/lib/libmali.so" > /etc/OpenCL/vendors/mali.icd + echo "/usr/lib/libmali.so" > /etc/OpenCL/vendors/mali.icd && \ + mkdir /opt/armnn COPY --from=builder-armnn \ /opt/armnn/libarmnn.so.?? \ diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index a4afa841ef..bde40f36e4 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -119,16 +119,12 @@ async def load(model: InferenceModel) -> InferenceModel: if model.loaded: return model - def _load() -> None: + def _load(model: InferenceModel) -> None: with lock: model.load() - loop = asyncio.get_running_loop() try: - if thread_pool is None: - model.load() - else: - await loop.run_in_executor(thread_pool, _load) + await run(_load, model) return model except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile): log.warning( @@ -138,10 +134,7 @@ async def load(model: InferenceModel) -> InferenceModel: ) ) model.clear_cache() - if thread_pool is None: - model.load() - else: - await loop.run_in_executor(thread_pool, _load) + await run(_load, model) return model diff --git a/machine-learning/app/models/__init__.py b/machine-learning/app/models/__init__.py index 31651bdee5..18d75e1632 100644 --- a/machine-learning/app/models/__init__.py +++ b/machine-learning/app/models/__init__.py @@ -21,4 +21,4 @@ def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any) case _: raise ValueError(f"Unknown model type {model_type}") - raise ValueError(f"Unknown ${model_type} model {model_name}") + raise ValueError(f"Unknown {model_type} model {model_name}") diff --git a/machine-learning/app/models/base.py b/machine-learning/app/models/base.py index e5ebb828f3..6097c7c987 100644 --- a/machine-learning/app/models/base.py +++ b/machine-learning/app/models/base.py @@ -1,17 +1,18 @@ from __future__ import annotations -import pickle from abc import ABC, abstractmethod from pathlib import Path from shutil import rmtree from typing import Any +import onnx import onnxruntime as ort from huggingface_hub import snapshot_download -from typing_extensions import Buffer +from onnx.shape_inference import infer_shapes +from onnx.tools.update_model_dims import update_inputs_outputs_dims import ann.ann -from app.models.constants import SUPPORTED_PROVIDERS +from app.models.constants import STATIC_INPUT_PROVIDERS, SUPPORTED_PROVIDERS from ..config import get_cache_dir, get_hf_model_name, log, settings from ..schemas import ModelRuntime, ModelType @@ -61,8 +62,7 @@ class InferenceModel(ABC): return self._predict(inputs) @abstractmethod - def _predict(self, inputs: Any) -> Any: - ... + def _predict(self, inputs: Any) -> Any: ... def configure(self, **model_kwargs: Any) -> None: pass @@ -78,8 +78,7 @@ class InferenceModel(ABC): ) @abstractmethod - def _load(self) -> None: - ... + def _load(self) -> None: ... def clear_cache(self) -> None: if not self.cache_dir.exists(): @@ -114,6 +113,13 @@ class InferenceModel(ABC): ) model_path = onnx_path + if any(provider in STATIC_INPUT_PROVIDERS for provider in self.providers): + static_path = model_path.parent / "static_1" / "model.onnx" + static_path.parent.mkdir(parents=True, exist_ok=True) + if not static_path.is_file(): + self._convert_to_static(model_path, static_path) + model_path = static_path + match model_path.suffix: case ".armnn": session = AnnSession(model_path) @@ -128,6 +134,42 @@ class InferenceModel(ABC): raise ValueError(f"Unsupported model file type: {model_path.suffix}") return session + def _convert_to_static(self, source_path: Path, target_path: Path) -> None: + inferred = infer_shapes(onnx.load(source_path)) + inputs = self._get_static_dims(inferred.graph.input) + outputs = self._get_static_dims(inferred.graph.output) + + # check_model gets called in update_inputs_outputs_dims and doesn't work for large models + check_model = onnx.checker.check_model + try: + + def check_model_stub(*args: Any, **kwargs: Any) -> None: + pass + + onnx.checker.check_model = check_model_stub + updated_model = update_inputs_outputs_dims(inferred, inputs, outputs) + finally: + onnx.checker.check_model = check_model + + onnx.save( + updated_model, + target_path, + save_as_external_data=True, + all_tensors_to_one_file=False, + size_threshold=1048576, + ) + + def _get_static_dims(self, graph_io: Any, dim_size: int = 1) -> dict[str, list[int]]: + return { + field.name: [ + d.dim_value if d.HasField("dim_value") else dim_size + for shape in field.type.ListFields() + if (dim := shape[1].shape.dim) + for d in dim + ] + for field in graph_io + } + @property def model_type(self) -> ModelType: return self._model_type @@ -154,7 +196,7 @@ class InferenceModel(ABC): @providers.setter def providers(self, providers: list[str]) -> None: - log.debug( + log.info( (f"Setting '{self.model_name}' execution providers to {providers}, " "in descending order of preference"), ) self._providers = providers @@ -209,7 +251,7 @@ class InferenceModel(ABC): @property def sess_options_default(self) -> ort.SessionOptions: - sess_options = PicklableSessionOptions() + sess_options = ort.SessionOptions() sess_options.enable_cpu_mem_arena = False # avoid thread contention between models @@ -241,15 +283,3 @@ class InferenceModel(ABC): @property def preferred_runtime_default(self) -> ModelRuntime: return ModelRuntime.ARMNN if ann.ann.is_available and settings.ann else ModelRuntime.ONNX - - -# HF deep copies configs, so we need to make session options picklable -class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc] - def __getstate__(self) -> bytes: - return pickle.dumps([(attr, getattr(self, attr)) for attr in dir(self) if not callable(getattr(self, attr))]) - - def __setstate__(self, state: Buffer) -> None: - self.__init__() # type: ignore[misc] - attrs: list[tuple[str, Any]] = pickle.loads(state) - for attr, val in attrs: - setattr(self, attr, val) diff --git a/machine-learning/app/models/cache.py b/machine-learning/app/models/cache.py index 1d6a0fc763..62afd05a09 100644 --- a/machine-learning/app/models/cache.py +++ b/machine-learning/app/models/cache.py @@ -80,20 +80,3 @@ class RevalidationPlugin(BasePlugin): # type: ignore[misc] key = client.build_key(key, namespace) if key in client._handlers: await client.expire(key, client.ttl) - - async def post_multi_get( - self, - client: SimpleMemoryCache, - keys: list[str], - ret: list[Any] | None = None, - namespace: str | None = None, - **kwargs: Any, - ) -> None: - if ret is None: - return - - for key, val in zip(keys, ret): - if namespace is not None: - key = client.build_key(key, namespace) - if val is not None and key in client._handlers: - await client.expire(key, client.ttl) diff --git a/machine-learning/app/models/clip.py b/machine-learning/app/models/clip.py index 469789155e..b0d73175a9 100644 --- a/machine-learning/app/models/clip.py +++ b/machine-learning/app/models/clip.py @@ -144,9 +144,7 @@ class OpenCLIPEncoder(BaseCLIPEncoder): def _load(self) -> None: super()._load() - text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] - context_length: int = text_cfg.get("context_length", 77) - pad_token: int = self.tokenizer_cfg["pad_token"] + self._load_tokenizer() size: list[int] | int = self.preprocess_cfg["size"] self.size = size[0] if isinstance(size, list) else size @@ -155,11 +153,19 @@ class OpenCLIPEncoder(BaseCLIPEncoder): self.mean = np.array(self.preprocess_cfg["mean"], dtype=np.float32) self.std = np.array(self.preprocess_cfg["std"], dtype=np.float32) + def _load_tokenizer(self) -> Tokenizer: log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'") + + text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] + context_length: int = text_cfg.get("context_length", 77) + pad_token: str = self.tokenizer_cfg["pad_token"] + self.tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix()) + pad_id: int = self.tokenizer.token_to_id(pad_token) self.tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id) self.tokenizer.enable_truncation(max_length=context_length) + log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: diff --git a/machine-learning/app/models/constants.py b/machine-learning/app/models/constants.py index edfe68aa70..18965d2b1d 100644 --- a/machine-learning/app/models/constants.py +++ b/machine-learning/app/models/constants.py @@ -51,11 +51,10 @@ _INSIGHTFACE_MODELS = { } -SUPPORTED_PROVIDERS = [ - "CUDAExecutionProvider", - "OpenVINOExecutionProvider", - "CPUExecutionProvider", -] +SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"] + + +STATIC_INPUT_PROVIDERS = ["OpenVINOExecutionProvider"] def is_openclip(model_name: str) -> bool: diff --git a/machine-learning/app/models/facial_recognition.py b/machine-learning/app/models/facial_recognition.py index 072fc807f9..894f5ec726 100644 --- a/machine-learning/app/models/facial_recognition.py +++ b/machine-learning/app/models/facial_recognition.py @@ -28,7 +28,10 @@ class FaceRecognizer(InferenceModel): def _load(self) -> None: self.det_model = RetinaFace(session=self._make_session(self.det_file)) - self.rec_model = ArcFaceONNX(self.rec_file.as_posix(), session=self._make_session(self.rec_file)) + self.rec_model = ArcFaceONNX( + self.rec_file.with_suffix(".onnx").as_posix(), + session=self._make_session(self.rec_file), + ) self.det_model.prepare( ctx_id=0, diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index adcb5f43c0..cf941c1bbf 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -1,7 +1,8 @@ import json -import pickle from io import BytesIO from pathlib import Path +from random import randint +from types import SimpleNamespace from typing import Any, Callable from unittest import mock @@ -13,10 +14,12 @@ from fastapi.testclient import TestClient from PIL import Image from pytest_mock import MockerFixture +from app.main import load + from .config import log, settings -from .models.base import InferenceModel, PicklableSessionOptions +from .models.base import InferenceModel from .models.cache import ModelCache -from .models.clip import OpenCLIPEncoder +from .models.clip import MCLIPEncoder, OpenCLIPEncoder from .models.facial_recognition import FaceRecognizer from .schemas import ModelRuntime, ModelType @@ -72,6 +75,17 @@ class TestBase: {"arena_extend_strategy": "kSameAsRequested"}, ] + def test_sets_openvino_device_id_if_possible(self, mocker: MockerFixture) -> None: + mocked = mocker.patch("app.models.base.ort.capi._pybind_state") + mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] + + encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]) + + assert encoder.provider_options == [ + {"device_id": "GPU.0"}, + {"arena_extend_strategy": "kSameAsRequested"}, + ] + def test_sets_provider_options_kwarg(self) -> None: encoder = OpenCLIPEncoder( "ViT-B-32__openai", @@ -119,7 +133,7 @@ class TestBase: def test_sets_default_cache_dir(self) -> None: encoder = OpenCLIPEncoder("ViT-B-32__openai") - assert encoder.cache_dir == Path("/cache/clip/ViT-B-32__openai") + assert encoder.cache_dir == Path(settings.cache_folder) / "clip" / "ViT-B-32__openai" def test_sets_cache_dir_kwarg(self) -> None: cache_dir = Path("/test_cache") @@ -170,7 +184,7 @@ class TestBase: encoder.clear_cache() mock_rmtree.assert_called_once_with(encoder.cache_dir) - info.assert_called_once() + info.assert_called_with(f"Cleared cache directory for model '{encoder.model_name}'.") def test_clear_cache_warns_if_path_does_not_exist(self, mocker: MockerFixture) -> None: mock_rmtree = mocker.patch("app.models.base.rmtree", autospec=True) @@ -267,7 +281,7 @@ class TestBase: def test_download(self, mocker: MockerFixture) -> None: mock_snapshot_download = mocker.patch("app.models.base.snapshot_download") - encoder = OpenCLIPEncoder("ViT-B-32__openai") + encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="/path/to/cache") encoder.download() mock_snapshot_download.assert_called_once_with( @@ -348,6 +362,60 @@ class TestCLIP: assert embedding.dtype == np.float32 mocked.run.assert_called_once() + def test_openclip_tokenizer( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_preprocess_cfg: Callable[[Path], dict[str, Any]], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + mocker.patch.object(OpenCLIPEncoder, "download") + mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) + mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mock_tokenizer = mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="text") + clip_encoder._load_tokenizer() + tokens = clip_encoder.tokenize("test search query") + + assert "text" in tokens + assert isinstance(tokens["text"], np.ndarray) + assert tokens["text"].shape == (1, 77) + assert tokens["text"].dtype == np.int32 + assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) + + def test_mclip_tokenizer( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_preprocess_cfg: Callable[[Path], dict[str, Any]], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + mocker.patch.object(OpenCLIPEncoder, "download") + mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) + mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mock_tokenizer = mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_attention_mask = [randint(0, 1) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids, attention_mask=mock_attention_mask) + + clip_encoder = MCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="text") + clip_encoder._load_tokenizer() + tokens = clip_encoder.tokenize("test search query") + + assert "input_ids" in tokens + assert "attention_mask" in tokens + assert isinstance(tokens["input_ids"], np.ndarray) + assert isinstance(tokens["attention_mask"], np.ndarray) + assert tokens["input_ids"].shape == (1, 77) + assert tokens["attention_mask"].shape == (1, 77) + assert np.allclose(tokens["input_ids"], np.array([mock_ids], dtype=np.int32), atol=0) + assert np.allclose(tokens["attention_mask"], np.array([mock_attention_mask], dtype=np.int32), atol=0) + class TestFaceRecognition: def test_set_min_score(self, mocker: MockerFixture) -> None: @@ -420,12 +488,75 @@ class TestCache: mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100) @mock.patch("app.models.cache.SimpleMemoryCache.expire") - async def test_revalidate(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None: + async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None: model_cache = ModelCache(ttl=100, revalidate=True) await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) mock_cache_expire.assert_called_once_with(mock.ANY, 100) + async def test_profiling(self, mock_get_model: mock.Mock) -> None: + model_cache = ModelCache(ttl=100, profiling=True) + await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) + profiling = await model_cache.get_profiling() + assert isinstance(profiling, dict) + assert profiling == model_cache.cache.profiling + + async def test_loads_mclip(self) -> None: + model_cache = ModelCache() + + model = await model_cache.get("XLM-Roberta-Large-Vit-B-32", ModelType.CLIP, mode="text") + + assert isinstance(model, MCLIPEncoder) + assert model.model_name == "XLM-Roberta-Large-Vit-B-32" + + async def test_raises_exception_if_invalid_model_type(self) -> None: + invalid: Any = SimpleNamespace(value="invalid") + model_cache = ModelCache() + + with pytest.raises(ValueError): + await model_cache.get("XLM-Roberta-Large-Vit-B-32", invalid, mode="text") + + async def test_raises_exception_if_unknown_model_name(self) -> None: + model_cache = ModelCache() + + with pytest.raises(ValueError): + await model_cache.get("test_model_name", ModelType.CLIP, mode="text") + + +@pytest.mark.asyncio +class TestLoad: + async def test_load(self) -> None: + mock_model = mock.Mock(spec=InferenceModel) + mock_model.loaded = False + + res = await load(mock_model) + + assert res is mock_model + mock_model.load.assert_called_once() + mock_model.clear_cache.assert_not_called() + + async def test_load_returns_model_if_loaded(self) -> None: + mock_model = mock.Mock(spec=InferenceModel) + mock_model.loaded = True + + res = await load(mock_model) + + assert res is mock_model + mock_model.load.assert_not_called() + + async def test_load_clears_cache_and_retries_if_os_error(self) -> None: + mock_model = mock.Mock(spec=InferenceModel) + mock_model.model_name = "test_model_name" + mock_model.model_type = ModelType.CLIP + mock_model.load.side_effect = [OSError, None] + mock_model.loaded = False + + res = await load(mock_model) + + assert res is mock_model + mock_model.clear_cache.assert_called_once() + assert mock_model.load.call_count == 2 + @pytest.mark.skipif( not settings.test_full, @@ -437,15 +568,21 @@ class TestEndpoints: ) -> None: byte_image = BytesIO() pil_image.save(byte_image, format="jpeg") + expected = responses["clip"]["image"] + response = deployed_app.post( "http://localhost:3003/predict", data={"modelName": "ViT-B-32__openai", "modelType": "clip", "options": json.dumps({"mode": "vision"})}, files={"image": byte_image.getvalue()}, ) + + actual = response.json() assert response.status_code == 200 - assert response.json() == responses["clip"]["image"] + assert np.allclose(expected, actual) def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None: + expected = responses["clip"]["text"] + response = deployed_app.post( "http://localhost:3003/predict", data={ @@ -455,12 +592,15 @@ class TestEndpoints: "options": json.dumps({"mode": "text"}), }, ) + + actual = response.json() assert response.status_code == 200 - assert response.json() == responses["clip"]["text"] + assert np.allclose(expected, actual) def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None: byte_image = BytesIO() pil_image.save(byte_image, format="jpeg") + expected = responses["facial-recognition"] response = deployed_app.post( "http://localhost:3003/predict", @@ -471,15 +611,13 @@ class TestEndpoints: }, files={"image": byte_image.getvalue()}, ) + + actual = response.json() assert response.status_code == 200 - assert response.json() == responses["facial-recognition"] - - -def test_sess_options() -> None: - sess_options = PicklableSessionOptions() - sess_options.intra_op_num_threads = 1 - sess_options.inter_op_num_threads = 1 - pickled = pickle.dumps(sess_options) - unpickled = pickle.loads(pickled) - assert unpickled.intra_op_num_threads == 1 - assert unpickled.inter_op_num_threads == 1 + assert len(expected) == len(actual) + for expected_face, actual_face in zip(expected, actual): + assert expected_face["imageHeight"] == actual_face["imageHeight"] + assert expected_face["imageWidth"] == actual_face["imageWidth"] + assert expected_face["boundingBox"] == actual_face["boundingBox"] + assert np.allclose(expected_face["embedding"], actual_face["embedding"]) + assert np.allclose(expected_face["score"], actual_face["score"]) diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 6f48828a0e..40402886ff 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:377aafafb5f58e577faeb8b2884b795c6ff0a92e4a53ef814ea3961286ceae18 as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 3ea02acde2..160b0b8e46 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "23.12.1" +version = "24.1.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, + {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, + {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, + {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, + {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, + {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, + {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, + {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, + {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, + {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, + {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, + {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, + {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, + {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, + {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, + {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, + {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, + {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, + {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, + {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, + {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, + {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, + {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, ] [package.dependencies] @@ -680,22 +680,22 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.109.0" +version = "0.109.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"}, - {file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"}, + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.35.0,<0.36.0" +starlette = ">=0.36.3,<0.37.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" @@ -735,19 +735,6 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] -[[package]] -name = "flask-basicauth" -version = "0.2.0" -description = "HTTP basic access authentication for Flask." -optional = false -python-versions = "*" -files = [ - {file = "Flask-BasicAuth-0.2.0.tar.gz", hash = "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff"}, -] - -[package.dependencies] -Flask = "*" - [[package]] name = "flask-cors" version = "4.0.0" @@ -762,6 +749,21 @@ files = [ [package.dependencies] Flask = ">=0.9" +[[package]] +name = "flask-login" +version = "0.6.3" +description = "User authentication and session management for Flask." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"}, + {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"}, +] + +[package.dependencies] +Flask = ">=1.0.4" +Werkzeug = ">=1.0.1" + [[package]] name = "flatbuffers" version = "23.5.26" @@ -1564,20 +1566,20 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.20.1" +version = "2.23.1" description = "Developer friendly load testing framework" optional = false python-versions = ">=3.8" files = [ - {file = "locust-2.20.1-py3-none-any.whl", hash = "sha256:70168ccf462f125e0e4304c1f8301d3c18f186e8f257bc6578e7bed5e74e59a7"}, - {file = "locust-2.20.1.tar.gz", hash = "sha256:9ba4c8658a158aed55774ac3650ac0139fcc1dfa65fea0dabb00ea35b0d56a4e"}, + {file = "locust-2.23.1-py3-none-any.whl", hash = "sha256:96013a460a4b4d6d4fd46c70e6ff1fd2b6e03b48ddb1b48d1513d3134ba2cecf"}, + {file = "locust-2.23.1.tar.gz", hash = "sha256:6cc729729e5ebf5852fc9d845302cfcf0ab0132f198e68b3eb0c88b438b6a863"}, ] [package.dependencies] ConfigArgParse = ">=1.5.5" flask = ">=2.0.0" -Flask-BasicAuth = ">=0.2.0" Flask-Cors = ">=3.0.10" +Flask-Login = ">=0.6.3" gevent = ">=22.10.2" geventhttpclient = ">=2.0.11" msgpack = ">=1.0.0" @@ -1986,35 +1988,36 @@ reference = ["Pillow", "google-re2"] [[package]] name = "onnxruntime" -version = "1.16.3" +version = "1.17.0" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.16.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:3bc41f323ac77acfed190be8ffdc47a6a75e4beeb3473fbf55eeb075ccca8df2"}, - {file = "onnxruntime-1.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:212741b519ee61a4822c79c47147d63a8b0ffde25cd33988d3d7be9fbd51005d"}, - {file = "onnxruntime-1.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f91f5497fe3df4ceee2f9e66c6148d9bfeb320cd6a71df361c66c5b8bac985a"}, - {file = "onnxruntime-1.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b1fc269cabd27f129fb9058917d6fdc89b188c49ed8700f300b945c81f889"}, - {file = "onnxruntime-1.16.3-cp310-cp310-win32.whl", hash = "sha256:f36b56a593b49a3c430be008c2aea6658d91a3030115729609ec1d5ffbaab1b6"}, - {file = "onnxruntime-1.16.3-cp310-cp310-win_amd64.whl", hash = "sha256:3c467eaa3d2429c026b10c3d17b78b7f311f718ef9d2a0d6938e5c3c2611b0cf"}, - {file = "onnxruntime-1.16.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a225bb683991001d111f75323d355b3590e75e16b5e0f07a0401e741a0143ea1"}, - {file = "onnxruntime-1.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aded21fe3d898edd86be8aa2eb995aa375e800ad3dfe4be9f618a20b8ee3630"}, - {file = "onnxruntime-1.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00cccc37a5195c8fca5011b9690b349db435986bd508eb44c9fce432da9228a4"}, - {file = "onnxruntime-1.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e253e572021563226a86f1c024f8f70cdae28f2fb1cc8c3a9221e8b1ce37db5"}, - {file = "onnxruntime-1.16.3-cp311-cp311-win32.whl", hash = "sha256:a82a8f0b4c978d08f9f5c7a6019ae51151bced9fd91e5aaa0c20a9e4ac7a60b6"}, - {file = "onnxruntime-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:78d81d9af457a1dc90db9a7da0d09f3ccb1288ea1236c6ab19f0ca61f3eee2d3"}, - {file = "onnxruntime-1.16.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:04ebcd29c20473596a1412e471524b2fb88d55e6301c40b98dd2407b5911595f"}, - {file = "onnxruntime-1.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9996bab0f202a6435ab867bc55598f15210d0b72794d5de83712b53d564084ae"}, - {file = "onnxruntime-1.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b8f5083f903408238883821dd8c775f8120cb4a604166dbdabe97f4715256d5"}, - {file = "onnxruntime-1.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c2dcf1b70f8434abb1116fe0975c00e740722aaf321997195ea3618cc00558e"}, - {file = "onnxruntime-1.16.3-cp38-cp38-win32.whl", hash = "sha256:d4a0151e1accd04da6711f6fd89024509602f82c65a754498e960b032359b02d"}, - {file = "onnxruntime-1.16.3-cp38-cp38-win_amd64.whl", hash = "sha256:e8aa5bba78afbd4d8a2654b14ec7462ff3ce4a6aad312a3c2d2c2b65009f2541"}, - {file = "onnxruntime-1.16.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:6829dc2a79d48c911fedaf4c0f01e03c86297d32718a3fdee7a282766dfd282a"}, - {file = "onnxruntime-1.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:76f876c53bfa912c6c242fc38213a6f13f47612d4360bc9d599bd23753e53161"}, - {file = "onnxruntime-1.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4137e5d443e2dccebe5e156a47f1d6d66f8077b03587c35f11ee0c7eda98b533"}, - {file = "onnxruntime-1.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56695c1a343c7c008b647fff3df44da63741fbe7b6003ef576758640719be7b"}, - {file = "onnxruntime-1.16.3-cp39-cp39-win32.whl", hash = "sha256:985a029798744ce4743fcf8442240fed35c8e4d4d30ec7d0c2cdf1388cd44408"}, - {file = "onnxruntime-1.16.3-cp39-cp39-win_amd64.whl", hash = "sha256:28ff758b17ce3ca6bcad3d936ec53bd7f5482e7630a13f6dcae518eba8f71d85"}, + {file = "onnxruntime-1.17.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d2b22a25a94109cc983443116da8d9805ced0256eb215c5e6bc6dcbabefeab96"}, + {file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c87d83c6f58d1af2675fc99e3dc810f2dbdb844bcefd0c1b7573632661f6fc"}, + {file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dba55723bf9b835e358f48c98a814b41692c393eb11f51e02ece0625c756b797"}, + {file = "onnxruntime-1.17.0-cp310-cp310-win32.whl", hash = "sha256:ee48422349cc500273beea7607e33c2237909f58468ae1d6cccfc4aecd158565"}, + {file = "onnxruntime-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f34cc46553359293854e38bdae2ab1be59543aad78a6317e7746d30e311110c3"}, + {file = "onnxruntime-1.17.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:16d26badd092c8c257fa57c458bb600d96dc15282c647ccad0ed7b2732e6c03b"}, + {file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f1273bebcdb47ed932d076c85eb9488bc4768fcea16d5f2747ca692fad4f9d3"}, + {file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb60fd3c2c1acd684752eb9680e89ae223e9801a9b0e0dc7b28adabe45a2e380"}, + {file = "onnxruntime-1.17.0-cp311-cp311-win32.whl", hash = "sha256:4b038324586bc905299e435f7c00007e6242389c856b82fe9357fdc3b1ef2bdc"}, + {file = "onnxruntime-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:93d39b3fa1ee01f034f098e1c7769a811a21365b4883f05f96c14a2b60c6028b"}, + {file = "onnxruntime-1.17.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:90c0890e36f880281c6c698d9bc3de2afbeee2f76512725ec043665c25c67d21"}, + {file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7466724e809a40e986b1637cba156ad9fc0d1952468bc00f79ef340bc0199552"}, + {file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d47bee7557a8b99c8681b6882657a515a4199778d6d5e24e924d2aafcef55b0a"}, + {file = "onnxruntime-1.17.0-cp312-cp312-win32.whl", hash = "sha256:bb1bf1ee575c665b8bbc3813ab906e091a645a24ccc210be7932154b8260eca1"}, + {file = "onnxruntime-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac2f286da3494b29b4186ca193c7d4e6a2c1f770c4184c7192c5da142c3dec28"}, + {file = "onnxruntime-1.17.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1ec485643b93e0a3896c655eb2426decd63e18a278bb7ccebc133b340723624f"}, + {file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c35809cda898c5a11911c69ceac8a2ac3925911854c526f73bad884582f911"}, + {file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa464aa4d81df818375239e481887b656e261377d5b6b9a4692466f5f3261edc"}, + {file = "onnxruntime-1.17.0-cp38-cp38-win32.whl", hash = "sha256:b7b337cd0586f7836601623cbd30a443df9528ef23965860d11c753ceeb009f2"}, + {file = "onnxruntime-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:fbb9faaf51d01aa2c147ef52524d9326744c852116d8005b9041809a71838878"}, + {file = "onnxruntime-1.17.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5a06ab84eaa350bf64b1d747b33ccf10da64221ed1f38f7287f15eccbec81603"}, + {file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d3d11db2c8242766212a68d0b139745157da7ce53bd96ba349a5c65e5a02357"}, + {file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5632077c3ab8b0cd4f74b0af9c4e924be012b1a7bcd7daa845763c6c6bf14b7d"}, + {file = "onnxruntime-1.17.0-cp39-cp39-win32.whl", hash = "sha256:61a12732cba869b3ad2d4e29ab6cb62c7a96f61b8c213f7fcb961ba412b70b37"}, + {file = "onnxruntime-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:461fa0fc7d9c392c352b6cccdedf44d818430f3d6eacd924bb804fdea2dcfd02"}, ] [package.dependencies] @@ -2027,19 +2030,21 @@ sympy = "*" [[package]] name = "onnxruntime-gpu" -version = "1.16.3" +version = "1.17.0" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime_gpu-1.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14bc735ad2b2286be9eadeea09bc190df38e8bce17e37b601761019cc7cc24f"}, - {file = "onnxruntime_gpu-1.16.3-cp310-cp310-win_amd64.whl", hash = "sha256:8de5ccfc005ea5ec50fbd104b7210c97623a9f8c13de6e64ce559b55956b757f"}, - {file = "onnxruntime_gpu-1.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5703454521a9c080ff3ac79b5d266e959cc735d442a1d8796763c7f92d6069dc"}, - {file = "onnxruntime_gpu-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:48bb615aed61f5620d1ad46b9005614e1a14de60f8218a1448cc9a643f23d399"}, - {file = "onnxruntime_gpu-1.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2811c8ea209aaedcc2600ca828025279c1b1242344af603122d28c2ea8ab26a4"}, - {file = "onnxruntime_gpu-1.16.3-cp38-cp38-win_amd64.whl", hash = "sha256:2e5a92770c9232776739f378804bf6fea20bae02878a50b7fe0f81e77a47ee92"}, - {file = "onnxruntime_gpu-1.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9305c7fc5981d7e04ad2afef1a403475fb84d658898567c91aa5a41c20ead356"}, - {file = "onnxruntime_gpu-1.16.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3ad8e7fbb22493267c23d61e997a6b2ac6236a08aa6b58a3a91848124c9b037"}, + {file = "onnxruntime_gpu-1.17.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f2a4e0468ac0bd8246996c3d5dbba92cbbaca874bcd7f9cee4e99ce6eb27f5b"}, + {file = "onnxruntime_gpu-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:0721b7930d7abed3730b2335e639e60d94ec411bb4d35a0347cc9c8b52c34540"}, + {file = "onnxruntime_gpu-1.17.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be0314afe399943904de7c1ca797cbcc63e6fad60eb85d3df6422f81dd94e79e"}, + {file = "onnxruntime_gpu-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:52125c24b21406d1431e43de1c98cea29c21e0cceba80db530b7e4c9216d86ea"}, + {file = "onnxruntime_gpu-1.17.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bb802d8033885c412269f8bc8877d8779b0dc874df6fb9df8b796cba7276ad66"}, + {file = "onnxruntime_gpu-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:8c43533e3e5335eaa78059fb86b849a4faded513a00c1feaaa205ca5af51c40f"}, + {file = "onnxruntime_gpu-1.17.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1d461455bba160836d6c11c648c8fd4e4500d5c17096a13e6c2c9d22a4abd436"}, + {file = "onnxruntime_gpu-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4398f2175a92f4b35d95279a6294a89c462f24de058a2736ee1d498bab5a16"}, + {file = "onnxruntime_gpu-1.17.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1d0e3805cd1c024aba7f4ae576fd08545fc27530a2aaad2b3c8ac0ee889fbd05"}, + {file = "onnxruntime_gpu-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc1da5b93363ee600b5b220b04eeec51ad2c2b3e96f0b7615b16b8a173c88001"}, ] [package.dependencies] @@ -2096,61 +2101,61 @@ numpy = [ [[package]] name = "orjson" -version = "3.9.12" +version = "3.9.13" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.9.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146"}, - {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3"}, - {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081"}, - {file = "orjson-3.9.12-cp310-none-win32.whl", hash = "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9"}, - {file = "orjson-3.9.12-cp310-none-win_amd64.whl", hash = "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da"}, - {file = "orjson-3.9.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c"}, - {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e"}, - {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073"}, - {file = "orjson-3.9.12-cp311-none-win32.whl", hash = "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc"}, - {file = "orjson-3.9.12-cp311-none-win_amd64.whl", hash = "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71"}, - {file = "orjson-3.9.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded"}, - {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8"}, - {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf"}, - {file = "orjson-3.9.12-cp312-none-win_amd64.whl", hash = "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549"}, - {file = "orjson-3.9.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee"}, - {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8"}, - {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80"}, - {file = "orjson-3.9.12-cp38-none-win32.whl", hash = "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52"}, - {file = "orjson-3.9.12-cp38-none-win_amd64.whl", hash = "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa"}, - {file = "orjson-3.9.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3"}, - {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9"}, - {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d"}, - {file = "orjson-3.9.12-cp39-none-win32.whl", hash = "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf"}, - {file = "orjson-3.9.12-cp39-none-win_amd64.whl", hash = "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa"}, - {file = "orjson-3.9.12.tar.gz", hash = "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4"}, + {file = "orjson-3.9.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fa6b67f8bef277c2a4aadd548d58796854e7d760964126c3209b19bccc6a74f1"}, + {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b812417199eeb169c25f67815cfb66fd8de7ff098bf57d065e8c1943a7ba5c8f"}, + {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ccd5bd222e5041069ad9d9868ab59e6dbc53ecde8d8c82b919954fbba43b46b"}, + {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaaf80957c38e9d3f796f355a80fad945e72cd745e6b64c210e635b7043b673e"}, + {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60da7316131185d0110a1848e9ad15311e6c8938ee0b5be8cbd7261e1d80ee8f"}, + {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b98cd948372f0eb219bc309dee4633db1278687161e3280d9e693b6076951d2"}, + {file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3869d65561f10071d3e7f35ae58fd377056f67d7aaed5222f318390c3ad30339"}, + {file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43fd6036b16bb6742d03dae62f7bdf8214d06dea47e4353cde7e2bd1358d186f"}, + {file = "orjson-3.9.13-cp310-none-win32.whl", hash = "sha256:0d3ba9d88e20765335260d7b25547d7c571eee2b698200f97afa7d8c7cd668fc"}, + {file = "orjson-3.9.13-cp310-none-win_amd64.whl", hash = "sha256:6e47153db080f5e87e8ba638f1a8b18995eede6b0abb93964d58cf11bcea362f"}, + {file = "orjson-3.9.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4584e8eb727bc431baaf1bf97e35a1d8a0109c924ec847395673dfd5f4ef6d6f"}, + {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f37f0cdd026ef777a4336e599d8194c8357fc14760c2a5ddcfdf1965d45504b"}, + {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d714595d81efab11b42bccd119977d94b25d12d3a806851ff6bfd286a4bce960"}, + {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9171e8e1a1f221953e38e84ae0abffe8759002fd8968106ee379febbb5358b33"}, + {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ab9dbdec3f13f3ea6f937564ce21651844cfbf2725099f2f490426acf683c23"}, + {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811ac076855e33e931549340288e0761873baf29276ad00f221709933c644330"}, + {file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:860d0f5b42d0c0afd73fa4177709f6e1b966ba691fcd72175affa902052a81d6"}, + {file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:838b898e8c1f26eb6b8d81b180981273f6f5110c76c22c384979aca854194f1b"}, + {file = "orjson-3.9.13-cp311-none-win32.whl", hash = "sha256:d3222db9df629ef3c3673124f2e05fb72bc4a320c117e953fec0d69dde82e36d"}, + {file = "orjson-3.9.13-cp311-none-win_amd64.whl", hash = "sha256:978117122ca4cc59b28af5322253017f6c5fc03dbdda78c7f4b94ae984c8dd43"}, + {file = "orjson-3.9.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:031df1026c7ea8303332d78711f180231e3ae8b564271fb748a03926587c5546"}, + {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd9a2101d04e85086ea6198786a3f016e45475f800712e6833e14bf9ce2832f"}, + {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:446d9ad04204e79229ae19502daeea56479e55cbc32634655d886f5a39e91b44"}, + {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b57c0954a9fdd2b05b9cec0f5a12a0bdce5bf021a5b3b09323041613972481ab"}, + {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:266e55c83f81248f63cc93d11c5e3a53df49a5d2598fa9e9db5f99837a802d5d"}, + {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31372ba3a9fe8ad118e7d22fba46bbc18e89039e3bfa89db7bc8c18ee722dca8"}, + {file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3b0c4da61f39899561e08e571f54472a09fa71717d9797928af558175ae5243"}, + {file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cc03a35bfc71c8ebf96ce49b82c2a7be6af4b3cd3ac34166fdb42ac510bbfff"}, + {file = "orjson-3.9.13-cp312-none-win_amd64.whl", hash = "sha256:49b7e3fe861cb246361825d1a238f2584ed8ea21e714bf6bb17cebb86772e61c"}, + {file = "orjson-3.9.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:62e9a99879c4d5a04926ac2518a992134bfa00d546ea5a4cae4b9be454d35a22"}, + {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d92a3e835a5100f1d5b566fff79217eab92223ca31900dba733902a182a35ab0"}, + {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23f21faf072ed3b60b5954686f98157e073f6a8068eaa58dbde83e87212eda84"}, + {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:828c502bb261588f7de897e06cb23c4b122997cb039d2014cb78e7dabe92ef0c"}, + {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16946d095212a3dec552572c5d9bca7afa40f3116ad49695a397be07d529f1fa"}, + {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3deadd8dc0e9ff844b5b656fa30a48dbee1c3b332d8278302dd9637f6b09f627"}, + {file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9b1b5adc5adf596c59dca57156b71ad301d73956f5bab4039b0e34dbf50b9fa0"}, + {file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ddc089315d030c54f0f03fb38286e2667c05009a78d659f108a8efcfbdf2e585"}, + {file = "orjson-3.9.13-cp38-none-win32.whl", hash = "sha256:ae77275a28667d9c82d4522b681504642055efa0368d73108511647c6499b31c"}, + {file = "orjson-3.9.13-cp38-none-win_amd64.whl", hash = "sha256:730385fdb99a21fce9bb84bb7fcbda72c88626facd74956bda712834b480729d"}, + {file = "orjson-3.9.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7e8e4a571d958910272af8d53a9cbe6599f9f5fd496a1bc51211183bb2072cbd"}, + {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfad553a36548262e7da0f3a7464270e13900b898800fb571a5d4b298c3f8356"}, + {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d691c44604941945b00e0a13b19a7d9c1a19511abadf0080f373e98fdeb6b31"}, + {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8c83718346de08d68b3cb1105c5d91e5fc39885d8610fdda16613d4e3941459"}, + {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ef57a53bfc2091a7cd50a640d9ae866bd7d92a5225a1bab6baa60ef62583f2"}, + {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9156b96afa38db71344522f5517077eaedf62fcd2c9148392ff93d801128809c"}, + {file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31fb66b41fb2c4c817d9610f0bc7d31345728d7b5295ac78b63603407432a2b2"}, + {file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a730bf07feacb0863974e67b206b7c503a62199de1cece2eb0d4c233ec29c11"}, + {file = "orjson-3.9.13-cp39-none-win32.whl", hash = "sha256:5ef58869f3399acbbe013518d8b374ee9558659eef14bca0984f67cb1fbd3c37"}, + {file = "orjson-3.9.13-cp39-none-win_amd64.whl", hash = "sha256:9bcf56efdb83244cde070e82a69c0f03c47c235f0a5cb6c81d9da23af7fbaae4"}, + {file = "orjson-3.9.13.tar.gz", hash = "sha256:fc6bc65b0cf524ee042e0bc2912b9206ef242edfba7426cf95763e4af01f527a"}, ] [[package]] @@ -2460,13 +2465,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.4" +version = "8.0.0" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, + {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, ] [package.dependencies] @@ -2474,7 +2479,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" +pluggy = ">=1.3.0,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] @@ -2482,17 +2487,17 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.23.3" +version = "0.23.5" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"}, - {file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"}, + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, ] [package.dependencies] -pytest = ">=7.0.0" +pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -2563,17 +2568,17 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.6" +version = "0.0.9" description = "A streaming multipart parser for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, - {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, ] [package.extras] -dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] [[package]] name = "pywin32" @@ -2831,28 +2836,28 @@ files = [ [[package]] name = "ruff" -version = "0.1.15" +version = "0.2.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, + {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, + {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, + {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, + {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, ] [[package]] @@ -3032,20 +3037,20 @@ files = [ [[package]] name = "starlette" -version = "0.35.1" +version = "0.36.3" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"}, - {file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"}, + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, ] [package.dependencies] anyio = ">=3.4.0,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] name = "sympy" diff --git a/machine-learning/responses.json b/machine-learning/responses.json index 10c581674b..209f05cffb 100644 --- a/machine-learning/responses.json +++ b/machine-learning/responses.json @@ -1,1570 +1,329 @@ { - "image-classification": [ - "matchstick", - "nematode", - "nematode worm", - "roundworm", - "theater curtain", - "theatre curtain", - "spotlight", - "spot", - "digital clock" + "clip": { + "image": [ + -0.013126737, -0.022995953, -0.0493738, -0.0063057775, 0.013601424, + -0.003761688, -0.03379882, 0.11106285, 0.024760082, 0.023903701, + 0.04418207, -0.013594999, 0.030850016, 0.0012876489, -0.012471005, + 0.009750715, 0.0095717255, 0.013320666, 0.0027921356, 0.03240264, + 0.033538498, 0.013624318, -0.0069946186, -0.0036184592, -0.009846507, + -0.017311024, -0.036686428, -0.0041808123, 0.030871637, -0.028624479, + -0.016515259, 0.014418001, -0.024542322, -0.0025438748, -0.049111884, + -0.023928944, 0.012270045, -0.016418075, 0.004895335, -0.15801854, + -0.026325515, 0.03166467, -0.017224329, 0.0411128, -0.022944424, + 0.015693054, -0.020919899, -0.010764121, -0.008499815, -0.020263294, + -0.009743323, -0.035395622, 0.03474742, 0.003049183, 0.009424564, + 0.010707678, 0.01664117, -0.0059027374, -0.013055344, 0.0018035833, + -0.003976456, -0.04325922, 0.014407317, 0.035728276, 0.002226939, + -0.006235411, -0.0073032235, -0.035129357, 0.001095443, -0.028552389, + -0.044300288, -0.012959393, 0.02259977, 0.017141517, -0.029432472, + -0.017583484, 0.010974336, 0.018141218, 0.0015389329, -0.008220305, + -0.0060965014, 0.004929384, 0.019477025, -0.033071984, 0.025183259, + 0.013607688, 0.01836233, 0.04586782, 0.0103442725, -0.036077496, + -0.029715508, 0.007203621, -0.7949153, 0.046866275, 0.026910711, + -0.0047834567, 0.033243995, 0.009379981, -0.03749048, -0.055274535, + -0.01955359, 0.012887587, 0.00922838, -0.0032776103, -0.011456734, + 0.0045412215, -0.11506394, 0.0348558, 0.029478835, -0.011811103, + -0.00483158, -0.010586126, -0.018853206, -0.01591496, -0.019360982, + -0.03211199, -0.013473663, -0.019630248, -0.017012835, 0.059128772, + -0.03396129, 0.0045991736, -0.015158291, 0.008241974, 0.004403056, + -0.007536049, -0.023821214, -0.0059521003, 0.015564905, 0.020600233, + 0.008175, 0.02100119, -0.0034459834, 0.1058016, 0.008383205, 0.03100292, + -0.023814196, -0.016157096, -0.008210107, -0.004146204, 0.016350364, + -0.056028433, 0.013261071, 0.034839876, -0.03236049, 0.026573967, + -0.018140865, 0.018515658, 0.013251766, 0.007693613, 0.0067239976, + -0.0013857568, 0.038114607, 0.0068016117, 0.036603037, 0.0040935865, + 0.010394745, -0.00082285365, -0.009811308, 0.020343611, -0.012164189, + -0.012208623, 0.0005465415, -0.015394064, 0.02499845, 0.021941017, + -0.016571017, -0.011810332, 0.017864, -0.010639794, -0.008609091, + -0.0007239709, 0.015229945, -0.0035874692, 0.018922612, -0.011209458, + -0.013052865, -0.009626533, -0.004419959, 0.007915186, 0.01094836, + 0.005509159, -0.0034862005, 0.01012292, -0.0059307595, -0.029599648, + 0.032845, -0.007011692, -0.014218981, 0.00790071, 0.017027052, + -0.022314077, -0.03041719, 0.015665755, 0.036747217, -0.018942915, + 0.008623111, 0.02179961, -0.022312569, 0.007024427, 0.016751591, + -0.0034192575, 0.024101255, -0.0046198783, 0.022274924, -0.015562676, + -0.0092551885, -0.0063787713, 0.045996074, 0.026235346, 0.009622556, + 0.05728027, 0.03168525, -0.017600676, 0.029278612, 0.01467962, + 0.032169178, 0.022459356, -0.012175933, -0.009438608, 0.027234165, + 0.013514767, -0.008831029, 0.010888894, 0.004518216, 0.009855367, + 0.012112431, -0.0073178695, 0.0072642234, 4.877679e-5, -0.01221576, + 0.023542404, -0.009026452, -0.055442516, 0.006579068, 0.033202186, + -0.007669379, 0.0010604112, -0.04271919, -0.029112164, 0.021844024, + 0.029739635, -0.026083348, 0.008940292, -0.039301652, -0.047215454, + 0.0018794702, -0.008740231, 0.029195482, 0.0037629968, -0.024923965, + -0.021407248, 0.009952853, -0.0055059143, 0.0044912454, 0.016966008, + -0.00081178773, -0.022250004, -0.014063889, -0.006170697, -0.0008208651, + -0.036218595, -0.0029040456, 0.03943083, -0.021814227, 0.017567957, + 0.035849728, -0.049075987, 0.0040634805, 0.009878297, 0.028557112, + 0.02336673, 0.010714448, 0.020129073, -0.030503238, 0.009350441, + 0.039086528, -0.0037483997, -0.0034365985, 0.019824414, 0.014027232, + 0.030565958, 0.0036307913, 0.0030920429, -0.009908996, 0.0027933475, + -7.140754e-5, -0.027733125, 0.0022445584, -0.032248124, 0.050226185, + 0.030529078, -0.040353864, 0.031086015, -0.0063569676, 0.031343475, + -0.020244656, -0.011442288, 0.018035123, -0.005479394, 0.01783419, + -0.036066547, -0.0106600635, 0.044636995, -0.030209303, -0.07192714, + 0.0128155155, 0.003505818, -0.0005725083, -0.01584388, -0.025725754, + 0.025868604, 0.10576061, -0.012738124, 0.0012224225, 0.0472961, + 0.021650923, 0.0061313445, 0.014010678, 0.016864019, 0.004049639, + 0.10989465, 0.011927816, 0.013589654, 0.011258818, 0.022496557, + -0.018828733, 0.021635532, 0.0116777215, 0.11320542, -0.0011280471, + 0.018990291, 0.001824643, -0.03793715, 0.0206918, -0.0050228164, + -0.013865701, 0.022277884, 0.019400347, 0.028610364, -0.023974052, + 0.0030309444, 0.027177742, 0.024541136, 0.023737634, 0.0012539584, + 0.0187086, -0.015451178, 0.015066189, -0.019812824, 0.050507285, + -0.0021846944, 0.041644007, -0.0070109894, -0.014599777, -0.05985813, + -0.036328156, -0.02293525, -0.0065515027, 0.016792618, -0.0059018973, + -0.008319917, 0.008072106, 0.0073447954, -0.052924518, -0.037344936, + 0.015524772, -0.0012835241, 0.014405327, 0.0057144985, 0.004945561, + -0.024654018, 0.011967616, 0.01832056, 0.019411784, 0.019788045, + -0.0006405928, -0.0015148119, -0.05064218, -0.031875107, -0.03803604, + -0.0096240705, 0.012371131, -0.019090319, 0.0075365147, -0.024229601, + 0.014469528, -0.004786435, 0.0011314518, 0.009256282, -0.04957284, + -0.0068631344, -0.010091242, -0.023295002, 0.03268865, 0.022269772, + 0.037733294, -0.015292435, -0.06330943, -0.00854154, 0.0027765913, + 0.0015374947, 0.0377278, 0.008772586, -0.01810512, -0.0025668603, + 0.014428339, 0.0027927365, 0.07493676, -0.022829408, -0.028912589, + 0.008928177, 0.011323267, 0.008405796, 0.016925976, 0.001739356, + -0.021090876, -0.0062678503, 0.010898773, -0.010470923, 0.015523946, + -0.027888289, 0.023872118, -0.048326068, 0.025968319, 0.0047795917, + -0.016123952, 0.00698612, -0.05154045, -0.003691712, -0.0101406425, + -0.0241034, 0.004006022, 0.0021649078, 0.0019942294, -0.009274028, + -0.006467623, -0.0010948133, -0.012350769, -0.0060371486, -0.0006392645, + 0.031422533, 0.015165475, -0.012650007, -0.005918423, 0.005781174, + -0.023262534, -0.0043034274, -0.010881872, -0.015937665, -0.0043740096, + -0.02981798, -0.0037422387, -0.029688178, 0.022320364, -0.0014900378, + -0.026122924, -0.04360404, 0.016354023, -0.02447563, 0.0205314, + 0.0042775236, -0.020184014, -0.0017819501, 0.036122557, 0.0036566693, + 0.07459051, -0.0035548757, 0.004874807, -0.028627345, -0.023153499, + 0.03710664, -0.000639956, -0.030509725, -0.005146651, -0.010251552, + 0.028408762, -0.008056198, -0.018420909, 0.02850364, -0.0075958185, + -0.008918139, 0.002778187, 0.06220242, -0.016280292, -0.026200369, + 0.05900717, -0.013802131, 0.005442568, -0.033114687, 0.010976371, + 0.008192846, 0.0031891295, 0.024811232, 0.009066575, -0.026441244, + 0.030676885, -0.014591597, -0.024314625, -0.037472498, -0.015021544, + -0.016501956, -0.0069196, 0.013831272, 0.056646723, 0.007946148, + -0.002477574, -0.030496774, -0.011770325, 0.06742689, -0.03180974, + -0.025615396 ], - "clip": { - "image": [ - -0.1503497064113617, - -0.26338839530944824, - -0.5655120611190796, - -0.07222450524568558, - 0.1557869017124176, - -0.04308490827679634, - -0.3871212303638458, - 1.2720786333084106, - 0.28359371423721313, - 0.2737857401371002, - 0.5060482025146484, - -0.1557128131389618, - 0.3533468246459961, - 0.01474827527999878, - -0.1428389996290207, - 0.11168161034584045, - 0.10963180661201477, - 0.1525699496269226, - 0.03198016434907913, - 0.37112998962402344, - 0.3841392397880554, - 0.1560487300157547, - -0.08011418581008911, - -0.0414440855383873, - -0.11277894675731659, - -0.19827546179294586, - -0.4201951026916504, - -0.047886259853839874, - 0.35359475016593933, - -0.32785624265670776, - -0.18916064500808716, - 0.16513843834400177, - -0.28110021352767944, - -0.02913704514503479, - -0.5625120401382446, - -0.27407437562942505, - 0.1405377984046936, - -0.18804830312728882, - 0.05606943368911743, - -1.8098962306976318, - -0.30152440071105957, - 0.36267736554145813, - -0.1972821056842804, - 0.4708936810493469, - -0.2627987265586853, - 0.17974340915679932, - -0.23960985243320465, - -0.12328865379095078, - -0.09735478460788727, - -0.23208953440189362, - -0.11159719526767731, - -0.40541020035743713, - 0.3979860842227936, - 0.03492492437362671, - 0.1079457551240921, - 0.12264229357242584, - 0.19060258567333221, - -0.06760812550783157, - -0.14953204989433289, - 0.020657464861869812, - -0.045544713735580444, - -0.49547797441482544, - 0.16501721739768982, - 0.409220427274704, - 0.025506392121315002, - -0.07141813635826111, - -0.08364877104759216, - -0.402360737323761, - 0.01254647970199585, - -0.3270309269428253, - -0.5074016451835632, - -0.14843346178531647, - 0.2588506042957306, - 0.19633343815803528, - -0.3371100425720215, - -0.20139610767364502, - 0.1256970763206482, - 0.2077842354774475, - 0.01762661337852478, - -0.09415242820978165, - -0.06982693821191788, - 0.05645954608917236, - 0.2230844497680664, - -0.3787960410118103, - 0.2884414792060852, - 0.15585792064666748, - 0.2103164792060852, - 0.5253552794456482, - 0.11847981810569763, - -0.4132201671600342, - -0.340353399515152, - 0.08250808715820312, - -9.104710578918457, - 0.5367923974990845, - 0.3082265555858612, - -0.054788097739219666, - 0.38076674938201904, - 0.10743528604507446, - -0.4294043183326721, - -0.6330970525741577, - -0.2239609658718109, - 0.14761009812355042, - 0.10569937527179718, - -0.03754069656133652, - -0.13122299313545227, - 0.052014321088790894, - -1.317906141281128, - 0.3992273807525635, - 0.33764201402664185, - -0.1352805197238922, - -0.05533941090106964, - -0.12124986946582794, - -0.21593835949897766, - -0.18228507041931152, - -0.22175437211990356, - -0.3678016662597656, - -0.1543227732181549, - -0.22483907639980316, - -0.19485993683338165, - 0.6772430539131165, - -0.38898220658302307, - 0.05267711728811264, - -0.1736181378364563, - 0.09440051019191742, - 0.05043143033981323, - -0.08631592988967896, - -0.27284130454063416, - -0.06817375123500824, - 0.17827574908733368, - 0.23594903945922852, - 0.0936334878206253, - 0.2405415028333664, - -0.03946849703788757, - 1.2118183374404907, - 0.09601867198944092, - 0.3550981879234314, - -0.2727612555027008, - -0.18505775928497314, - -0.09403584897518158, - -0.047489557415246964, - 0.18727253377437592, - -0.6417330503463745, - 0.15188869833946228, - 0.39904549717903137, - -0.3706473708152771, - 0.30436980724334717, - -0.20777952671051025, - 0.21207189559936523, - 0.15178178250789642, - 0.08812069892883301, - 0.07701482623815536, - -0.015871748328208923, - 0.4365525543689728, - 0.07790355384349823, - 0.41923996806144714, - 0.04688695818185806, - 0.11905840039253235, - -0.009424615651369095, - -0.11237604916095734, - 0.233009934425354, - -0.1393250972032547, - -0.13983336091041565, - 0.0062601566314697266, - -0.17631873488426208, - 0.2863244414329529, - 0.2513056695461273, - -0.18979927897453308, - -0.1352718472480774, - 0.20460893213748932, - -0.12186478078365326, - -0.09860621392726898, - -0.008292056620121002, - 0.1744387447834015, - -0.04108913987874985, - 0.21673311293125153, - -0.12838992476463318, - -0.14950336515903473, - -0.11025911569595337, - -0.050624407827854156, - 0.09065848588943481, - 0.12539921700954437, - 0.06310015916824341, - -0.03992995619773865, - 0.11594507843255997, - -0.0679289698600769, - -0.3390244245529175, - 0.37619641423225403, - -0.08030971884727478, - -0.16285991668701172, - 0.09049184620380402, - 0.19502219557762146, - -0.2555782198905945, - -0.3483888506889343, - 0.17943069338798523, - 0.4208906292915344, - -0.21696600317955017, - 0.09876695275306702, - 0.24968630075454712, - -0.25556057691574097, - 0.08045558631420135, - 0.1918676197528839, - -0.0391634926199913, - 0.2760481834411621, - -0.052914783358573914, - 0.2551308572292328, - -0.17824983596801758, - -0.10600590705871582, - -0.07306042313575745, - 0.5268251299858093, - 0.3004921078681946, - 0.11021347343921661, - 0.6560711860656738, - 0.3629128336906433, - -0.20159263908863068, - 0.3353482186794281, - 0.16813652217388153, - 0.3684561848640442, - 0.2572426199913025, - -0.1394597291946411, - -0.1081065982580185, - 0.3119319677352905, - 0.1547945886850357, - -0.1011475920677185, - 0.12471853196620941, - 0.05174969136714935, - 0.11287996917963028, - 0.13873223960399628, - -0.08381669968366623, - 0.08320209383964539, - 0.0005587935447692871, - -0.1399155706167221, - 0.26964786648750305, - -0.10338637977838516, - -0.635022759437561, - 0.07535472512245178, - 0.38028770685195923, - -0.08784236013889313, - 0.012145690619945526, - -0.4892919957637787, - -0.33344197273254395, - 0.2501947581768036, - 0.3406282663345337, - -0.29875123500823975, - 0.10239893198013306, - -0.4501486122608185, - -0.5407907962799072, - 0.02152668684720993, - -0.10010775923728943, - 0.3343956470489502, - 0.043100178241729736, - -0.28547170758247375, - -0.2451920360326767, - 0.11399783194065094, - -0.06306293606758118, - 0.0514407679438591, - 0.1943231225013733, - -0.0092984139919281, - -0.25484439730644226, - -0.16108255088329315, - -0.07067693769931793, - -0.00940198078751564, - -0.4148368835449219, - -0.033262044191360474, - 0.4516279101371765, - -0.24985359609127045, - 0.20121824741363525, - 0.4106118083000183, - -0.5621019005775452, - 0.046541422605514526, - 0.11314355581998825, - 0.3270842730998993, - 0.2676352858543396, - 0.12272027134895325, - 0.23055218160152435, - -0.3493749797344208, - 0.10709720849990845, - 0.44768550992012024, - -0.04293309152126312, - -0.03936107084155083, - 0.2270624190568924, - 0.16066348552703857, - 0.35009339451789856, - 0.04158636927604675, - 0.03541511297225952, - -0.11349448561668396, - 0.03199382871389389, - -0.0008175931870937347, - -0.317646861076355, - 0.025708124041557312, - -0.3693602383136749, - 0.5752753019332886, - 0.34967079758644104, - -0.46220043301582336, - 0.356049120426178, - -0.07281075417995453, - 0.3589980900287628, - -0.2318757176399231, - -0.13105618953704834, - 0.2065689116716385, - -0.06275947391986847, - 0.2042674571275711, - -0.4130946397781372, - -0.1220964565873146, - 0.5112582445144653, - -0.3460087478160858, - -0.8238317966461182, - 0.14678490161895752, - 0.04015420377254486, - -0.006557643413543701, - -0.18147070705890656, - -0.29465460777282715, - 0.2962910532951355, - 1.2113488912582397, - -0.14589866995811462, - 0.01400098204612732, - 0.5417146682739258, - 0.24798265099525452, - 0.07022618502378464, - 0.16047419607639313, - 0.19315579533576965, - 0.046383827924728394, - 1.2587014436721802, - 0.13661745190620422, - 0.15565162897109985, - 0.12895506620407104, - 0.257668673992157, - -0.2156587839126587, - 0.24780665338039398, - 0.1337527483701706, - 1.296618938446045, - -0.012920260429382324, - 0.21750850975513458, - 0.020899254828691483, - -0.4345206022262573, - 0.23699785768985748, - -0.057529620826244354, - -0.1588137149810791, - 0.2551633417606354, - 0.22220531105995178, - 0.3276950418949127, - -0.2745910882949829, - 0.03471551090478897, - 0.3112860321998596, - 0.2810869812965393, - 0.2718833386898041, - 0.01436258852481842, - 0.21428215503692627, - -0.17697292566299438, - 0.17256298661231995, - -0.22692933678627014, - 0.5784951448440552, - -0.025022784247994423, - 0.4769775867462158, - -0.08030113577842712, - -0.1672212779521942, - -0.6855964064598083, - -0.41609156131744385, - -0.26269420981407166, - -0.07503907382488251, - 0.19233737885951996, - -0.06759896874427795, - -0.09529343247413635, - 0.09245525300502777, - 0.08412420749664307, - -0.606180727481842, - -0.42773720622062683, - 0.17781633138656616, - -0.014700733125209808, - 0.1649937480688095, - 0.06545217335224152, - 0.056644488126039505, - -0.282379686832428, - 0.13707347214221954, - 0.20983807742595673, - 0.22233684360980988, - 0.22664642333984375, - -0.007336974143981934, - -0.017350323498249054, - -0.5800395011901855, - -0.3650877773761749, - -0.43565335869789124, - -0.11023098230361938, - 0.14169475436210632, - -0.2186550498008728, - 0.08632096648216248, - -0.2775184214115143, - 0.16572964191436768, - -0.054821647703647614, - 0.012959688901901245, - 0.10601806640625, - -0.5677923560142517, - -0.07860828936100006, - -0.11558187007904053, - -0.2668137848377228, - 0.37440529465675354, - 0.2550721764564514, - 0.4321855306625366, - -0.17515484988689423, - -0.7251265048980713, - -0.09783211350440979, - 0.03180219605565071, - 0.017610028386116028, - 0.4321219027042389, - 0.10047803819179535, - -0.20737037062644958, - -0.029400426894426346, - 0.16525790095329285, - 0.03198770433664322, - 0.8583027124404907, - -0.2614802122116089, - -0.33115634322166443, - 0.10226110368967056, - 0.12969331443309784, - 0.0962774008512497, - 0.1938648819923401, - 0.019921928644180298, - -0.24156862497329712, - -0.07179004698991776, - 0.12483114004135132, - -0.11993065476417542, - 0.17780695855617523, - -0.3194235563278198, - 0.2734241485595703, - -0.5535119771957397, - 0.29743289947509766, - 0.05474330484867096, - -0.18467886745929718, - 0.08001690357923508, - -0.5903282761573792, - -0.0422833077609539, - -0.11614792048931122, - -0.2760723829269409, - 0.04588329792022705, - 0.024796903133392334, - 0.0228412002325058, - -0.10622230172157288, - -0.0740782767534256, - -0.012539714574813843, - -0.14146174490451813, - -0.06914753466844559, - -0.007321890443563461, - 0.35990434885025024, - 0.173700213432312, - -0.1448882520198822, - -0.06778709590435028, - 0.06621548533439636, - -0.26644161343574524, - -0.04929056763648987, - -0.12463732063770294, - -0.1825450360774994, - -0.050098665058612823, - -0.34152570366859436, - -0.04286198690533638, - -0.34003931283950806, - 0.2556508779525757, - -0.017066851258277893, - -0.2992037534713745, - -0.49942725896835327, - 0.18731364607810974, - -0.2803362011909485, - 0.2351609766483307, - 0.04899322986602783, - -0.23118168115615845, - -0.020409684628248215, - 0.4137369692325592, - 0.041882023215293884, - 0.8543367385864258, - -0.0407162606716156, - 0.055834680795669556, - -0.3278890550136566, - -0.2651934027671814, - 0.42500773072242737, - -0.007330574095249176, - -0.34944894909858704, - -0.05894789099693298, - -0.11741754412651062, - 0.32538557052612305, - -0.09227307885885239, - -0.2109876275062561, - 0.32647180557250977, - -0.0870003029704094, - -0.10214601457118988, - 0.03182033449411392, - 0.7124476432800293, - -0.18646913766860962, - -0.30009108781814575, - 0.6758496761322021, - -0.15808522701263428, - 0.06233774125576019, - -0.37928515672683716, - 0.1257198303937912, - 0.09383836388587952, - 0.03652769327163696, - 0.28418007493019104, - 0.10384587943553925, - -0.3028501868247986, - 0.3513643443584442, - -0.16712747514247894, - -0.27849292755126953, - -0.42919832468032837, - -0.1720525026321411, - -0.18900787830352783, - -0.07925420254468918, - 0.15841886401176453, - 0.6488138437271118, - 0.09101241081953049, - -0.02837720513343811, - -0.3493013381958008, - -0.13481371104717255, - 0.7722870111465454, - -0.3643395006656647, - -0.29339152574539185 - ], - "text": [ - -0.051369935274124146, - -0.010725006461143494, - -0.11009600013494492, - -0.08671483397483826, - -0.1376112848520279, - 0.1834997683763504, - -0.13518013060092926, - -1.2175710201263428, - 0.21137484908103943, - -0.18747025728225708, - -0.04556228220462799, - 0.2627124488353729, - 0.026277093216776848, - 0.022868335247039795, - 0.3758852779865265, - -0.050838619470596313, - 0.29562997817993164, - 0.20151537656784058, - -0.02015306055545807, - 0.17147132754325867, - 0.14357797801494598, - -0.08850984275341034, - 0.02603408694267273, - -0.2109367996454239, - -0.08127906918525696, - 0.1460433006286621, - -0.1448330283164978, - 0.19058109819889069, - -0.16784363985061646, - 0.05917847529053688, - -0.08631782233715057, - -0.16270577907562256, - 0.16088467836380005, - 0.07714083790779114, - 0.03789868205785751, - 0.1956929862499237, - 0.0517326295375824, - -0.005512930452823639, - 0.047351937741041183, - -0.1909642219543457, - 0.010126054286956787, - -0.27198368310928345, - -0.08384807407855988, - 0.19902220368385315, - -0.03116871416568756, - -0.06306224316358566, - -0.13961419463157654, - 0.0525326132774353, - -0.008902296423912048, - 0.04049867391586304, - -0.0951114073395729, - -0.10472963750362396, - 0.15850012004375458, - 0.24902492761611938, - 0.26900243759155273, - 0.09727084636688232, - 0.01945263147354126, - 0.005587100982666016, - -0.0347808301448822, - 0.3930657207965851, - 0.22567930817604065, - -0.2227778136730194, - 0.051797881722450256, - -0.14941716194152832, - 0.06470682471990585, - -0.09520977735519409, - -0.07954283058643341, - 0.13018378615379333, - 0.25618842244148254, - -0.25645968317985535, - 0.060703642666339874, - -0.14322586357593536, - -0.18528185784816742, - 0.06203678250312805, - -0.03347042202949524, - -0.0051424503326416016, - -0.5088537335395813, - -0.1840534806251526, - 0.1848325878381729, - 0.052688274532556534, - -0.1965670883655548, - -0.20634984970092773, - 0.04999275505542755, - 0.2836085557937622, - 0.07079711556434631, - 0.44851911067962646, - 0.1325448453426361, - -0.41299372911453247, - 0.3724905252456665, - -0.14482314884662628, - -0.16094544529914856, - 0.0793130099773407, - -2.0048556327819824, - 0.3593715727329254, - 0.0009432807564735413, - 0.09033863246440887, - -0.13612447679042816, - 0.20753170549869537, - 0.056115202605724335, - -0.13530956208705902, - 0.20019665360450745, - 0.20947805047035217, - 0.011267989873886108, - -0.09066569805145264, - -0.0007635504007339478, - 0.0008731484413146973, - -0.44167017936706543, - -0.3381350040435791, - 0.06586254388093948, - -0.16046567261219025, - 0.13803477585315704, - 0.22680139541625977, - 0.06841648370027542, - -0.04588864743709564, - -0.15522271394729614, - -0.2671688497066498, - -0.20172488689422607, - -0.11681345105171204, - -0.2891874313354492, - 0.10894495993852615, - 0.016581878066062927, - 0.08065705746412277, - 0.03441505879163742, - -0.040672171860933304, - 0.23110319674015045, - -0.037525858730077744, - -0.08831262588500977, - -0.08008699119091034, - 0.09568070620298386, - 0.18096476793289185, - 0.06148066371679306, - -0.367189884185791, - -0.185734823346138, - 7.826099395751953, - -0.13802570104599, - 0.2800956070423126, - -0.22982153296470642, - -0.09386709332466125, - -0.15627573430538177, - 0.037662118673324585, - -0.13656215369701385, - -0.07198184728622437, - -0.322614461183548, - -0.006186550483107567, - -0.46435683965682983, - 0.12803569436073303, - -0.11408974975347519, - -0.006137289106845856, - -0.14130638539791107, - -0.07269007712602615, - -0.02471087872982025, - -0.01719208061695099, - 0.04794128239154816, - 0.015703098848462105, - -0.011346891522407532, - -0.2799241840839386, - 0.04372157156467438, - 0.1896992325782776, - -0.13235250115394592, - 0.2291410267353058, - -0.1392405480146408, - -0.34844085574150085, - -0.12091708183288574, - -0.06026161462068558, - -0.013828547671437263, - 0.11202779412269592, - 0.277866005897522, - -0.4359501302242279, - -0.06519348919391632, - -0.008573257364332676, - -0.14178775250911713, - -0.11654964089393616, - 0.27719753980636597, - -0.017636612057685852, - 0.05240386724472046, - -0.32324519753456116, - 0.0938805490732193, - 0.004964321851730347, - 0.32904016971588135, - 0.08840127289295197, - -0.22231429815292358, - 0.1611260622739792, - -0.25504690408706665, - 0.3107917904853821, - -0.15648266673088074, - -0.14989043772220612, - 0.14963313937187195, - 0.23006673157215118, - 0.2181217074394226, - -0.21481798589229584, - 0.01277482882142067, - -0.5298252105712891, - -0.38569486141204834, - -0.15905818343162537, - 0.38914966583251953, - -0.05173046141862869, - 0.016740625724196434, - -0.016509413719177246, - -0.250088095664978, - -0.18937590718269348, - 0.4233970642089844, - -0.18640199303627014, - -0.36222168803215027, - 0.13211572170257568, - -0.12458311021327972, - -0.17373305559158325, - -0.012691006064414978, - 0.2151109278202057, - 0.07149481773376465, - -0.3380594253540039, - 0.23961129784584045, - 0.17417001724243164, - 0.21425491571426392, - -0.3403988480567932, - -0.18938428163528442, - 0.26312142610549927, - -0.18937340378761292, - -0.2695082128047943, - 0.3464704751968384, - -0.026021957397460938, - 0.16364264488220215, - -0.18288151919841766, - 0.04950743168592453, - 0.1870364248752594, - 0.05565011501312256, - 0.04865076020359993, - 0.15473288297653198, - 0.07724197208881378, - -0.08260702341794968, - -0.23326924443244934, - 0.39012813568115234, - 0.30722934007644653, - -0.2548608183860779, - -0.10277849435806274, - 0.31726837158203125, - -0.04298326373100281, - -0.24891462922096252, - -0.041203320026397705, - -0.10855470597743988, - -0.0399850457906723, - 0.016369149088859558, - 0.2801763117313385, - 0.22338557243347168, - -0.2106330692768097, - -0.19607603549957275, - 0.40184202790260315, - 0.013695113360881805, - -0.024221263825893402, - 0.44003885984420776, - -0.10847768187522888, - -0.24203643202781677, - 0.26467961072921753, - -0.05966983735561371, - 0.12467685341835022, - -0.21156349778175354, - 0.3611948788166046, - 0.04894602298736572, - -0.09056273102760315, - 0.01707540452480316, - 0.076592355966568, - 0.027092672884464264, - -0.41262251138687134, - 0.13822166621685028, - 0.09504467248916626, - 0.002575445920228958, - -0.3847529888153076, - 0.18197567760944366, - 0.2379690706729889, - -0.05150367692112923, - 0.2653059959411621, - -0.15765774250030518, - 0.1880730241537094, - 0.021861106157302856, - -0.11092785745859146, - 0.08940362930297852, - 0.3101727366447449, - 0.014927387237548828, - -0.2911876440048218, - -0.16078510880470276, - -0.06823819875717163, - 0.19686979055404663, - 0.2373345047235489, - 0.08917825669050217, - 0.060556091368198395, - 0.017128556966781616, - 0.04221831634640694, - 0.07160300761461258, - 0.114939846098423, - 0.07517443597316742, - 0.1443810611963272, - -0.060701385140419006, - 0.09209010750055313, - -0.01475987583398819, - -0.0017274729907512665, - -0.06395074725151062, - -0.13522477447986603, - -0.3005772829055786, - -0.25623619556427, - 0.012192284688353539, - 0.17816416919231415, - -0.08423683047294617, - 0.1499529331922531, - -0.0016323104500770569, - -0.3038976788520813, - -0.011781338602304459, - 0.17713011801242828, - 0.2583661377429962, - -0.21670076251029968, - -0.1923849880695343, - -0.21187667548656464, - -0.027309250086545944, - -0.07633239030838013, - 7.819303512573242, - 0.16145764291286469, - -0.30060699582099915, - -0.1365492194890976, - -0.0063630640506744385, - -0.1362326741218567, - -0.16472479701042175, - 0.17343278229236603, - 0.24356240034103394, - 0.6252191066741943, - -0.09942090511322021, - -0.18946921825408936, - -0.3351835608482361, - 0.35568395256996155, - 0.11762797087430954, - -0.3656516671180725, - 0.19118718802928925, - -3.4465088844299316, - 0.22562159597873688, - -0.2150695025920868, - -0.06499379128217697, - 0.046910446137189865, - -0.05743652582168579, - -0.33147257566452026, - 0.2564307153224945, - 0.09259609133005142, - -0.08864793181419373, - -0.11425864696502686, - -0.19367149472236633, - -0.06630873680114746, - -0.007083814591169357, - -0.05304165184497833, - 0.2017112821340561, - 0.10328061133623123, - -0.04912707582116127, - -0.060788594186306, - 0.0784585103392601, - 0.28052055835723877, - -0.33164721727371216, - 0.017689572647213936, - -0.001561986282467842, - 0.39189884066581726, - -0.12148138135671616, - 0.1046065166592598, - 0.021570973098278046, - -0.012959085404872894, - 0.2276451140642166, - -0.15909601747989655, - 0.09318748116493225, - 0.03624418377876282, - 0.41286107897758484, - 0.4439883232116699, - -0.574947714805603, - 0.2063872516155243, - -0.11515115946531296, - 0.15398114919662476, - -0.10527925938367844, - 0.08131930232048035, - -0.10869307070970535, - -0.012484684586524963, - 0.12625205516815186, - 0.2636316418647766, - -0.07193168997764587, - 0.08365315198898315, - 0.07778038084506989, - -0.08492550998926163, - -0.31494593620300293, - -0.30747660994529724, - -0.12434972077608109, - -0.14759579300880432, - -0.0856187641620636, - -0.015103459358215332, - 0.21642933785915375, - 0.24999216198921204, - -0.25810444355010986, - -0.10437635332345963, - -0.09068337082862854, - 0.015814702957868576, - -0.13024312257766724, - -0.031961895525455475, - -0.050593845546245575, - 0.47274336218833923, - -0.24634651839733124, - 0.2797456383705139, - -0.2669164538383484, - -0.18765005469322205, - -0.12668517231941223, - -0.07038992643356323, - -0.15561681985855103, - 0.16797593235969543, - 0.08408385515213013, - 0.05721508711576462, - 0.055883586406707764, - 0.07930487394332886, - 0.06364040076732635, - 0.2569333016872406, - 0.14447829127311707, - -0.051580414175987244, - 0.06861788034439087, - 0.02721872180700302, - 0.06561324000358582, - 0.12471484392881393, - 0.1832738071680069, - -0.16585436463356018, - -0.08117838203907013, - -0.06088650971651077, - -0.2081316113471985, - 0.1322343945503235, - 0.01742127537727356, - -0.17162840068340302, - 0.0042439959943294525, - -0.13668203353881836, - -0.07343199849128723, - 0.2740647494792938, - 0.1601387858390808, - -0.12330605089664459, - -0.3267219662666321, - -0.3382628858089447, - 0.3690100908279419, - 0.18473923206329346, - -0.17084713280200958, - -0.05962555855512619, - -0.0792207270860672, - -0.06036840379238129, - -0.3774709701538086, - -0.05851760879158974, - 0.3206423819065094, - 0.0651409924030304, - -0.058551669120788574, - -0.36781901121139526, - -0.24176156520843506, - -0.4543174207210541, - -0.08731119334697723, - -0.045513980090618134, - -0.01741885021328926, - 0.034853145480155945, - -0.03033122420310974, - -0.5135809183120728, - -0.3117552399635315, - 0.11883510649204254, - 0.03182804584503174, - 0.304332971572876, - -0.018284954130649567, - 0.00013580918312072754, - -0.30424895882606506, - 0.14760465919971466, - -0.21293771266937256, - 0.23776817321777344, - -0.24130550026893616, - 0.05505291372537613, - 0.0050969719886779785, - -0.02879290282726288, - 0.030616000294685364, - -0.05236539989709854, - 0.0355415940284729, - -0.10839346051216125, - -0.15302366018295288, - 0.17155241966247559, - 0.024070631712675095, - 0.09996841847896576, - 0.3937240242958069, - 0.11562097072601318, - -0.03887242078781128, - 0.08371567726135254, - -0.034323759377002716, - -0.15314003825187683, - 0.21605932712554932, - 0.2662332057952881, - 0.01804041489958763, - -1.175376534461975, - -0.04393252357840538, - 0.1724747121334076, - 0.17443567514419556, - -0.03206155076622963, - -0.023589134216308594, - 0.0018865615129470825, - 0.195848286151886, - 0.10615190118551254, - -0.04240237921476364, - -0.1930321455001831, - -0.4515952467918396, - 0.06612616777420044, - -0.23157468438148499, - -0.5452786087989807, - 0.20692341029644012, - -0.0644945502281189, - 0.29818546772003174, - 0.4354862868785858, - 0.05778267979621887, - -0.0014227330684661865, - 0.07666969299316406, - 0.026816070079803467, - 0.24234911799430847, - 0.24426233768463135, - -0.06976135820150375, - -0.002162843942642212, - 0.040565088391304016, - 0.12837275862693787, - -0.28299012780189514, - 0.33027389645576477 - ] - }, - "facial-recognition": [ - { - "imageWidth": 600, - "imageHeight": 800, - "boundingBox": { - "x1": 690.0, - "y1": -89.0, - "x2": 833.0, - "y2": 96.0 - }, - "score": 0.03575617074966431, - "embedding": [ - -0.43665632605552673, - -0.5930537581443787, - -0.12699729204177856, - 0.3985028862953186, - 0.18789690732955933, - -0.25987985730171204, - 0.14818175137043, - -0.5422291159629822, - -0.0671021044254303, - -0.1319030374288559, - 0.056408628821372986, - 0.046094197779893875, - -0.14984919130802155, - 0.04322558641433716, - 0.023826055228710175, - -0.09063439071178436, - 0.07891753315925598, - -0.2935708165168762, - -0.6277135014533997, - -0.2904231548309326, - 0.18039005994796753, - 0.21837681531906128, - 0.17909450829029083, - -0.04030478745698929, - -0.03556056320667267, - -0.07568575441837311, - 0.12771207094192505, - -0.13466131687164307, - -0.23686951398849487, - 0.36429697275161743, - 0.2955845892429352, - 0.2086743414402008, - 0.11252538859844208, - 0.4769151210784912, - -0.05477480590343475, - 0.030100278556346893, - -0.049531325697898865, - 0.040458545088768005, - 0.23517772555351257, - 0.17130395770072937, - 0.17269372940063477, - 0.08591301739215851, - 0.046999648213386536, - -0.17151862382888794, - -0.24437746405601501, - 0.31105315685272217, - -0.23971444368362427, - -0.3174452781677246, - -0.026422448456287384, - -0.26203349232673645, - -0.1855347454547882, - -0.3104425370693207, - 0.6385250091552734, - 0.2749706506729126, - 0.006675023585557938, - 0.05378580465912819, - -0.20257888734340668, - -0.4839984178543091, - 0.2170865386724472, - -0.4781228303909302, - -0.12367318570613861, - -0.09901124238967896, - 0.1863373965024948, - 0.3114345669746399, - -0.12165745347738266, - 0.13010038435459137, - 0.1253461092710495, - 0.10728863626718521, - 0.3747178912162781, - -0.12302650511264801, - -0.1263274848461151, - -0.1562153398990631, - 0.260276198387146, - 0.15841349959373474, - 0.5164251327514648, - -0.31015825271606445, - 0.24754373729228973, - 0.10240863263607025, - -0.11818322539329529, - -0.14073267579078674, - 0.027111530303955078, - 0.09927573800086975, - -0.10066951811313629, - 0.4808421730995178, - -0.042361728847026825, - -0.08512216061353683, - 0.1369529813528061, - 0.3037898540496826, - 0.11138055473566055, - -0.3182139992713928, - -0.5708587169647217, - -0.14786981046199799, - 0.49985525012016296, - -0.23231984674930573, - 0.13856683671474457, - -0.5383139848709106, - -0.05995427444577217, - 0.2796868085861206, - -0.3244798481464386, - 0.16510958969593048, - 0.5714607834815979, - -0.1512063443660736, - 0.20110568404197693, - -0.49805426597595215, - -0.20088790357112885, - -0.046678103506565094, - 0.2465328425168991, - 0.02250899374485016, - -0.1409173160791397, - 0.3807566463947296, - 0.3381146490573883, - 0.05011143907904625, - -0.23718394339084625, - -0.20052078366279602, - -0.1408103108406067, - -0.34221047163009644, - 0.11998140811920166, - 0.24424004554748535, - 0.1376989781856537, - -0.25339990854263306, - -0.4108094573020935, - -0.28673601150512695, - -0.20673272013664246, - 0.46043485403060913, - 0.4178845286369324, - 0.10520245134830475, - -0.14469142258167267, - 0.08073662221431732, - -0.3737245798110962, - 0.13030850887298584, - -0.08456054329872131, - 0.21937909722328186, - -0.2270081490278244, - 0.3039504289627075, - 0.009785190224647522, - -0.07245694845914841, - 0.5029141306877136, - -0.24968916177749634, - 0.31788119673728943, - 0.12665590643882751, - -0.0364842563867569, - 0.21702805161476135, - -0.09277956187725067, - 0.17766769230365753, - -0.1201881617307663, - 0.008044496178627014, - -0.26986125111579895, - 0.29888248443603516, - -0.2848595380783081, - 0.30066442489624023, - -0.14317002892494202, - 0.5380052328109741, - 0.03084031492471695, - 0.023038823157548904, - 0.7386217713356018, - 0.003468744456768036, - 0.23797431588172913, - -0.11183349043130875, - 0.0678468644618988, - -0.23546601831912994, - 0.3935474753379822, - 0.005377739667892456, - 0.13494043052196503, - 0.1370638608932495, - -0.02944491058588028, - 0.14705342054367065, - -0.4812065362930298, - 0.27262356877326965, - -0.05196662247180939, - -0.3097267150878906, - 0.08714988827705383, - 0.10841232538223267, - -0.11757145822048187, - -0.5010467767715454, - -0.32369980216026306, - -0.21964779496192932, - -0.19810467958450317, - 0.14780977368354797, - -0.04624304920434952, - 0.24638010561466217, - -0.0671030580997467, - -0.31719157099723816, - 0.269559383392334, - 0.37117093801498413, - -0.3964727520942688, - 0.21541666984558105, - -0.12243526428937912, - -0.5392556190490723, - 0.0464024543762207, - 0.3657010495662689, - -0.042127206921577454, - -0.03063909336924553, - 0.2190942019224167, - 0.16005609929561615, - -0.03320079296827316, - -0.0949995294213295, - 0.33176088333129883, - 0.2253836989402771, - -0.016216054558753967, - -0.4241701662540436, - 0.5294063091278076, - -0.011592432856559753, - -0.21875163912773132, - -0.06394624710083008, - 0.24449443817138672, - -0.056584421545267105, - -0.09727923572063446, - -0.3978732228279114, - -0.11175088584423065, - 0.08514271676540375, - -0.05761905014514923, - -0.049855880439281464, - 0.17287252843379974, - 0.41813868284225464, - -0.3043341636657715, - 0.308758020401001, - -0.6604494452476501, - -0.13869403302669525, - 0.07291632890701294, - -0.0432523638010025, - 0.3740164041519165, - 0.17014223337173462, - -0.2646957337856293, - -0.346534788608551, - 0.13010692596435547, - 0.21517504751682281, - 0.740301251411438, - 0.3460628092288971, - -0.5115481615066528, - 0.46967509388923645, - -0.00984795019030571, - -0.13301578164100647, - -0.006184384226799011, - 0.013667777180671692, - 0.1699303388595581, - -0.3161454498767853, - 0.29015013575553894, - 0.6519798040390015, - 0.13776443898677826, - 0.5275151133537292, - 0.14721794426441193, - -0.11468257009983063, - -0.05685025453567505, - 0.21696926653385162, - -0.34107062220573425, - 0.0935278832912445, - -0.039688196033239365, - -0.13109605014324188, - 0.07406829297542572, - 0.1509123593568802, - 0.18835929036140442, - 0.19146737456321716, - -0.38988304138183594, - 0.4697469472885132, - -0.11145250499248505, - 0.039728209376335144, - 0.8268787264823914, - -0.09761662781238556, - -0.04332102835178375, - 0.2700135111808777, - 0.1207934319972992, - 0.05877719447016716, - 0.028245486319065094, - 0.20692101120948792, - 0.6844056844711304, - -0.3498411178588867, - -0.11976329982280731, - -0.396377295255661, - 0.23799002170562744, - 0.05757361650466919, - 0.07855354994535446, - 0.3798258602619171, - -0.036588408052921295, - 0.06831938028335571, - 0.10845135152339935, - -0.1865023374557495, - 0.0892765000462532, - -0.27789002656936646, - 0.31810519099235535, - 0.4251457452774048, - -0.035256966948509216, - -0.2807217240333557, - 0.07315991818904877, - 0.13499341905117035, - -0.11333761364221573, - -0.0008842200040817261, - 0.10874118655920029, - 0.296818345785141, - 0.008288972079753876, - 0.24116197228431702, - 0.01130960788577795, - -0.30095404386520386, - -0.4752867817878723, - 0.1992175281047821, - -0.16108214855194092, - 0.01783856749534607, - 0.5126014947891235, - -0.08679923415184021, - 0.3416588008403778, - 0.3235914707183838, - 0.2577085494995117, - 0.2144274115562439, - -0.1597137153148651, - -0.26682955026626587, - 0.22788375616073608, - -0.38956791162490845, - 0.08458005636930466, - -0.15929272770881653, - 0.2421140819787979, - -0.24793750047683716, - -0.3152828514575958, - 0.15945720672607422, - -0.16866135597229004, - 0.19472730159759521, - 0.40839430689811707, - 0.24238601326942444, - -0.2364349514245987, - 0.2985266149044037, - 0.12915723025798798, - 0.32706815004348755, - 0.5018091201782227, - -0.4053831696510315, - -0.023235589265823364, - -0.11315590143203735, - 0.007632426917552948, - -0.22626228630542755, - 0.28817909955978394, - -0.5816531181335449, - 0.15515218675136566, - -0.016097508370876312, - -0.016345905140042305, - 0.09585585445165634, - -0.010664891451597214, - 0.1402921974658966, - -0.22450345754623413, - -0.1396103948354721, - -0.4073222875595093, - -0.2477686107158661, - 0.12040270864963531, - -0.06779105961322784, - 0.44510531425476074, - 0.33206677436828613, - 0.1980731040239334, - -0.06460791826248169, - -0.25242677330970764, - -0.1272629350423813, - 0.4465603232383728, - -0.09844805300235748, - -0.18762269616127014, - 0.16189783811569214, - 0.23589575290679932, - -0.4479854106903076, - 0.21351021528244019, - -0.33205288648605347, - 0.28407710790634155, - -0.09519840776920319, - 0.03558272495865822, - -0.5180788636207581, - -0.273823618888855, - 0.03172869607806206, - 0.22928576171398163, - 0.4715774655342102, - -0.48383235931396484, - 0.01422564685344696, - -0.0810236856341362, - 0.1938459277153015, - -0.060681067407131195, - -0.03779950737953186, - -0.2875831723213196, - 0.024652402848005295, - 0.052711859345436096, - -0.22610321640968323, - 0.46830445528030396, - 0.2961697578430176, - 0.14641568064689636, - -0.24234764277935028, - 0.30126383900642395, - -0.011164642870426178, - 0.38622337579727173, - -0.124844990670681, - 0.3365071415901184, - 0.1739971935749054, - -0.27030548453330994, - 0.36919164657592773, - 0.2617016136646271, - -0.15373270213603973, - -0.4315706789493561, - 0.35697025060653687, - 0.04389272257685661, - -0.0654754787683487, - 0.5542906522750854, - 0.01996980607509613, - 0.43128183484077454, - -0.014291856437921524, - -0.33983248472213745, - 0.3250855803489685, - 0.21585243940353394, - -0.3445807695388794, - 0.23752379417419434, - 0.18115344643592834, - -0.25867414474487305, - -0.16033515334129333, - -0.16151021420955658, - -0.2330635040998459, - 0.14865264296531677, - -0.3179035186767578, - 0.2721554934978485, - -0.05992010980844498, - 0.161936953663826, - 0.07594355195760727, - -0.16281592845916748, - 0.44893062114715576, - -0.4305216670036316, - -0.038787614554166794, - -0.11722588539123535, - 0.07254093140363693, - -0.29970499873161316, - 0.1654062271118164, - -0.15089675784111023, - 0.12507867813110352, - 0.4372529983520508, - 0.13540124893188477, - 0.1339181661605835, - 0.013776864856481552, - 0.2695162892341614, - -0.29998600482940674, - -0.08645695447921753, - 0.12768305838108063, - 0.23375648260116577, - -0.07325033098459244, - -0.0443335622549057, - 0.047095995396375656, - 0.09582670032978058, - 0.2350919246673584, - 0.18061956763267517, - 0.3537953197956085, - 0.1293836236000061, - 0.33010751008987427, - 0.18966645002365112, - 0.07585176825523376, - 0.005968615412712097, - -0.13233722746372223, - 0.1710568368434906, - -0.020040705800056458, - -0.2805648446083069, - -0.09103457629680634, - 0.19508640468120575, - -0.21115663647651672, - -0.1624927520751953, - 0.07147711515426636, - -0.2013818919658661, - 0.15193939208984375, - 0.041464678943157196, - 0.010748650878667831, - 0.02909122407436371, - -0.22078242897987366, - 0.06446809321641922, - -0.2740311324596405, - -0.5190434455871582, - -0.20539811253547668, - 0.17622530460357666, - -0.28688907623291016, - 0.03056890144944191, - 0.29645925760269165, - -0.08893127739429474, - -0.4425870180130005, - 0.09070369601249695, - 0.08005643635988235, - 0.009866252541542053, - -0.07386989891529083, - 0.0668322741985321, - -0.34370890259742737, - 0.23668520152568817, - -0.08478264510631561, - -0.2740020751953125, - -0.3166845142841339, - -0.11662208288908005, - 0.20027956366539001, - 0.3377249538898468, - -0.30414462089538574, - -0.6180194616317749, - 0.0430230051279068, - -0.24733665585517883, - -0.20657919347286224, - -0.37058353424072266, - 0.0064486004412174225, - 0.2548515200614929, - 0.029220985248684883, - -0.4174918234348297, - 0.06511752307415009, - -0.37452077865600586, - 0.2269931435585022, - 0.22139698266983032, - 0.28097647428512573, - 0.10008563101291656, - -0.03995315730571747, - -0.3350542485713959, - 0.28511708974838257, - 0.18131467700004578, - -0.8796138763427734, - -0.04131913185119629, - -0.6237051486968994, - 0.0517050176858902, - 0.2354174554347992, - -0.0033704787492752075, - 0.15842004120349884, - 0.02000284567475319, - -0.22027355432510376, - -0.2730841040611267, - -0.23035141825675964, - -0.07705625146627426, - 0.002099640667438507 - ] - } + "text": [ + -0.0040579583, -0.00084722764, -0.008696951, -0.006850008, -0.010870523, + 0.014495447, -0.010678498, -0.09618138, 0.016697474, -0.014809047, + -0.0035991871, 0.020752821, 0.0020757387, 0.0018064519, 0.02969283, + -0.0040159826, 0.02335311, 0.015918557, -0.0015919582, 0.013545261, + 0.011341818, -0.006991808, 0.0020565446, -0.016662853, -0.0064206184, + 0.011536576, -0.01144098, 0.015054818, -0.013258694, 0.0046747606, + -0.00681864, -0.012852865, 0.012708946, 0.006093663, 0.0029938417, + 0.015458671, 0.0040865405, -0.0004354532, 0.0037405093, -0.015085074, + 0.0007998808, -0.021485215, -0.0066235093, 0.015721628, -0.002462181, + -0.0049815965, -0.011028703, 0.0041498104, -0.00070322485, 0.0031991813, + -0.0075132507, -0.008273014, 0.0125206, 0.019671565, 0.02124969, + 0.0076838327, 0.0015366874, 0.0004413452, -0.0027475145, 0.031049952, + 0.01782742, -0.01759819, 0.0040917504, -0.011803108, 0.0051114787, + -0.0075210207, -0.0062834355, 0.010283767, 0.02023746, -0.020258851, + 0.004795256, -0.011313993, -0.014636256, 0.004900588, -0.0026439666, + -0.0004062344, -0.040196564, -0.014539185, 0.014600707, 0.004162044, + -0.0155277, -0.016300475, 0.0039491425, 0.022403471, 0.0055926195, + 0.03543051, 0.01047029, -0.03262415, 0.02942466, -0.011440199, + -0.012713757, 0.0062652803, -0.15837249, 0.028388312, 7.452041e-5, + 0.007136255, -0.010753105, 0.016393846, 0.004432782, -0.010688704, + 0.015814407, 0.01654759, 0.0008900756, -0.007162077, -6.0264443e-5, + 6.894444e-5, -0.034889467, -0.026710762, 0.005202752, -0.012675916, + 0.010903986, 0.017916093, 0.005404525, -0.003624909, -0.012261727, + -0.021104869, -0.01593513, -0.009227664, -0.022844192, 0.008606035, + 0.0013098373, 0.00637147, 0.0027185818, -0.0032128745, 0.018255865, + -0.002964337, -0.006976183, -0.0063263937, 0.0075582284, 0.014295236, + 0.00485664, -0.029005948, -0.014672015, 0.61821824, -0.010903263, + 0.022125997, -0.018154604, -0.007414954, -0.012344926, 0.0029751004, + -0.010787629, -0.0056861844, -0.025484746, -0.0004887071, -0.036681578, + 0.010114145, -0.009012449, -0.00048479583, -0.011162415, -0.0057421126, + -0.0019520421, -0.0013580753, 0.0037870558, 0.0012404326, -0.00089634134, + -0.022112457, 0.0034537334, 0.014985147, -0.010455136, 0.018100852, + -0.010999219, -0.027524924, -0.009551776, -0.0047603208, -0.001092369, + 0.008849578, 0.021949856, -0.034437556, -0.0051499153, -0.0006772509, + -0.011200381, -0.009206776, 0.021897016, -0.0013931778, 0.0041396013, + -0.025534542, 0.0074160174, 0.00039215147, 0.025992293, 0.0069832364, + -0.0175616, 0.01272807, -0.020147255, 0.02455081, -0.01236127, + -0.011840565, 0.011820177, 0.018173985, 0.017230362, -0.016969377, + 0.0010091222, -0.04185319, -0.030467693, -0.012564729, 0.030740628, + -0.004086395, 0.0013223978, -0.0013041743, -0.01975558, -0.014959637, + 0.033446018, -0.014724724, -0.028613493, 0.010436393, -0.009841343, + -0.013723956, -0.0010025625, 0.016992576, 0.0056477, -0.026704773, + 0.018927934, 0.013758461, 0.016924908, -0.026889605, -0.01496036, + 0.02078507, -0.0149594685, -0.021289647, 0.027369255, -0.00205557, + 0.0129268635, -0.014446633, 0.0039108247, 0.014774828, 0.004396043, + 0.0038431762, 0.012223014, 0.0061016707, -0.006525442, -0.018426975, + 0.03081795, 0.024269402, -0.020132616, -0.008118887, 0.025062446, + -0.0033954307, -0.019662865, -0.0032548332, -0.008575233, -0.003158561, + 0.0012930515, 0.02213235, 0.017646195, -0.016638828, -0.0154889, + 0.031743307, 0.001081875, -0.0019133464, 0.034760594, -0.008569126, + -0.019119555, 0.020908207, -0.0047135833, 0.00984879, -0.016712308, + 0.028532412, 0.0038664932, -0.0071539935, 0.0013488994, 0.0060503725, + 0.0021401793, -0.032594826, 0.010918716, 0.0075080344, 0.00020341178, + -0.030393362, 0.014375046, 0.018798219, -0.0040685013, 0.020957684, + -0.012454064, 0.014856742, 0.0017268835, -0.008762698, 0.007062434, + 0.024501909, 0.0011791736, -0.023002177, -0.012701125, -0.0053904364, + 0.015551624, 0.018748082, 0.00704452, 0.0047835982, 0.0013530678, + 0.0033350172, 0.0056562345, 0.009079597, 0.0059383595, 0.011405316, + -0.004795079, 0.007274586, -0.0011659514, -0.0001364172, -0.0050517535, + -0.010681983, -0.023743946, -0.020241234, 0.0009631201, 0.014073974, + -0.00665422, 0.011845411, -0.0001289105, -0.024006248, -0.0009306585, + 0.0139923245, 0.020409467, -0.017118154, -0.0151973255, -0.016737074, + -0.002157259, -0.0060298163, 0.61768156, 0.01275426, -0.023746304, + -0.010786622, -0.00050265377, -0.010761652, -0.013012264, 0.013700237, + 0.019240098, 0.049388826, -0.007853694, -0.014966961, -0.026477624, + 0.02809707, 0.009291939, -0.028884491, 0.015102742, -0.27225503, + 0.01782282, -0.016989257, -0.0051341387, 0.0037056766, -0.004537146, + -0.026184445, 0.020256622, 0.0073146136, -0.0070027146, -0.009025792, + -0.015298917, -0.0052380697, -0.0005596046, -0.0041900063, 0.015934054, + 0.008158574, -0.0038807616, -0.0048019756, 0.0061978237, 0.022159556, + -0.02619826, 0.0013973896, -0.00012341494, 0.030957809, -0.009596324, + 0.008263321, 0.0017040323, -0.0010236687, 0.017982712, -0.012567677, + 0.007361281, 0.0028631007, 0.032613713, 0.035072606, -0.045417674, + 0.016303446, -0.009096281, 0.012163677, -0.008316459, 0.006423764, + -0.008586175, -0.0009862242, 0.009973197, 0.020825483, -0.005682246, + 0.0066081304, 0.0061441967, -0.00670868, -0.024878936, -0.024288971, + -0.009822955, -0.011659227, -0.0067634145, -0.0011930552, 0.017096667, + 0.01974797, -0.020388834, -0.008245143, -0.0071634515, 0.0012492571, + -0.010288493, -0.0025248309, -0.0039965925, 0.037344053, -0.019459987, + 0.022098366, -0.021084892, -0.014823354, -0.010007409, -0.005560381, + -0.012292843, 0.0132691385, 0.0066421456, 0.0045196814, 0.0044144704, + 0.0062646614, 0.0050272197, 0.020296281, 0.011412983, -0.0040745772, + 0.00542041, 0.0021500897, 0.005183101, 0.00985178, 0.014477596, + -0.0131016085, -0.0064126155, -0.004809687, -0.016441243, 0.010445765, + 0.0013761928, -0.0135576585, 0.0003352349, -0.010797083, -0.0058007324, + 0.021649584, 0.012650062, -0.009740497, -0.025809184, -0.026720846, + 0.029149767, 0.014593344, -0.0134959705, -0.004710099, -0.0062580137, + -0.0047687683, -0.029818097, -0.004622532, 0.02532894, 0.0051457905, + -0.0046252706, -0.02905562, -0.019097809, -0.035888474, -0.006897086, + -0.0035953831, -0.0013759647, 0.0027531807, -0.002395984, -0.040570017, + -0.02462688, 0.009387292, 0.0025142033, 0.02404064, -0.0014443685, + 1.0727288e-5, -0.024033979, 0.011659959, -0.016820917, 0.018782362, + -0.019061793, 0.0043488434, 0.00040266776, -0.0022744886, 0.0024185092, + -0.0041366024, 0.0028075825, -0.0085624885, -0.012087987, 0.013551666, + 0.0019014167, 0.007896904, 0.031102024, 0.0091334, -0.0030707342, + 0.0066130627, -0.002711352, -0.012097188, 0.017067473, 0.021030908, + 0.0014250687, -0.092848144, -0.0034704215, 0.013624546, 0.013779425, + -0.0025326884, -0.0018633928, 0.00014903376, 0.01547092, 0.008385425, + -0.0033495796, -0.015248458, -0.0356735, 0.005223496, -0.018293105, + -0.043073945, 0.016345823, -0.0050947615, 0.023554962, 0.034400985, + 0.0045644785, -0.00011241743, 0.0060564913, 0.0021182992, 0.01914424, + 0.019295372, -0.00551076, -0.00017086207, 0.0032044165, 0.010140755, + -0.022354674, 0.026089797 ] -} \ No newline at end of file + }, + "facial-recognition": [ + { + "imageWidth": 600, + "imageHeight": 800, + "boundingBox": { + "x1": 690.0, + "y1": -89.0, + "x2": 833.0, + "y2": 96.0 + }, + "score": 0.03575617074966431, + "embedding": [ + -0.43665668, -0.59305364, -0.12699714, 0.3985032, 0.1878969, + -0.25987914, 0.14818184, -0.542229, -0.06710237, -0.1319032, + 0.056408346, 0.046093762, -0.14984925, 0.043225512, 0.023826078, + -0.09063442, 0.07891726, -0.29357076, -0.6277133, -0.29042292, + 0.18038993, 0.21837695, 0.17909442, -0.040304773, -0.035560638, + -0.07568607, 0.1277122, -0.13466191, -0.2368693, 0.3642968, 0.29558533, + 0.20867407, 0.11252518, 0.47691494, -0.054775044, 0.030100197, + -0.049531147, 0.04045874, 0.23517768, 0.17130391, 0.17269331, + 0.08591308, 0.046999797, -0.17151847, -0.2443775, 0.3110528, + -0.23971468, -0.31744513, -0.026422635, -0.26203394, -0.18553479, + -0.31044272, 0.6385251, 0.27497086, 0.006674953, 0.053785797, + -0.20257844, -0.48399794, 0.21708605, -0.4781224, -0.12367296, + -0.099010885, 0.18633766, 0.31143454, -0.12165704, 0.13010044, + 0.12534627, 0.107288495, 0.37471777, -0.123026475, -0.1263274, + -0.15621608, 0.26027548, 0.15841314, 0.5164254, -0.31015784, 0.24754328, + 0.10240883, -0.1181829, -0.14073256, 0.027111322, 0.09927598, + -0.10066943, 0.4808423, -0.042361684, -0.08512197, 0.13695274, + 0.30378994, 0.11138052, -0.318214, -0.5708592, -0.14786953, 0.49985552, + -0.23231967, 0.13856675, -0.5383139, -0.059954256, 0.2796868, + -0.32447946, 0.16510965, 0.57146084, -0.15120608, 0.20110571, + -0.49805385, -0.2008879, -0.046678245, 0.24653266, 0.022508677, + -0.14091778, 0.38075653, 0.33811444, 0.05011098, -0.2371835, + -0.20052075, -0.14081016, -0.3422103, 0.11998144, 0.24423985, + 0.13769919, -0.25340003, -0.41080874, -0.28673622, -0.20673269, + 0.4604351, 0.4178845, 0.105202496, -0.1446912, 0.0807363, -0.37372503, + 0.13030809, -0.08456054, 0.21937889, -0.22700784, 0.3039499, + 0.009784861, -0.07245704, 0.50291365, -0.24968931, 0.3178813, + 0.12665558, -0.036484346, 0.21702805, -0.09277919, 0.17766781, + -0.12018812, 0.008044228, -0.26986086, 0.29888278, -0.28485933, + 0.30066437, -0.14316985, 0.53800535, 0.030840248, 0.023039162, + 0.73862207, 0.0034680888, 0.23797399, -0.11183337, 0.067846656, + -0.23546576, 0.39354736, 0.0053778216, 0.13494004, 0.1370637, + -0.029445097, 0.14705376, -0.48120612, 0.27262342, -0.05196667, + -0.3097266, 0.08714986, 0.10841283, -0.11757159, -0.5010461, + -0.32369986, -0.21964747, -0.19810468, 0.14780998, -0.04624281, + 0.24638015, -0.06710279, -0.31719172, 0.26955876, 0.37117082, + -0.3964724, 0.21541706, -0.12243534, -0.5392555, 0.04640211, 0.3657012, + -0.042127043, -0.030638859, 0.21909437, 0.16005577, -0.03320134, + -0.0949998, 0.33176076, 0.22538322, -0.016216129, -0.42417043, + 0.52940613, -0.011592716, -0.21875188, -0.06394625, 0.24449442, + -0.05658462, -0.09727913, -0.3978734, -0.11175068, 0.085142605, + -0.057618782, -0.0498557, 0.17287247, 0.41813853, -0.30433404, + 0.3087585, -0.6604493, -0.13869359, 0.072916515, -0.043251924, + 0.37401634, 0.17014223, -0.26469553, -0.34653437, 0.13010754, + 0.21517499, 0.74030113, 0.3460628, -0.5115478, 0.4696753, -0.009848075, + -0.1330159, -0.0061842054, 0.013667986, 0.16993025, -0.3161455, + 0.29015008, 0.65197945, 0.13776428, 0.5275149, 0.1472181, -0.114682674, + -0.05685012, 0.21696919, -0.34107065, 0.09352806, -0.03968816, + -0.13109599, 0.07406853, 0.15091223, 0.18835881, 0.19146737, -0.3898828, + 0.469747, -0.11145213, 0.039727956, 0.8268787, -0.09761663, + -0.043320894, 0.27001414, 0.12079324, 0.05877747, 0.028245524, + 0.20692128, 0.68440485, -0.34984088, -0.119763374, -0.39637753, + 0.23799005, 0.057573274, 0.07855352, 0.37982583, -0.0365879, + 0.068318695, 0.10845077, -0.18650186, 0.08927679, -0.27789003, + 0.31810492, 0.4251458, -0.03525705, -0.28072172, 0.07316002, 0.13499324, + -0.11333761, -0.0008841604, 0.10874095, 0.29681873, 0.008288942, + 0.24116173, 0.011309357, -0.3009541, -0.4752865, 0.19921738, + -0.16108191, 0.017838746, 0.51260126, -0.086799264, 0.34165853, + 0.32359147, 0.25770876, 0.21442738, -0.15971375, -0.26682994, + 0.22788364, -0.38956794, 0.084580205, -0.15929273, 0.24211408, + -0.24793725, -0.31528267, 0.15945697, -0.16866091, 0.19472758, 0.408394, + 0.24238603, -0.23643477, 0.29852632, 0.12915722, 0.327068, 0.501809, + -0.40538347, -0.023235738, -0.11315605, 0.007632144, -0.22626217, + 0.28817925, -0.5816528, 0.1551521, -0.016097836, -0.01634605, + 0.095855944, -0.010664792, 0.1402924, -0.22450349, -0.13961065, + -0.40732136, -0.24776831, 0.12040292, -0.06779129, 0.44510496, + 0.33206633, 0.19807269, -0.06460787, -0.2524265, -0.12726343, + 0.44656014, -0.09844789, -0.18762295, 0.16189753, 0.23589599, + -0.44798508, 0.2135099, -0.33205217, 0.28407755, -0.0951985, + 0.035582896, -0.51807857, -0.27382392, 0.03172898, 0.22928514, + 0.47157723, -0.48383215, 0.014225766, -0.08102345, 0.19384615, + -0.060681015, -0.037799604, -0.2875836, 0.024652202, 0.052712113, + -0.22610298, 0.46830428, 0.29616976, 0.14641494, -0.24234764, + 0.30126396, -0.011165038, 0.38622355, -0.12484505, 0.33650652, + 0.17399745, -0.2703057, 0.36919123, 0.26170117, -0.1537327, -0.43157104, + 0.35697, 0.043892622, -0.065475196, 0.5542902, 0.019970104, 0.43128124, + -0.014292087, -0.33983213, 0.3250854, 0.21585244, -0.34458104, + 0.23752448, 0.18115376, -0.2586738, -0.16033548, -0.16151018, + -0.23306333, 0.14865296, -0.31790328, 0.27215546, -0.059920013, + 0.16193654, 0.075943366, -0.16281635, 0.4489306, -0.43052202, + -0.038787995, -0.11722573, 0.07254093, -0.2997051, 0.16540596, + -0.15089649, 0.12507877, 0.43725327, 0.13540109, 0.13391787, + 0.013777234, 0.26951605, -0.2999856, -0.08645636, 0.12768297, + 0.23375636, -0.07325045, -0.04433371, 0.04709586, 0.09582621, + 0.23509142, 0.18061984, 0.35379466, 0.12938409, 0.33010754, 0.18966632, + 0.07585195, 0.0059688687, -0.13233723, 0.17105722, -0.020040989, + -0.2805646, -0.091034755, 0.1950869, -0.21115655, -0.16249251, + 0.07147664, -0.20138165, 0.15193966, 0.041464765, 0.01074836, + 0.029091328, -0.22078216, 0.06446775, -0.27403125, -0.51904315, + -0.20539844, 0.176225, -0.28688902, 0.030568387, 0.2964594, + -0.088931546, -0.4425866, 0.09070322, 0.08005672, 0.009866249, + -0.07386999, 0.06683251, -0.34370828, 0.23668535, -0.0847823, + -0.27400133, -0.31668398, -0.116622224, 0.20027944, 0.33772525, + -0.3041445, -0.61801887, 0.043022886, -0.24733649, -0.20657904, + -0.37058303, 0.00644885, 0.2548513, 0.029221226, -0.41749227, + 0.065117866, -0.3745206, 0.22699282, 0.22139677, 0.28097618, 0.10008535, + -0.039953396, -0.33505437, 0.28511694, 0.18131426, -0.879614, + -0.041319087, -0.62370497, 0.05170501, 0.23541749, -0.0033701807, + 0.15842043, 0.020002551, -0.22027364, -0.2730838, -0.23035137, + -0.077056274, 0.002099529 + ] + } + ] +} diff --git a/misc/release/notes.tmpl b/misc/release/notes.tmpl index 8310a7388b..9487cd1091 100644 --- a/misc/release/notes.tmpl +++ b/misc/release/notes.tmpl @@ -28,7 +28,7 @@ If you find the project helpful, you can support Immich via the following channe - One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) -- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7 It is a great way to let me know that you want me to continue developing and working on this project for years to come. diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index a9ac5b3381..6081988b7a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -180,4 +180,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift index 8e5db08b1e..c84b037daf 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift @@ -9,6 +9,7 @@ import Flutter import BackgroundTasks import path_provider_foundation import CryptoKit +import Network class BackgroundServicePlugin: NSObject, FlutterPlugin { @@ -335,8 +336,8 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - if (requireCharging ?? false) { + let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false + if (requireCharging) { UIDevice.current.isBatteryMonitoringEnabled = true if (UIDevice.current.batteryState == .unplugged) { // The device is unplugged and we have required charging @@ -347,6 +348,20 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { } } + // If we have required Wi-Fi, we can check the isExpensive property + let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false + if (requireWifi) { + let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) + let isExpensive = wifiMonitor.currentPath.isExpensive + if (isExpensive) { + // The network is expensive and we have required Wi-Fi + // Therfore, we will simply complete the task without + // running it + task.setTaskCompleted(success: true) + return + } + } + // Schedule the next sync task so we can run this again later scheduleBackgroundFetch() diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index 3b99718d79..6a61b00530 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; extension ContextHelper on BuildContext { - // Returns the current size from MediaQuery - Size get size => MediaQuery.sizeOf(this); + // Returns the current padding from MediaQuery + EdgeInsets get padding => MediaQuery.paddingOf(this); // Returns the current width from MediaQuery - double get width => size.width; + double get width => MediaQuery.sizeOf(this).width; // Returns the current height from MediaQuery - double get height => size.height; + double get height => MediaQuery.sizeOf(this).height; // Returns true if the app is running on a mobile device (!tablets) bool get isMobile => width < 550; diff --git a/mobile/lib/extensions/maplibrecontroller_extensions.dart b/mobile/lib/extensions/maplibrecontroller_extensions.dart index 0c1e62e308..e01655b3a8 100644 --- a/mobile/lib/extensions/maplibrecontroller_extensions.dart +++ b/mobile/lib/extensions/maplibrecontroller_extensions.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:flutter/services.dart'; @@ -6,6 +7,8 @@ import 'package:immich_mobile/modules/map/utils/map_utils.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; extension MapMarkers on MaplibreMapController { + static var _completer = Completer()..complete(); + Future addGeoJSONSourceForMarkers(List markers) async { return addSource( MapUtils.defaultSourceId, @@ -16,6 +19,12 @@ extension MapMarkers on MaplibreMapController { } Future reloadAllLayersForMarkers(List markers) async { + // Wait for previous reload to complete + if (!_completer.isCompleted) { + return _completer.future; + } + _completer = Completer(); + // !! Make sure to remove layers before sources else the native // maplibre library would crash when removing the source saying that // the source is still in use @@ -36,6 +45,8 @@ extension MapMarkers on MaplibreMapController { MapUtils.defaultHeatMapLayerId, MapUtils.defaultHeatMapLayerProperties, ); + + _completer.complete(); } Future addMarkerAtLatLng(LatLng centre) async { diff --git a/mobile/lib/modules/activities/widgets/activity_tile.dart b/mobile/lib/modules/activities/widgets/activity_tile.dart index da5dacd58a..cb434d22de 100644 --- a/mobile/lib/modules/activities/widgets/activity_tile.dart +++ b/mobile/lib/modules/activities/widgets/activity_tile.dart @@ -3,8 +3,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/modules/activities/models/activity.model.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; class ActivityTile extends HookConsumerWidget { @@ -106,7 +106,10 @@ class _ActivityAssetThumbnail extends StatelessWidget { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), image: DecorationImage( - image: ImmichImage.remoteThumbnailProviderForId(assetId), + image: ImmichRemoteImageProvider( + assetId: assetId, + isThumbnail: true, + ), fit: BoxFit.cover, ), ), diff --git a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart index 1d1961cce5..60b524aad3 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart @@ -49,7 +49,7 @@ class AlbumThumbnailListTile extends StatelessWidget { type: ThumbnailFormat.WEBP, ), httpHeaders: { - "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.WEBP), errorWidget: (context, url, error) => diff --git a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart index 57dd787719..f70c706f35 100644 --- a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart +++ b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart @@ -16,7 +16,11 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { }, child: Stack( children: [ - ImmichImage(asset, width: 500, height: 500), + ImmichImage.thumbnail( + asset, + width: 500, + height: 500, + ), ], ), ); diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 4bb97ea918..7555a79fe1 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -75,7 +75,7 @@ class SharingPage extends HookConsumerWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: ImmichImage( + child: ImmichImage.thumbnail( album.thumbnail, width: 60, height: 60, diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart new file mode 100644 index 0000000000..4c1e9fc5c8 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart @@ -0,0 +1,106 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:photo_manager/photo_manager.dart'; + +/// The local image provider for an asset +/// Only viable +class ImmichLocalImageProvider extends ImageProvider { + final Asset asset; + + ImmichLocalImageProvider({ + required this.asset, + }) : assert(asset.local != null, 'Only usable when asset.local is set'); + + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key + /// that describes the precise image to load. + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(asset); + } + + @override + ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) { + final chunkEvents = StreamController(); + return MultiImageStreamCompleter( + codec: _codec(key, decode, chunkEvents), + scale: 1.0, + chunkEvents: chunkEvents.stream, + informationCollector: () sync* { + yield ErrorDescription(asset.fileName); + }, + ); + } + + // Streams in each stage of the image as we ask for it + Stream _codec( + Asset key, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async* { + // Load a small thumbnail + final thumbBytes = await asset.local?.thumbnailDataWithSize( + const ThumbnailSize.square(256), + quality: 80, + ); + if (thumbBytes != null) { + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + final codec = await decode(buffer); + yield codec; + } else { + debugPrint("Loading thumb for ${asset.fileName} failed"); + } + + if (asset.isImage) { + /// Using 2K thumbnail for local iOS image to avoid double swiping issue + if (Platform.isIOS) { + final largeImageBytes = await asset.local + ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); + if (largeImageBytes == null) { + throw StateError( + "Loading thumb for local photo ${asset.fileName} failed", + ); + } + final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes); + final codec = await decode(buffer); + yield codec; + } else { + // Use the original file for Android + final File? file = await asset.local?.originFile; + if (file == null) { + throw StateError("Opening file for asset ${asset.fileName} failed"); + } + try { + final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); + final codec = await decode(buffer); + yield codec; + } catch (error) { + throw StateError("Loading asset ${asset.fileName} failed"); + } finally { + if (Platform.isIOS) { + // Clean up this file + await file.delete(); + } + } + } + } + + chunkEvents.close(); + } + + @override + bool operator ==(Object other) { + if (other is! ImmichLocalImageProvider) return false; + if (identical(this, other)) return true; + return asset == other.asset; + } + + @override + int get hashCode => asset.hashCode; +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart new file mode 100644 index 0000000000..9f9af7aded --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:openapi/api.dart' as api; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; + +/// Our Image Provider HTTP client to make the request +final _httpClient = HttpClient()..autoUncompress = false; + +/// The remote image provider +class ImmichRemoteImageProvider extends ImageProvider { + /// The [Asset.remoteId] of the asset to fetch + final String assetId; + + // If this is a thumbnail, we stop at loading the + // smallest version of the remote image + final bool isThumbnail; + + ImmichRemoteImageProvider({ + required this.assetId, + this.isThumbnail = false, + }); + + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key + /// that describes the precise image to load. + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture('$assetId,$isThumbnail'); + } + + @override + ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { + final id = key.split(',').first; + final chunkEvents = StreamController(); + return MultiImageStreamCompleter( + codec: _codec(id, decode, chunkEvents), + scale: 1.0, + chunkEvents: chunkEvents.stream, + ); + } + + /// Whether to show the original file or load a compressed version + bool get _useOriginal => Store.get( + AppSettingsEnum.loadOriginal.storeKey, + AppSettingsEnum.loadOriginal.defaultValue, + ); + + /// Whether to load the preview thumbnail first or not + bool get _loadPreview => Store.get( + AppSettingsEnum.loadPreview.storeKey, + AppSettingsEnum.loadPreview.defaultValue, + ); + + // Streams in each stage of the image as we ask for it + Stream _codec( + String key, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async* { + // Load a preview to the chunk events + if (_loadPreview || isThumbnail) { + final preview = getThumbnailUrlForRemoteId( + assetId, + type: api.ThumbnailFormat.WEBP, + ); + + yield await _loadFromUri( + Uri.parse(preview), + decode, + chunkEvents, + ); + } + + // Guard thumnbail rendering + if (isThumbnail) { + await chunkEvents.close(); + return; + } + + // Load the higher resolution version of the image + final url = getThumbnailUrlForRemoteId( + assetId, + type: api.ThumbnailFormat.JPEG, + ); + final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); + yield codec; + + // Load the final remote image + if (_useOriginal) { + // Load the original image + final url = getImageUrlFromId(assetId); + final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); + yield codec; + } + await chunkEvents.close(); + } + + // Loads the codec from the URI and sends the events to the [chunkEvents] stream + Future _loadFromUri( + Uri uri, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async { + final request = await _httpClient.getUrl(uri); + request.headers.add( + 'x-immich-user-token', + Store.get(StoreKey.accessToken), + ); + final response = await request.close(); + // Chunks of the completed image can be shown + final data = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: (cumulative, total) { + chunkEvents.add( + ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + ), + ); + }, + ); + + // Decode the response + final buffer = await ui.ImmutableBuffer.fromUint8List(data); + return decode(buffer); + } + + @override + bool operator ==(Object other) { + if (other is! ImmichRemoteImageProvider) return false; + if (identical(this, other)) return true; + return assetId == other.assetId; + } + + @override + int get hashCode => assetId.hashCode; +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart new file mode 100644 index 0000000000..8332d8d3d7 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; +import 'package:openapi/api.dart' as api; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; + +/// The remote image provider +class ImmichRemoteThumbnailProvider extends ImageProvider { + /// The [Asset.remoteId] of the asset to fetch + final String assetId; + + /// Our HTTP client to make the request + final _httpClient = HttpClient()..autoUncompress = false; + + ImmichRemoteThumbnailProvider({ + required this.assetId, + }); + + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key + /// that describes the precise image to load. + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(assetId); + } + + @override + ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { + final chunkEvents = StreamController(); + return MultiImageStreamCompleter( + codec: _codec(key, decode, chunkEvents), + scale: 1.0, + chunkEvents: chunkEvents.stream, + ); + } + + // Streams in each stage of the image as we ask for it + Stream _codec( + String key, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async* { + // Load a preview to the chunk events + final preview = getThumbnailUrlForRemoteId( + assetId, + type: api.ThumbnailFormat.WEBP, + ); + + yield await _loadFromUri( + Uri.parse(preview), + decode, + chunkEvents, + ); + + await chunkEvents.close(); + } + + // Loads the codec from the URI and sends the events to the [chunkEvents] stream + Future _loadFromUri( + Uri uri, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async { + final request = await _httpClient.getUrl(uri); + request.headers.add( + 'x-immich-user-token', + Store.get(StoreKey.accessToken), + ); + final response = await request.close(); + // Chunks of the completed image can be shown + final data = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: (cumulative, total) { + chunkEvents.add( + ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + ), + ); + }, + ); + + // Decode the response + final buffer = await ui.ImmutableBuffer.fromUint8List(data); + return decode(buffer); + } + + @override + bool operator ==(Object other) { + if (other is! ImmichRemoteImageProvider) return false; + if (identical(this, other)) return true; + return assetId == other.assetId; + } + + @override + int get hashCode => assetId.hashCode; +} diff --git a/mobile/lib/modules/asset_viewer/services/asset_description.service.dart b/mobile/lib/modules/asset_viewer/services/asset_description.service.dart index 9abf69a93a..09de411e5d 100644 --- a/mobile/lib/modules/asset_viewer/services/asset_description.service.dart +++ b/mobile/lib/modules/asset_viewer/services/asset_description.service.dart @@ -36,7 +36,7 @@ class AssetDescriptionService { Future readLatest(String assetRemoteId, int localExifId) async { final latestAssetFromServer = - await _api.assetApi.getAssetById(assetRemoteId); + await _api.assetApi.getAssetInfo(assetRemoteId); final localExifInfo = await _db.exifInfos.get(localExifId); if (latestAssetFromServer != null && localExifInfo != null) { diff --git a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart index 301fbdfe29..db527c6e23 100644 --- a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart +++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart @@ -25,12 +25,12 @@ class ImageViewerService { // Download LivePhotos image and motion part if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { var imageResponse = - await _apiService.assetApi.downloadFileOldWithHttpInfo( + await _apiService.downloadApi.downloadFileWithHttpInfo( asset.remoteId!, ); var motionReponse = - await _apiService.assetApi.downloadFileOldWithHttpInfo( + await _apiService.downloadApi.downloadFileWithHttpInfo( asset.livePhotoVideoId!, ); @@ -71,8 +71,8 @@ class ImageViewerService { return entity != null; } else { - var res = await _apiService.assetApi - .downloadFileOldWithHttpInfo(asset.remoteId!); + var res = await _apiService.downloadApi + .downloadFileWithHttpInfo(asset.remoteId!); if (res.statusCode != 200) { _log.severe( diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 3e898b8227..6fa41c8124 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -26,7 +26,6 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart' import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; import 'package:immich_mobile/modules/home/ui/upload_dialog.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; -import 'package:immich_mobile/shared/cache/original_image_provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; @@ -42,8 +41,6 @@ import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.da import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:isar/isar.dart'; import 'package:openapi/api.dart' show ThumbnailFormat; @@ -79,8 +76,6 @@ class GalleryViewerPage extends HookConsumerWidget { final isPlayingMotionVideo = useState(false); final isPlayingVideo = useState(false); Offset? localPosition; - final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; - final header = {"Authorization": authToken}; final currentIndex = useState(initialIndex); final currentAsset = loadAsset(currentIndex.value); final isTrashEnabled = @@ -137,53 +132,18 @@ class GalleryViewerPage extends HookConsumerWidget { void toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - /// Original (large) image of a remote asset. Required asset.isRemote - ImageProvider remoteOriginalProvider(Asset asset) => - CachedNetworkImageProvider( - getImageUrl(asset), - cacheKey: getImageCacheKey(asset), - headers: header, - ); - - /// Original (large) image of a local asset. Required asset.isLocal - ImageProvider localOriginalProvider(Asset asset) => - OriginalImageProvider(asset); - - ImageProvider finalImageProvider(Asset asset) { - if (ImmichImage.useLocal(asset)) { - return localOriginalProvider(asset); - } else if (isLoadOriginal.value) { - return remoteOriginalProvider(asset); - } else if (isLoadPreview.value) { - return ImmichImage.remoteThumbnailProvider(asset, jpeg, header); - } - return ImmichImage.remoteThumbnailProvider(asset, webp, header); - } - - Iterable allImageProviders(Asset asset) sync* { - if (ImmichImage.useLocal(asset)) { - yield ImmichImage.localImageProvider(asset); - yield localOriginalProvider(asset); - } else { - yield ImmichImage.remoteThumbnailProvider(asset, webp, header); - if (isLoadPreview.value) { - yield ImmichImage.remoteThumbnailProvider(asset, jpeg, header); - } - if (isLoadOriginal.value) { - yield remoteOriginalProvider(asset); - } - } - } - void precacheNextImage(int index) { void onError(Object exception, StackTrace? stackTrace) { // swallow error silently + debugPrint('Error precaching next image: $exception, $stackTrace'); } if (index < totalAssets && index >= 0) { final asset = loadAsset(index); - for (final imageProvider in allImageProviders(asset)) { - precacheImage(imageProvider, context, onError: onError); - } + precacheImage( + ImmichImage.imageProvider(asset: asset), + context, + onError: onError, + ); } } @@ -525,8 +485,7 @@ class GalleryViewerPage extends HookConsumerWidget { imageUrl: '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId', httpHeaders: { - "Authorization": - "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), @@ -768,6 +727,10 @@ class GalleryViewerPage extends HookConsumerWidget { isZoomed.value = state != PhotoViewScaleState.initial; ref.read(showControlsProvider.notifier).show = !isZoomed.value; }, + loadingBuilder: (context, event, index) => ImmichImage.thumbnail( + asset(), + fit: BoxFit.contain, + ), pageController: controller, scrollPhysics: isZoomed.value ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in @@ -784,47 +747,11 @@ class GalleryViewerPage extends HookConsumerWidget { stackIndex.value = -1; HapticFeedback.selectionClick(); }, - loadingBuilder: (context, event, index) { - final a = loadAsset(index); - if (ImmichImage.useLocal(a)) { - return Image( - image: ImmichImage.localImageProvider(a), - fit: BoxFit.contain, - ); - } - // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve - // Three-Stage Loading (WEBP -> JPEG -> Original) - final webPThumbnail = CachedNetworkImage( - imageUrl: getThumbnailUrl(a, type: webp), - cacheKey: getThumbnailCacheKey(a, type: webp), - httpHeaders: header, - progressIndicatorBuilder: (_, __, ___) => const Center( - child: ImmichLoadingIndicator(), - ), - fadeInDuration: const Duration(milliseconds: 0), - fit: BoxFit.contain, - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), - ); - - // loading the preview in the loadingBuilder only - // makes sense if the original is loaded in the builder - return isLoadPreview.value && isLoadOriginal.value - ? CachedNetworkImage( - imageUrl: getThumbnailUrl(a, type: jpeg), - cacheKey: getThumbnailCacheKey(a, type: jpeg), - httpHeaders: header, - fit: BoxFit.contain, - fadeInDuration: const Duration(milliseconds: 0), - placeholder: (_, __) => webPThumbnail, - errorWidget: (_, __, ___) => webPThumbnail, - ) - : webPThumbnail; - }, builder: (context, index) { final a = index == currentIndex.value ? asset() : loadAsset(index); - final ImageProvider provider = finalImageProvider(a); + final ImageProvider provider = + ImmichImage.imageProvider(asset: a); if (a.isImage && !isPlayingMotionVideo.value) { return PhotoViewGalleryPageOptions( diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 1df312095a..72aa397f67 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -21,40 +21,45 @@ class VideoViewerPage extends HookConsumerWidget { final Asset asset; final bool isMotionVideo; final Widget? placeholder; - final VoidCallback onVideoEnded; + final VoidCallback? onVideoEnded; final VoidCallback? onPlaying; final VoidCallback? onPaused; + final Duration hideControlsTimer; + final bool showControls; + final bool showDownloadingIndicator; const VideoViewerPage({ super.key, required this.asset, - required this.isMotionVideo, - required this.onVideoEnded, + this.isMotionVideo = false, + this.onVideoEnded, this.onPlaying, this.onPaused, this.placeholder, + this.showControls = true, + this.hideControlsTimer = const Duration(seconds: 5), + this.showDownloadingIndicator = true, }); @override Widget build(BuildContext context, WidgetRef ref) { if (asset.isLocal && asset.livePhotoVideoId == null) { final AsyncValue videoFile = ref.watch(_fileFamily(asset.local!)); - return videoFile.when( - data: (data) => VideoPlayer( - file: data, - isMotionVideo: false, - onVideoEnded: () {}, - ), - error: (error, stackTrace) => Icon( - Icons.image_not_supported_outlined, - color: context.primaryColor, - ), - loading: () => const Center( - child: SizedBox( - width: 75, - height: 75, - child: CircularProgressIndicator.adaptive(), + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: videoFile.when( + data: (data) => VideoPlayer( + file: data, + isMotionVideo: false, + onVideoEnded: () {}, ), + error: (error, stackTrace) => Icon( + Icons.image_not_supported_outlined, + color: context.primaryColor, + ), + loading: () => showDownloadingIndicator + ? const Center(child: ImmichLoadingIndicator()) + : Container(), ), ); } @@ -68,21 +73,30 @@ class VideoViewerPage extends HookConsumerWidget { children: [ VideoPlayer( url: videoUrl, - jwtToken: Store.get(StoreKey.accessToken), + accessToken: Store.get(StoreKey.accessToken), isMotionVideo: isMotionVideo, onVideoEnded: onVideoEnded, onPaused: onPaused, onPlaying: onPlaying, placeholder: placeholder, + hideControlsTimer: hideControlsTimer, + showControls: showControls, + showDownloadingIndicator: showDownloadingIndicator, ), - if (downloadAssetStatus == DownloadAssetStatus.loading) - SizedBox( + AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: (downloadAssetStatus == DownloadAssetStatus.loading && + showDownloadingIndicator) + ? 1.0 + : 0.0, + child: SizedBox( height: context.height, width: context.width, child: const Center( child: ImmichLoadingIndicator(), ), ), + ), ], ); } @@ -99,10 +113,12 @@ final _fileFamily = class VideoPlayer extends StatefulWidget { final String? url; - final String? jwtToken; + final String? accessToken; final File? file; final bool isMotionVideo; - final VoidCallback onVideoEnded; + final VoidCallback? onVideoEnded; + final Duration hideControlsTimer; + final bool showControls; final Function()? onPlaying; final Function()? onPaused; @@ -111,16 +127,23 @@ class VideoPlayer extends StatefulWidget { /// usually, a thumbnail of the video final Widget? placeholder; + final bool showDownloadingIndicator; + const VideoPlayer({ super.key, this.url, - this.jwtToken, + this.accessToken, this.file, - required this.onVideoEnded, + this.onVideoEnded, required this.isMotionVideo, this.onPlaying, this.onPaused, this.placeholder, + this.hideControlsTimer = const Duration( + seconds: 5, + ), + this.showControls = true, + this.showDownloadingIndicator = true, }); @override @@ -149,7 +172,7 @@ class _VideoPlayerState extends State { if (videoPlayerController.value.position == videoPlayerController.value.duration) { WakelockPlus.disable(); - widget.onVideoEnded(); + widget.onVideoEnded?.call(); } } }); @@ -160,7 +183,7 @@ class _VideoPlayerState extends State { videoPlayerController = widget.file == null ? VideoPlayerController.networkUrl( Uri.parse(widget.url!), - httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}, + httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""}, ) : VideoPlayerController.file(widget.file!); @@ -184,9 +207,9 @@ class _VideoPlayerState extends State { autoInitialize: true, allowFullScreen: false, allowedScreenSleep: false, - showControls: !widget.isMotionVideo, + showControls: widget.showControls && !widget.isMotionVideo, customControls: const VideoPlayerControls(), - hideControlsTimer: const Duration(seconds: 5), + hideControlsTimer: widget.hideControlsTimer, ); } @@ -216,9 +239,10 @@ class _VideoPlayerState extends State { child: Stack( children: [ if (widget.placeholder != null) widget.placeholder!, - const Center( - child: ImmichLoadingIndicator(), - ), + if (widget.showDownloadingIndicator) + const Center( + child: ImmichLoadingIndicator(), + ), ], ), ), diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index fd92222ef5..48d6f71cf9 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -302,8 +302,7 @@ class BackupService { onProgress: ((bytes, totalBytes) => uploadProgressCb(bytes, totalBytes)), ); - req.headers["Authorization"] = - "Bearer ${Store.get(StoreKey.accessToken)}"; + req.headers["x-immich-user-token"] = Store.get(StoreKey.accessToken); req.headers["Transfer-Encoding"] = "chunked"; req.fields['deviceAssetId'] = entity.id; diff --git a/mobile/lib/modules/backup/services/backup_verification.service.dart b/mobile/lib/modules/backup/services/backup_verification.service.dart index 1447abe90a..95e3a8d58b 100644 --- a/mobile/lib/modules/backup/services/backup_verification.service.dart +++ b/mobile/lib/modules/backup/services/backup_verification.service.dart @@ -136,7 +136,7 @@ class BackupVerificationService { ExifInfo? exif = remote.exifInfo; if (exif != null && exif.lat != null) return false; if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetApi.getAssetById(remote.remoteId!); + final dto = await apiService.assetApi.getAssetInfo(remote.remoteId!); if (dto != null && dto.exifInfo != null) { exif = ExifInfo.fromDto(dto.exifInfo!); } @@ -165,8 +165,8 @@ class BackupVerificationService { // (skip first few KBs containing metadata) final Uint64List localImage = _fakeDecodeImg(local, await file.readAsBytes()); - final res = await apiService.assetApi - .downloadFileOldWithHttpInfo(remote.remoteId!); + final res = await apiService.downloadApi + .downloadFileWithHttpInfo(remote.remoteId!); final Uint64List remoteImage = _fakeDecodeImg(remote, res.bodyBytes); final eq = const ListEquality().equals(remoteImage, localImage); diff --git a/mobile/lib/modules/backup/views/backup_options_page.dart b/mobile/lib/modules/backup/views/backup_options_page.dart index e323caf5be..ca787ec86b 100644 --- a/mobile/lib/modules/backup/views/backup_options_page.dart +++ b/mobile/lib/modules/backup/views/backup_options_page.dart @@ -264,7 +264,7 @@ class BackupOptionsPage extends HookConsumerWidget { "backup_controller_page_background_description", ).tr(), ), - if (isBackgroundEnabled && Platform.isAndroid) + if (isBackgroundEnabled) SwitchListTile.adaptive( title: const Text("backup_controller_page_background_wifi") .tr(), diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart index 7087456c97..bd671ca6ba 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -1,19 +1,22 @@ import 'dart:collection'; +import 'dart:developer'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + import 'asset_grid_data_structure.dart'; -import 'group_divider_title.dart'; import 'disable_multi_select_button.dart'; import 'draggable_scrollbar_custom.dart'; +import 'group_divider_title.dart'; typedef ImmichAssetGridSelectionListener = void Function( bool, @@ -72,6 +75,9 @@ class ImmichAssetGridViewState extends State { final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); + /// The timestamp when the haptic feedback was last invoked + int _hapticFeedbackTS = 0; + DateTime? _prevItemTime; bool _scrolling = false; final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); @@ -399,6 +405,8 @@ class ImmichAssetGridViewState extends State { if (widget.preselectedAssets != null) { _selectedAssets.addAll(widget.preselectedAssets!); } + + _itemPositionsListener.itemPositions.addListener(_hapticsListener); } @override @@ -415,6 +423,41 @@ class ImmichAssetGridViewState extends State { widget.visibleItemsListener?.call(values); } + void _hapticsListener() { + /// throttle interval for the haptic feedback in microseconds. + /// Currently set to 100ms. + const feedbackInterval = 100000; + + final values = _itemPositionsListener.itemPositions.value; + final start = values.firstOrNull; + + if (start != null) { + final pos = start.index; + final maxLength = widget.renderList.elements.length; + if (pos < 0 || pos >= maxLength) { + return; + } + + final date = widget.renderList.elements[pos].date; + + // only provide the feedback if the prev. date is known. + // Otherwise the app would provide the haptic feedback + // on startup. + if (_prevItemTime == null) { + _prevItemTime = date; + } else if (_prevItemTime?.year != date.year || + _prevItemTime?.month != date.month) { + _prevItemTime = date; + + final now = Timeline.now; + if (now > (_hapticFeedbackTS + feedbackInterval)) { + _hapticFeedbackTS = now; + HapticFeedback.heavyImpact(); + } + } + } + } + void _scrollToTop() { // for some reason, this is necessary as well in order // to correctly reposition the drag thumb scroll bar diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 6454d9ba24..6b0e83e527 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -136,10 +136,8 @@ class ThumbnailImage extends StatelessWidget { tag: isFromDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: ImmichImage( + child: ImmichImage.thumbnail( asset, - useGrayBoxPlaceholder: useGrayBoxPlaceholder, - fit: BoxFit.cover, ), ), ); diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index db78a5b9a4..0dd764e755 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -125,6 +125,19 @@ class ControlBottomAppBar extends ConsumerWidget { .tr(), onPressed: enabled ? onFavorite : null, ), + if (hasLocal && hasRemote && onDelete != null) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 90), + child: ControlBoxButton( + iconData: Icons.delete_sweep_outlined, + label: "control_bottom_app_bar_delete".tr(), + onPressed: enabled + ? () => handleRemoteDelete(!trashEnabled, onDelete!) + : null, + onLongPressed: + enabled ? () => showForceDeleteDialog(onDelete!) : null, + ), + ), if (hasRemote && onDeleteServer != null) ConstrainedBox( constraints: const BoxConstraints(maxWidth: 85), @@ -172,19 +185,6 @@ class ControlBottomAppBar extends ConsumerWidget { : null, ), ), - if (hasLocal && hasRemote && onDelete != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "control_bottom_app_bar_delete".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDelete!) - : null, - onLongPressed: - enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), if (hasRemote && onEditTime != null) ControlBoxButton( iconData: Icons.edit_calendar_outlined, diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index 79873ef6e8..4c8a12bb2f 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -515,7 +515,7 @@ class EmailInput extends StatelessWidget { } } -class PasswordInput extends StatelessWidget { +class PasswordInput extends HookConsumerWidget { final TextEditingController controller; final FocusNode? focusNode; final Function()? onSubmit; @@ -528,9 +528,11 @@ class PasswordInput extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final isPasswordVisible = useState(false); + return TextFormField( - obscureText: true, + obscureText: !isPasswordVisible.value, controller: controller, decoration: InputDecoration( labelText: 'login_form_label_password'.tr(), @@ -540,6 +542,14 @@ class PasswordInput extends StatelessWidget { fontWeight: FontWeight.normal, fontSize: 14, ), + suffixIcon: IconButton( + onPressed: () => isPasswordVisible.value = !isPasswordVisible.value, + icon: Icon( + isPasswordVisible.value + ? Icons.visibility_off_sharp + : Icons.visibility_sharp, + ), + ), ), autofillHints: const [AutofillHints.password], keyboardType: TextInputType.text, diff --git a/mobile/lib/modules/map/utils/map_utils.dart b/mobile/lib/modules/map/utils/map_utils.dart index 5fec97ea03..46af81ce1d 100644 --- a/mobile/lib/modules/map/utils/map_utils.dart +++ b/mobile/lib/modules/map/utils/map_utils.dart @@ -19,17 +19,17 @@ class MapUtils { ["linear"], ["heatmap-density"], 0.0, - "rgba(246,239,247,0.0)", - 0.2, - "rgb(208,209,230)", - 0.4, - "rgb(166,189,219)", - 0.6, - "rgb(103,169,207)", - 0.8, - "rgb(28,144,153)", + "rgba(103,58,183,0.0)", + 0.3, + "rgb(103,58,183)", + 0.5, + "rgb(33,149,243)", + 0.7, + "rgb(76,175,79)", + 0.95, + "rgb(255,235,59)", 1.0, - "rgb(1,108,89)", + "rgb(255,86,34)", ], heatmapIntensity: [ Expressions.interpolate, ["linear"], // @@ -44,6 +44,7 @@ class MapUtils { 4, 8, 9, 16, ], + heatmapOpacity: 0.7, ); static Map _addFeature(MapMarker marker) => { diff --git a/mobile/lib/modules/map/views/map_location_picker_page.dart b/mobile/lib/modules/map/views/map_location_picker_page.dart index a232638f74..b41c1184c1 100644 --- a/mobile/lib/modules/map/views/map_location_picker_page.dart +++ b/mobile/lib/modules/map/views/map_location_picker_page.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:immich_mobile/modules/map/utils/map_utils.dart'; -import 'package:geolocator/geolocator.dart'; @RoutePage() class MapLocationPickerPage extends HookConsumerWidget { @@ -46,15 +45,13 @@ class MapLocationPickerPage extends HookConsumerWidget { } Future getCurrentLocation() async { - var (currentLocation, locationPermission) = + var (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context); - if (locationPermission == LocationPermission.denied || - locationPermission == LocationPermission.deniedForever) { - return; - } + if (currentLocation == null) { return; } + var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); selectedLatLng.value = currentLatLng; @@ -67,40 +64,35 @@ class MapLocationPickerPage extends HookConsumerWidget { backgroundColor: ctx.themeData.cardColor, appBar: _AppBar(onClose: onClose), extendBodyBehindAppBar: true, - body: Column( - children: [ - style.widgetWhen( - onData: (style) => Expanded( - child: Container( - clipBehavior: Clip.antiAliasWithSaveLayer, - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(40), - bottomRight: Radius.circular(40), - ), - ), - child: MaplibreMap( - initialCameraPosition: - CameraPosition(target: initialLatLng, zoom: 12), - styleString: style, - onMapCreated: (mapController) => - controller.value = mapController, - onStyleLoadedCallback: onStyleLoaded, - onMapClick: onMapClick, - dragEnabled: false, - tiltGesturesEnabled: false, - myLocationEnabled: false, - attributionButtonMargins: const Point(20, 15), - ), - ), + primary: true, + body: style.widgetWhen( + onData: (style) => Container( + clipBehavior: Clip.antiAliasWithSaveLayer, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(40), + bottomRight: Radius.circular(40), ), ), - _BottomBar( - selectedLatLng: selectedLatLng, - onUseLocation: () => onClose(selectedLatLng.value), - onGetCurrentLocation: getCurrentLocation, + child: MaplibreMap( + initialCameraPosition: + CameraPosition(target: initialLatLng, zoom: 12), + styleString: style, + onMapCreated: (mapController) => + controller.value = mapController, + onStyleLoadedCallback: onStyleLoaded, + onMapClick: onMapClick, + dragEnabled: false, + tiltGesturesEnabled: false, + myLocationEnabled: false, + attributionButtonMargins: const Point(20, 15), ), - ], + ), + ), + bottomNavigationBar: _BottomBar( + selectedLatLng: selectedLatLng, + onUseLocation: () => onClose(selectedLatLng.value), + onGetCurrentLocation: getCurrentLocation, ), ), ), @@ -116,17 +108,15 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 25), - child: Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: ElevatedButton( - onPressed: onClose, - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - ), - child: const Icon(Icons.arrow_back_ios_new_rounded), + padding: const EdgeInsets.only(top: 25), + child: Align( + alignment: Alignment.centerLeft, + child: ElevatedButton( + onPressed: onClose, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), ), + child: const Icon(Icons.arrow_back_ios_new_rounded), ), ), ); @@ -150,38 +140,42 @@ class _BottomBar extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 150, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.public, size: 18), - const SizedBox(width: 15), - ValueListenableBuilder( - valueListenable: selectedLatLng, - builder: (_, value, __) => Text( - "${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}", + height: 150 + context.padding.bottom, + child: Padding( + padding: EdgeInsets.only(bottom: context.padding.bottom), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.public, size: 18), + const SizedBox(width: 15), + ValueListenableBuilder( + valueListenable: selectedLatLng, + builder: (_, value, __) => Text( + "${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}", + ), ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - onPressed: onUseLocation, - child: const Text("map_location_picker_page_use_location").tr(), - ), - ElevatedButton( - onPressed: onGetCurrentLocation, - child: const Icon(Icons.my_location), - ), - ], - ), - ], + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: onUseLocation, + child: + const Text("map_location_picker_page_use_location").tr(), + ), + ElevatedButton( + onPressed: onGetCurrentLocation, + child: const Icon(Icons.my_location), + ), + ], + ), + ], + ), ), ); } diff --git a/mobile/lib/modules/map/views/map_page.dart b/mobile/lib/modules/map/views/map_page.dart index 1882e32616..f1b9addb13 100644 --- a/mobile/lib/modules/map/views/map_page.dart +++ b/mobile/lib/modules/map/views/map_page.dart @@ -41,6 +41,7 @@ class MapPage extends HookConsumerWidget { final bottomSheetStreamController = useStreamController(); final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); final assetsDebouncer = useDebouncer(); + final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); final isLoading = useProcessingOverlay(); final scrollController = useScrollController(); final markerDebouncer = @@ -77,7 +78,9 @@ class MapPage extends HookConsumerWidget { // removes all sources and layers and re-adds them with the updated markers Future reloadLayers() async { if (mapController.value != null) { - mapController.value!.reloadAllLayersForMarkers(markers.value); + layerDebouncer.run( + () => mapController.value!.reloadAllLayersForMarkers(markers.value), + ); } } @@ -217,10 +220,9 @@ class MapPage extends HookConsumerWidget { } void onZoomToLocation() async { - final location = await MapUtils.checkPermAndGetLocation(context); - if (location.$2 != null) { - if (location.$2 == LocationPermission.unableToDetermine && - context.mounted) { + final (location, error) = await MapUtils.checkPermAndGetLocation(context); + if (error != null) { + if (error == LocationPermission.unableToDetermine && context.mounted) { ImmichToast.show( context: context, gravity: ToastGravity.BOTTOM, @@ -231,10 +233,10 @@ class MapPage extends HookConsumerWidget { return; } - if (mapController.value != null && location.$1 != null) { + if (mapController.value != null && location != null) { mapController.value!.animateCamera( CameraUpdate.newLatLngZoom( - LatLng(location.$1!.latitude, location.$1!.longitude), + LatLng(location.latitude, location.longitude), mapZoomToAssetLevel, ), duration: const Duration(milliseconds: 800), @@ -386,6 +388,7 @@ class _MapWithMarker extends StatelessWidget { dragEnabled: false, myLocationEnabled: false, attributionButtonPosition: AttributionButtonPosition.TopRight, + rotateGesturesEnabled: false, ), ), ValueListenableBuilder( diff --git a/mobile/lib/modules/map/widgets/map_asset_grid.dart b/mobile/lib/modules/map/widgets/map_asset_grid.dart index 411039f981..d1f187e258 100644 --- a/mobile/lib/modules/map/widgets/map_asset_grid.dart +++ b/mobile/lib/modules/map/widgets/map_asset_grid.dart @@ -231,7 +231,14 @@ class _MapSheetDragRegion extends StatelessWidget { physics: const ClampingScrollPhysics(), child: Card( margin: EdgeInsets.zero, - shape: context.isMobile ? null : const BeveledRectangleBorder(), + shape: context.isMobile + ? const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20), + ), + ) + : const BeveledRectangleBorder(), elevation: 0.0, child: Stack( children: [ diff --git a/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart b/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart index cec5114ee1..e7cd6f6227 100644 --- a/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart +++ b/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart @@ -89,8 +89,7 @@ class _AssetMarkerIcon extends StatelessWidget { imageUrl, cacheKey: cacheKey, headers: { - "Authorization": - "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, errorListener: (_) => const Icon(Icons.image_not_supported_outlined), diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index d9ccaed39f..b5f6ab46e0 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -3,31 +3,27 @@ import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; class MemoryCard extends StatelessWidget { final Asset asset; - final void Function() onTap; - final void Function() onClose; final String title; - final String? rightCornerText; final bool showTitle; + final Function()? onVideoEnded; const MemoryCard({ required this.asset, - required this.onTap, - required this.onClose, required this.title, required this.showTitle, - this.rightCornerText, + this.onVideoEnded, super.key, }); - String get authToken => 'Bearer ${Store.get(StoreKey.accessToken)}'; + String get accessToken => Store.get(StoreKey.accessToken); @override Widget build(BuildContext context) { @@ -55,7 +51,7 @@ class MemoryCard extends StatelessWidget { cacheKey: getThumbnailCacheKey( asset, ), - headers: {"Authorization": authToken}, + headers: {"x-immich-user-token": accessToken}, ), fit: BoxFit.cover, ), @@ -63,37 +59,49 @@ class MemoryCard extends StatelessWidget { child: Container(color: Colors.black.withOpacity(0.2)), ), ), - GestureDetector( - onTap: onTap, - child: ImmichImage( - asset, - fit: BoxFit.fitWidth, - height: double.infinity, - width: double.infinity, - type: ThumbnailFormat.JPEG, - preferredLocalAssetSize: 2048, - ), - ), - Positioned( - top: 2.0, - left: 2.0, - child: IconButton( - onPressed: onClose, - icon: const Icon(Icons.close_rounded), - color: Colors.grey[400], - ), - ), - Positioned( - right: 18.0, - top: 18.0, - child: Text( - rightCornerText ?? "", - style: TextStyle( - color: Colors.grey[200], - fontSize: 12.0, - fontWeight: FontWeight.bold, - ), - ), + LayoutBuilder( + builder: (context, constraints) { + // Determine the fit using the aspect ratio + BoxFit fit = BoxFit.fitWidth; + if (asset.width != null && asset.height != null) { + final aspectRatio = asset.height! / asset.width!; + final phoneAspectRatio = + constraints.maxWidth / constraints.maxHeight; + // Look for a 25% difference in either direction + if (phoneAspectRatio * .75 < aspectRatio && + phoneAspectRatio * 1.25 > aspectRatio) { + // Cover to look nice if we have nearly the same aspect ratio + fit = BoxFit.cover; + } + } + + if (asset.isImage) { + return Hero( + tag: 'memory-${asset.id}', + child: ImmichImage( + asset, + fit: fit, + height: double.infinity, + width: double.infinity, + ), + ); + } else { + return Hero( + tag: 'memory-${asset.id}', + child: VideoViewerPage( + asset: asset, + showDownloadingIndicator: false, + placeholder: ImmichImage( + asset, + fit: fit, + ), + hideControlsTimer: const Duration(seconds: 2), + onVideoEnded: onVideoEnded, + showControls: false, + ), + ); + } + }, ), if (showTitle) Positioned( diff --git a/mobile/lib/modules/memories/ui/memory_epilogue.dart b/mobile/lib/modules/memories/ui/memory_epilogue.dart index 4e32ae6ac5..8dd28637df 100644 --- a/mobile/lib/modules/memories/ui/memory_epilogue.dart +++ b/mobile/lib/modules/memories/ui/memory_epilogue.dart @@ -16,7 +16,7 @@ class _MemoryEpilogueState extends State late final _animationController = AnimationController( vsync: this, duration: const Duration( - seconds: 3, + seconds: 2, ), )..repeat( reverse: true, @@ -29,7 +29,7 @@ class _MemoryEpilogueState extends State super.initState(); _animation = CurvedAnimation( parent: _animationController, - curve: Curves.easeInOut, + curve: Curves.easeIn, ); } @@ -41,74 +41,82 @@ class _MemoryEpilogueState extends State @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.check_circle_outline_sharp, - color: immichDarkThemePrimaryColor, - size: 64.0, - ), - const SizedBox(height: 16.0), - Text( - 'All caught up', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, + return SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.check_circle_outline_sharp, + color: immichDarkThemePrimaryColor, + size: 64.0, + ), + const SizedBox(height: 16.0), + Text( + 'All caught up', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.white, + ), + ), + const SizedBox(height: 16.0), + Text( + 'Check back tomorrow for more memories', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ), + const SizedBox(height: 16.0), + TextButton( + onPressed: widget.onStartOver, + child: Text( + 'Start Over', + style: context.textTheme.displayMedium?.copyWith( + color: immichDarkThemePrimaryColor, ), - ), - const SizedBox(height: 16.0), - Text( - 'Check back tomorrow for more memories', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), - ), - const SizedBox(height: 16.0), - TextButton( - onPressed: widget.onStartOver, - child: Text( - 'Start Over', - style: context.textTheme.displayMedium?.copyWith( - color: immichDarkThemePrimaryColor, ), ), - ), - ], + ], + ), ), - ), - Column( - children: [ - SizedBox( - height: 48, - child: AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Transform.translate( - offset: Offset(0, 5 * _animationController.value), - child: child, - ); - }, - child: const Icon( - size: 32, - Icons.expand_less_sharp, - color: Colors.white, - ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Column( + children: [ + SizedBox( + height: 48, + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, 8 * _animationController.value), + child: child, + ); + }, + child: const Icon( + size: 32, + Icons.expand_less_sharp, + color: Colors.white, + ), + ), + ), + Text( + 'Swipe up to close', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ), + ], ), ), - Text( - 'Swipe up to close', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), - ), - ], - ), - ], + ), + ], + ), ); } } diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index 4e6d4f81a6..6b11e668db 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/mobile/lib/modules/memories/ui/memory_lane.dart @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; -import 'package:openapi/api.dart'; class MemoryLane extends HookConsumerWidget { const MemoryLane({super.key}); @@ -16,70 +15,74 @@ class MemoryLane extends HookConsumerWidget { final memoryLane = memoryLaneFutureProvider .whenData( (memories) => memories != null - ? Container( - margin: const EdgeInsets.only(top: 10, left: 10), + ? SizedBox( height: 200, child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: memories.length, + padding: const EdgeInsets.only( + right: 8.0, + bottom: 8, + top: 10, + left: 10, + ), itemBuilder: (context, index) { final memory = memories[index]; - return Padding( - padding: const EdgeInsets.only(right: 8.0, bottom: 8), - child: GestureDetector( - onTap: () { - HapticFeedback.heavyImpact(); - context.pushRoute( - MemoryRoute( - memories: memories, - memoryIndex: index, + return GestureDetector( + onTap: () { + HapticFeedback.heavyImpact(); + context.pushRoute( + MemoryRoute( + memories: memories, + memoryIndex: index, + ), + ); + }, + child: Stack( + children: [ + Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(13.0), ), - ); - }, - child: Stack( - children: [ - Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(13.0), + clipBehavior: Clip.hardEdge, + child: ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.2), + BlendMode.darken, ), - clipBehavior: Clip.hardEdge, - child: ColorFiltered( - colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.2), - BlendMode.darken, - ), + child: Hero( + tag: 'memory-${memory.assets[0].id}', child: ImmichImage( memory.assets[0], fit: BoxFit.cover, width: 130, height: 200, useGrayBoxPlaceholder: true, - type: ThumbnailFormat.JPEG, ), ), ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 114, - ), - child: Text( - memory.title, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Colors.white, - fontSize: 15, - ), + ), + Positioned( + bottom: 16, + left: 16, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 114, + ), + child: Text( + memory.title, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 15, ), ), ), - ], - ), + ), + ], ), ); }, diff --git a/mobile/lib/modules/memories/ui/memory_progress_indicator.dart b/mobile/lib/modules/memories/ui/memory_progress_indicator.dart new file mode 100644 index 0000000000..697d910a4e --- /dev/null +++ b/mobile/lib/modules/memories/ui/memory_progress_indicator.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class MemoryProgressIndicator extends StatelessWidget { + /// The number of ticks in the progress indicator + final int ticks; + + /// The current value of the indicator + final double value; + + const MemoryProgressIndicator({ + super.key, + required this.ticks, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final tickWidth = constraints.maxWidth / ticks; + return ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(2.0)), + child: Stack( + children: [ + LinearProgressIndicator( + value: value, + backgroundColor: Colors.grey[600], + color: immichDarkThemePrimaryColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + ticks, + (i) => Container( + width: tickWidth, + height: 4, + decoration: BoxDecoration( + border: i == 0 + ? null + : Border( + left: BorderSide( + color: context.colorScheme.onSecondaryContainer, + width: 1, + ), + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index fbc04feae5..199af835c9 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -7,9 +7,9 @@ import 'package:immich_mobile/modules/memories/models/memory.dart'; import 'package:immich_mobile/modules/memories/ui/memory_bottom_info.dart'; import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart'; +import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; -import 'package:openapi/api.dart' as api; @RoutePage() class MemoryPage extends HookConsumerWidget { @@ -24,15 +24,28 @@ class MemoryPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final memoryPageController = usePageController(initialPage: memoryIndex); - final memoryAssetPageController = usePageController(); final currentMemory = useState(memories[memoryIndex]); final currentAssetPage = useState(0); + final currentMemoryIndex = useState(memoryIndex); final assetProgress = useState( "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}", ); const bgColor = Colors.black; + /// The list of all of the asset page controllers + final memoryAssetPageControllers = + List.generate(memories.length, (i) => usePageController()); + + /// The main vertically scrolling page controller with each list of memories + final memoryPageController = usePageController(initialPage: memoryIndex); + + // The Page Controller that scrolls horizontally with all of the assets + useEffect(() { + // Memories is an immersive activity + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + return null; + }); + toNextMemory() { memoryPageController.nextPage( duration: const Duration(milliseconds: 500), @@ -43,7 +56,10 @@ class MemoryPage extends HookConsumerWidget { toNextAsset(int currentAssetIndex) { if (currentAssetIndex + 1 < currentMemory.value.assets.length) { // Go to the next asset - memoryAssetPageController.nextPage( + PageController controller = + memoryAssetPageControllers[currentMemoryIndex.value]; + + controller.nextPage( curve: Curves.easeInOut, duration: const Duration(milliseconds: 500), ); @@ -96,23 +112,21 @@ class MemoryPage extends HookConsumerWidget { // Gets the thumbnail url and precaches it final precaches = >[]; - precaches.add( - ImmichImage.precacheAsset( - asset, + precaches.addAll([ + precacheImage( + ImmichImage.imageProvider( + asset: asset, + ), context, - type: api.ThumbnailFormat.WEBP, - size: 2048, ), - ); - precaches.add( - ImmichImage.precacheAsset( - asset, + precacheImage( + ImmichImage.imageProvider( + asset: asset, + isThumbnail: true, + ), context, - type: api.ThumbnailFormat.JPEG, - size: 2048, ), - ); - + ]); await Future.wait(precaches); } @@ -154,67 +168,127 @@ class MemoryPage extends HookConsumerWidget { }, child: Scaffold( backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - HapticFeedback.mediumImpact(); - if (pageNumber < memories.length) { - currentMemory.value = memories[pageNumber]; - } + body: PopScope( + onPopInvoked: (didPop) { + // Remove immersive mode and go back to normal mode + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + }, + child: SafeArea( + child: PageView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + scrollDirection: Axis.vertical, + controller: memoryPageController, + onPageChanged: (pageNumber) { + HapticFeedback.mediumImpact(); + if (pageNumber < memories.length) { + currentMemoryIndex.value = pageNumber; + currentMemory.value = memories[pageNumber]; + } - currentAssetPage.value = 0; + currentAssetPage.value = 0; - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - return Column( - children: [ - Expanded( - child: PageView.builder( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - controller: memoryAssetPageController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Container( - color: Colors.black, - child: MemoryCard( - asset: asset, - onTap: () => toNextAsset(index), - onClose: () => context.popRoute(), - rightCornerText: assetProgress.value, - title: memories[mIndex].title, - showTitle: index == 0, - ), - ); - }, + updateProgressText(); + }, + itemCount: memories.length + 1, + itemBuilder: (context, mIndex) { + // Build last page + if (mIndex == memories.length) { + return MemoryEpilogue( + onStartOver: () => memoryPageController.animateToPage( + 0, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, + ); + } + // Build horizontal page + final assetController = memoryAssetPageControllers[mIndex]; + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + top: 8.0, + bottom: 2.0, + ), + child: AnimatedBuilder( + animation: assetController, + builder: (context, child) { + double value = 0.0; + if (assetController.hasClients) { + // We can only access [page] if this has clients + value = assetController.page ?? 0; + } + return MemoryProgressIndicator( + ticks: memories[mIndex].assets.length, + value: (value + 1) / memories[mIndex].assets.length, + ); + }, + ), + ), + Expanded( + child: Stack( + children: [ + PageView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + controller: assetController, + onPageChanged: onAssetChanged, + scrollDirection: Axis.horizontal, + itemCount: memories[mIndex].assets.length, + itemBuilder: (context, index) { + final asset = memories[mIndex].assets[index]; + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + toNextAsset(index); + }, + child: Container( + color: Colors.black, + child: MemoryCard( + asset: asset, + title: memories[mIndex].title, + showTitle: index == 0, + ), + ), + ); + }, + ), + Positioned( + top: 8, + left: 8, + child: MaterialButton( + minWidth: 0, + onPressed: () { + // auto_route doesn't invoke pop scope, so + // turn off full screen mode here + // https://github.com/Milad-Akarie/auto_route_library/issues/1799 + context.popRoute(); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + ); + }, + shape: const CircleBorder(), + color: Colors.white.withOpacity(0.2), + elevation: 0, + child: const Icon( + Icons.close_rounded, + color: Colors.white, + ), + ), + ), + ], + ), + ), + MemoryBottomInfo(memory: memories[mIndex]), + ], + ); + }, + ), ), ), ), diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart index e838c59e12..aa3403f2a1 100644 --- a/mobile/lib/modules/search/ui/curated_people_row.dart +++ b/mobile/lib/modules/search/ui/curated_people_row.dart @@ -51,7 +51,7 @@ class CuratedPeopleRow extends StatelessWidget { itemBuilder: (context, index) { final person = content[index]; final headers = { - "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }; return Padding( padding: const EdgeInsets.only(right: 18.0), diff --git a/mobile/lib/modules/search/ui/thumbnail_with_info.dart b/mobile/lib/modules/search/ui/thumbnail_with_info.dart index e9be93ad49..6d447526ce 100644 --- a/mobile/lib/modules/search/ui/thumbnail_with_info.dart +++ b/mobile/lib/modules/search/ui/thumbnail_with_info.dart @@ -46,8 +46,7 @@ class ThumbnailWithInfo extends StatelessWidget { fit: BoxFit.cover, imageUrl: imageUrl!, httpHeaders: { - "Authorization": - "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart index 8e09f47c34..0b7eeea51d 100644 --- a/mobile/lib/modules/search/views/person_result_page.dart +++ b/mobile/lib/modules/search/views/person_result_page.dart @@ -123,8 +123,7 @@ class PersonResultPage extends HookConsumerWidget { backgroundImage: NetworkImage( getFaceThumbnailUrl(personId), headers: { - "Authorization": - "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, ), ), diff --git a/mobile/lib/modules/trash/services/trash.service.dart b/mobile/lib/modules/trash/services/trash.service.dart index cfcae27653..9a9ff5d0b6 100644 --- a/mobile/lib/modules/trash/services/trash.service.dart +++ b/mobile/lib/modules/trash/services/trash.service.dart @@ -22,7 +22,7 @@ class TrashService { try { List remoteIds = assetList.where((a) => a.isRemote).map((e) => e.remoteId!).toList(); - await _apiService.assetApi.restoreAssetsOld(BulkIdsDto(ids: remoteIds)); + await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteIds)); return true; } catch (error, stack) { _log.severe("Cannot restore assets ${error.toString()}", error, stack); @@ -32,7 +32,7 @@ class TrashService { Future emptyTrash() async { try { - await _apiService.assetApi.emptyTrashOld(); + await _apiService.trashApi.emptyTrash(); } catch (error, stack) { _log.severe("Cannot empty trash ${error.toString()}", error, stack); } @@ -40,7 +40,7 @@ class TrashService { Future restoreTrash() async { try { - await _apiService.assetApi.restoreTrashOld(); + await _apiService.trashApi.restoreTrash(); } catch (error, stack) { _log.severe("Cannot restore trash ${error.toString()}", error, stack); } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index c5986677e7..f32c75c37e 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -355,6 +355,9 @@ abstract class _$AppRouter extends RootStackRouter { onPlaying: args.onPlaying, onPaused: args.onPaused, placeholder: args.placeholder, + showControls: args.showControls, + hideControlsTimer: args.hideControlsTimer, + showDownloadingIndicator: args.showDownloadingIndicator, ), ); }, @@ -1390,11 +1393,14 @@ class VideoViewerRoute extends PageRouteInfo { VideoViewerRoute({ Key? key, required Asset asset, - required bool isMotionVideo, - required void Function() onVideoEnded, + bool isMotionVideo = false, + void Function()? onVideoEnded, void Function()? onPlaying, void Function()? onPaused, Widget? placeholder, + bool showControls = true, + Duration hideControlsTimer = const Duration(seconds: 5), + bool showDownloadingIndicator = true, List? children, }) : super( VideoViewerRoute.name, @@ -1406,6 +1412,9 @@ class VideoViewerRoute extends PageRouteInfo { onPlaying: onPlaying, onPaused: onPaused, placeholder: placeholder, + showControls: showControls, + hideControlsTimer: hideControlsTimer, + showDownloadingIndicator: showDownloadingIndicator, ), initialChildren: children, ); @@ -1420,11 +1429,14 @@ class VideoViewerRouteArgs { const VideoViewerRouteArgs({ this.key, required this.asset, - required this.isMotionVideo, - required this.onVideoEnded, + this.isMotionVideo = false, + this.onVideoEnded, this.onPlaying, this.onPaused, this.placeholder, + this.showControls = true, + this.hideControlsTimer = const Duration(seconds: 5), + this.showDownloadingIndicator = true, }); final Key? key; @@ -1433,7 +1445,7 @@ class VideoViewerRouteArgs { final bool isMotionVideo; - final void Function() onVideoEnded; + final void Function()? onVideoEnded; final void Function()? onPlaying; @@ -1441,8 +1453,14 @@ class VideoViewerRouteArgs { final Widget? placeholder; + final bool showControls; + + final Duration hideControlsTimer; + + final bool showDownloadingIndicator; + @override String toString() { - return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder}'; + return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}'; } } diff --git a/mobile/lib/shared/cache/custom_image_cache.dart b/mobile/lib/shared/cache/custom_image_cache.dart index 650ab81c6b..79338cbda5 100644 --- a/mobile/lib/shared/cache/custom_image_cache.dart +++ b/mobile/lib/shared/cache/custom_image_cache.dart @@ -1,6 +1,5 @@ import 'package:flutter/painting.dart'; - -import 'original_image_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_provider.dart'; /// [ImageCache] that uses two caches for small and large images /// so that a single large image does not evict all small iamges @@ -34,7 +33,7 @@ final class CustomImageCache implements ImageCache { @override bool containsKey(Object key) => - (key is OriginalImageProvider ? _large : _small).containsKey(key); + (key is ImmichLocalImageProvider ? _large : _small).containsKey(key); @override int get currentSize => _small.currentSize + _large.currentSize; @@ -44,7 +43,7 @@ final class CustomImageCache implements ImageCache { @override bool evict(Object key, {bool includeLive = true}) => - (key is OriginalImageProvider ? _large : _small) + (key is ImmichLocalImageProvider ? _large : _small) .evict(key, includeLive: includeLive); @override @@ -60,10 +59,10 @@ final class CustomImageCache implements ImageCache { ImageStreamCompleter Function() loader, { ImageErrorListener? onError, }) => - (key is OriginalImageProvider ? _large : _small) + (key is ImmichLocalImageProvider ? _large : _small) .putIfAbsent(key, loader, onError: onError); @override ImageCacheStatus statusForKey(Object key) => - (key is OriginalImageProvider ? _large : _small).statusForKey(key); + (key is ImmichLocalImageProvider ? _large : _small).statusForKey(key); } diff --git a/mobile/lib/shared/cache/original_image_provider.dart b/mobile/lib/shared/cache/original_image_provider.dart deleted file mode 100644 index e06d815a49..0000000000 --- a/mobile/lib/shared/cache/original_image_provider.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; - -/// Loads the original image for local assets -@immutable -final class OriginalImageProvider extends ImageProvider { - final Asset asset; - - const OriginalImageProvider(this.asset); - - @override - Future obtainKey(ImageConfiguration configuration) => - SynchronousFuture(this); - - @override - ImageStreamCompleter loadImage( - OriginalImageProvider key, - ImageDecoderCallback decode, - ) => - MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: 1.0, - informationCollector: () sync* { - yield ErrorDescription(asset.fileName); - }, - ); - - Future _loadAsync( - OriginalImageProvider key, - ImageDecoderCallback decode, - ) async { - final ui.ImmutableBuffer buffer; - if (asset.isImage) { - final File? file = await asset.local?.originFile; - if (file == null) { - throw StateError("Opening file for asset ${asset.fileName} failed"); - } - try { - buffer = await ui.ImmutableBuffer.fromFilePath(file.path); - } catch (error) { - throw StateError("Loading asset ${asset.fileName} failed"); - } - } else { - final thumbBytes = await asset.local?.thumbnailData; - if (thumbBytes == null) { - throw StateError("Loading thumb for video ${asset.fileName} failed"); - } - buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - } - try { - final codec = await decode(buffer); - debugPrint("Decoded image ${asset.fileName}"); - return codec; - } catch (error) { - throw StateError("Decoding asset ${asset.fileName} failed"); - } - } - - @override - bool operator ==(Object other) { - if (other is! OriginalImageProvider) return false; - if (identical(this, other)) return true; - return asset == other.asset; - } - - @override - int get hashCode => asset.hashCode; -} diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index 0ed69dea75..afd49adc6a 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -34,7 +34,10 @@ class Asset { isTrashed = remote.isTrashed, isReadOnly = remote.isReadOnly, isOffline = remote.isOffline, - stackParentId = remote.stackParentId, + // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // stack handling to properly handle it + stackParentId = + remote.stackParentId == remote.id ? null : remote.stackParentId, stackCount = remote.stackCount; Asset.local(AssetEntity local, List hash) @@ -290,7 +293,6 @@ class Asset { width: a.width ?? width, height: a.height ?? height, exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo, - stackCount: a.stackCount ?? stackCount, ); } else if (isRemote) { return _copyWith( @@ -305,7 +307,9 @@ class Asset { id: id, remoteId: remoteId, livePhotoVideoId: livePhotoVideoId, - stackParentId: stackParentId, + // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // stack handling to properly handle it + stackParentId: stackParentId == remoteId ? null : stackParentId, stackCount: stackCount, isFavorite: isFavorite, isArchived: isArchived, @@ -323,8 +327,10 @@ class Asset { width: a.width, height: a.height, livePhotoVideoId: a.livePhotoVideoId, - stackParentId: a.stackParentId, - stackCount: a.stackCount ?? stackCount, + // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // stack handling to properly handle it + stackParentId: a.stackParentId == a.remoteId ? null : a.stackParentId, + stackCount: a.stackCount, // isFavorite + isArchived are not set by device-only assets isFavorite: a.isFavorite, isArchived: a.isArchived, @@ -431,17 +437,17 @@ class Asset { "remoteId": "${remoteId ?? "N/A"}", "localId": "${localId ?? "N/A"}", "checksum": "$checksum", - "ownerId": $ownerId, + "ownerId": $ownerId, "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", "stackCount": "$stackCount", "stackParentId": "${stackParentId ?? "N/A"}", "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, + "fileModifiedAt": "$fileModifiedAt", + "updatedAt": "$updatedAt", + "durationInSeconds": $durationInSeconds, "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, + "fileName": "$fileName", + "isFavorite": $isFavorite, "isRemote": $isRemote, "storage": "$storage", "width": ${width ?? "N/A"}, diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index c78777da5a..6b12916d27 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -106,6 +108,10 @@ class WebsocketNotifier extends StateNotifier { final accessToken = Store.get(StoreKey.accessToken); try { final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); + final headers = {"x-immich-user-token": accessToken}; + if (endpoint.userInfo.isNotEmpty) { + headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; + } debugPrint("Attempting to connect to websocket"); // Configure socket transports must be specified @@ -118,7 +124,7 @@ class WebsocketNotifier extends StateNotifier { .enableForceNew() .enableForceNewConnection() .enableAutoConnect() - .setExtraHeaders({"Authorization": "Bearer $accessToken"}) + .setExtraHeaders(headers) .build(), ); diff --git a/mobile/lib/shared/services/api.service.dart b/mobile/lib/shared/services/api.service.dart index 656de826cf..9e44136fe7 100644 --- a/mobile/lib/shared/services/api.service.dart +++ b/mobile/lib/shared/services/api.service.dart @@ -24,6 +24,8 @@ class ApiService { late SharedLinkApi sharedLinkApi; late SystemConfigApi systemConfigApi; late ActivityApi activityApi; + late DownloadApi downloadApi; + late TrashApi trashApi; ApiService() { final endpoint = Store.tryGet(StoreKey.serverEndpoint); @@ -31,12 +33,12 @@ class ApiService { setEndpoint(endpoint); } } - String? _authToken; + String? _accessToken; setEndpoint(String endpoint) { _apiClient = ApiClient(basePath: endpoint); - if (_authToken != null) { - setAccessToken(_authToken!); + if (_accessToken != null) { + setAccessToken(_accessToken!); } userApi = UserApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient); @@ -51,6 +53,8 @@ class ApiService { sharedLinkApi = SharedLinkApi(_apiClient); systemConfigApi = SystemConfigApi(_apiClient); activityApi = ActivityApi(_apiClient); + downloadApi = DownloadApi(_apiClient); + trashApi = TrashApi(_apiClient); } Future resolveAndSetEndpoint(String serverUrl) async { @@ -134,8 +138,8 @@ class ApiService { } setAccessToken(String accessToken) { - _authToken = accessToken; - _apiClient.addDefaultHeader('Authorization', 'Bearer $accessToken'); + _accessToken = accessToken; + _apiClient.addDefaultHeader('x-immich-user-token', accessToken); } ApiClient get apiClient => _apiClient; diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index 737d4d56ad..48f8c63524 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -129,7 +129,7 @@ class AssetService { // fileSize is always filled on the server but not set on client if (a.exifInfo?.fileSize == null) { if (a.isRemote) { - final dto = await _apiService.assetApi.getAssetById(a.remoteId!); + final dto = await _apiService.assetApi.getAssetInfo(a.remoteId!); if (dto != null && dto.exifInfo != null) { final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id); if (newExif != a.exifInfo) { diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart index d503910797..20ee40d588 100644 --- a/mobile/lib/shared/services/share.service.dart +++ b/mobile/lib/shared/services/share.service.dart @@ -31,8 +31,8 @@ class ShareService { final tempDir = await getTemporaryDirectory(); final fileName = asset.fileName; final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetApi - .downloadFileOldWithHttpInfo(asset.remoteId!); + final res = await _apiService.downloadApi + .downloadFileWithHttpInfo(asset.remoteId!); if (res.statusCode != 200) { _log.severe( diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index 575a841f11..21418d5274 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -1,16 +1,16 @@ -import 'package:cached_network_image/cached_network_image.dart'; +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:octo_image/octo_image.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:openapi/api.dart' as api; import 'package:photo_manager_image_provider/photo_manager_image_provider.dart'; -/// Renders an Asset using local data if available, else remote data class ImmichImage extends StatelessWidget { const ImmichImage( this.asset, { @@ -18,23 +18,89 @@ class ImmichImage extends StatelessWidget { this.height, this.fit = BoxFit.cover, this.useGrayBoxPlaceholder = false, - this.useProgressIndicator = false, - this.type = api.ThumbnailFormat.WEBP, - this.preferredLocalAssetSize = 250, + this.isThumbnail = false, + this.thumbnailSize = 250, super.key, }); + final Asset? asset; final bool useGrayBoxPlaceholder; - final bool useProgressIndicator; final double? width; final double? height; final BoxFit fit; - final api.ThumbnailFormat type; - final int preferredLocalAssetSize; + final bool isThumbnail; + final int thumbnailSize; + /// Factory constructor to use the thumbnail variant + factory ImmichImage.thumbnail( + Asset? asset, { + BoxFit fit = BoxFit.cover, + double? width, + double? height, + }) { + // Use the width and height to derive thumbnail size + final thumbnailSize = max(width ?? 250, height ?? 250).toInt(); + + return ImmichImage( + asset, + isThumbnail: true, + fit: fit, + width: width, + height: height, + useGrayBoxPlaceholder: true, + thumbnailSize: thumbnailSize, + ); + } + + // Helper function to return the image provider for the asset + // either by using the asset ID or the asset itself + /// [asset] is the Asset to request, or else use [assetId] to get a remote + /// image provider + /// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail + /// The size of the square thumbnail to request. Ignored if isThumbnail + /// is not true + static ImageProvider imageProvider({ + Asset? asset, + String? assetId, + bool isThumbnail = false, + int thumbnailSize = 250, + }) { + if (asset == null && assetId == null) { + throw Exception('Must supply either asset or assetId'); + } + + if (asset == null) { + return ImmichRemoteImageProvider( + assetId: assetId!, + isThumbnail: isThumbnail, + ); + } + + if (useLocal(asset) && isThumbnail) { + return AssetEntityImageProvider( + asset.local!, + isOriginal: false, + thumbnailSize: ThumbnailSize.square(thumbnailSize), + ); + } else if (useLocal(asset) && !isThumbnail) { + return ImmichLocalImageProvider( + asset: asset, + ); + } else { + return ImmichRemoteImageProvider( + assetId: asset.remoteId!, + isThumbnail: isThumbnail, + ); + } + } + + static bool useLocal(Asset asset) => + !asset.isRemote || + asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); @override Widget build(BuildContext context) { - if (this.asset == null) { + + if (asset == null) { return Container( decoration: const BoxDecoration( color: Colors.grey, @@ -48,96 +114,39 @@ class ImmichImage extends StatelessWidget { ), ); } - final Asset asset = this.asset!; - if (useLocal(asset)) { - return Image( - image: localImageProvider(asset, size: preferredLocalAssetSize), - width: width, - height: height, - fit: fit, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded || frame != null) { - return child; - } - // Show loading if desired - return Stack( - children: [ - if (useGrayBoxPlaceholder) - const SizedBox.square( - dimension: 250, - child: DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ), - ), - if (useProgressIndicator) - const Center( - child: CircularProgressIndicator(), - ), - ], + return OctoImage( + fadeInDuration: const Duration(milliseconds: 0), + fadeOutDuration: const Duration(milliseconds: 400), + placeholderBuilder: (context) { + if (useGrayBoxPlaceholder) { + // Use the gray box placeholder + return const SizedBox.expand( + child: DecoratedBox( + decoration: BoxDecoration(color: Colors.grey), + ), ); - }, - errorBuilder: (context, error, stackTrace) { - if (error is PlatformException && - error.code == "The asset not found!") { - debugPrint( - "Asset ${asset.localId} does not exist anymore on device!", - ); - } else { - debugPrint( - "Error getting thumb for assetId=${asset.localId}: $error", - ); - } - return Icon( - Icons.image_not_supported_outlined, - color: context.primaryColor, - ); - }, - ); - } - final String? token = Store.get(StoreKey.accessToken); - final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type); - return CachedNetworkImage( - imageUrl: thumbnailRequestUrl, - httpHeaders: {"Authorization": "Bearer $token"}, - cacheKey: getThumbnailCacheKey(asset, type: type), + } + // No placeholder + return const SizedBox(); + }, + image: ImmichImage.imageProvider( + asset: asset, + isThumbnail: isThumbnail, + ), width: width, height: height, - // keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and - // maxHeightDiskCache = null allows to simply store the webp thumbnail - // from the server and use it for all rendered thumbnail sizes fit: fit, - fadeInDuration: const Duration(milliseconds: 250), - progressIndicatorBuilder: (context, url, downloadProgress) { - // Show loading if desired - return Stack( - children: [ - if (useGrayBoxPlaceholder) - const SizedBox.square( - dimension: 250, - child: DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ), - ), - if (useProgressIndicator) - Transform.scale( - scale: 2, - child: Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 1, - value: downloadProgress.progress, - ), - ), - ), - ], - ); - }, - errorWidget: (context, url, error) { - if (error is HttpExceptionWithStatus && - error.statusCode >= 400 && - error.statusCode < 500) { - debugPrint("Evicting thumbnail '$url' from cache: $error"); - CachedNetworkImage.evictFromCache(url); + errorBuilder: (context, error, stackTrace) { + if (error is PlatformException && + error.code == "The asset not found!") { + debugPrint( + "Asset ${asset?.localId} does not exist anymore on device!", + ); + } else { + debugPrint( + "Error getting thumb for assetId=${asset?.localId}: $error", + ); } return Icon( Icons.image_not_supported_outlined, @@ -146,65 +155,4 @@ class ImmichImage extends StatelessWidget { }, ); } - - static AssetEntityImageProvider localImageProvider( - Asset asset, { - int size = 250, - }) => - AssetEntityImageProvider( - asset.local!, - isOriginal: false, - thumbnailSize: ThumbnailSize.square(size), - ); - - static CachedNetworkImageProvider remoteThumbnailProvider( - Asset asset, - api.ThumbnailFormat type, - Map authHeader, - ) => - CachedNetworkImageProvider( - getThumbnailUrl(asset, type: type), - cacheKey: getThumbnailCacheKey(asset, type: type), - headers: authHeader, - ); - - /// TODO: refactor image providers to separate class - static CachedNetworkImageProvider remoteThumbnailProviderForId( - String assetId, { - api.ThumbnailFormat type = api.ThumbnailFormat.WEBP, - }) => - CachedNetworkImageProvider( - getThumbnailUrlForRemoteId(assetId, type: type), - cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type), - headers: { - "Authorization": 'Bearer ${Store.get(StoreKey.accessToken)}', - }, - ); - - /// Precaches this asset for instant load the next time it is shown - static Future precacheAsset( - Asset asset, - BuildContext context, { - type = api.ThumbnailFormat.WEBP, - size = 250, - }) { - if (useLocal(asset)) { - // Precache the local image - return precacheImage( - localImageProvider(asset, size: size), - context, - ); - } else { - final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; - // Precache the remote image since we are not using local images - return precacheImage( - remoteThumbnailProvider(asset, type, {"Authorization": authToken}), - context, - ); - } - } - - static bool useLocal(Asset asset) => - !asset.isRemote || - asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); } diff --git a/mobile/lib/shared/ui/user_avatar.dart b/mobile/lib/shared/ui/user_avatar.dart index 95f76de43f..68ed2edbdc 100644 --- a/mobile/lib/shared/ui/user_avatar.dart +++ b/mobile/lib/shared/ui/user_avatar.dart @@ -13,7 +13,7 @@ Widget userAvatar(BuildContext context, User u, {double? radius}) { backgroundColor: context.primaryColor.withAlpha(50), foregroundImage: CachedNetworkImageProvider( url, - headers: {"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"}, + headers: {"x-immich-user-token": Store.get(StoreKey.accessToken)}, cacheKey: "user-${u.id}-profile", ), // silence errors if user has no profile image, use initials as fallback diff --git a/mobile/lib/shared/ui/user_circle_avatar.dart b/mobile/lib/shared/ui/user_circle_avatar.dart index 3dc0e65c1b..103d8970e3 100644 --- a/mobile/lib/shared/ui/user_circle_avatar.dart +++ b/mobile/lib/shared/ui/user_circle_avatar.dart @@ -51,7 +51,7 @@ class UserCircleAvatar extends ConsumerWidget { placeholder: (_, __) => Image.memory(kTransparentImage), imageUrl: profileImageUrl, httpHeaders: { - "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}", + "x-immich-user-token": Store.get(StoreKey.accessToken), }, fadeInDuration: const Duration(milliseconds: 300), errorWidget: (context, error, stackTrace) => textIcon, diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 487faa639a..3e13db11d2 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -56,7 +56,11 @@ String getAlbumThumbNailCacheKey( } String getImageUrl(final Asset asset) { - return '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}?isThumb=false'; + return getImageUrlFromId(asset.remoteId!); +} + +String getImageUrlFromId(final String id) { + return '${Store.get(StoreKey.serverEndpoint)}/asset/file/$id?isThumb=false'; } String getImageCacheKey(final Asset asset) { diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 6fc968a491..aeb3d49cb4 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -95,7 +95,6 @@ doc/OAuthApi.md doc/OAuthAuthorizeResponseDto.md doc/OAuthCallbackDto.md doc/OAuthConfigDto.md -doc/OAuthConfigResponseDto.md doc/PartnerApi.md doc/PartnerResponseDto.md doc/PathEntityType.md @@ -121,6 +120,7 @@ doc/SearchExploreResponseDto.md doc/SearchFacetCountResponseDto.md doc/SearchFacetResponseDto.md doc/SearchResponseDto.md +doc/SearchSuggestionType.md doc/ServerConfigDto.md doc/ServerFeaturesDto.md doc/ServerInfoApi.md @@ -292,7 +292,6 @@ lib/model/model_type.dart lib/model/o_auth_authorize_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart -lib/model/o_auth_config_response_dto.dart lib/model/partner_response_dto.dart lib/model/path_entity_type.dart lib/model/path_type.dart @@ -315,6 +314,7 @@ lib/model/search_explore_response_dto.dart lib/model/search_facet_count_response_dto.dart lib/model/search_facet_response_dto.dart lib/model/search_response_dto.dart +lib/model/search_suggestion_type.dart lib/model/server_config_dto.dart lib/model/server_features_dto.dart lib/model/server_info_response_dto.dart @@ -462,7 +462,6 @@ test/o_auth_api_test.dart test/o_auth_authorize_response_dto_test.dart test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart -test/o_auth_config_response_dto_test.dart test/partner_api_test.dart test/partner_response_dto_test.dart test/path_entity_type_test.dart @@ -488,6 +487,7 @@ test/search_explore_response_dto_test.dart test/search_facet_count_response_dto_test.dart test/search_facet_response_dto_test.dart test/search_response_dto_test.dart +test/search_suggestion_type_test.dart test/server_config_dto_test.dart test/server_features_dto_test.dart test/server_info_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index ac05f83b69..12d20ea13d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -94,26 +94,19 @@ Class | Method | HTTP request | Description *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | -*AssetApi* | [**downloadArchiveOld**](doc//AssetApi.md#downloadarchiveold) | **POST** /asset/download/archive | -*AssetApi* | [**downloadFileOld**](doc//AssetApi.md#downloadfileold) | **POST** /asset/download/{id} | -*AssetApi* | [**emptyTrashOld**](doc//AssetApi.md#emptytrashold) | **POST** /asset/trash/empty | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | *AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | -*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | *AssetApi* | [**getAssetInfo**](doc//AssetApi.md#getassetinfo) | **GET** /asset/{id} | *AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | *AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics | *AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | -*AssetApi* | [**getDownloadInfoOld**](doc//AssetApi.md#getdownloadinfoold) | **POST** /asset/download/info | *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | *AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random | *AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | *AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -*AssetApi* | [**restoreAssetsOld**](doc//AssetApi.md#restoreassetsold) | **POST** /asset/restore | -*AssetApi* | [**restoreTrashOld**](doc//AssetApi.md#restoretrashold) | **POST** /asset/trash/restore | *AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs | *AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets | *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -149,7 +142,6 @@ Class | Method | HTTP request | Description *LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan | *LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | -*OAuthApi* | [**generateOAuthConfig**](doc//OAuthApi.md#generateoauthconfig) | **POST** /oauth/config | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | *OAuthApi* | [**startOAuth**](doc//OAuthApi.md#startoauth) | **POST** /oauth/authorize | @@ -169,8 +161,11 @@ Class | Method | HTTP request | Description *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | +*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | +*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **GET** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | +*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **GET** /search/smart | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | @@ -299,7 +294,6 @@ Class | Method | HTTP request | Description - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) - - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PathEntityType](doc//PathEntityType.md) - [PathType](doc//PathType.md) @@ -322,6 +316,7 @@ Class | Method | HTTP request | Description - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md) - [SearchResponseDto](doc//SearchResponseDto.md) + - [SearchSuggestionType](doc//SearchSuggestionType.md) - [ServerConfigDto](doc//ServerConfigDto.md) - [ServerFeaturesDto](doc//ServerFeaturesDto.md) - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index dbff762d30..623d88b388 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -12,26 +12,19 @@ Method | HTTP request | Description [**checkBulkUpload**](AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | [**deleteAssets**](AssetApi.md#deleteassets) | **DELETE** /asset | -[**downloadArchiveOld**](AssetApi.md#downloadarchiveold) | **POST** /asset/download/archive | -[**downloadFileOld**](AssetApi.md#downloadfileold) | **POST** /asset/download/{id} | -[**emptyTrashOld**](AssetApi.md#emptytrashold) | **POST** /asset/trash/empty | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | [**getAllUserAssetsByDeviceId**](AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | -[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | [**getAssetInfo**](AssetApi.md#getassetinfo) | **GET** /asset/{id} | [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | [**getAssetStatistics**](AssetApi.md#getassetstatistics) | **GET** /asset/statistics | [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | -[**getDownloadInfoOld**](AssetApi.md#getdownloadinfoold) | **POST** /asset/download/info | [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | [**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random | [**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | [**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -[**restoreAssetsOld**](AssetApi.md#restoreassetsold) | **POST** /asset/restore | -[**restoreTrashOld**](AssetApi.md#restoretrashold) | **POST** /asset/trash/restore | [**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs | [**searchAssets**](AssetApi.md#searchassets) | **GET** /assets | [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -209,170 +202,6 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **downloadArchiveOld** -> MultipartFile downloadArchiveOld(assetIdsDto, key) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final assetIdsDto = AssetIdsDto(); // AssetIdsDto | -final key = key_example; // String | - -try { - final result = api_instance.downloadArchiveOld(assetIdsDto, key); - print(result); -} catch (e) { - print('Exception when calling AssetApi->downloadArchiveOld: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)| | - **key** | **String**| | [optional] - -### Return type - -[**MultipartFile**](MultipartFile.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/octet-stream - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **downloadFileOld** -> MultipartFile downloadFileOld(id, key) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final key = key_example; // String | - -try { - final result = api_instance.downloadFileOld(id, key); - print(result); -} catch (e) { - print('Exception when calling AssetApi->downloadFileOld: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **id** | **String**| | - **key** | **String**| | [optional] - -### Return type - -[**MultipartFile**](MultipartFile.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/octet-stream - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **emptyTrashOld** -> emptyTrashOld() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); - -try { - api_instance.emptyTrashOld(); -} catch (e) { - print('Exception when calling AssetApi->emptyTrashOld: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **getAllAssets** > List getAllAssets(ifNoneMatch, isArchived, isFavorite, skip, take, updatedAfter, updatedBefore, userId) @@ -501,65 +330,6 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **getAssetById** -> AssetResponseDto getAssetById(id, key) - - - -Get a single asset's information - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final key = key_example; // String | - -try { - final result = api_instance.getAssetById(id, key); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getAssetById: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **id** | **String**| | - **key** | **String**| | [optional] - -### Return type - -[**AssetResponseDto**](AssetResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **getAssetInfo** > AssetResponseDto getAssetInfo(id, key) @@ -888,63 +658,6 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **getDownloadInfoOld** -> DownloadResponseDto getDownloadInfoOld(downloadInfoDto, key) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final downloadInfoDto = DownloadInfoDto(); // DownloadInfoDto | -final key = key_example; // String | - -try { - final result = api_instance.getDownloadInfoOld(downloadInfoDto, key); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getDownloadInfoOld: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **downloadInfoDto** | [**DownloadInfoDto**](DownloadInfoDto.md)| | - **key** | **String**| | [optional] - -### Return type - -[**DownloadResponseDto**](DownloadResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **getMapMarkers** > List getMapMarkers(fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite) @@ -1266,110 +979,6 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **restoreAssetsOld** -> restoreAssetsOld(bulkIdsDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | - -try { - api_instance.restoreAssetsOld(bulkIdsDto); -} catch (e) { - print('Exception when calling AssetApi->restoreAssetsOld: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **restoreTrashOld** -> restoreTrashOld() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); - -try { - api_instance.restoreTrashOld(); -} catch (e) { - print('Exception when calling AssetApi->restoreTrashOld: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **runAssetJobs** > runAssetJobs(assetJobsDto) @@ -1425,7 +1034,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **searchAssets** -> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked) +> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) @@ -1484,13 +1093,14 @@ final type = ; // AssetTypeEnum | final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final webpPath = webpPath_example; // String | +final withArchived = true; // bool | final withDeleted = true; // bool | final withExif = true; // bool | final withPeople = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked); + final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->searchAssets: $e\n'); @@ -1537,6 +1147,7 @@ Name | Type | Description | Notes **updatedAfter** | **DateTime**| | [optional] **updatedBefore** | **DateTime**| | [optional] **webpPath** | **String**| | [optional] + **withArchived** | **bool**| | [optional] **withDeleted** | **bool**| | [optional] **withExif** | **bool**| | [optional] **withPeople** | **bool**| | [optional] diff --git a/mobile/openapi/doc/OAuthApi.md b/mobile/openapi/doc/OAuthApi.md index fa6b19df53..ce36f7163a 100644 --- a/mobile/openapi/doc/OAuthApi.md +++ b/mobile/openapi/doc/OAuthApi.md @@ -10,7 +10,6 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**finishOAuth**](OAuthApi.md#finishoauth) | **POST** /oauth/callback | -[**generateOAuthConfig**](OAuthApi.md#generateoauthconfig) | **POST** /oauth/config | [**linkOAuthAccount**](OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | [**redirectOAuthToMobile**](OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | [**startOAuth**](OAuthApi.md#startoauth) | **POST** /oauth/authorize | @@ -58,49 +57,6 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **generateOAuthConfig** -> OAuthConfigResponseDto generateOAuthConfig(oAuthConfigDto) - - - -@deprecated use feature flags and /oauth/authorize - -### Example -```dart -import 'package:openapi/api.dart'; - -final api_instance = OAuthApi(); -final oAuthConfigDto = OAuthConfigDto(); // OAuthConfigDto | - -try { - final result = api_instance.generateOAuthConfig(oAuthConfigDto); - print(result); -} catch (e) { - print('Exception when calling OAuthApi->generateOAuthConfig: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **oAuthConfigDto** | [**OAuthConfigDto**](OAuthConfigDto.md)| | - -### Return type - -[**OAuthConfigResponseDto**](OAuthConfigResponseDto.md) - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **linkOAuthAccount** > UserResponseDto linkOAuthAccount(oAuthCallbackDto) diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index dcf453b55d..8c924428f6 100644 --- a/mobile/openapi/doc/SearchApi.md +++ b/mobile/openapi/doc/SearchApi.md @@ -10,8 +10,11 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | +[**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | [**search**](SearchApi.md#search) | **GET** /search | +[**searchMetadata**](SearchApi.md#searchmetadata) | **GET** /search/metadata | [**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person | +[**searchSmart**](SearchApi.md#searchsmart) | **GET** /search/smart | # **getExploreData** @@ -65,8 +68,8 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **search** -> SearchResponseDto search(clip, motion, q, query, recent, smart, type, withArchived) +# **getSearchSuggestions** +> List getSearchSuggestions(type, country, make, model, state) @@ -89,17 +92,82 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = SearchApi(); -final clip = true; // bool | @deprecated +final type = ; // SearchSuggestionType | +final country = country_example; // String | +final make = make_example; // String | +final model = model_example; // String | +final state = state_example; // String | + +try { + final result = api_instance.getSearchSuggestions(type, country, make, model, state); + print(result); +} catch (e) { + print('Exception when calling SearchApi->getSearchSuggestions: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **type** | [**SearchSuggestionType**](.md)| | + **country** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **state** | **String**| | [optional] + +### Return type + +**List** + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **search** +> SearchResponseDto search(clip, motion, page, q, query, recent, size, smart, type, withArchived) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final clip = true; // bool | final motion = true; // bool | +final page = 8.14; // num | final q = q_example; // String | final query = query_example; // String | final recent = true; // bool | +final size = 8.14; // num | final smart = true; // bool | final type = type_example; // String | final withArchived = true; // bool | try { - final result = api_instance.search(clip, motion, q, query, recent, smart, type, withArchived); + final result = api_instance.search(clip, motion, page, q, query, recent, size, smart, type, withArchived); print(result); } catch (e) { print('Exception when calling SearchApi->search: $e\n'); @@ -110,11 +178,13 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **clip** | **bool**| @deprecated | [optional] + **clip** | **bool**| | [optional] **motion** | **bool**| | [optional] + **page** | **num**| | [optional] **q** | **String**| | [optional] **query** | **String**| | [optional] **recent** | **bool**| | [optional] + **size** | **num**| | [optional] **smart** | **bool**| | [optional] **type** | **String**| | [optional] **withArchived** | **bool**| | [optional] @@ -134,6 +204,141 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **searchMetadata** +> SearchResponseDto searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final checksum = checksum_example; // String | +final city = city_example; // String | +final country = country_example; // String | +final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final deviceAssetId = deviceAssetId_example; // String | +final deviceId = deviceId_example; // String | +final encodedVideoPath = encodedVideoPath_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final isArchived = true; // bool | +final isEncoded = true; // bool | +final isExternal = true; // bool | +final isFavorite = true; // bool | +final isMotion = true; // bool | +final isOffline = true; // bool | +final isReadOnly = true; // bool | +final isVisible = true; // bool | +final lensModel = lensModel_example; // String | +final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final make = make_example; // String | +final model = model_example; // String | +final order = ; // AssetOrder | +final originalFileName = originalFileName_example; // String | +final originalPath = originalPath_example; // String | +final page = 8.14; // num | +final resizePath = resizePath_example; // String | +final size = 8.14; // num | +final state = state_example; // String | +final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final type = ; // AssetTypeEnum | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final webpPath = webpPath_example; // String | +final withArchived = true; // bool | +final withDeleted = true; // bool | +final withExif = true; // bool | +final withPeople = true; // bool | +final withStacked = true; // bool | + +try { + final result = api_instance.searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchMetadata: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **checksum** | **String**| | [optional] + **city** | **String**| | [optional] + **country** | **String**| | [optional] + **createdAfter** | **DateTime**| | [optional] + **createdBefore** | **DateTime**| | [optional] + **deviceAssetId** | **String**| | [optional] + **deviceId** | **String**| | [optional] + **encodedVideoPath** | **String**| | [optional] + **id** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isEncoded** | **bool**| | [optional] + **isExternal** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isMotion** | **bool**| | [optional] + **isOffline** | **bool**| | [optional] + **isReadOnly** | **bool**| | [optional] + **isVisible** | **bool**| | [optional] + **lensModel** | **String**| | [optional] + **libraryId** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] + **originalFileName** | **String**| | [optional] + **originalPath** | **String**| | [optional] + **page** | **num**| | [optional] + **resizePath** | **String**| | [optional] + **size** | **num**| | [optional] + **state** | **String**| | [optional] + **takenAfter** | **DateTime**| | [optional] + **takenBefore** | **DateTime**| | [optional] + **trashedAfter** | **DateTime**| | [optional] + **trashedBefore** | **DateTime**| | [optional] + **type** | [**AssetTypeEnum**](.md)| | [optional] + **updatedAfter** | **DateTime**| | [optional] + **updatedBefore** | **DateTime**| | [optional] + **webpPath** | **String**| | [optional] + **withArchived** | **bool**| | [optional] + **withDeleted** | **bool**| | [optional] + **withExif** | **bool**| | [optional] + **withPeople** | **bool**| | [optional] + **withStacked** | **bool**| | [optional] + +### Return type + +[**SearchResponseDto**](SearchResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **searchPerson** > List searchPerson(name, withHidden) @@ -191,3 +396,118 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **searchSmart** +> SearchResponseDto searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final query = query_example; // String | +final city = city_example; // String | +final country = country_example; // String | +final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final deviceId = deviceId_example; // String | +final isArchived = true; // bool | +final isEncoded = true; // bool | +final isExternal = true; // bool | +final isFavorite = true; // bool | +final isMotion = true; // bool | +final isOffline = true; // bool | +final isReadOnly = true; // bool | +final isVisible = true; // bool | +final lensModel = lensModel_example; // String | +final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final make = make_example; // String | +final model = model_example; // String | +final page = 8.14; // num | +final size = 8.14; // num | +final state = state_example; // String | +final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final type = ; // AssetTypeEnum | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final withArchived = true; // bool | +final withDeleted = true; // bool | +final withExif = true; // bool | + +try { + final result = api_instance.searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchSmart: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **query** | **String**| | + **city** | **String**| | [optional] + **country** | **String**| | [optional] + **createdAfter** | **DateTime**| | [optional] + **createdBefore** | **DateTime**| | [optional] + **deviceId** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isEncoded** | **bool**| | [optional] + **isExternal** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isMotion** | **bool**| | [optional] + **isOffline** | **bool**| | [optional] + **isReadOnly** | **bool**| | [optional] + **isVisible** | **bool**| | [optional] + **lensModel** | **String**| | [optional] + **libraryId** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **page** | **num**| | [optional] + **size** | **num**| | [optional] + **state** | **String**| | [optional] + **takenAfter** | **DateTime**| | [optional] + **takenBefore** | **DateTime**| | [optional] + **trashedAfter** | **DateTime**| | [optional] + **trashedBefore** | **DateTime**| | [optional] + **type** | [**AssetTypeEnum**](.md)| | [optional] + **updatedAfter** | **DateTime**| | [optional] + **updatedBefore** | **DateTime**| | [optional] + **withArchived** | **bool**| | [optional] + **withDeleted** | **bool**| | [optional] + **withExif** | **bool**| | [optional] + +### Return type + +[**SearchResponseDto**](SearchResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/SearchAssetResponseDto.md b/mobile/openapi/doc/SearchAssetResponseDto.md index 2fc33feb41..5b55a559dc 100644 --- a/mobile/openapi/doc/SearchAssetResponseDto.md +++ b/mobile/openapi/doc/SearchAssetResponseDto.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **count** | **int** | | **facets** | [**List**](SearchFacetResponseDto.md) | | [default to const []] **items** | [**List**](AssetResponseDto.md) | | [default to const []] +**nextPage** | **String** | | **total** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/OAuthConfigResponseDto.md b/mobile/openapi/doc/SearchSuggestionType.md similarity index 59% rename from mobile/openapi/doc/OAuthConfigResponseDto.md rename to mobile/openapi/doc/SearchSuggestionType.md index 8abc6bb607..e37b3f0de5 100644 --- a/mobile/openapi/doc/OAuthConfigResponseDto.md +++ b/mobile/openapi/doc/SearchSuggestionType.md @@ -1,4 +1,4 @@ -# openapi.model.OAuthConfigResponseDto +# openapi.model.SearchSuggestionType ## Load the model package ```dart @@ -8,11 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**autoLaunch** | **bool** | | [optional] -**buttonText** | **String** | | [optional] -**enabled** | **bool** | | -**passwordLoginEnabled** | **bool** | | -**url** | **String** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8b9a320566..f2903e3516 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -131,7 +131,6 @@ part 'model/model_type.dart'; part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; -part 'model/o_auth_config_response_dto.dart'; part 'model/partner_response_dto.dart'; part 'model/path_entity_type.dart'; part 'model/path_type.dart'; @@ -154,6 +153,7 @@ part 'model/search_explore_response_dto.dart'; part 'model/search_facet_count_response_dto.dart'; part 'model/search_facet_response_dto.dart'; part 'model/search_response_dto.dart'; +part 'model/search_suggestion_type.dart'; part 'model/server_config_dto.dart'; part 'model/server_features_dto.dart'; part 'model/server_info_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 67127392c6..4dec34d407 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -159,150 +159,6 @@ class AssetApi { } } - /// Performs an HTTP 'POST /asset/download/archive' operation and returns the [Response]. - /// Parameters: - /// - /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - Future downloadArchiveOldWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/download/archive'; - - // ignore: prefer_final_locals - Object? postBody = assetIdsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - Future downloadArchiveOld(AssetIdsDto assetIdsDto, { String? key, }) async { - final response = await downloadArchiveOldWithHttpInfo(assetIdsDto, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile; - - } - return null; - } - - /// Performs an HTTP 'POST /asset/download/{id}' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [String] key: - Future downloadFileOldWithHttpInfo(String id, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/download/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [String] key: - Future downloadFileOld(String id, { String? key, }) async { - final response = await downloadFileOldWithHttpInfo(id, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile; - - } - return null; - } - - /// Performs an HTTP 'POST /asset/trash/empty' operation and returns the [Response]. - Future emptyTrashOldWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/asset/trash/empty'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future emptyTrashOld() async { - final response = await emptyTrashOldWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Get all AssetEntity belong to the user /// /// Note: This method returns the HTTP [Response]. @@ -470,67 +326,6 @@ class AssetApi { return null; } - /// Get a single asset's information - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [String] key: - Future getAssetByIdWithHttpInfo(String id, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/assetById/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get a single asset's information - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [String] key: - Future getAssetById(String id, { String? key, }) async { - final response = await getAssetByIdWithHttpInfo(id, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetResponseDto',) as AssetResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /asset/{id}' operation and returns the [Response]. /// Parameters: /// @@ -847,61 +642,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'POST /asset/download/info' operation and returns the [Response]. - /// Parameters: - /// - /// * [DownloadInfoDto] downloadInfoDto (required): - /// - /// * [String] key: - Future getDownloadInfoOldWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/download/info'; - - // ignore: prefer_final_locals - Object? postBody = downloadInfoDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [DownloadInfoDto] downloadInfoDto (required): - /// - /// * [String] key: - Future getDownloadInfoOld(DownloadInfoDto downloadInfoDto, { String? key, }) async { - final response = await getDownloadInfoOldWithHttpInfo(downloadInfoDto, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'DownloadResponseDto',) as DownloadResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response]. /// Parameters: /// @@ -1323,78 +1063,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'POST /asset/restore' operation and returns the [Response]. - /// Parameters: - /// - /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssetsOldWithHttpInfo(BulkIdsDto bulkIdsDto,) async { - // ignore: prefer_const_declarations - final path = r'/asset/restore'; - - // ignore: prefer_final_locals - Object? postBody = bulkIdsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssetsOld(BulkIdsDto bulkIdsDto,) async { - final response = await restoreAssetsOldWithHttpInfo(bulkIdsDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - - /// Performs an HTTP 'POST /asset/trash/restore' operation and returns the [Response]. - Future restoreTrashOldWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/asset/trash/restore'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future restoreTrashOld() async { - final response = await restoreTrashOldWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response]. /// Parameters: /// @@ -1509,6 +1177,8 @@ class AssetApi { /// /// * [String] webpPath: /// + /// * [bool] withArchived: + /// /// * [bool] withDeleted: /// /// * [bool] withExif: @@ -1516,7 +1186,7 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -1635,6 +1305,9 @@ class AssetApi { if (webpPath != null) { queryParams.addAll(_queryParams('', 'webpPath', webpPath)); } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } if (withDeleted != null) { queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); } @@ -1736,6 +1409,8 @@ class AssetApi { /// /// * [String] webpPath: /// + /// * [bool] withArchived: + /// /// * [bool] withDeleted: /// /// * [bool] withExif: @@ -1743,8 +1418,8 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { - final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); + Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/o_auth_api.dart b/mobile/openapi/lib/api/o_auth_api.dart index c0d6bc06e0..0337384798 100644 --- a/mobile/openapi/lib/api/o_auth_api.dart +++ b/mobile/openapi/lib/api/o_auth_api.dart @@ -63,58 +63,6 @@ class OAuthApi { return null; } - /// @deprecated use feature flags and /oauth/authorize - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [OAuthConfigDto] oAuthConfigDto (required): - Future generateOAuthConfigWithHttpInfo(OAuthConfigDto oAuthConfigDto,) async { - // ignore: prefer_const_declarations - final path = r'/oauth/config'; - - // ignore: prefer_final_locals - Object? postBody = oAuthConfigDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// @deprecated use feature flags and /oauth/authorize - /// - /// Parameters: - /// - /// * [OAuthConfigDto] oAuthConfigDto (required): - Future generateOAuthConfig(OAuthConfigDto oAuthConfigDto,) async { - final response = await generateOAuthConfigWithHttpInfo(oAuthConfigDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'OAuthConfigResponseDto',) as OAuthConfigResponseDto; - - } - return null; - } - /// Performs an HTTP 'POST /oauth/link' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index e2bde2a17f..16d2b3003e 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -60,26 +60,109 @@ class SearchApi { return null; } + /// Performs an HTTP 'GET /search/suggestions' operation and returns the [Response]. + /// Parameters: + /// + /// * [SearchSuggestionType] type (required): + /// + /// * [String] country: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [String] state: + Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async { + // ignore: prefer_const_declarations + final path = r'/search/suggestions'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + queryParams.addAll(_queryParams('', 'type', type)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [SearchSuggestionType] type (required): + /// + /// * [String] country: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [String] state: + Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async { + final response = await getSearchSuggestionsWithHttpInfo(type, country: country, make: make, model: model, state: state, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Performs an HTTP 'GET /search' operation and returns the [Response]. /// Parameters: /// /// * [bool] clip: - /// @deprecated /// /// * [bool] motion: /// + /// * [num] page: + /// /// * [String] q: /// /// * [String] query: /// /// * [bool] recent: /// + /// * [num] size: + /// /// * [bool] smart: /// /// * [String] type: /// /// * [bool] withArchived: - Future searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { + Future searchWithHttpInfo({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, bool? smart, String? type, bool? withArchived, }) async { // ignore: prefer_const_declarations final path = r'/search'; @@ -96,6 +179,9 @@ class SearchApi { if (motion != null) { queryParams.addAll(_queryParams('', 'motion', motion)); } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } if (q != null) { queryParams.addAll(_queryParams('', 'q', q)); } @@ -105,6 +191,9 @@ class SearchApi { if (recent != null) { queryParams.addAll(_queryParams('', 'recent', recent)); } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } if (smart != null) { queryParams.addAll(_queryParams('', 'smart', smart)); } @@ -132,23 +221,357 @@ class SearchApi { /// Parameters: /// /// * [bool] clip: - /// @deprecated /// /// * [bool] motion: /// + /// * [num] page: + /// /// * [String] q: /// /// * [String] query: /// /// * [bool] recent: /// + /// * [num] size: + /// /// * [bool] smart: /// /// * [String] type: /// /// * [bool] withArchived: - Future search({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { - final response = await searchWithHttpInfo( clip: clip, motion: motion, q: q, query: query, recent: recent, smart: smart, type: type, withArchived: withArchived, ); + Future search({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, bool? smart, String? type, bool? withArchived, }) async { + final response = await searchWithHttpInfo( clip: clip, motion: motion, page: page, q: q, query: query, recent: recent, size: size, smart: smart, type: type, withArchived: withArchived, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /search/metadata' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] checksum: + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] id: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [AssetOrder] order: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [num] page: + /// + /// * [String] resizePath: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [String] webpPath: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [bool] withStacked: + Future searchMetadataWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + // ignore: prefer_const_declarations + final path = r'/search/metadata'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (checksum != null) { + queryParams.addAll(_queryParams('', 'checksum', checksum)); + } + if (city != null) { + queryParams.addAll(_queryParams('', 'city', city)); + } + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (createdAfter != null) { + queryParams.addAll(_queryParams('', 'createdAfter', createdAfter)); + } + if (createdBefore != null) { + queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); + } + if (deviceAssetId != null) { + queryParams.addAll(_queryParams('', 'deviceAssetId', deviceAssetId)); + } + if (deviceId != null) { + queryParams.addAll(_queryParams('', 'deviceId', deviceId)); + } + if (encodedVideoPath != null) { + queryParams.addAll(_queryParams('', 'encodedVideoPath', encodedVideoPath)); + } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isEncoded != null) { + queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); + } + if (isExternal != null) { + queryParams.addAll(_queryParams('', 'isExternal', isExternal)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isMotion != null) { + queryParams.addAll(_queryParams('', 'isMotion', isMotion)); + } + if (isOffline != null) { + queryParams.addAll(_queryParams('', 'isOffline', isOffline)); + } + if (isReadOnly != null) { + queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly)); + } + if (isVisible != null) { + queryParams.addAll(_queryParams('', 'isVisible', isVisible)); + } + if (lensModel != null) { + queryParams.addAll(_queryParams('', 'lensModel', lensModel)); + } + if (libraryId != null) { + queryParams.addAll(_queryParams('', 'libraryId', libraryId)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (originalFileName != null) { + queryParams.addAll(_queryParams('', 'originalFileName', originalFileName)); + } + if (originalPath != null) { + queryParams.addAll(_queryParams('', 'originalPath', originalPath)); + } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } + if (resizePath != null) { + queryParams.addAll(_queryParams('', 'resizePath', resizePath)); + } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + if (takenAfter != null) { + queryParams.addAll(_queryParams('', 'takenAfter', takenAfter)); + } + if (takenBefore != null) { + queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); + } + if (trashedAfter != null) { + queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); + } + if (trashedBefore != null) { + queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } + if (updatedBefore != null) { + queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); + } + if (webpPath != null) { + queryParams.addAll(_queryParams('', 'webpPath', webpPath)); + } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } + if (withDeleted != null) { + queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); + } + if (withExif != null) { + queryParams.addAll(_queryParams('', 'withExif', withExif)); + } + if (withPeople != null) { + queryParams.addAll(_queryParams('', 'withPeople', withPeople)); + } + if (withStacked != null) { + queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] checksum: + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] id: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [AssetOrder] order: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [num] page: + /// + /// * [String] resizePath: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [String] webpPath: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [bool] withStacked: + Future searchMetadata({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchMetadataWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -220,4 +643,263 @@ class SearchApi { } return null; } + + /// Performs an HTTP 'GET /search/smart' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] query (required): + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [num] page: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + Future searchSmartWithHttpInfo(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async { + // ignore: prefer_const_declarations + final path = r'/search/smart'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (city != null) { + queryParams.addAll(_queryParams('', 'city', city)); + } + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (createdAfter != null) { + queryParams.addAll(_queryParams('', 'createdAfter', createdAfter)); + } + if (createdBefore != null) { + queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); + } + if (deviceId != null) { + queryParams.addAll(_queryParams('', 'deviceId', deviceId)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isEncoded != null) { + queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); + } + if (isExternal != null) { + queryParams.addAll(_queryParams('', 'isExternal', isExternal)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isMotion != null) { + queryParams.addAll(_queryParams('', 'isMotion', isMotion)); + } + if (isOffline != null) { + queryParams.addAll(_queryParams('', 'isOffline', isOffline)); + } + if (isReadOnly != null) { + queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly)); + } + if (isVisible != null) { + queryParams.addAll(_queryParams('', 'isVisible', isVisible)); + } + if (lensModel != null) { + queryParams.addAll(_queryParams('', 'lensModel', lensModel)); + } + if (libraryId != null) { + queryParams.addAll(_queryParams('', 'libraryId', libraryId)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } + queryParams.addAll(_queryParams('', 'query', query)); + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + if (takenAfter != null) { + queryParams.addAll(_queryParams('', 'takenAfter', takenAfter)); + } + if (takenBefore != null) { + queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); + } + if (trashedAfter != null) { + queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); + } + if (trashedBefore != null) { + queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } + if (updatedBefore != null) { + queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); + } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } + if (withDeleted != null) { + queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); + } + if (withExif != null) { + queryParams.addAll(_queryParams('', 'withExif', withExif)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] query (required): + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [num] page: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + Future searchSmart(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async { + final response = await searchSmartWithHttpInfo(query, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, page: page, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a305ded56a..a8cf4c34c1 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -344,8 +344,6 @@ class ApiClient { return OAuthCallbackDto.fromJson(value); case 'OAuthConfigDto': return OAuthConfigDto.fromJson(value); - case 'OAuthConfigResponseDto': - return OAuthConfigResponseDto.fromJson(value); case 'PartnerResponseDto': return PartnerResponseDto.fromJson(value); case 'PathEntityType': @@ -390,6 +388,8 @@ class ApiClient { return SearchFacetResponseDto.fromJson(value); case 'SearchResponseDto': return SearchResponseDto.fromJson(value); + case 'SearchSuggestionType': + return SearchSuggestionTypeTypeTransformer().decode(value); case 'ServerConfigDto': return ServerConfigDto.fromJson(value); case 'ServerFeaturesDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index c01b24703c..f37ba588a3 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -109,6 +109,9 @@ String parameterToString(dynamic value) { if (value is ReactionType) { return ReactionTypeTypeTransformer().encode(value).toString(); } + if (value is SearchSuggestionType) { + return SearchSuggestionTypeTypeTransformer().encode(value).toString(); + } if (value is SharedLinkType) { return SharedLinkTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/o_auth_config_response_dto.dart b/mobile/openapi/lib/model/o_auth_config_response_dto.dart deleted file mode 100644 index 83fd87b6c8..0000000000 --- a/mobile/openapi/lib/model/o_auth_config_response_dto.dart +++ /dev/null @@ -1,157 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class OAuthConfigResponseDto { - /// Returns a new [OAuthConfigResponseDto] instance. - OAuthConfigResponseDto({ - this.autoLaunch, - this.buttonText, - required this.enabled, - required this.passwordLoginEnabled, - this.url, - }); - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? autoLaunch; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? buttonText; - - bool enabled; - - bool passwordLoginEnabled; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? url; - - @override - bool operator ==(Object other) => identical(this, other) || other is OAuthConfigResponseDto && - other.autoLaunch == autoLaunch && - other.buttonText == buttonText && - other.enabled == enabled && - other.passwordLoginEnabled == passwordLoginEnabled && - other.url == url; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (autoLaunch == null ? 0 : autoLaunch!.hashCode) + - (buttonText == null ? 0 : buttonText!.hashCode) + - (enabled.hashCode) + - (passwordLoginEnabled.hashCode) + - (url == null ? 0 : url!.hashCode); - - @override - String toString() => 'OAuthConfigResponseDto[autoLaunch=$autoLaunch, buttonText=$buttonText, enabled=$enabled, passwordLoginEnabled=$passwordLoginEnabled, url=$url]'; - - Map toJson() { - final json = {}; - if (this.autoLaunch != null) { - json[r'autoLaunch'] = this.autoLaunch; - } else { - // json[r'autoLaunch'] = null; - } - if (this.buttonText != null) { - json[r'buttonText'] = this.buttonText; - } else { - // json[r'buttonText'] = null; - } - json[r'enabled'] = this.enabled; - json[r'passwordLoginEnabled'] = this.passwordLoginEnabled; - if (this.url != null) { - json[r'url'] = this.url; - } else { - // json[r'url'] = null; - } - return json; - } - - /// Returns a new [OAuthConfigResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static OAuthConfigResponseDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return OAuthConfigResponseDto( - autoLaunch: mapValueOfType(json, r'autoLaunch'), - buttonText: mapValueOfType(json, r'buttonText'), - enabled: mapValueOfType(json, r'enabled')!, - passwordLoginEnabled: mapValueOfType(json, r'passwordLoginEnabled')!, - url: mapValueOfType(json, r'url'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = OAuthConfigResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = OAuthConfigResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of OAuthConfigResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = OAuthConfigResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'enabled', - 'passwordLoginEnabled', - }; -} - diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index 98291307d4..abdbc5d4e3 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -16,6 +16,7 @@ class SearchAssetResponseDto { required this.count, this.facets = const [], this.items = const [], + required this.nextPage, required this.total, }); @@ -25,6 +26,8 @@ class SearchAssetResponseDto { List items; + String? nextPage; + int total; @override @@ -32,6 +35,7 @@ class SearchAssetResponseDto { other.count == count && _deepEquality.equals(other.facets, facets) && _deepEquality.equals(other.items, items) && + other.nextPage == nextPage && other.total == total; @override @@ -40,16 +44,22 @@ class SearchAssetResponseDto { (count.hashCode) + (facets.hashCode) + (items.hashCode) + + (nextPage == null ? 0 : nextPage!.hashCode) + (total.hashCode); @override - String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, total=$total]'; + String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, nextPage=$nextPage, total=$total]'; Map toJson() { final json = {}; json[r'count'] = this.count; json[r'facets'] = this.facets; json[r'items'] = this.items; + if (this.nextPage != null) { + json[r'nextPage'] = this.nextPage; + } else { + // json[r'nextPage'] = null; + } json[r'total'] = this.total; return json; } @@ -65,6 +75,7 @@ class SearchAssetResponseDto { count: mapValueOfType(json, r'count')!, facets: SearchFacetResponseDto.listFromJson(json[r'facets']), items: AssetResponseDto.listFromJson(json[r'items']), + nextPage: mapValueOfType(json, r'nextPage'), total: mapValueOfType(json, r'total')!, ); } @@ -116,6 +127,7 @@ class SearchAssetResponseDto { 'count', 'facets', 'items', + 'nextPage', 'total', }; } diff --git a/mobile/openapi/lib/model/search_suggestion_type.dart b/mobile/openapi/lib/model/search_suggestion_type.dart new file mode 100644 index 0000000000..d33b4a69d9 --- /dev/null +++ b/mobile/openapi/lib/model/search_suggestion_type.dart @@ -0,0 +1,94 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class SearchSuggestionType { + /// Instantiate a new enum with the provided [value]. + const SearchSuggestionType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const country = SearchSuggestionType._(r'country'); + static const state = SearchSuggestionType._(r'state'); + static const city = SearchSuggestionType._(r'city'); + static const cameraMake = SearchSuggestionType._(r'camera-make'); + static const cameraModel = SearchSuggestionType._(r'camera-model'); + + /// List of all possible values in this [enum][SearchSuggestionType]. + static const values = [ + country, + state, + city, + cameraMake, + cameraModel, + ]; + + static SearchSuggestionType? fromJson(dynamic value) => SearchSuggestionTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SearchSuggestionType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [SearchSuggestionType] to String, +/// and [decode] dynamic data back to [SearchSuggestionType]. +class SearchSuggestionTypeTypeTransformer { + factory SearchSuggestionTypeTypeTransformer() => _instance ??= const SearchSuggestionTypeTypeTransformer._(); + + const SearchSuggestionTypeTypeTransformer._(); + + String encode(SearchSuggestionType data) => data.value; + + /// Decodes a [dynamic value][data] to a SearchSuggestionType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + SearchSuggestionType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'country': return SearchSuggestionType.country; + case r'state': return SearchSuggestionType.state; + case r'city': return SearchSuggestionType.city; + case r'camera-make': return SearchSuggestionType.cameraMake; + case r'camera-model': return SearchSuggestionType.cameraModel; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [SearchSuggestionTypeTypeTransformer] instance. + static SearchSuggestionTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 8d0910f1f0..a7a63f38e6 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -36,21 +36,6 @@ void main() { // TODO }); - //Future downloadArchiveOld(AssetIdsDto assetIdsDto, { String key }) async - test('test downloadArchiveOld', () async { - // TODO - }); - - //Future downloadFileOld(String id, { String key }) async - test('test downloadFileOld', () async { - // TODO - }); - - //Future emptyTrashOld() async - test('test emptyTrashOld', () async { - // TODO - }); - // Get all AssetEntity belong to the user // //Future> getAllAssets({ String ifNoneMatch, bool isArchived, bool isFavorite, int skip, int take, DateTime updatedAfter, DateTime updatedBefore, String userId }) async @@ -65,13 +50,6 @@ void main() { // TODO }); - // Get a single asset's information - // - //Future getAssetById(String id, { String key }) async - test('test getAssetById', () async { - // TODO - }); - //Future getAssetInfo(String id, { String key }) async test('test getAssetInfo', () async { // TODO @@ -102,11 +80,6 @@ void main() { // TODO }); - //Future getDownloadInfoOld(DownloadInfoDto downloadInfoDto, { String key }) async - test('test getDownloadInfoOld', () async { - // TODO - }); - //Future> getMapMarkers({ DateTime fileCreatedAfter, DateTime fileCreatedBefore, bool isArchived, bool isFavorite }) async test('test getMapMarkers', () async { // TODO @@ -132,22 +105,12 @@ void main() { // TODO }); - //Future restoreAssetsOld(BulkIdsDto bulkIdsDto) async - test('test restoreAssetsOld', () async { - // TODO - }); - - //Future restoreTrashOld() async - test('test restoreTrashOld', () async { - // TODO - }); - //Future runAssetJobs(AssetJobsDto assetJobsDto) async test('test runAssetJobs', () async { // TODO }); - //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/o_auth_api_test.dart b/mobile/openapi/test/o_auth_api_test.dart index 52326d423f..055963aaec 100644 --- a/mobile/openapi/test/o_auth_api_test.dart +++ b/mobile/openapi/test/o_auth_api_test.dart @@ -22,13 +22,6 @@ void main() { // TODO }); - // @deprecated use feature flags and /oauth/authorize - // - //Future generateOAuthConfig(OAuthConfigDto oAuthConfigDto) async - test('test generateOAuthConfig', () async { - // TODO - }); - //Future linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto) async test('test linkOAuthAccount', () async { // TODO diff --git a/mobile/openapi/test/o_auth_config_response_dto_test.dart b/mobile/openapi/test/o_auth_config_response_dto_test.dart deleted file mode 100644 index fd10307b31..0000000000 --- a/mobile/openapi/test/o_auth_config_response_dto_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -import 'package:openapi/api.dart'; -import 'package:test/test.dart'; - -// tests for OAuthConfigResponseDto -void main() { - // final instance = OAuthConfigResponseDto(); - - group('test OAuthConfigResponseDto', () { - // bool autoLaunch - test('to test the property `autoLaunch`', () async { - // TODO - }); - - // String buttonText - test('to test the property `buttonText`', () async { - // TODO - }); - - // bool enabled - test('to test the property `enabled`', () async { - // TODO - }); - - // bool passwordLoginEnabled - test('to test the property `passwordLoginEnabled`', () async { - // TODO - }); - - // String url - test('to test the property `url`', () async { - // TODO - }); - - - }); - -} diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 769ad31943..d89c47e748 100644 --- a/mobile/openapi/test/search_api_test.dart +++ b/mobile/openapi/test/search_api_test.dart @@ -22,15 +22,30 @@ void main() { // TODO }); - //Future search({ bool clip, bool motion, String q, String query, bool recent, bool smart, String type, bool withArchived }) async + //Future> getSearchSuggestions(SearchSuggestionType type, { String country, String make, String model, String state }) async + test('test getSearchSuggestions', () async { + // TODO + }); + + //Future search({ bool clip, bool motion, num page, String q, String query, bool recent, num size, bool smart, String type, bool withArchived }) async test('test search', () async { // TODO }); + //Future searchMetadata({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + test('test searchMetadata', () async { + // TODO + }); + //Future> searchPerson(String name, { bool withHidden }) async test('test searchPerson', () async { // TODO }); + //Future searchSmart(String query, { String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceId, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, num page, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, bool withArchived, bool withDeleted, bool withExif }) async + test('test searchSmart', () async { + // TODO + }); + }); } diff --git a/mobile/openapi/test/search_asset_response_dto_test.dart b/mobile/openapi/test/search_asset_response_dto_test.dart index 87a7a61e9d..56e8276171 100644 --- a/mobile/openapi/test/search_asset_response_dto_test.dart +++ b/mobile/openapi/test/search_asset_response_dto_test.dart @@ -31,6 +31,11 @@ void main() { // TODO }); + // String nextPage + test('to test the property `nextPage`', () async { + // TODO + }); + // int total test('to test the property `total`', () async { // TODO diff --git a/mobile/openapi/test/search_suggestion_type_test.dart b/mobile/openapi/test/search_suggestion_type_test.dart new file mode 100644 index 0000000000..f86a7de90a --- /dev/null +++ b/mobile/openapi/test/search_suggestion_type_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for SearchSuggestionType +void main() { + + group('test SearchSuggestionType', () { + + }); + +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index de7bbea5ca..ffa57f826b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -960,7 +960,7 @@ packages: source: hosted version: "0.5.0" octo_image: - dependency: transitive + dependency: "direct main" description: name: octo_image sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 095566cf46..ddfed62dad 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: wakelock_plus: ^1.1.4 flutter_local_notifications: ^16.3.2 timezone: ^0.9.2 + octo_image: ^2.0.0 openapi: path: openapi diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index b6c1541906..76012383cf 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -18,7 +18,8 @@ function dart { function typescript { rm -rf ./typescript-sdk/client - npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ./typescript-sdk/client --additional-properties=useSingleRequestParameter=true + npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ./typescript-sdk/axios-client --additional-properties=useSingleRequestParameter=true,supportsES6=true + npx --yes oazapfts --optimistic --argumentStyle=object immich-openapi-specs.json typescript-sdk/fetch-client.ts npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b031456a2a..67f6e45c2c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1055,58 +1055,6 @@ ] } }, - "/asset/assetById/{id}": { - "get": { - "deprecated": true, - "description": "Get a single asset's information", - "operationId": "getAssetById", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/bulk-upload-check": { "post": { "description": "Checks if assets exist by checksums", @@ -1265,160 +1213,6 @@ ] } }, - "/asset/download/archive": { - "post": { - "operationId": "downloadArchiveOld", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetIdsDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/octet-stream": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, - "/asset/download/info": { - "post": { - "operationId": "getDownloadInfoOld", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadInfoDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, - "/asset/download/{id}": { - "post": { - "operationId": "downloadFileOld", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/octet-stream": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/exist": { "post": { "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup", @@ -1732,41 +1526,6 @@ ] } }, - "/asset/restore": { - "post": { - "operationId": "restoreAssetsOld", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/search-terms": { "get": { "operationId": "getAssetSearchTerms", @@ -2217,56 +1976,6 @@ ] } }, - "/asset/trash/empty": { - "post": { - "operationId": "emptyTrashOld", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, - "/asset/trash/restore": { - "post": { - "operationId": "restoreTrashOld", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/upload": { "post": { "operationId": "uploadFile", @@ -2421,6 +2130,7 @@ }, "/assets": { "get": { + "deprecated": true, "operationId": "searchAssets", "parameters": [ { @@ -2721,6 +2431,14 @@ "type": "string" } }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "withDeleted", "required": false, @@ -3935,39 +3653,6 @@ ] } }, - "/oauth/config": { - "post": { - "deprecated": true, - "description": "@deprecated use feature flags and /oauth/authorize", - "operationId": "generateOAuthConfig", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthConfigDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthConfigResponseDto" - } - } - }, - "description": "" - } - }, - "tags": [ - "OAuth" - ] - } - }, "/oauth/link": { "post": { "operationId": "linkOAuthAccount", @@ -4678,13 +4363,13 @@ }, "/search": { "get": { + "deprecated": true, "operationId": "search", "parameters": [ { "name": "clip", "required": false, "in": "query", - "description": "@deprecated", "deprecated": true, "schema": { "type": "boolean" @@ -4698,6 +4383,14 @@ "type": "boolean" } }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, { "name": "q", "required": false, @@ -4722,6 +4415,14 @@ "type": "boolean" } }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, { "name": "smart", "required": false, @@ -4816,6 +4517,377 @@ ] } }, + "/search/metadata": { + "get": { + "operationId": "searchMetadata", + "parameters": [ + { + "name": "checksum", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "createdAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "createdBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "deviceAssetId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "encodedVideoPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEncoded", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isExternal", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMotion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isOffline", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isReadOnly", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isVisible", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "lensModel", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "libraryId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "originalFileName", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "originalPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "resizePath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "takenAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "webpPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withPeople", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/person": { "get": { "operationId": "searchPerson", @@ -4868,6 +4940,372 @@ ] } }, + "/search/smart": { + "get": { + "operationId": "searchSmart", + "parameters": [ + { + "name": "city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "createdAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "createdBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "deviceId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEncoded", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isExternal", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMotion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isOffline", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isReadOnly", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isVisible", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "lensModel", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "libraryId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "query", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "takenAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, + "/search/suggestions": { + "get": { + "operationId": "getSearchSuggestions", + "parameters": [ + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "required": true, + "in": "query", + "schema": { + "$ref": "#/components/schemas/SearchSuggestionType" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/server-info": { "get": { "operationId": "getServerInfo", @@ -8399,30 +8837,6 @@ ], "type": "object" }, - "OAuthConfigResponseDto": { - "properties": { - "autoLaunch": { - "type": "boolean" - }, - "buttonText": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "passwordLoginEnabled": { - "type": "boolean" - }, - "url": { - "type": "string" - } - }, - "required": [ - "enabled", - "passwordLoginEnabled" - ], - "type": "object" - }, "PartnerResponseDto": { "properties": { "avatarColor": { @@ -8806,6 +9220,10 @@ }, "type": "array" }, + "nextPage": { + "nullable": true, + "type": "string" + }, "total": { "type": "integer" } @@ -8814,6 +9232,7 @@ "count", "facets", "items", + "nextPage", "total" ], "type": "object" @@ -8899,6 +9318,16 @@ ], "type": "object" }, + "SearchSuggestionType": { + "enum": [ + "country", + "state", + "city", + "camera-make", + "camera-model" + ], + "type": "string" + }, "ServerConfigDto": { "properties": { "externalDomain": { diff --git a/open-api/typescript-sdk/client/.gitignore b/open-api/typescript-sdk/axios-client/.gitignore similarity index 100% rename from open-api/typescript-sdk/client/.gitignore rename to open-api/typescript-sdk/axios-client/.gitignore diff --git a/open-api/typescript-sdk/client/.npmignore b/open-api/typescript-sdk/axios-client/.npmignore similarity index 100% rename from open-api/typescript-sdk/client/.npmignore rename to open-api/typescript-sdk/axios-client/.npmignore diff --git a/open-api/typescript-sdk/client/.openapi-generator-ignore b/open-api/typescript-sdk/axios-client/.openapi-generator-ignore similarity index 100% rename from open-api/typescript-sdk/client/.openapi-generator-ignore rename to open-api/typescript-sdk/axios-client/.openapi-generator-ignore diff --git a/open-api/typescript-sdk/client/.openapi-generator/FILES b/open-api/typescript-sdk/axios-client/.openapi-generator/FILES similarity index 76% rename from open-api/typescript-sdk/client/.openapi-generator/FILES rename to open-api/typescript-sdk/axios-client/.openapi-generator/FILES index 16b445eee6..a80cd4f07b 100644 --- a/open-api/typescript-sdk/client/.openapi-generator/FILES +++ b/open-api/typescript-sdk/axios-client/.openapi-generator/FILES @@ -1,6 +1,5 @@ .gitignore .npmignore -.openapi-generator-ignore api.ts base.ts common.ts diff --git a/open-api/typescript-sdk/client/.openapi-generator/VERSION b/open-api/typescript-sdk/axios-client/.openapi-generator/VERSION similarity index 100% rename from open-api/typescript-sdk/client/.openapi-generator/VERSION rename to open-api/typescript-sdk/axios-client/.openapi-generator/VERSION diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/axios-client/api.ts similarity index 94% rename from open-api/typescript-sdk/client/api.ts rename to open-api/typescript-sdk/axios-client/api.ts index f8866b7904..80e9588c12 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -2383,43 +2383,6 @@ export interface OAuthConfigDto { */ 'redirectUri': string; } -/** - * - * @export - * @interface OAuthConfigResponseDto - */ -export interface OAuthConfigResponseDto { - /** - * - * @type {boolean} - * @memberof OAuthConfigResponseDto - */ - 'autoLaunch'?: boolean; - /** - * - * @type {string} - * @memberof OAuthConfigResponseDto - */ - 'buttonText'?: string; - /** - * - * @type {boolean} - * @memberof OAuthConfigResponseDto - */ - 'enabled': boolean; - /** - * - * @type {boolean} - * @memberof OAuthConfigResponseDto - */ - 'passwordLoginEnabled': boolean; - /** - * - * @type {string} - * @memberof OAuthConfigResponseDto - */ - 'url'?: string; -} /** * * @export @@ -2924,6 +2887,12 @@ export interface SearchAssetResponseDto { * @memberof SearchAssetResponseDto */ 'items': Array; + /** + * + * @type {string} + * @memberof SearchAssetResponseDto + */ + 'nextPage': string | null; /** * * @type {number} @@ -3026,6 +2995,23 @@ export interface SearchResponseDto { */ 'assets': SearchAssetResponseDto; } +/** + * + * @export + * @enum {string} + */ + +export const SearchSuggestionType = { + Country: 'country', + State: 'state', + City: 'city', + CameraMake: 'camera-make', + CameraModel: 'camera-model' +} as const; + +export type SearchSuggestionType = typeof SearchSuggestionType[keyof typeof SearchSuggestionType]; + + /** * * @export @@ -6981,140 +6967,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {AssetIdsDto} assetIdsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - downloadArchiveOld: async (assetIdsDto: AssetIdsDto, key?: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'assetIdsDto' is not null or undefined - assertParamExists('downloadArchiveOld', 'assetIdsDto', assetIdsDto) - const localVarPath = `/asset/download/archive`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {string} id - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - downloadFileOld: async (id: string, key?: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('downloadFileOld', 'id', id) - const localVarPath = `/asset/download/{id}` - .replace(`{${"id"}}`, encodeURIComponent(String(id))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - emptyTrashOld: async (options: RawAxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/trash/empty`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * Get all AssetEntity belong to the user * @param {string} [ifNoneMatch] ETag of data already cached on the client @@ -7230,54 +7082,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * Get a single asset\'s information - * @param {string} id - * @param {string} [key] - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - getAssetById: async (id: string, key?: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('getAssetById', 'id', id) - const localVarPath = `/asset/assetById/{id}` - .replace(`{${"id"}}`, encodeURIComponent(String(id))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -7553,55 +7357,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {DownloadInfoDto} downloadInfoDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDownloadInfoOld: async (downloadInfoDto: DownloadInfoDto, key?: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'downloadInfoDto' is not null or undefined - assertParamExists('getDownloadInfoOld', 'downloadInfoDto', downloadInfoDto) - const localVarPath = `/asset/download/info`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (key !== undefined) { - localVarQueryParameter['key'] = key; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(downloadInfoDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} [fileCreatedAfter] @@ -7937,88 +7692,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {BulkIdsDto} bulkIdsDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - restoreAssetsOld: async (bulkIdsDto: BulkIdsDto, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'bulkIdsDto' is not null or undefined - assertParamExists('restoreAssetsOld', 'bulkIdsDto', bulkIdsDto) - const localVarPath = `/asset/restore`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - restoreTrashOld: async (options: RawAxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/trash/restore`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -8110,14 +7783,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} [updatedAfter] * @param {string} [updatedBefore] * @param {string} [webpPath] + * @param {boolean} [withArchived] * @param {boolean} [withDeleted] * @param {boolean} [withExif] * @param {boolean} [withPeople] * @param {boolean} [withStacked] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - searchAssets: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + searchAssets: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/assets`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -8299,6 +7974,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['webpPath'] = webpPath; } + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + if (withDeleted !== undefined) { localVarQueryParameter['withDeleted'] = withDeleted; } @@ -8697,43 +8376,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AssetApi.deleteAssets']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * - * @param {AssetIdsDto} assetIdsDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async downloadArchiveOld(assetIdsDto: AssetIdsDto, key?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchiveOld(assetIdsDto, key, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.downloadArchiveOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, - /** - * - * @param {string} id - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async downloadFileOld(id: string, key?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFileOld(id, key, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.downloadFileOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async emptyTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrashOld(options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.emptyTrashOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, /** * Get all AssetEntity belong to the user * @param {string} [ifNoneMatch] ETag of data already cached on the client @@ -8765,20 +8407,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AssetApi.getAllUserAssetsByDeviceId']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * Get a single asset\'s information - * @param {string} id - * @param {string} [key] - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - async getAssetById(id: string, key?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(id, key, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.getAssetById']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, /** * * @param {string} id @@ -8853,19 +8481,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AssetApi.getCuratedObjects']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * - * @param {DownloadInfoDto} downloadInfoDto - * @param {string} [key] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getDownloadInfoOld(downloadInfoDto: DownloadInfoDto, key?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfoOld(downloadInfoDto, key, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.getDownloadInfoOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, /** * * @param {string} [fileCreatedAfter] @@ -8949,29 +8564,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AssetApi.getTimeBuckets']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * - * @param {BulkIdsDto} bulkIdsDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async restoreAssetsOld(bulkIdsDto: BulkIdsDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssetsOld(bulkIdsDto, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.restoreAssetsOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async restoreTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrashOld(options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.restoreTrashOld']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, /** * * @param {AssetJobsDto} assetJobsDto @@ -9022,15 +8614,17 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} [updatedAfter] * @param {string} [updatedBefore] * @param {string} [webpPath] + * @param {boolean} [withArchived] * @param {boolean} [withDeleted] * @param {boolean} [withExif] * @param {boolean} [withPeople] * @param {boolean} [withStacked] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async searchAssets(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked, options); + async searchAssets(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['AssetApi.searchAssets']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); @@ -9151,32 +8745,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath deleteAssets(requestParameters: AssetApiDeleteAssetsRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.deleteAssets(requestParameters.assetBulkDeleteDto, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {AssetApiDownloadArchiveOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - downloadArchiveOld(requestParameters: AssetApiDownloadArchiveOldRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.downloadArchiveOld(requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(axios, basePath)); - }, - /** - * - * @param {AssetApiDownloadFileOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - downloadFileOld(requestParameters: AssetApiDownloadFileOldRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.downloadFileOld(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath)); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - emptyTrashOld(options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.emptyTrashOld(options).then((request) => request(axios, basePath)); - }, /** * Get all AssetEntity belong to the user * @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters. @@ -9195,16 +8763,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); }, - /** - * Get a single asset\'s information - * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - getAssetById(requestParameters: AssetApiGetAssetByIdRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath)); - }, /** * * @param {AssetApiGetAssetInfoRequest} requestParameters Request parameters. @@ -9256,15 +8814,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getCuratedObjects(options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath)); }, - /** - * - * @param {AssetApiGetDownloadInfoOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDownloadInfoOld(requestParameters: AssetApiGetDownloadInfoOldRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.getDownloadInfoOld(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(axios, basePath)); - }, /** * * @param {AssetApiGetMapMarkersRequest} requestParameters Request parameters. @@ -9310,23 +8859,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, requestParameters.personId, requestParameters.userId, requestParameters.withPartners, requestParameters.withStacked, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {AssetApiRestoreAssetsOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - restoreAssetsOld(requestParameters: AssetApiRestoreAssetsOldRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.restoreAssetsOld(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath)); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - restoreTrashOld(options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.restoreTrashOld(options).then((request) => request(axios, basePath)); - }, /** * * @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters. @@ -9340,10 +8872,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise> { - return localVarFp.searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); + return localVarFp.searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); }, /** * @@ -9435,48 +8968,6 @@ export interface AssetApiDeleteAssetsRequest { readonly assetBulkDeleteDto: AssetBulkDeleteDto } -/** - * Request parameters for downloadArchiveOld operation in AssetApi. - * @export - * @interface AssetApiDownloadArchiveOldRequest - */ -export interface AssetApiDownloadArchiveOldRequest { - /** - * - * @type {AssetIdsDto} - * @memberof AssetApiDownloadArchiveOld - */ - readonly assetIdsDto: AssetIdsDto - - /** - * - * @type {string} - * @memberof AssetApiDownloadArchiveOld - */ - readonly key?: string -} - -/** - * Request parameters for downloadFileOld operation in AssetApi. - * @export - * @interface AssetApiDownloadFileOldRequest - */ -export interface AssetApiDownloadFileOldRequest { - /** - * - * @type {string} - * @memberof AssetApiDownloadFileOld - */ - readonly id: string - - /** - * - * @type {string} - * @memberof AssetApiDownloadFileOld - */ - readonly key?: string -} - /** * Request parameters for getAllAssets operation in AssetApi. * @export @@ -9554,27 +9045,6 @@ export interface AssetApiGetAllUserAssetsByDeviceIdRequest { readonly deviceId: string } -/** - * Request parameters for getAssetById operation in AssetApi. - * @export - * @interface AssetApiGetAssetByIdRequest - */ -export interface AssetApiGetAssetByIdRequest { - /** - * - * @type {string} - * @memberof AssetApiGetAssetById - */ - readonly id: string - - /** - * - * @type {string} - * @memberof AssetApiGetAssetById - */ - readonly key?: string -} - /** * Request parameters for getAssetInfo operation in AssetApi. * @export @@ -9652,27 +9122,6 @@ export interface AssetApiGetAssetThumbnailRequest { readonly key?: string } -/** - * Request parameters for getDownloadInfoOld operation in AssetApi. - * @export - * @interface AssetApiGetDownloadInfoOldRequest - */ -export interface AssetApiGetDownloadInfoOldRequest { - /** - * - * @type {DownloadInfoDto} - * @memberof AssetApiGetDownloadInfoOld - */ - readonly downloadInfoDto: DownloadInfoDto - - /** - * - * @type {string} - * @memberof AssetApiGetDownloadInfoOld - */ - readonly key?: string -} - /** * Request parameters for getMapMarkers operation in AssetApi. * @export @@ -9904,20 +9353,6 @@ export interface AssetApiGetTimeBucketsRequest { readonly withStacked?: boolean } -/** - * Request parameters for restoreAssetsOld operation in AssetApi. - * @export - * @interface AssetApiRestoreAssetsOldRequest - */ -export interface AssetApiRestoreAssetsOldRequest { - /** - * - * @type {BulkIdsDto} - * @memberof AssetApiRestoreAssetsOld - */ - readonly bulkIdsDto: BulkIdsDto -} - /** * Request parameters for runAssetJobs operation in AssetApi. * @export @@ -10190,6 +9625,13 @@ export interface AssetApiSearchAssetsRequest { */ readonly webpPath?: string + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withArchived?: boolean + /** * * @type {boolean} @@ -10462,38 +9904,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).deleteAssets(requestParameters.assetBulkDeleteDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiDownloadArchiveOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public downloadArchiveOld(requestParameters: AssetApiDownloadArchiveOldRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).downloadArchiveOld(requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @param {AssetApiDownloadFileOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public downloadFileOld(requestParameters: AssetApiDownloadFileOldRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).downloadFileOld(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public emptyTrashOld(options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).emptyTrashOld(options).then((request) => request(this.axios, this.basePath)); - } - /** * Get all AssetEntity belong to the user * @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters. @@ -10516,18 +9926,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); } - /** - * Get a single asset\'s information - * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - * @memberof AssetApi - */ - public getAssetById(requestParameters: AssetApiGetAssetByIdRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiGetAssetInfoRequest} requestParameters Request parameters. @@ -10591,17 +9989,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiGetDownloadInfoOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public getDownloadInfoOld(requestParameters: AssetApiGetDownloadInfoOldRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).getDownloadInfoOld(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiGetMapMarkersRequest} requestParameters Request parameters. @@ -10657,27 +10044,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, requestParameters.personId, requestParameters.userId, requestParameters.withPartners, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiRestoreAssetsOldRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public restoreAssetsOld(requestParameters: AssetApiRestoreAssetsOldRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).restoreAssetsOld(requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public restoreTrashOld(options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).restoreTrashOld(options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters. @@ -10693,11 +10059,12 @@ export class AssetApi extends BaseAPI { * * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ public searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); } /** @@ -13391,42 +12758,6 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * @deprecated use feature flags and /oauth/authorize - * @param {OAuthConfigDto} oAuthConfigDto - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - generateOAuthConfig: async (oAuthConfigDto: OAuthConfigDto, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'oAuthConfigDto' is not null or undefined - assertParamExists('generateOAuthConfig', 'oAuthConfigDto', oAuthConfigDto) - const localVarPath = `/oauth/config`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(oAuthConfigDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -13595,19 +12926,6 @@ export const OAuthApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['OAuthApi.finishOAuth']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * @deprecated use feature flags and /oauth/authorize - * @param {OAuthConfigDto} oAuthConfigDto - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - async generateOAuthConfig(oAuthConfigDto: OAuthConfigDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.generateOAuthConfig(oAuthConfigDto, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['OAuthApi.generateOAuthConfig']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -13673,16 +12991,6 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath finishOAuth(requestParameters: OAuthApiFinishOAuthRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.finishOAuth(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath)); }, - /** - * @deprecated use feature flags and /oauth/authorize - * @param {OAuthApiGenerateOAuthConfigRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - generateOAuthConfig(requestParameters: OAuthApiGenerateOAuthConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.generateOAuthConfig(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {OAuthApiLinkOAuthAccountRequest} requestParameters Request parameters. @@ -13734,20 +13042,6 @@ export interface OAuthApiFinishOAuthRequest { readonly oAuthCallbackDto: OAuthCallbackDto } -/** - * Request parameters for generateOAuthConfig operation in OAuthApi. - * @export - * @interface OAuthApiGenerateOAuthConfigRequest - */ -export interface OAuthApiGenerateOAuthConfigRequest { - /** - * - * @type {OAuthConfigDto} - * @memberof OAuthApiGenerateOAuthConfig - */ - readonly oAuthConfigDto: OAuthConfigDto -} - /** * Request parameters for linkOAuthAccount operation in OAuthApi. * @export @@ -13794,18 +13088,6 @@ export class OAuthApi extends BaseAPI { return OAuthApiFp(this.configuration).finishOAuth(requestParameters.oAuthCallbackDto, options).then((request) => request(this.axios, this.basePath)); } - /** - * @deprecated use feature flags and /oauth/authorize - * @param {OAuthApiGenerateOAuthConfigRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - * @memberof OAuthApi - */ - public generateOAuthConfig(requestParameters: OAuthApiGenerateOAuthConfigRequest, options?: RawAxiosRequestConfig) { - return OAuthApiFp(this.configuration).generateOAuthConfig(requestParameters.oAuthConfigDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {OAuthApiLinkOAuthAccountRequest} requestParameters Request parameters. @@ -15256,18 +14538,86 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio }, /** * - * @param {boolean} [clip] @deprecated + * @param {SearchSuggestionType} type + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSearchSuggestions: async (type: SearchSuggestionType, country?: string, make?: string, model?: string, state?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'type' is not null or undefined + assertParamExists('getSearchSuggestions', 'type', type) + const localVarPath = `/search/suggestions`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {boolean} [clip] * @param {boolean} [motion] + * @param {number} [page] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {number} [size] * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + search: async (clip?: boolean, motion?: boolean, page?: number, q?: string, query?: string, recent?: boolean, size?: number, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/search`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -15297,6 +14647,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['motion'] = motion; } + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + if (q !== undefined) { localVarQueryParameter['q'] = q; } @@ -15309,6 +14663,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['recent'] = recent; } + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + if (smart !== undefined) { localVarQueryParameter['smart'] = smart; } @@ -15323,6 +14681,265 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [checksum] + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [encodedVideoPath] + * @param {string} [id] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {AssetOrder} [order] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {number} [page] + * @param {string} [resizePath] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {string} [webpPath] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {boolean} [withStacked] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchMetadata: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/search/metadata`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (checksum !== undefined) { + localVarQueryParameter['checksum'] = checksum; + } + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (deviceAssetId !== undefined) { + localVarQueryParameter['deviceAssetId'] = deviceAssetId; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (encodedVideoPath !== undefined) { + localVarQueryParameter['encodedVideoPath'] = encodedVideoPath; + } + + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (order !== undefined) { + localVarQueryParameter['order'] = order; + } + + if (originalFileName !== undefined) { + localVarQueryParameter['originalFileName'] = originalFileName; + } + + if (originalPath !== undefined) { + localVarQueryParameter['originalPath'] = originalPath; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (resizePath !== undefined) { + localVarQueryParameter['resizePath'] = resizePath; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (webpPath !== undefined) { + localVarQueryParameter['webpPath'] = webpPath; + } + + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + if (withPeople !== undefined) { + localVarQueryParameter['withPeople'] = withPeople; + } + + if (withStacked !== undefined) { + localVarQueryParameter['withStacked'] = withStacked; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -15373,6 +14990,217 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} query + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceId] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {number} [page] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchSmart: async (query: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceId?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, page?: number, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'query' is not null or undefined + assertParamExists('searchSmart', 'query', query) + const localVarPath = `/search/smart`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (query !== undefined) { + localVarQueryParameter['query'] = query; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -15405,23 +15233,94 @@ export const SearchApiFp = function(configuration?: Configuration) { }, /** * - * @param {boolean} [clip] @deprecated + * @param {SearchSuggestionType} type + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getSearchSuggestions(type: SearchSuggestionType, country?: string, make?: string, model?: string, state?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchSuggestions(type, country, make, model, state, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.getSearchSuggestions']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @param {boolean} [clip] * @param {boolean} [motion] + * @param {number} [page] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {number} [size] * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async search(clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, smart, type, withArchived, options); + async search(clip?: boolean, motion?: boolean, page?: number, q?: string, query?: string, recent?: boolean, size?: number, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, page, q, query, recent, size, smart, type, withArchived, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['SearchApi.search']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @param {string} [checksum] + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [encodedVideoPath] + * @param {string} [id] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {AssetOrder} [order] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {number} [page] + * @param {string} [resizePath] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {string} [webpPath] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {boolean} [withStacked] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchMetadata(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.searchMetadata']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @param {string} name @@ -15435,6 +15334,48 @@ export const SearchApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @param {string} query + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceId] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {number} [page] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchSmart(query: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceId?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, page?: number, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.searchSmart']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, } }; @@ -15455,12 +15396,31 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat }, /** * - * @param {SearchApiSearchRequest} requestParameters Request parameters. + * @param {SearchApiGetSearchSuggestionsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ + getSearchSuggestions(requestParameters: SearchApiGetSearchSuggestionsRequest, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.getSearchSuggestions(requestParameters.type, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.state, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {SearchApiSearchRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); + return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.page, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.size, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {SearchApiSearchMetadataRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchMetadata(requestParameters: SearchApiSearchMetadataRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.searchMetadata(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); }, /** * @@ -15471,9 +15431,60 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchSmart(requestParameters: SearchApiSearchSmartRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.searchSmart(requestParameters.query, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceId, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.page, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, options).then((request) => request(axios, basePath)); + }, }; }; +/** + * Request parameters for getSearchSuggestions operation in SearchApi. + * @export + * @interface SearchApiGetSearchSuggestionsRequest + */ +export interface SearchApiGetSearchSuggestionsRequest { + /** + * + * @type {SearchSuggestionType} + * @memberof SearchApiGetSearchSuggestions + */ + readonly type: SearchSuggestionType + + /** + * + * @type {string} + * @memberof SearchApiGetSearchSuggestions + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof SearchApiGetSearchSuggestions + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof SearchApiGetSearchSuggestions + */ + readonly model?: string + + /** + * + * @type {string} + * @memberof SearchApiGetSearchSuggestions + */ + readonly state?: string +} + /** * Request parameters for search operation in SearchApi. * @export @@ -15481,7 +15492,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat */ export interface SearchApiSearchRequest { /** - * @deprecated + * * @type {boolean} * @memberof SearchApiSearch */ @@ -15494,6 +15505,13 @@ export interface SearchApiSearchRequest { */ readonly motion?: boolean + /** + * + * @type {number} + * @memberof SearchApiSearch + */ + readonly page?: number + /** * * @type {string} @@ -15515,6 +15533,13 @@ export interface SearchApiSearchRequest { */ readonly recent?: boolean + /** + * + * @type {number} + * @memberof SearchApiSearch + */ + readonly size?: number + /** * * @type {boolean} @@ -15537,6 +15562,300 @@ export interface SearchApiSearchRequest { readonly withArchived?: boolean } +/** + * Request parameters for searchMetadata operation in SearchApi. + * @export + * @interface SearchApiSearchMetadataRequest + */ +export interface SearchApiSearchMetadataRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly checksum?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly deviceAssetId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly deviceId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly encodedVideoPath?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly id?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isVisible?: boolean + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly lensModel?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly libraryId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly model?: string + + /** + * + * @type {AssetOrder} + * @memberof SearchApiSearchMetadata + */ + readonly order?: AssetOrder + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly originalFileName?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly originalPath?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchMetadata + */ + readonly page?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly resizePath?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchMetadata + */ + readonly size?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly trashedBefore?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof SearchApiSearchMetadata + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly updatedBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly webpPath?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withExif?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withPeople?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withStacked?: boolean +} + /** * Request parameters for searchPerson operation in SearchApi. * @export @@ -15558,6 +15877,230 @@ export interface SearchApiSearchPersonRequest { readonly withHidden?: boolean } +/** + * Request parameters for searchSmart operation in SearchApi. + * @export + * @interface SearchApiSearchSmartRequest + */ +export interface SearchApiSearchSmartRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly query: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly deviceId?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isVisible?: boolean + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly lensModel?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly libraryId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly model?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchSmart + */ + readonly page?: number + + /** + * + * @type {number} + * @memberof SearchApiSearchSmart + */ + readonly size?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly trashedBefore?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof SearchApiSearchSmart + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly updatedBefore?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withExif?: boolean +} + /** * SearchApi - object-oriented interface * @export @@ -15577,13 +16120,36 @@ export class SearchApi extends BaseAPI { /** * - * @param {SearchApiSearchRequest} requestParameters Request parameters. + * @param {SearchApiGetSearchSuggestionsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SearchApi */ + public getSearchSuggestions(requestParameters: SearchApiGetSearchSuggestionsRequest, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).getSearchSuggestions(requestParameters.type, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.state, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SearchApiSearchRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + * @memberof SearchApi + */ public search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig) { - return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); + return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.page, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.size, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SearchApiSearchMetadataRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchMetadata(requestParameters: SearchApiSearchMetadataRequest = {}, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).searchMetadata(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); } /** @@ -15596,6 +16162,17 @@ export class SearchApi extends BaseAPI { public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig) { return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchSmart(requestParameters: SearchApiSearchSmartRequest, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).searchSmart(requestParameters.query, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceId, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.page, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, options).then((request) => request(this.axios, this.basePath)); + } } /** diff --git a/open-api/typescript-sdk/client/base.ts b/open-api/typescript-sdk/axios-client/base.ts similarity index 100% rename from open-api/typescript-sdk/client/base.ts rename to open-api/typescript-sdk/axios-client/base.ts diff --git a/open-api/typescript-sdk/client/common.ts b/open-api/typescript-sdk/axios-client/common.ts similarity index 100% rename from open-api/typescript-sdk/client/common.ts rename to open-api/typescript-sdk/axios-client/common.ts diff --git a/open-api/typescript-sdk/client/configuration.ts b/open-api/typescript-sdk/axios-client/configuration.ts similarity index 100% rename from open-api/typescript-sdk/client/configuration.ts rename to open-api/typescript-sdk/axios-client/configuration.ts diff --git a/open-api/typescript-sdk/client/git_push.sh b/open-api/typescript-sdk/axios-client/git_push.sh similarity index 100% rename from open-api/typescript-sdk/client/git_push.sh rename to open-api/typescript-sdk/axios-client/git_push.sh diff --git a/open-api/typescript-sdk/client/index.ts b/open-api/typescript-sdk/axios-client/index.ts similarity index 100% rename from open-api/typescript-sdk/client/index.ts rename to open-api/typescript-sdk/axios-client/index.ts diff --git a/open-api/typescript-sdk/axios.ts b/open-api/typescript-sdk/axios.ts new file mode 100644 index 0000000000..9cda702043 --- /dev/null +++ b/open-api/typescript-sdk/axios.ts @@ -0,0 +1,4 @@ +export * from './axios-client'; +export * as base from './axios-client/base'; +export * as configuration from './axios-client/configuration'; +export * as common from './axios-client/common'; diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts new file mode 100644 index 0000000000..00b8c68326 --- /dev/null +++ b/open-api/typescript-sdk/fetch-client.ts @@ -0,0 +1,2718 @@ +/** + * Immich + * 1.94.1 + * DO NOT MODIFY - This file has been generated using oazapfts. + * See https://www.npmjs.com/package/oazapfts + */ +import * as Oazapfts from "@oazapfts/runtime"; +import * as QS from "@oazapfts/runtime/query"; +export const defaults: Oazapfts.Defaults = { + headers: {}, + baseUrl: "/api", +}; +const oazapfts = Oazapfts.runtime(defaults); +export const servers = { + server1: "/api" +}; +export type ReactionLevel = "album" | "asset"; +export type ReactionType = "comment" | "like"; +export type UserAvatarColor = "primary" | "pink" | "red" | "yellow" | "blue" | "green" | "purple" | "orange" | "gray" | "amber"; +export type UserDto = { + avatarColor: UserAvatarColor; + email: string; + id: string; + name: string; + profileImagePath: string; +}; +export type ActivityResponseDto = { + assetId: string | null; + comment?: string | null; + createdAt: string; + id: string; + "type": "comment" | "like"; + user: UserDto; +}; +export type ActivityCreateDto = { + albumId: string; + assetId?: string; + comment?: string; + "type": ReactionType; +}; +export type ActivityStatisticsResponseDto = { + comments: number; +}; +export type ExifResponseDto = { + city?: string | null; + country?: string | null; + dateTimeOriginal?: string | null; + description?: string | null; + exifImageHeight?: number | null; + exifImageWidth?: number | null; + exposureTime?: string | null; + fNumber?: number | null; + fileSizeInByte?: number | null; + focalLength?: number | null; + iso?: number | null; + latitude?: number | null; + lensModel?: string | null; + longitude?: number | null; + make?: string | null; + model?: string | null; + modifyDate?: string | null; + orientation?: string | null; + projectionType?: string | null; + state?: string | null; + timeZone?: string | null; +}; +export type UserResponseDto = { + avatarColor: UserAvatarColor; + createdAt: string; + deletedAt: string | null; + email: string; + externalPath: string | null; + id: string; + isAdmin: boolean; + memoriesEnabled?: boolean; + name: string; + oauthId: string; + profileImagePath: string; + quotaSizeInBytes: number | null; + quotaUsageInBytes: number | null; + shouldChangePassword: boolean; + storageLabel: string | null; + updatedAt: string; +}; +export type AssetFaceWithoutPersonResponseDto = { + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + id: string; + imageHeight: number; + imageWidth: number; +}; +export type PersonWithFacesResponseDto = { + birthDate: string | null; + faces: AssetFaceWithoutPersonResponseDto[]; + id: string; + isHidden: boolean; + name: string; + thumbnailPath: string; +}; +export type SmartInfoResponseDto = { + objects?: string[] | null; + tags?: string[] | null; +}; +export type TagTypeEnum = "OBJECT" | "FACE" | "CUSTOM"; +export type TagResponseDto = { + id: string; + name: string; + "type": TagTypeEnum; + userId: string; +}; +export type AssetTypeEnum = "IMAGE" | "VIDEO" | "AUDIO" | "OTHER"; +export type AssetResponseDto = { + /** base64 encoded sha1 hash */ + checksum: string; + deviceAssetId: string; + deviceId: string; + duration: string; + exifInfo?: ExifResponseDto; + fileCreatedAt: string; + fileModifiedAt: string; + hasMetadata: boolean; + id: string; + isArchived: boolean; + isExternal: boolean; + isFavorite: boolean; + isOffline: boolean; + isReadOnly: boolean; + isTrashed: boolean; + libraryId: string; + livePhotoVideoId?: string | null; + localDateTime: string; + originalFileName: string; + originalPath: string; + owner?: UserResponseDto; + ownerId: string; + people?: PersonWithFacesResponseDto[]; + resized: boolean; + smartInfo?: SmartInfoResponseDto; + stack?: AssetResponseDto[]; + stackCount: number | null; + stackParentId?: string | null; + tags?: TagResponseDto[]; + thumbhash: string | null; + "type": AssetTypeEnum; + updatedAt: string; +}; +export type AlbumResponseDto = { + albumName: string; + albumThumbnailAssetId: string | null; + assetCount: number; + assets: AssetResponseDto[]; + createdAt: string; + description: string; + endDate?: string; + hasSharedLink: boolean; + id: string; + isActivityEnabled: boolean; + lastModifiedAssetTimestamp?: string; + owner: UserResponseDto; + ownerId: string; + shared: boolean; + sharedUsers: UserResponseDto[]; + startDate?: string; + updatedAt: string; +}; +export type CreateAlbumDto = { + albumName: string; + assetIds?: string[]; + description?: string; + sharedWithUserIds?: string[]; +}; +export type AlbumCountResponseDto = { + notShared: number; + owned: number; + shared: number; +}; +export type UpdateAlbumDto = { + albumName?: string; + albumThumbnailAssetId?: string; + description?: string; + isActivityEnabled?: boolean; +}; +export type BulkIdsDto = { + ids: string[]; +}; +export type BulkIdResponseDto = { + error?: "duplicate" | "no_permission" | "not_found" | "unknown"; + id: string; + success: boolean; +}; +export type AddUsersDto = { + sharedUserIds: string[]; +}; +export type ApiKeyResponseDto = { + createdAt: string; + id: string; + name: string; + updatedAt: string; +}; +export type ApiKeyCreateDto = { + name?: string; +}; +export type ApiKeyCreateResponseDto = { + apiKey: ApiKeyResponseDto; + secret: string; +}; +export type ApiKeyUpdateDto = { + name: string; +}; +export type AssetBulkDeleteDto = { + force?: boolean; + ids: string[]; +}; +export type AssetBulkUpdateDto = { + dateTimeOriginal?: string; + ids: string[]; + isArchived?: boolean; + isFavorite?: boolean; + latitude?: number; + longitude?: number; + removeParent?: boolean; + stackParentId?: string; +}; +export type AssetBulkUploadCheckItem = { + /** base64 or hex encoded sha1 hash */ + checksum: string; + id: string; +}; +export type AssetBulkUploadCheckDto = { + assets: AssetBulkUploadCheckItem[]; +}; +export type AssetBulkUploadCheckResult = { + action: "accept" | "reject"; + assetId?: string; + id: string; + reason?: "duplicate" | "unsupported-format"; +}; +export type AssetBulkUploadCheckResponseDto = { + results: AssetBulkUploadCheckResult[]; +}; +export type CuratedLocationsResponseDto = { + city: string; + deviceAssetId: string; + deviceId: string; + id: string; + resizePath: string; +}; +export type CuratedObjectsResponseDto = { + deviceAssetId: string; + deviceId: string; + id: string; + "object": string; + resizePath: string; +}; +export type CheckExistingAssetsDto = { + deviceAssetIds: string[]; + deviceId: string; +}; +export type CheckExistingAssetsResponseDto = { + existingIds: string[]; +}; +export type AssetJobName = "regenerate-thumbnail" | "refresh-metadata" | "transcode-video"; +export type AssetJobsDto = { + assetIds: string[]; + name: AssetJobName; +}; +export type MapMarkerResponseDto = { + id: string; + lat: number; + lon: number; +}; +export type MemoryLaneResponseDto = { + assets: AssetResponseDto[]; + title: string; +}; +export type UpdateStackParentDto = { + newParentId: string; + oldParentId: string; +}; +export type AssetStatsResponseDto = { + images: number; + total: number; + videos: number; +}; +export type ThumbnailFormat = "JPEG" | "WEBP"; +export type TimeBucketSize = "DAY" | "MONTH"; +export type TimeBucketResponseDto = { + count: number; + timeBucket: string; +}; +export type CreateAssetDto = { + assetData: Blob; + deviceAssetId: string; + deviceId: string; + duration?: string; + fileCreatedAt: string; + fileModifiedAt: string; + isArchived?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + libraryId?: string; + livePhotoData?: Blob; + sidecarData?: Blob; +}; +export type AssetFileUploadResponseDto = { + duplicate: boolean; + id: string; +}; +export type UpdateAssetDto = { + dateTimeOriginal?: string; + description?: string; + isArchived?: boolean; + isFavorite?: boolean; + latitude?: number; + longitude?: number; +}; +export type AssetOrder = "asc" | "desc"; +export type EntityType = "ASSET" | "ALBUM"; +export type AuditDeletesResponseDto = { + ids: string[]; + needsFullSync: boolean; +}; +export type PathEntityType = "asset" | "person" | "user"; +export type PathType = "original" | "jpeg_thumbnail" | "webp_thumbnail" | "encoded_video" | "sidecar" | "face" | "profile"; +export type FileReportItemDto = { + checksum?: string; + entityId: string; + entityType: PathEntityType; + pathType: PathType; + pathValue: string; +}; +export type FileReportDto = { + extras: string[]; + orphans: FileReportItemDto[]; +}; +export type FileChecksumDto = { + filenames: string[]; +}; +export type FileChecksumResponseDto = { + checksum: string; + filename: string; +}; +export type FileReportFixDto = { + items: FileReportItemDto[]; +}; +export type SignUpDto = { + email: string; + name: string; + password: string; +}; +export type ChangePasswordDto = { + newPassword: string; + password: string; +}; +export type AuthDeviceResponseDto = { + createdAt: string; + current: boolean; + deviceOS: string; + deviceType: string; + id: string; + updatedAt: string; +}; +export type LoginCredentialDto = { + email: string; + password: string; +}; +export type LoginResponseDto = { + accessToken: string; + isAdmin: boolean; + name: string; + profileImagePath: string; + shouldChangePassword: boolean; + userEmail: string; + userId: string; +}; +export type LogoutResponseDto = { + redirectUri: string; + successful: boolean; +}; +export type ValidateAccessTokenResponseDto = { + authStatus: boolean; +}; +export type AssetIdsDto = { + assetIds: string[]; +}; +export type DownloadInfoDto = { + albumId?: string; + archiveSize?: number; + assetIds?: string[]; + userId?: string; +}; +export type DownloadArchiveInfo = { + assetIds: string[]; + size: number; +}; +export type DownloadResponseDto = { + archives: DownloadArchiveInfo[]; + totalSize: number; +}; +export type PersonResponseDto = { + birthDate: string | null; + id: string; + isHidden: boolean; + name: string; + thumbnailPath: string; +}; +export type AssetFaceResponseDto = { + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + id: string; + imageHeight: number; + imageWidth: number; + person: (PersonResponseDto) | null; +}; +export type FaceDto = { + id: string; +}; +export type JobCountsDto = { + active: number; + completed: number; + delayed: number; + failed: number; + paused: number; + waiting: number; +}; +export type QueueStatusDto = { + isActive: boolean; + isPaused: boolean; +}; +export type JobStatusDto = { + jobCounts: JobCountsDto; + queueStatus: QueueStatusDto; +}; +export type AllJobStatusResponseDto = { + backgroundTask: JobStatusDto; + faceDetection: JobStatusDto; + facialRecognition: JobStatusDto; + library: JobStatusDto; + metadataExtraction: JobStatusDto; + migration: JobStatusDto; + search: JobStatusDto; + sidecar: JobStatusDto; + smartSearch: JobStatusDto; + storageTemplateMigration: JobStatusDto; + thumbnailGeneration: JobStatusDto; + videoConversion: JobStatusDto; +}; +export type JobName = "thumbnailGeneration" | "metadataExtraction" | "videoConversion" | "faceDetection" | "facialRecognition" | "smartSearch" | "backgroundTask" | "storageTemplateMigration" | "migration" | "search" | "sidecar" | "library"; +export type JobCommand = "start" | "pause" | "resume" | "empty" | "clear-failed"; +export type JobCommandDto = { + command: JobCommand; + force: boolean; +}; +export type LibraryType = "UPLOAD" | "EXTERNAL"; +export type LibraryResponseDto = { + assetCount: number; + createdAt: string; + exclusionPatterns: string[]; + id: string; + importPaths: string[]; + name: string; + ownerId: string; + refreshedAt: string | null; + "type": LibraryType; + updatedAt: string; +}; +export type CreateLibraryDto = { + exclusionPatterns?: string[]; + importPaths?: string[]; + isVisible?: boolean; + isWatched?: boolean; + name?: string; + "type": LibraryType; +}; +export type UpdateLibraryDto = { + exclusionPatterns?: string[]; + importPaths?: string[]; + isVisible?: boolean; + name?: string; +}; +export type ScanLibraryDto = { + refreshAllFiles?: boolean; + refreshModifiedFiles?: boolean; +}; +export type LibraryStatsResponseDto = { + photos: number; + total: number; + usage: number; + videos: number; +}; +export type OAuthConfigDto = { + redirectUri: string; +}; +export type OAuthAuthorizeResponseDto = { + url: string; +}; +export type OAuthCallbackDto = { + url: string; +}; +export type PartnerResponseDto = { + avatarColor: UserAvatarColor; + createdAt: string; + deletedAt: string | null; + email: string; + externalPath: string | null; + id: string; + inTimeline?: boolean; + isAdmin: boolean; + memoriesEnabled?: boolean; + name: string; + oauthId: string; + profileImagePath: string; + quotaSizeInBytes: number | null; + quotaUsageInBytes: number | null; + shouldChangePassword: boolean; + storageLabel: string | null; + updatedAt: string; +}; +export type UpdatePartnerDto = { + inTimeline: boolean; +}; +export type PeopleResponseDto = { + people: PersonResponseDto[]; + total: number; +}; +export type PeopleUpdateItem = { + /** Person date of birth. + Note: the mobile app cannot currently set the birth date to null. */ + birthDate?: string | null; + /** Asset is used to get the feature face thumbnail. */ + featureFaceAssetId?: string; + /** Person id. */ + id: string; + /** Person visibility */ + isHidden?: boolean; + /** Person name. */ + name?: string; +}; +export type PeopleUpdateDto = { + people: PeopleUpdateItem[]; +}; +export type PersonUpdateDto = { + /** Person date of birth. + Note: the mobile app cannot currently set the birth date to null. */ + birthDate?: string | null; + /** Asset is used to get the feature face thumbnail. */ + featureFaceAssetId?: string; + /** Person visibility */ + isHidden?: boolean; + /** Person name. */ + name?: string; +}; +export type MergePersonDto = { + ids: string[]; +}; +export type AssetFaceUpdateItem = { + assetId: string; + personId: string; +}; +export type AssetFaceUpdateDto = { + data: AssetFaceUpdateItem[]; +}; +export type PersonStatisticsResponseDto = { + assets: number; +}; +export type SearchFacetCountResponseDto = { + count: number; + value: string; +}; +export type SearchFacetResponseDto = { + counts: SearchFacetCountResponseDto[]; + fieldName: string; +}; +export type SearchAlbumResponseDto = { + count: number; + facets: SearchFacetResponseDto[]; + items: AlbumResponseDto[]; + total: number; +}; +export type SearchAssetResponseDto = { + count: number; + facets: SearchFacetResponseDto[]; + items: AssetResponseDto[]; + nextPage: string | null; + total: number; +}; +export type SearchResponseDto = { + albums: SearchAlbumResponseDto; + assets: SearchAssetResponseDto; +}; +export type SearchExploreItem = { + data: AssetResponseDto; + value: string; +}; +export type SearchExploreResponseDto = { + fieldName: string; + items: SearchExploreItem[]; +}; +export type SearchSuggestionType = "country" | "state" | "city" | "camera-make" | "camera-model"; +export type ServerInfoResponseDto = { + diskAvailable: string; + diskAvailableRaw: number; + diskSize: string; + diskSizeRaw: number; + diskUsagePercentage: number; + diskUse: string; + diskUseRaw: number; +}; +export type ServerConfigDto = { + externalDomain: string; + isInitialized: boolean; + isOnboarded: boolean; + loginPageMessage: string; + oauthButtonText: string; + trashDays: number; +}; +export type ServerFeaturesDto = { + configFile: boolean; + facialRecognition: boolean; + map: boolean; + oauth: boolean; + oauthAutoLaunch: boolean; + passwordLogin: boolean; + reverseGeocoding: boolean; + search: boolean; + sidecar: boolean; + smartSearch: boolean; + trash: boolean; +}; +export type ServerMediaTypesResponseDto = { + image: string[]; + sidecar: string[]; + video: string[]; +}; +export type ServerPingResponse = {}; +export type ServerPingResponseRead = { + res: string; +}; +export type UsageByUserDto = { + photos: number; + quotaSizeInBytes: number | null; + usage: number; + userId: string; + userName: string; + videos: number; +}; +export type ServerStatsResponseDto = { + photos: number; + usage: number; + usageByUser: UsageByUserDto[]; + videos: number; +}; +export type ServerThemeDto = { + customCss: string; +}; +export type ServerVersionResponseDto = { + major: number; + minor: number; + patch: number; +}; +export type SharedLinkType = "ALBUM" | "INDIVIDUAL"; +export type SharedLinkResponseDto = { + album?: AlbumResponseDto; + allowDownload: boolean; + allowUpload: boolean; + assets: AssetResponseDto[]; + createdAt: string; + description: string | null; + expiresAt: string | null; + id: string; + key: string; + password: string | null; + showMetadata: boolean; + token?: string | null; + "type": SharedLinkType; + userId: string; +}; +export type SharedLinkCreateDto = { + albumId?: string; + allowDownload?: boolean; + allowUpload?: boolean; + assetIds?: string[]; + description?: string; + expiresAt?: string | null; + password?: string; + showMetadata?: boolean; + "type": SharedLinkType; +}; +export type SharedLinkEditDto = { + allowDownload?: boolean; + allowUpload?: boolean; + /** Few clients cannot send null to set the expiryTime to never. + Setting this flag and not sending expiryAt is considered as null instead. + Clients that can send null values can ignore this. */ + changeExpiryTime?: boolean; + description?: string; + expiresAt?: string | null; + password?: string; + showMetadata?: boolean; +}; +export type AssetIdsResponseDto = { + assetId: string; + error?: "duplicate" | "no_permission" | "not_found"; + success: boolean; +}; +export type TranscodeHwAccel = "nvenc" | "qsv" | "vaapi" | "rkmpp" | "disabled"; +export type AudioCodec = "mp3" | "aac" | "libopus"; +export type VideoCodec = "h264" | "hevc" | "vp9"; +export type CqMode = "auto" | "cqp" | "icq"; +export type ToneMapping = "hable" | "mobius" | "reinhard" | "disabled"; +export type TranscodePolicy = "all" | "optimal" | "bitrate" | "required" | "disabled"; +export type SystemConfigFFmpegDto = { + accel: TranscodeHwAccel; + acceptedAudioCodecs: AudioCodec[]; + acceptedVideoCodecs: VideoCodec[]; + bframes: number; + cqMode: CqMode; + crf: number; + gopSize: number; + maxBitrate: string; + npl: number; + preferredHwDevice: string; + preset: string; + refs: number; + targetAudioCodec: AudioCodec; + targetResolution: string; + targetVideoCodec: VideoCodec; + temporalAQ: boolean; + threads: number; + tonemap: ToneMapping; + transcode: TranscodePolicy; + twoPass: boolean; +}; +export type JobSettingsDto = { + concurrency: number; +}; +export type SystemConfigJobDto = { + backgroundTask: JobSettingsDto; + faceDetection: JobSettingsDto; + library: JobSettingsDto; + metadataExtraction: JobSettingsDto; + migration: JobSettingsDto; + search: JobSettingsDto; + sidecar: JobSettingsDto; + smartSearch: JobSettingsDto; + thumbnailGeneration: JobSettingsDto; + videoConversion: JobSettingsDto; +}; +export type SystemConfigLibraryScanDto = { + cronExpression: string; + enabled: boolean; +}; +export type SystemConfigLibraryWatchDto = { + enabled: boolean; + interval: number; + usePolling: boolean; +}; +export type SystemConfigLibraryDto = { + scan: SystemConfigLibraryScanDto; + watch: SystemConfigLibraryWatchDto; +}; +export type LogLevel = "verbose" | "debug" | "log" | "warn" | "error" | "fatal"; +export type SystemConfigLoggingDto = { + enabled: boolean; + level: LogLevel; +}; +export type ClipMode = "vision" | "text"; +export type ModelType = "facial-recognition" | "clip"; +export type ClipConfig = { + enabled: boolean; + mode?: ClipMode; + modelName: string; + modelType?: ModelType; +}; +export type RecognitionConfig = { + enabled: boolean; + maxDistance: number; + minFaces: number; + minScore: number; + modelName: string; + modelType?: ModelType; +}; +export type SystemConfigMachineLearningDto = { + clip: ClipConfig; + enabled: boolean; + facialRecognition: RecognitionConfig; + url: string; +}; +export type SystemConfigMapDto = { + darkStyle: string; + enabled: boolean; + lightStyle: string; +}; +export type SystemConfigNewVersionCheckDto = { + enabled: boolean; +}; +export type SystemConfigOAuthDto = { + autoLaunch: boolean; + autoRegister: boolean; + buttonText: string; + clientId: string; + clientSecret: string; + enabled: boolean; + issuerUrl: string; + mobileOverrideEnabled: boolean; + mobileRedirectUri: string; + scope: string; + signingAlgorithm: string; + storageLabelClaim: string; +}; +export type SystemConfigPasswordLoginDto = { + enabled: boolean; +}; +export type SystemConfigReverseGeocodingDto = { + enabled: boolean; +}; +export type SystemConfigServerDto = { + externalDomain: string; + loginPageMessage: string; +}; +export type SystemConfigStorageTemplateDto = { + enabled: boolean; + hashVerificationEnabled: boolean; + template: string; +}; +export type SystemConfigThemeDto = { + customCss: string; +}; +export type Colorspace = "srgb" | "p3"; +export type SystemConfigThumbnailDto = { + colorspace: Colorspace; + jpegSize: number; + quality: number; + webpSize: number; +}; +export type SystemConfigTrashDto = { + days: number; + enabled: boolean; +}; +export type SystemConfigDto = { + ffmpeg: SystemConfigFFmpegDto; + job: SystemConfigJobDto; + library: SystemConfigLibraryDto; + logging: SystemConfigLoggingDto; + machineLearning: SystemConfigMachineLearningDto; + map: SystemConfigMapDto; + newVersionCheck: SystemConfigNewVersionCheckDto; + oauth: SystemConfigOAuthDto; + passwordLogin: SystemConfigPasswordLoginDto; + reverseGeocoding: SystemConfigReverseGeocodingDto; + server: SystemConfigServerDto; + storageTemplate: SystemConfigStorageTemplateDto; + theme: SystemConfigThemeDto; + thumbnail: SystemConfigThumbnailDto; + trash: SystemConfigTrashDto; +}; +export type MapTheme = "light" | "dark"; +export type SystemConfigTemplateStorageOptionDto = { + dayOptions: string[]; + hourOptions: string[]; + minuteOptions: string[]; + monthOptions: string[]; + presetOptions: string[]; + secondOptions: string[]; + weekOptions: string[]; + yearOptions: string[]; +}; +export type CreateTagDto = { + name: string; + "type": TagTypeEnum; +}; +export type UpdateTagDto = { + name?: string; +}; +export type CreateUserDto = { + email: string; + externalPath?: string | null; + memoriesEnabled?: boolean; + name: string; + password: string; + quotaSizeInBytes?: number | null; + storageLabel?: string | null; +}; +export type UpdateUserDto = { + avatarColor?: UserAvatarColor; + email?: string; + externalPath?: string; + id: string; + isAdmin?: boolean; + memoriesEnabled?: boolean; + name?: string; + password?: string; + quotaSizeInBytes?: number | null; + shouldChangePassword?: boolean; + storageLabel?: string; +}; +export type CreateProfileImageDto = { + file: Blob; +}; +export type CreateProfileImageResponseDto = { + profileImagePath: string; + userId: string; +}; +export function getActivities({ albumId, assetId, level, $type, userId }: { + albumId: string; + assetId?: string; + level?: ReactionLevel; + $type?: ReactionType; + userId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ActivityResponseDto[]; + }>(`/activity${QS.query(QS.explode({ + albumId, + assetId, + level, + "type": $type, + userId + }))}`, { + ...opts + })); +} +export function createActivity({ activityCreateDto }: { + activityCreateDto: ActivityCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: ActivityResponseDto; + }>("/activity", oazapfts.json({ + ...opts, + method: "POST", + body: activityCreateDto + }))); +} +export function getActivityStatistics({ albumId, assetId }: { + albumId: string; + assetId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ActivityStatisticsResponseDto; + }>(`/activity/statistics${QS.query(QS.explode({ + albumId, + assetId + }))}`, { + ...opts + })); +} +export function deleteActivity({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/activity/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getAllAlbums({ assetId, shared }: { + assetId?: string; + shared?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumResponseDto[]; + }>(`/album${QS.query(QS.explode({ + assetId, + shared + }))}`, { + ...opts + })); +} +export function createAlbum({ createAlbumDto }: { + createAlbumDto: CreateAlbumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: AlbumResponseDto; + }>("/album", oazapfts.json({ + ...opts, + method: "POST", + body: createAlbumDto + }))); +} +export function getAlbumCount(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumCountResponseDto; + }>("/album/count", { + ...opts + })); +} +export function deleteAlbum({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/album/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getAlbumInfo({ id, key, withoutAssets }: { + id: string; + key?: string; + withoutAssets?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumResponseDto; + }>(`/album/${encodeURIComponent(id)}${QS.query(QS.explode({ + key, + withoutAssets + }))}`, { + ...opts + })); +} +export function updateAlbumInfo({ id, updateAlbumDto }: { + id: string; + updateAlbumDto: UpdateAlbumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumResponseDto; + }>(`/album/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PATCH", + body: updateAlbumDto + }))); +} +export function removeAssetFromAlbum({ id, bulkIdsDto }: { + id: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/album/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} +export function addAssetsToAlbum({ id, key, bulkIdsDto }: { + id: string; + key?: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/album/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ + key + }))}`, oazapfts.json({ + ...opts, + method: "PUT", + body: bulkIdsDto + }))); +} +export function removeUserFromAlbum({ id, userId }: { + id: string; + userId: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/album/${encodeURIComponent(id)}/user/${encodeURIComponent(userId)}`, { + ...opts, + method: "DELETE" + })); +} +export function addUsersToAlbum({ id, addUsersDto }: { + id: string; + addUsersDto: AddUsersDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumResponseDto; + }>(`/album/${encodeURIComponent(id)}/users`, oazapfts.json({ + ...opts, + method: "PUT", + body: addUsersDto + }))); +} +export function getApiKeys(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ApiKeyResponseDto[]; + }>("/api-key", { + ...opts + })); +} +export function createApiKey({ apiKeyCreateDto }: { + apiKeyCreateDto: ApiKeyCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: ApiKeyCreateResponseDto; + }>("/api-key", oazapfts.json({ + ...opts, + method: "POST", + body: apiKeyCreateDto + }))); +} +export function deleteApiKey({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/api-key/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getApiKey({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ApiKeyResponseDto; + }>(`/api-key/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateApiKey({ id, apiKeyUpdateDto }: { + id: string; + apiKeyUpdateDto: ApiKeyUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ApiKeyResponseDto; + }>(`/api-key/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: apiKeyUpdateDto + }))); +} +export function deleteAssets({ assetBulkDeleteDto }: { + assetBulkDeleteDto: AssetBulkDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/asset", oazapfts.json({ + ...opts, + method: "DELETE", + body: assetBulkDeleteDto + }))); +} +/** + * Get all AssetEntity belong to the user + */ +export function getAllAssets({ ifNoneMatch, isArchived, isFavorite, skip, take, updatedAfter, updatedBefore, userId }: { + ifNoneMatch?: string; + isArchived?: boolean; + isFavorite?: boolean; + skip?: number; + take?: number; + updatedAfter?: string; + updatedBefore?: string; + userId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/asset${QS.query(QS.explode({ + isArchived, + isFavorite, + skip, + take, + updatedAfter, + updatedBefore, + userId + }))}`, { + ...opts, + headers: oazapfts.mergeHeaders(opts?.headers, { + "if-none-match": ifNoneMatch + }) + })); +} +export function updateAssets({ assetBulkUpdateDto }: { + assetBulkUpdateDto: AssetBulkUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/asset", oazapfts.json({ + ...opts, + method: "PUT", + body: assetBulkUpdateDto + }))); +} +/** + * Checks if assets exist by checksums + */ +export function checkBulkUpload({ assetBulkUploadCheckDto }: { + assetBulkUploadCheckDto: AssetBulkUploadCheckDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetBulkUploadCheckResponseDto; + }>("/asset/bulk-upload-check", oazapfts.json({ + ...opts, + method: "POST", + body: assetBulkUploadCheckDto + }))); +} +export function getCuratedLocations(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: CuratedLocationsResponseDto[]; + }>("/asset/curated-locations", { + ...opts + })); +} +export function getCuratedObjects(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: CuratedObjectsResponseDto[]; + }>("/asset/curated-objects", { + ...opts + })); +} +/** + * Get all asset of a device that are in the database, ID only. + */ +export function getAllUserAssetsByDeviceId({ deviceId }: { + deviceId: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: string[]; + }>(`/asset/device/${encodeURIComponent(deviceId)}`, { + ...opts + })); +} +/** + * Checks if multiple assets exist on the server and returns all existing - used by background backup + */ +export function checkExistingAssets({ checkExistingAssetsDto }: { + checkExistingAssetsDto: CheckExistingAssetsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: CheckExistingAssetsResponseDto; + }>("/asset/exist", oazapfts.json({ + ...opts, + method: "POST", + body: checkExistingAssetsDto + }))); +} +export function serveFile({ id, isThumb, isWeb, key }: { + id: string; + isThumb?: boolean; + isWeb?: boolean; + key?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/asset/file/${encodeURIComponent(id)}${QS.query(QS.explode({ + isThumb, + isWeb, + key + }))}`, { + ...opts + })); +} +export function runAssetJobs({ assetJobsDto }: { + assetJobsDto: AssetJobsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/asset/jobs", oazapfts.json({ + ...opts, + method: "POST", + body: assetJobsDto + }))); +} +export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite }: { + fileCreatedAfter?: string; + fileCreatedBefore?: string; + isArchived?: boolean; + isFavorite?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MapMarkerResponseDto[]; + }>(`/asset/map-marker${QS.query(QS.explode({ + fileCreatedAfter, + fileCreatedBefore, + isArchived, + isFavorite + }))}`, { + ...opts + })); +} +export function getMemoryLane({ day, month }: { + day: number; + month: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryLaneResponseDto[]; + }>(`/asset/memory-lane${QS.query(QS.explode({ + day, + month + }))}`, { + ...opts + })); +} +export function getRandom({ count }: { + count?: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/asset/random${QS.query(QS.explode({ + count + }))}`, { + ...opts + })); +} +export function getAssetSearchTerms(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: string[]; + }>("/asset/search-terms", { + ...opts + })); +} +export function updateStackParent({ updateStackParentDto }: { + updateStackParentDto: UpdateStackParentDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/asset/stack/parent", oazapfts.json({ + ...opts, + method: "PUT", + body: updateStackParentDto + }))); +} +export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: { + isArchived?: boolean; + isFavorite?: boolean; + isTrashed?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetStatsResponseDto; + }>(`/asset/statistics${QS.query(QS.explode({ + isArchived, + isFavorite, + isTrashed + }))}`, { + ...opts + })); +} +export function getAssetThumbnail({ format, id, key }: { + format?: ThumbnailFormat; + id: string; + key?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/asset/thumbnail/${encodeURIComponent(id)}${QS.query(QS.explode({ + format, + key + }))}`, { + ...opts + })); +} +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, timeBucket, userId, withPartners, withStacked }: { + albumId?: string; + isArchived?: boolean; + isFavorite?: boolean; + isTrashed?: boolean; + key?: string; + personId?: string; + size: TimeBucketSize; + timeBucket: string; + userId?: string; + withPartners?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/asset/time-bucket${QS.query(QS.explode({ + albumId, + isArchived, + isFavorite, + isTrashed, + key, + personId, + size, + timeBucket, + userId, + withPartners, + withStacked + }))}`, { + ...opts + })); +} +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, userId, withPartners, withStacked }: { + albumId?: string; + isArchived?: boolean; + isFavorite?: boolean; + isTrashed?: boolean; + key?: string; + personId?: string; + size: TimeBucketSize; + userId?: string; + withPartners?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TimeBucketResponseDto[]; + }>(`/asset/time-buckets${QS.query(QS.explode({ + albumId, + isArchived, + isFavorite, + isTrashed, + key, + personId, + size, + userId, + withPartners, + withStacked + }))}`, { + ...opts + })); +} +export function uploadFile({ key, createAssetDto }: { + key?: string; + createAssetDto: CreateAssetDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: AssetFileUploadResponseDto; + }>(`/asset/upload${QS.query(QS.explode({ + key + }))}`, oazapfts.multipart({ + ...opts, + method: "POST", + body: createAssetDto + }))); +} +export function getAssetInfo({ id, key }: { + id: string; + key?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto; + }>(`/asset/${encodeURIComponent(id)}${QS.query(QS.explode({ + key + }))}`, { + ...opts + })); +} +export function updateAsset({ id, updateAssetDto }: { + id: string; + updateAssetDto: UpdateAssetDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto; + }>(`/asset/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: updateAssetDto + }))); +} +export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { + checksum?: string; + city?: string; + country?: string; + createdAfter?: string; + createdBefore?: string; + deviceAssetId?: string; + deviceId?: string; + encodedVideoPath?: string; + id?: string; + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + lensModel?: string; + libraryId?: string; + make?: string; + model?: string; + order?: AssetOrder; + originalFileName?: string; + originalPath?: string; + page?: number; + resizePath?: string; + size?: number; + state?: string; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + $type?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + webpPath?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; + withPeople?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/assets${QS.query(QS.explode({ + checksum, + city, + country, + createdAfter, + createdBefore, + deviceAssetId, + deviceId, + encodedVideoPath, + id, + isArchived, + isEncoded, + isExternal, + isFavorite, + isMotion, + isOffline, + isReadOnly, + isVisible, + lensModel, + libraryId, + make, + model, + order, + originalFileName, + originalPath, + page, + resizePath, + size, + state, + takenAfter, + takenBefore, + trashedAfter, + trashedBefore, + "type": $type, + updatedAfter, + updatedBefore, + webpPath, + withArchived, + withDeleted, + withExif, + withPeople, + withStacked + }))}`, { + ...opts + })); +} +export function getAuditDeletes({ after, entityType, userId }: { + after: string; + entityType: EntityType; + userId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AuditDeletesResponseDto; + }>(`/audit/deletes${QS.query(QS.explode({ + after, + entityType, + userId + }))}`, { + ...opts + })); +} +export function getAuditFiles(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: FileReportDto; + }>("/audit/file-report", { + ...opts + })); +} +export function getFileChecksums({ fileChecksumDto }: { + fileChecksumDto: FileChecksumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: FileChecksumResponseDto[]; + }>("/audit/file-report/checksum", oazapfts.json({ + ...opts, + method: "POST", + body: fileChecksumDto + }))); +} +export function fixAuditFiles({ fileReportFixDto }: { + fileReportFixDto: FileReportFixDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/audit/file-report/fix", oazapfts.json({ + ...opts, + method: "POST", + body: fileReportFixDto + }))); +} +export function signUpAdmin({ signUpDto }: { + signUpDto: SignUpDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: UserResponseDto; + }>("/auth/admin-sign-up", oazapfts.json({ + ...opts, + method: "POST", + body: signUpDto + }))); +} +export function changePassword({ changePasswordDto }: { + changePasswordDto: ChangePasswordDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto; + }>("/auth/change-password", oazapfts.json({ + ...opts, + method: "POST", + body: changePasswordDto + }))); +} +export function logoutAuthDevices(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/auth/devices", { + ...opts, + method: "DELETE" + })); +} +export function getAuthDevices(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AuthDeviceResponseDto[]; + }>("/auth/devices", { + ...opts + })); +} +export function logoutAuthDevice({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/auth/devices/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function login({ loginCredentialDto }: { + loginCredentialDto: LoginCredentialDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: LoginResponseDto; + }>("/auth/login", oazapfts.json({ + ...opts, + method: "POST", + body: loginCredentialDto + }))); +} +export function logout(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: LogoutResponseDto; + }>("/auth/logout", { + ...opts, + method: "POST" + })); +} +export function validateAccessToken(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ValidateAccessTokenResponseDto; + }>("/auth/validateToken", { + ...opts, + method: "POST" + })); +} +export function downloadArchive({ key, assetIdsDto }: { + key?: string; + assetIdsDto: AssetIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/download/archive${QS.query(QS.explode({ + key + }))}`, oazapfts.json({ + ...opts, + method: "POST", + body: assetIdsDto + }))); +} +export function downloadFile({ id, key }: { + id: string; + key?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/download/asset/${encodeURIComponent(id)}${QS.query(QS.explode({ + key + }))}`, { + ...opts, + method: "POST" + })); +} +export function getDownloadInfo({ key, downloadInfoDto }: { + key?: string; + downloadInfoDto: DownloadInfoDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: DownloadResponseDto; + }>(`/download/info${QS.query(QS.explode({ + key + }))}`, oazapfts.json({ + ...opts, + method: "POST", + body: downloadInfoDto + }))); +} +export function getFaces({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetFaceResponseDto[]; + }>(`/face${QS.query(QS.explode({ + id + }))}`, { + ...opts + })); +} +export function reassignFacesById({ id, faceDto }: { + id: string; + faceDto: FaceDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonResponseDto; + }>(`/face/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: faceDto + }))); +} +export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AllJobStatusResponseDto; + }>("/jobs", { + ...opts + })); +} +export function sendJobCommand({ id, jobCommandDto }: { + id: JobName; + jobCommandDto: JobCommandDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: JobStatusDto; + }>(`/jobs/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: jobCommandDto + }))); +} +export function getLibraries(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: LibraryResponseDto[]; + }>("/library", { + ...opts + })); +} +export function createLibrary({ createLibraryDto }: { + createLibraryDto: CreateLibraryDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: LibraryResponseDto; + }>("/library", oazapfts.json({ + ...opts, + method: "POST", + body: createLibraryDto + }))); +} +export function deleteLibrary({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/library/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getLibraryInfo({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: LibraryResponseDto; + }>(`/library/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateLibrary({ id, updateLibraryDto }: { + id: string; + updateLibraryDto: UpdateLibraryDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: LibraryResponseDto; + }>(`/library/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: updateLibraryDto + }))); +} +export function removeOfflineFiles({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/library/${encodeURIComponent(id)}/removeOffline`, { + ...opts, + method: "POST" + })); +} +export function scanLibrary({ id, scanLibraryDto }: { + id: string; + scanLibraryDto: ScanLibraryDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/library/${encodeURIComponent(id)}/scan`, oazapfts.json({ + ...opts, + method: "POST", + body: scanLibraryDto + }))); +} +export function getLibraryStatistics({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: LibraryStatsResponseDto; + }>(`/library/${encodeURIComponent(id)}/statistics`, { + ...opts + })); +} +export function startOAuth({ oAuthConfigDto }: { + oAuthConfigDto: OAuthConfigDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: OAuthAuthorizeResponseDto; + }>("/oauth/authorize", oazapfts.json({ + ...opts, + method: "POST", + body: oAuthConfigDto + }))); +} +export function finishOAuth({ oAuthCallbackDto }: { + oAuthCallbackDto: OAuthCallbackDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: LoginResponseDto; + }>("/oauth/callback", oazapfts.json({ + ...opts, + method: "POST", + body: oAuthCallbackDto + }))); +} +export function linkOAuthAccount({ oAuthCallbackDto }: { + oAuthCallbackDto: OAuthCallbackDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: UserResponseDto; + }>("/oauth/link", oazapfts.json({ + ...opts, + method: "POST", + body: oAuthCallbackDto + }))); +} +export function redirectOAuthToMobile(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/oauth/mobile-redirect", { + ...opts + })); +} +export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: UserResponseDto; + }>("/oauth/unlink", { + ...opts, + method: "POST" + })); +} +export function getPartners({ direction }: { + direction: "shared-by" | "shared-with"; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PartnerResponseDto[]; + }>(`/partner${QS.query(QS.explode({ + direction + }))}`, { + ...opts + })); +} +export function removePartner({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/partner/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function createPartner({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: PartnerResponseDto; + }>(`/partner/${encodeURIComponent(id)}`, { + ...opts, + method: "POST" + })); +} +export function updatePartner({ id, updatePartnerDto }: { + id: string; + updatePartnerDto: UpdatePartnerDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PartnerResponseDto; + }>(`/partner/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: updatePartnerDto + }))); +} +export function getAllPeople({ withHidden }: { + withHidden?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PeopleResponseDto; + }>(`/person${QS.query(QS.explode({ + withHidden + }))}`, { + ...opts + })); +} +export function createPerson(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: PersonResponseDto; + }>("/person", { + ...opts, + method: "POST" + })); +} +export function updatePeople({ peopleUpdateDto }: { + peopleUpdateDto: PeopleUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>("/person", oazapfts.json({ + ...opts, + method: "PUT", + body: peopleUpdateDto + }))); +} +export function getPerson({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonResponseDto; + }>(`/person/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updatePerson({ id, personUpdateDto }: { + id: string; + personUpdateDto: PersonUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonResponseDto; + }>(`/person/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: personUpdateDto + }))); +} +export function getPersonAssets({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/person/${encodeURIComponent(id)}/assets`, { + ...opts + })); +} +export function mergePerson({ id, mergePersonDto }: { + id: string; + mergePersonDto: MergePersonDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: BulkIdResponseDto[]; + }>(`/person/${encodeURIComponent(id)}/merge`, oazapfts.json({ + ...opts, + method: "POST", + body: mergePersonDto + }))); +} +export function reassignFaces({ id, assetFaceUpdateDto }: { + id: string; + assetFaceUpdateDto: AssetFaceUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonResponseDto[]; + }>(`/person/${encodeURIComponent(id)}/reassign`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetFaceUpdateDto + }))); +} +export function getPersonStatistics({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonStatisticsResponseDto; + }>(`/person/${encodeURIComponent(id)}/statistics`, { + ...opts + })); +} +export function getPersonThumbnail({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/person/${encodeURIComponent(id)}/thumbnail`, { + ...opts + })); +} +export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { + clip?: boolean; + motion?: boolean; + page?: number; + q?: string; + query?: string; + recent?: boolean; + size?: number; + smart?: boolean; + $type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER"; + withArchived?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>(`/search${QS.query(QS.explode({ + clip, + motion, + page, + q, + query, + recent, + size, + smart, + "type": $type, + withArchived + }))}`, { + ...opts + })); +} +export function getExploreData(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchExploreResponseDto[]; + }>("/search/explore", { + ...opts + })); +} +export function searchMetadata({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { + checksum?: string; + city?: string; + country?: string; + createdAfter?: string; + createdBefore?: string; + deviceAssetId?: string; + deviceId?: string; + encodedVideoPath?: string; + id?: string; + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + lensModel?: string; + libraryId?: string; + make?: string; + model?: string; + order?: AssetOrder; + originalFileName?: string; + originalPath?: string; + page?: number; + resizePath?: string; + size?: number; + state?: string; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + $type?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + webpPath?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; + withPeople?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>(`/search/metadata${QS.query(QS.explode({ + checksum, + city, + country, + createdAfter, + createdBefore, + deviceAssetId, + deviceId, + encodedVideoPath, + id, + isArchived, + isEncoded, + isExternal, + isFavorite, + isMotion, + isOffline, + isReadOnly, + isVisible, + lensModel, + libraryId, + make, + model, + order, + originalFileName, + originalPath, + page, + resizePath, + size, + state, + takenAfter, + takenBefore, + trashedAfter, + trashedBefore, + "type": $type, + updatedAfter, + updatedBefore, + webpPath, + withArchived, + withDeleted, + withExif, + withPeople, + withStacked + }))}`, { + ...opts + })); +} +export function searchPerson({ name, withHidden }: { + name: string; + withHidden?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PersonResponseDto[]; + }>(`/search/person${QS.query(QS.explode({ + name, + withHidden + }))}`, { + ...opts + })); +} +export function searchSmart({ city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, query, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif }: { + city?: string; + country?: string; + createdAfter?: string; + createdBefore?: string; + deviceId?: string; + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + lensModel?: string; + libraryId?: string; + make?: string; + model?: string; + page?: number; + query: string; + size?: number; + state?: string; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + $type?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>(`/search/smart${QS.query(QS.explode({ + city, + country, + createdAfter, + createdBefore, + deviceId, + isArchived, + isEncoded, + isExternal, + isFavorite, + isMotion, + isOffline, + isReadOnly, + isVisible, + lensModel, + libraryId, + make, + model, + page, + query, + size, + state, + takenAfter, + takenBefore, + trashedAfter, + trashedBefore, + "type": $type, + updatedAfter, + updatedBefore, + withArchived, + withDeleted, + withExif + }))}`, { + ...opts + })); +} +export function getSearchSuggestions({ country, make, model, state, $type }: { + country?: string; + make?: string; + model?: string; + state?: string; + $type: SearchSuggestionType; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: string[]; + }>(`/search/suggestions${QS.query(QS.explode({ + country, + make, + model, + state, + "type": $type + }))}`, { + ...opts + })); +} +export function getServerInfo(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerInfoResponseDto; + }>("/server-info", { + ...opts + })); +} +export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", { + ...opts, + method: "POST" + })); +} +export function getServerConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerConfigDto; + }>("/server-info/config", { + ...opts + })); +} +export function getServerFeatures(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerFeaturesDto; + }>("/server-info/features", { + ...opts + })); +} +export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerMediaTypesResponseDto; + }>("/server-info/media-types", { + ...opts + })); +} +export function pingServer(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerPingResponseRead; + }>("/server-info/ping", { + ...opts + })); +} +export function getServerStatistics(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerStatsResponseDto; + }>("/server-info/statistics", { + ...opts + })); +} +export function getTheme(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerThemeDto; + }>("/server-info/theme", { + ...opts + })); +} +export function getServerVersion(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerVersionResponseDto; + }>("/server-info/version", { + ...opts + })); +} +export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SharedLinkResponseDto[]; + }>("/shared-link", { + ...opts + })); +} +export function createSharedLink({ sharedLinkCreateDto }: { + sharedLinkCreateDto: SharedLinkCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: SharedLinkResponseDto; + }>("/shared-link", oazapfts.json({ + ...opts, + method: "POST", + body: sharedLinkCreateDto + }))); +} +export function getMySharedLink({ key, password, token }: { + key?: string; + password?: string; + token?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SharedLinkResponseDto; + }>(`/shared-link/me${QS.query(QS.explode({ + key, + password, + token + }))}`, { + ...opts + })); +} +export function removeSharedLink({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/shared-link/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getSharedLinkById({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SharedLinkResponseDto; + }>(`/shared-link/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateSharedLink({ id, sharedLinkEditDto }: { + id: string; + sharedLinkEditDto: SharedLinkEditDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SharedLinkResponseDto; + }>(`/shared-link/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PATCH", + body: sharedLinkEditDto + }))); +} +export function removeSharedLinkAssets({ id, key, assetIdsDto }: { + id: string; + key?: string; + assetIdsDto: AssetIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetIdsResponseDto[]; + }>(`/shared-link/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ + key + }))}`, oazapfts.json({ + ...opts, + method: "DELETE", + body: assetIdsDto + }))); +} +export function addSharedLinkAssets({ id, key, assetIdsDto }: { + id: string; + key?: string; + assetIdsDto: AssetIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetIdsResponseDto[]; + }>(`/shared-link/${encodeURIComponent(id)}/assets${QS.query(QS.explode({ + key + }))}`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetIdsDto + }))); +} +export function getConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SystemConfigDto; + }>("/system-config", { + ...opts + })); +} +export function updateConfig({ systemConfigDto }: { + systemConfigDto: SystemConfigDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SystemConfigDto; + }>("/system-config", oazapfts.json({ + ...opts, + method: "PUT", + body: systemConfigDto + }))); +} +export function getConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SystemConfigDto; + }>("/system-config/defaults", { + ...opts + })); +} +export function getMapStyle({ theme }: { + theme: MapTheme; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: object; + }>(`/system-config/map/style.json${QS.query(QS.explode({ + theme + }))}`, { + ...opts + })); +} +export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SystemConfigTemplateStorageOptionDto; + }>("/system-config/storage-template-options", { + ...opts + })); +} +export function getAllTags(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagResponseDto[]; + }>("/tag", { + ...opts + })); +} +export function createTag({ createTagDto }: { + createTagDto: CreateTagDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: TagResponseDto; + }>("/tag", oazapfts.json({ + ...opts, + method: "POST", + body: createTagDto + }))); +} +export function deleteTag({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/tag/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getTagById({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagResponseDto; + }>(`/tag/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateTag({ id, updateTagDto }: { + id: string; + updateTagDto: UpdateTagDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagResponseDto; + }>(`/tag/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PATCH", + body: updateTagDto + }))); +} +export function untagAssets({ id, assetIdsDto }: { + id: string; + assetIdsDto: AssetIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetIdsResponseDto[]; + }>(`/tag/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "DELETE", + body: assetIdsDto + }))); +} +export function getTagAssets({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/tag/${encodeURIComponent(id)}/assets`, { + ...opts + })); +} +export function tagAssets({ id, assetIdsDto }: { + id: string; + assetIdsDto: AssetIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetIdsResponseDto[]; + }>(`/tag/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetIdsDto + }))); +} +export function emptyTrash(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/trash/empty", { + ...opts, + method: "POST" + })); +} +export function restoreTrash(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/trash/restore", { + ...opts, + method: "POST" + })); +} +export function restoreAssets({ bulkIdsDto }: { + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/trash/restore/assets", oazapfts.json({ + ...opts, + method: "POST", + body: bulkIdsDto + }))); +} +export function getAllUsers({ isAll }: { + isAll: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto[]; + }>(`/user${QS.query(QS.explode({ + isAll + }))}`, { + ...opts + })); +} +export function createUser({ createUserDto }: { + createUserDto: CreateUserDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: UserResponseDto; + }>("/user", oazapfts.json({ + ...opts, + method: "POST", + body: createUserDto + }))); +} +export function updateUser({ updateUserDto }: { + updateUserDto: UpdateUserDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto; + }>("/user", oazapfts.json({ + ...opts, + method: "PUT", + body: updateUserDto + }))); +} +export function getUserById({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto; + }>(`/user/info/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function getMyUserInfo(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto; + }>("/user/me", { + ...opts + })); +} +export function deleteProfileImage(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/user/profile-image", { + ...opts, + method: "DELETE" + })); +} +export function createProfileImage({ createProfileImageDto }: { + createProfileImageDto: CreateProfileImageDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: CreateProfileImageResponseDto; + }>("/user/profile-image", oazapfts.multipart({ + ...opts, + method: "POST", + body: createProfileImageDto + }))); +} +export function getProfileImage({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/user/profile-image/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function deleteUser({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto; + }>(`/user/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function restoreUser({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: UserResponseDto; + }>(`/user/${encodeURIComponent(id)}/restore`, { + ...opts, + method: "POST" + })); +} diff --git a/open-api/typescript-sdk/fetch.ts b/open-api/typescript-sdk/fetch.ts new file mode 100644 index 0000000000..5441cd8268 --- /dev/null +++ b/open-api/typescript-sdk/fetch.ts @@ -0,0 +1 @@ +export * from './fetch-client'; diff --git a/open-api/typescript-sdk/index.ts b/open-api/typescript-sdk/index.ts deleted file mode 100644 index eb7f6b43e0..0000000000 --- a/open-api/typescript-sdk/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './client'; -export * as base from './client/base'; -export * as configuration from './client/configuration'; -export * as common from './client/common'; diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 1f367d429d..5346a47086 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -7,19 +7,31 @@ "": { "name": "@immich/sdk", "version": "1.92.1", - "license": "MIT", - "dependencies": { - "axios": "^1.6.7" - }, + "license": "GNU Affero General Public License version 3", "devDependencies": { + "@oazapfts/runtime": "^1.0.0", "@types/node": "^20.11.0", "typescript": "^5.3.3" + }, + "peerDependencies": { + "axios": "^1.6.7" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } } }, + "node_modules/@oazapfts/runtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz", + "integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==", + "dev": true + }, "node_modules/@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -28,12 +40,16 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true, + "peer": true }, "node_modules/axios": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "optional": true, + "peer": true, "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -44,6 +60,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -55,6 +73,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -69,6 +89,8 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "optional": true, + "peer": true, "engines": { "node": ">=4.0" }, @@ -82,6 +104,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "optional": true, + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -95,6 +119,8 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -103,6 +129,8 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "optional": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -113,7 +141,9 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "optional": true, + "peer": true }, "node_modules/typescript": { "version": "5.3.3", diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index e1c94e5b66..05e7843f9a 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -3,23 +3,33 @@ "version": "1.92.1", "description": "", "type": "module", - "main": "./build/index.js", - "types": "./build/index.d.ts", + "main": "./build/fetch/index.js", + "types": "./build/fetch/index.d.ts", "exports": { + "./axios": { + "types": "./build/axios/axios.d.ts", + "default": "./build/axios/axios.js" + }, ".": { - "types": "./build/index.d.ts", - "default": "./build/index.js" + "types": "./build/fetch/fetch.d.ts", + "default": "./build/fetch/fetch.js" } }, "scripts": { - "build": "tsc -b ./tsconfig.json" + "build": "tsc -b ./tsconfig.axios.json ./tsconfig.fetch.json" }, - "license": "MIT", + "license": "GNU Affero General Public License version 3", "devDependencies": { + "@oazapfts/runtime": "^1.0.0", "@types/node": "^20.11.0", "typescript": "^5.3.3" }, - "dependencies": { + "peerDependencies": { "axios": "^1.6.7" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } } } diff --git a/open-api/typescript-sdk/tsconfig.json b/open-api/typescript-sdk/tsconfig.axios.json similarity index 64% rename from open-api/typescript-sdk/tsconfig.json rename to open-api/typescript-sdk/tsconfig.axios.json index 62c64e5be0..9f0d06bbff 100644 --- a/open-api/typescript-sdk/tsconfig.json +++ b/open-api/typescript-sdk/tsconfig.axios.json @@ -1,9 +1,11 @@ { + "include": ["axios.ts", "axios-client/**/*"], "compilerOptions": { + "target": "esnext", "strict": true, "skipLibCheck": true, "declaration": true, - "outDir": "build", + "outDir": "build/axios", "module": "esnext", "moduleResolution": "Bundler", "lib": ["esnext"] diff --git a/open-api/typescript-sdk/tsconfig.fetch.json b/open-api/typescript-sdk/tsconfig.fetch.json new file mode 100644 index 0000000000..58ef1ffa6b --- /dev/null +++ b/open-api/typescript-sdk/tsconfig.fetch.json @@ -0,0 +1,13 @@ +{ + "include": ["fetch.ts"], + "compilerOptions": { + "target": "esnext", + "strict": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "build/fetch", + "module": "esnext", + "moduleResolution": "Bundler", + "lib": ["esnext", "dom"] + } +} diff --git a/renovate.json b/renovate.json index 9bc4ad6a62..7cb136b1a3 100644 --- a/renovate.json +++ b/renovate.json @@ -83,10 +83,7 @@ "ignorePaths": ["mobile/openapi/pubspec.yaml"], "ignoreDeps": [ "http", - "latlong2", - "vector_map_tiles", - "flutter_map", - "flutter_map_heatmap" + "intl" ], "labels": ["dependencies", "renovate"] } diff --git a/server/Dockerfile b/server/Dockerfile index 4c9b92d5b6..eb62b8812a 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240130@sha256:a11ac5c56f0ccce1f218954c07c43caadf489557252ba5b9ca1c5977aaa25999 as dev +FROM ghcr.io/immich-app/base-server-dev:20240213@sha256:16646a37bae065b51e68cb2ba7a63027b29504d43a30644625382afbe326114a as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -40,7 +40,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240130@sha256:ce23a32154540b906df3c971766bcd991561c60331794e0ebb780947ac48113f +FROM ghcr.io/immich-app/base-server-prod:20240213@sha256:61d159d069c5b522f16de9733fb79feb0e82c0b099d16f026196f344d12a1e5e WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/e2e/api/setup.ts b/server/e2e/api/setup.ts index 7223a1c028..8d44b07cbb 100644 --- a/server/e2e/api/setup.ts +++ b/server/e2e/api/setup.ts @@ -1,7 +1,7 @@ import { PostgreSqlContainer } from '@testcontainers/postgresql'; export default async () => { - const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.1.11') + const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') .withDatabase('immich') .withUsername('postgres') .withPassword('postgres') diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index e38f08d0ca..5993a70400 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -14,7 +14,7 @@ import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/i import { AssetRepository } from '@app/infra/repositories'; import { INestApplication } from '@nestjs/common'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; import request from 'supertest'; import { api } from '../../client'; import { generateAsset, testApp, today, yesterday } from '../utils'; @@ -169,7 +169,11 @@ describe(`${AssetController.name} (e2e)`, () => { { should: 'should reject size as a string', query: { size: 'abc' }, - expected: ['size must not be less than 1', 'size must be an integer number'], + expected: [ + 'size must not be greater than 1000', + 'size must not be less than 1', + 'size must be an integer number', + ], }, { should: 'should reject an invalid size', @@ -478,7 +482,7 @@ describe(`${AssetController.name} (e2e)`, () => { }), }, { - should: 'sohuld search by make', + should: 'should search by make', deferred: () => ({ query: { make: 'Cannon' }, assets: [asset3], @@ -517,91 +521,6 @@ describe(`${AssetController.name} (e2e)`, () => { } }); - // TODO remove with deprecated endpoint - describe('GET /asset/assetById/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get(`/asset/assetById/${uuidStub.notFound}`); - expect(body).toEqual(errorStub.unauthorized); - expect(status).toBe(401); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(server) - .get(`/asset/assetById/${uuidStub.invalid}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(['id must be a UUID'])); - }); - - it('should require access', async () => { - const { status, body } = await request(server) - .get(`/asset/assetById/${asset4.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.noPermission); - }); - - it('should get the asset info', async () => { - const { status, body } = await request(server) - .get(`/asset/assetById/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); - }); - - it('should work with a shared link', async () => { - const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }); - - const { status, body } = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`); - expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); - }); - - it('should not send people data for shared links for un-authenticated users', async () => { - const personRepository = app.get(IPersonRepository); - const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' }); - - await personRepository.createFaces([ - { - assetId: asset1.id, - personId: person.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ isFavorite: true }); - expect(status).toEqual(200); - expect(body).toMatchObject({ - id: asset1.id, - isFavorite: true, - people: [ - { - birthDate: null, - id: expect.any(String), - isHidden: false, - name: 'Test Person', - thumbnailPath: '', - }, - ], - }); - - const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }); - - const data = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`); - expect(data.status).toBe(200); - expect(data.body).toMatchObject({ people: [] }); - }); - }); - describe('GET /asset/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`); @@ -643,6 +562,47 @@ describe(`${AssetController.name} (e2e)`, () => { expect(status).toBe(200); expect(body).toMatchObject({ id: asset1.id }); }); + + it('should not send people data for shared links for un-authenticated users', async () => { + const personRepository = app.get(IPersonRepository); + const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' }); + + await personRepository.createFaces([ + { + assetId: asset1.id, + personId: person.id, + embedding: Array.from({ length: 512 }, Math.random), + }, + ]); + + const { status, body } = await request(server) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ isFavorite: true }); + expect(status).toEqual(200); + expect(body).toMatchObject({ + id: asset1.id, + isFavorite: true, + people: [ + { + birthDate: null, + id: expect.any(String), + isHidden: false, + name: 'Test Person', + thumbnailPath: '', + }, + ], + }); + + const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.INDIVIDUAL, + assetIds: [asset1.id], + }); + + const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`); + expect(data.status).toBe(200); + expect(data.body).toMatchObject({ people: [] }); + }); }); describe('POST /asset/upload', () => { @@ -920,46 +880,6 @@ describe(`${AssetController.name} (e2e)`, () => { }); }); - describe('POST /asset/download/info', () => { - it('should require authentication', async () => { - const { status, body } = await request(server) - .post(`/asset/download/info`) - .send({ assetIds: [asset1.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should download info', async () => { - const { status, body } = await request(server) - .post('/asset/download/info') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ assetIds: [asset1.id] }); - - expect(status).toBe(201); - expect(body).toEqual(expect.objectContaining({ archives: [expect.objectContaining({ assetIds: [asset1.id] })] })); - }); - }); - - describe('POST /asset/download/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).post(`/asset/download/${asset1.id}`); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should download file', async () => { - const asset = await api.assetApi.upload(server, user1.accessToken, 'example'); - const response = await request(server) - .post(`/asset/download/${asset.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(response.status).toBe(200); - expect(response.headers['content-type']).toEqual('image/jpeg'); - }); - }); - describe('GET /asset/statistics', () => { beforeEach(async () => { await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true }); @@ -1459,20 +1379,20 @@ describe(`${AssetController.name} (e2e)`, () => { }); }); + const getAssetIdsWithoutFaces = async () => { + const assetPagination = usePagination(10, (pagination) => + assetRepository.getWithout(pagination, WithoutProperty.FACES), + ); + let assets: AssetEntity[] = []; + for await (const assetsPage of assetPagination) { + assets = [...assets, ...assetsPage]; + } + return assets.map((a) => a.id); + }; + describe(AssetRepository.name, () => { describe('getWithout', () => { describe('WithoutProperty.FACES', () => { - const getAssetIdsWithoutFaces = async () => { - const assetPagination = usePagination(10, (pagination) => - assetRepository.getWithout(pagination, WithoutProperty.FACES), - ); - let assets: AssetEntity[] = []; - for await (const assetsPage of assetPagination) { - assets = [...assets, ...assetsPage]; - } - return assets.map((a) => a.id); - }; - beforeEach(async () => { await assetRepository.save({ id: asset1.id, resizePath: '/path/to/resize' }); expect(await getAssetIdsWithoutFaces()).toContain(asset1.id); diff --git a/server/e2e/api/specs/auth.e2e-spec.ts b/server/e2e/api/specs/auth.e2e-spec.ts index eeb4723faa..e514d2b803 100644 --- a/server/e2e/api/specs/auth.e2e-spec.ts +++ b/server/e2e/api/specs/auth.e2e-spec.ts @@ -153,9 +153,10 @@ describe(`${AuthController.name} (e2e)`, () => { expect(token).toBeDefined(); const cookies = headers['set-cookie']; - expect(cookies).toHaveLength(2); + expect(cookies).toHaveLength(3); expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`); expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'); + expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'); }); }); diff --git a/server/e2e/api/specs/download.e2e-spec.ts b/server/e2e/api/specs/download.e2e-spec.ts new file mode 100644 index 0000000000..9f8c477df9 --- /dev/null +++ b/server/e2e/api/specs/download.e2e-spec.ts @@ -0,0 +1,89 @@ +import { AssetResponseDto, IAssetRepository, LibraryResponseDto, LoginResponseDto, mapAsset } from '@app/domain'; +import { AssetController } from '@app/immich'; +import { AssetEntity } from '@app/infra/entities'; +import { INestApplication } from '@nestjs/common'; +import { errorStub, userDto } from '@test/fixtures'; +import request from 'supertest'; +import { api } from '../../client'; +import { generateAsset, testApp } from '../utils'; + +describe(`${AssetController.name} (e2e)`, () => { + let app: INestApplication; + let server: any; + let assetRepository: IAssetRepository; + let user1: LoginResponseDto; + let libraries: LibraryResponseDto[]; + let asset1: AssetResponseDto; + + const createAsset = async ( + loginResponse: LoginResponseDto, + fileCreatedAt: Date, + other: Partial = {}, + ) => { + const asset = await assetRepository.create( + generateAsset(loginResponse.userId, libraries, { fileCreatedAt, ...other }), + ); + + return mapAsset(asset); + }; + + beforeAll(async () => { + app = await testApp.create(); + server = app.getHttpServer(); + assetRepository = app.get(IAssetRepository); + + await testApp.reset(); + + await api.authApi.adminSignUp(server); + const admin = await api.authApi.adminLogin(server); + + await api.userApi.create(server, admin.accessToken, userDto.user1); + user1 = await api.authApi.login(server, userDto.user1); + libraries = await api.libraryApi.getAll(server, user1.accessToken); + asset1 = await createAsset(user1, new Date('1970-01-01')); + }); + + afterAll(async () => { + await testApp.teardown(); + }); + + describe('POST /download/info', () => { + it('should require authentication', async () => { + const { status, body } = await request(server) + .post(`/download/info`) + .send({ assetIds: [asset1.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should download info', async () => { + const { status, body } = await request(server) + .post('/download/info') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset1.id] }); + + expect(status).toBe(201); + expect(body).toEqual(expect.objectContaining({ archives: [expect.objectContaining({ assetIds: [asset1.id] })] })); + }); + }); + + describe('POST /download/asset/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).post(`/download/asset/${asset1.id}`); + + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should download file', async () => { + const asset = await api.assetApi.upload(server, user1.accessToken, 'example'); + const response = await request(server) + .post(`/download/asset/${asset.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toEqual('image/jpeg'); + }); + }); +}); diff --git a/server/e2e/api/specs/library.e2e-spec.ts b/server/e2e/api/specs/library.e2e-spec.ts index 0ce192e479..75c973466e 100644 --- a/server/e2e/api/specs/library.e2e-spec.ts +++ b/server/e2e/api/specs/library.e2e-spec.ts @@ -11,7 +11,8 @@ describe(`${LibraryController.name} (e2e)`, () => { let admin: LoginResponseDto; beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); + const app = await testApp.create(); + server = app.getHttpServer(); }); afterAll(async () => { @@ -98,6 +99,36 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); + it('should not create an external library with duplicate import paths', async () => { + const { status, body } = await request(server) + .post('/library') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + type: LibraryType.EXTERNAL, + name: 'My Awesome Library', + importPaths: ['/path', '/path'], + exclusionPatterns: ['**/Raw/**'], + }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(["All importPaths's elements must be unique"])); + }); + + it('should not create an external library with duplicate exclusion patterns', async () => { + const { status, body } = await request(server) + .post('/library') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + type: LibraryType.EXTERNAL, + name: 'My Awesome Library', + importPaths: ['/path/to/import'], + exclusionPatterns: ['**/Raw/**', '**/Raw/**'], + }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(["All exclusionPatterns's elements must be unique"])); + }); + it('should create an upload library with defaults', async () => { const { status, body } = await request(server) .post('/library') @@ -229,7 +260,7 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); - it('should not allow an empty import path', async () => { + it('should reject an empty import path', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) @@ -239,6 +270,16 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual(errorStub.badRequest(['each value in importPaths should not be empty'])); }); + it('should reject duplicate import paths', async () => { + const { status, body } = await request(server) + .put(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ importPaths: ['/path', '/path'] }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(["All importPaths's elements must be unique"])); + }); + it('should change the exclusion pattern', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) @@ -253,7 +294,17 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); - it('should not allow an empty exclusion pattern', async () => { + it('should reject duplicate exclusion patterns', async () => { + const { status, body } = await request(server) + .put(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(["All exclusionPatterns's elements must be unique"])); + }); + + it('should reject an empty exclusion pattern', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) diff --git a/server/e2e/api/specs/search.e2e-spec.ts b/server/e2e/api/specs/search.e2e-spec.ts index 8d17ee9304..74988396d7 100644 --- a/server/e2e/api/specs/search.e2e-spec.ts +++ b/server/e2e/api/specs/search.e2e-spec.ts @@ -1,7 +1,7 @@ import { AssetResponseDto, IAssetRepository, - ISmartInfoRepository, + ISearchRepository, LibraryResponseDto, LoginResponseDto, mapAsset, @@ -20,14 +20,14 @@ describe(`${SearchController.name}`, () => { let accessToken: string; let libraries: LibraryResponseDto[]; let assetRepository: IAssetRepository; - let smartInfoRepository: ISmartInfoRepository; + let smartInfoRepository: ISearchRepository; let asset1: AssetResponseDto; beforeAll(async () => { app = await testApp.create(); server = app.getHttpServer(); assetRepository = app.get(IAssetRepository); - smartInfoRepository = app.get(ISmartInfoRepository); + smartInfoRepository = app.get(ISearchRepository); }); afterAll(async () => { diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts index b9ae897b33..8d2a1b79bc 100644 --- a/server/e2e/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -1,7 +1,7 @@ import { AssetResponseDto } from '@app/domain'; import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; import request from 'supertest'; type UploadDto = Partial & { content?: Buffer; filename?: string }; @@ -34,9 +34,7 @@ export const assetApi = { return body as AssetResponseDto; }, get: async (server: any, accessToken: string, id: string): Promise => { - const { body, status } = await request(server) - .get(`/asset/assetById/${id}`) - .set('Authorization', `Bearer ${accessToken}`); + const { body, status } = await request(server).get(`/asset/${id}`).set('Authorization', `Bearer ${accessToken}`); expect(status).toBe(200); return body as AssetResponseDto; }, diff --git a/server/e2e/docker-compose.server-e2e.yml b/server/e2e/docker-compose.server-e2e.yml index c9d656cedb..abb76ef25d 100644 --- a/server/e2e/docker-compose.server-e2e.yml +++ b/server/e2e/docker-compose.server-e2e.yml @@ -21,7 +21,7 @@ services: - database database: - image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 command: -c fsync=off -c shared_preload_libraries=vectors.so environment: POSTGRES_PASSWORD: postgres diff --git a/server/e2e/jobs/setup.ts b/server/e2e/jobs/setup.ts index 601d99cc28..d1f566d372 100644 --- a/server/e2e/jobs/setup.ts +++ b/server/e2e/jobs/setup.ts @@ -25,7 +25,7 @@ export default async () => { if (process.env.DB_HOSTNAME === undefined) { // DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container. - const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.1.11') + const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') .withExposedPorts(5432) .withDatabase('immich') .withUsername('postgres') diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index d5fefa701b..cb19117668 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -2,7 +2,7 @@ import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryController } from '@app/immich'; import { AssetType, LibraryType } from '@app/infra/entities'; import { errorStub, uuidStub } from '@test/fixtures'; -import * as fs from 'fs'; +import * as fs from 'node:fs'; import request from 'supertest'; import { utimes } from 'utimes'; import { @@ -18,7 +18,8 @@ describe(`${LibraryController.name} (e2e)`, () => { let admin: LoginResponseDto; beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); + const app = await testApp.create(); + server = app.getHttpServer(); }); beforeEach(async () => { @@ -264,7 +265,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); @@ -273,7 +274,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200001); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_001); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true }); @@ -289,7 +290,7 @@ describe(`${LibraryController.name} (e2e)`, () => { exifImageWidth: 800, exposureTime: '1/15', fNumber: 22, - fileSizeInByte: 114225, + fileSizeInByte: 114_225, focalLength: 35, iso: 1000, make: 'NIKON CORPORATION', @@ -311,7 +312,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); @@ -320,7 +321,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true }); @@ -351,7 +352,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); @@ -360,7 +361,7 @@ describe(`${LibraryController.name} (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); - await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); + await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshAllFiles: true }); @@ -375,7 +376,7 @@ describe(`${LibraryController.name} (e2e)`, () => { exifImageWidth: 800, exposureTime: '1/15', fNumber: 22, - fileSizeInByte: 114225, + fileSizeInByte: 114_225, focalLength: 35, iso: 1000, make: 'NIKON CORPORATION', diff --git a/server/package-lock.json b/server/package-lock.json index a1c3969f88..966639fa8d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -7,10 +7,10 @@ "": { "name": "immich", "version": "1.94.1", - "license": "UNLICENSED", + "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.3", + "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -25,7 +25,6 @@ "@types/picomatch": "^2.3.3", "archiver": "^6.0.0", "async-lock": "^1.4.0", - "axios": "^1.5.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", "chokidar": "^3.5.3", @@ -47,7 +46,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^3.0.1", + "picomatch": "^4.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -68,7 +67,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.11", + "@types/jest": "29.5.12", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -83,7 +82,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -125,9 +124,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.9.tgz", - "integrity": "sha512-r5jqwpWOgowqe9KSDqJ3iSbmsEt2XPjSvRG4DSI2T9s31bReoMtreo8b7wkRa2B3hbcDnstFbn8q27VvJDqRaQ==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.2.tgz", + "integrity": "sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -151,13 +150,25 @@ } } }, + "node_modules/@angular-devkit/core/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.9.tgz", - "integrity": "sha512-5ti7g45F2KjDJS0DbgnOGI1GyKxGpn4XsKTYJFJrSAWj6VpuvPy/DINRrXNuRVo09VPEkqA+IW7QwaG9icptQg==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.2.tgz", + "integrity": "sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.9", + "@angular-devkit/core": "17.1.2", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -170,15 +181,15 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.0.9.tgz", - "integrity": "sha512-tznzzB26sy8jVUlV9HhXcbFYZcIIFMAiDMOuyLko2LZFjfoqW+OPvwa1mwAQwvVVSQZVAKvdndFhzwyl/axwFQ==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.1.2.tgz", + "integrity": "sha512-bvXykYzSST05qFdlgIzUguNOb3z0hCa8HaTwtqdmQo9aFPf+P+/AC56I64t1iTchMjQtf3JrBQhYM25gUdcGbg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", "ansi-colors": "4.1.3", - "inquirer": "9.2.11", + "inquirer": "9.2.12", "symbol-observable": "4.0.0", "yargs-parser": "21.1.1" }, @@ -241,12 +252,12 @@ } }, "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", - "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.9", + "@ljharb/through": "^2.3.11", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", @@ -1554,29 +1565,14 @@ } }, "node_modules/@immich/cli": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.6.tgz", - "integrity": "sha512-RLl1w3+qijJgas6YoAKMVdE1OivjlWxu5s2myNbSU7EK9nCv7yntJiLzr6uTyCq52xoL9dlUwXfH5gkaTXqrOA==", - "dependencies": { - "axios": "^1.6.2", - "byte-size": "^8.1.1", - "cli-progress": "^3.12.0", - "commander": "^11.0.0", - "form-data": "^4.0.0", - "glob": "^10.3.1", - "graceful-fs": "^4.2.11", - "yaml": "^2.3.1" - }, + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.7.tgz", + "integrity": "sha512-36bpL7SCnbWuaHwuvVmV0iw2dgxX6umk3DhQ5rThJ6C9vOVZs8WY2zMU0voTATlEPoJkpwcQTOMLKFLTPL5OJw==", "bin": { - "immich": "dist/src/index.js" - } - }, - "node_modules/@immich/cli/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "immich": "dist/index.js" + }, "engines": { - "node": ">=16" + "node": ">=20.0.0" } }, "node_modules/@ioredis/commands": { @@ -2128,9 +2124,9 @@ "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2138,12 +2134,12 @@ } }, "node_modules/@ljharb/through": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", - "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", + "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.5" }, "engines": { "node": ">= 0.4" @@ -2209,6 +2205,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -2230,11 +2231,11 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.0.1.tgz", - "integrity": "sha512-8Td36l2i5x9+iQWjPB5Bd5+6u5Eangb5DclNcwrdwKqvd28xE92MSW97P4JV52C2kxrTjZwx8ck/wObAwtpQPw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", + "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", "dependencies": { - "tslib": "2.6.0" + "tslib": "2.6.2" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2242,31 +2243,31 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.0.1.tgz", - "integrity": "sha512-YJtfJXfnQinN7OvGx/Qd6jlQFu56zVnI1SppftSS7gkthB2CbJQAjkrfCEPDjp11wbPptBhUnatIL2N+nH/3kA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", + "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", "dependencies": { - "@nestjs/bull-shared": "^10.0.1", - "tslib": "2.6.0" + "@nestjs/bull-shared": "^10.1.0", + "tslib": "2.6.2" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "bullmq": "^3.0.0 || ^4.0.0" + "bullmq": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, "node_modules/@nestjs/cli": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.0.tgz", - "integrity": "sha512-37h+wSDItY0NE/x3a/M9yb2cXzfsD4qoE26rHgFn592XXLelDN12wdnfn7dTIaiRZT7WOCdQ+BYP9mQikR4AsA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", + "integrity": "sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", - "@angular-devkit/schematics-cli": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", + "@angular-devkit/schematics-cli": "17.1.2", "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", - "chokidar": "3.5.3", + "chokidar": "3.6.0", "cli-table3": "0.6.3", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.0.2", @@ -2281,7 +2282,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.89.0", + "webpack": "5.90.1", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2291,7 +2292,7 @@ "node": ">= 16.14" }, "peerDependencies": { - "@swc/cli": "^0.1.62", + "@swc/cli": "^0.1.62 || ^0.3.0", "@swc/core": "^1.3.62" }, "peerDependenciesMeta": { @@ -2373,9 +2374,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.1.tgz", - "integrity": "sha512-YuxeIlVemVQCuXMkNbBpNlmwZgp/Cu6dwCOjki63mhyYHEFX48GNNA4zZn5MFRjF4h7VSceABsScROuzsxs9LA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.2.tgz", + "integrity": "sha512-yrtohmQlN5J/gFS/ui86SO+KIfUKeL39JPR3f/3AWgpz+duIfc9cGkfh7FGZQMfG9ZqXf7Zw+PRO9G+D4iEbPw==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -2388,7 +2389,7 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -2400,41 +2401,25 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@nestjs/config": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", - "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", + "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", "dependencies": { - "dotenv": "16.3.1", + "dotenv": "16.4.1", "dotenv-expand": "10.0.0", "lodash": "4.17.21", - "uuid": "9.0.0" + "uuid": "9.0.1" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "reflect-metadata": "^0.1.13" - } - }, - "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "rxjs": "^7.1.0" } }, "node_modules/@nestjs/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.1.tgz", - "integrity": "sha512-mh6FwTKh2R3CmLRuB50BF5q/lzc+Mz+7qAlEvpgCiTSIfSXzbQ47vWpfgLirwkL3SlCvtFS8onxOeI69RpxvXA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.2.tgz", + "integrity": "sha512-JW3bQvDFY1gB+xXR6E5DzCdKftRszyWtd0YyDkdlKh1+44e2IGybFhSa5HcQBOiRqdVgPqAM5Vqc81rmhgeBnQ==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2453,7 +2438,7 @@ "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/websockets": "^10.0.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -2468,20 +2453,15 @@ } } }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@nestjs/mapped-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.4.tgz", - "integrity": "sha512-xl+gUSp0B+ln1VSNoUftlglk8dfpUes3DHGxKZ5knuBxS5g2H/8p9/DSBOYWUfO5f4u9s6ffBPZ71WO+tbe5SA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "class-transformer": "^0.4.0 || ^0.5.0", "class-validator": "^0.13.0 || ^0.14.0", - "reflect-metadata": "^0.1.12" + "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { "class-transformer": { @@ -2493,9 +2473,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.1.tgz", - "integrity": "sha512-Rj21quI5h4Lry7q9an+nO4ADQiQUy9A6XK74o5aTUHo3Ysm25ujqh2NgU4XbT3M2oXU9qzhE59OfhkQ7ZUvTAg==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.2.tgz", + "integrity": "sha512-jS0uQ5VCsOezhzW6xzlWquGEaDHQ9B+JTVsuxbac+3XOPt6sbKry8HHOWyjKss9wAKo0lDRSFvpV5oQAF/yl6w==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2512,15 +2492,10 @@ "@nestjs/core": "^10.0.0" } }, - "node_modules/@nestjs/platform-express/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.1.tgz", - "integrity": "sha512-ClGIQyGre7D8XBkOAsT/imDaLWpEfQFELnhZcZiIRI5T/p9mfVFrhvzK4srSFivmUqIQbIFGT1doOTNrkSU59Q==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.2.tgz", + "integrity": "sha512-BnU2tmH6cMSY6PUxen8QKmmLu7fZwe5b/IEx0kipBV9jGnidii+9VhW9sbdbZOQhrZVdGLByYEuaX7rdguq0lg==", "dependencies": { "socket.io": "4.7.4", "tslib": "2.6.2" @@ -2535,71 +2510,60 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@nestjs/schedule": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.0.tgz", - "integrity": "sha512-zz4h54m/F/1qyQKvMJCRphmuwGqJltDAkFxUXCVqJBXEs5kbPt93Pza3heCQOcMH22MZNhGlc9DmDMLXVHmgVQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.1.tgz", + "integrity": "sha512-cz2FNjsuoma+aGsG0cMmG6Dqg/BezbBWet1UTHtAuu6d2mXNTVcmoEQM2DIVG5Lfwb2hfSE2yZt8Moww+7y+mA==", "dependencies": { - "cron": "3.1.3", + "cron": "3.1.6", "uuid": "9.0.1" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "reflect-metadata": "^0.1.12" - } - }, - "node_modules/@nestjs/schedule/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/@nestjs/schematics": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.0.tgz", - "integrity": "sha512-HQWvD3F7O0Sv3qHS2jineWxPLmBTLlyjT6VdSw2EAIXulitmV+ErxB3TCVQQORlNkl5p5cwRYWyBaOblDbNFIQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", + "integrity": "sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", "comment-json": "4.2.3", - "jsonc-parser": "3.2.0", + "jsonc-parser": "3.2.1", "pluralize": "8.0.0" }, "peerDependencies": { "typescript": ">=4.8.2" } }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, "node_modules/@nestjs/swagger": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.2.0.tgz", - "integrity": "sha512-W7WPq561/79w27ZEgViXS7c5hqPwT7QXhsLsSeu2jeBROUhMM825QKDFKbMmtb643IW5dznJ4PjherlZZgtMvg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", + "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", "dependencies": { - "@nestjs/mapped-types": "2.0.4", + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.11.0" + "swagger-ui-dist": "5.11.2" }, "peerDependencies": { - "@fastify/static": "^6.0.0", + "@fastify/static": "^6.0.0 || ^7.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0", "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12" + "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { "@fastify/static": { @@ -2614,9 +2578,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.1.tgz", - "integrity": "sha512-74aSAugWT31jSPnStyRWDXgjHXWO3GYaUfAZ2T7Dml88UGkGy95iwaWgYy7aYM8/xVFKcDYkfL5FAYqZYce/yg==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.2.tgz", + "integrity": "sha512-jetqEPqOPuxmhBinkizmJQg4UZ2IRFrUoMrBDSgg0ogQClokKjnLgkoC5de+Jfm2kub/VpqorHB0me8cCr5jEQ==", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -2640,43 +2604,25 @@ } } }, - "node_modules/@nestjs/testing/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/@nestjs/typeorm": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.1.tgz", - "integrity": "sha512-YVFYL7D25VAVp5/G+KLXIgsRfYomA+VaFZBpm2rtwrrBOmkXNrxr7kuI2bBBO/Xy4kKBDe6wbvIVVFeEA7/ngA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", + "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", "dependencies": { "uuid": "9.0.1" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.1.13 || ^0.2.0", "rxjs": "^7.2.0", "typeorm": "^0.3.0" } }, - "node_modules/@nestjs/typeorm/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@nestjs/websockets": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.1.tgz", - "integrity": "sha512-4GckGRWQ6Ce0YnIoAysQof5a+/TZruLjbD8YHzWSbhykX33EJbK4mKYWSiL3pEI6w0RhwlpMU1cW7cFxV/gyjQ==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.2.tgz", + "integrity": "sha512-SMQM8hcRgngOabnluRLei46JUYvZ6j3DaZ2bDNYu57G3nlmkxXNktz7/pJG07NcetZL4CyF9pNyuICZql4Nhww==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2686,7 +2632,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-socket.io": "^10.0.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -2695,11 +2641,6 @@ } } }, - "node_modules/@nestjs/websockets/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2779,9 +2720,9 @@ } }, "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -2848,12 +2789,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@testcontainers/postgresql": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.6.0.tgz", - "integrity": "sha512-gHYpsXkVLpCkJ0jg7xE5n8pogTNvw2LyhkXeJm04raH1yz020jcHAB2a1tRW1J2D8mM8WhFH+xH6pzH2ZggFdg==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", + "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", "dev": true, "dependencies": { - "testcontainers": "^10.6.0" + "testcontainers": "^10.7.1" } }, "node_modules/@tsconfig/node10": { @@ -3064,9 +3005,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", - "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { @@ -3161,9 +3102,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.11", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", - "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3227,9 +3168,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dependencies": { "undici-types": "~5.26.4" } @@ -3329,9 +3270,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "node_modules/@types/send": { @@ -3446,16 +3387,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3481,15 +3422,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -3509,13 +3450,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3526,13 +3467,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3553,9 +3494,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3566,13 +3507,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3618,17 +3559,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -3643,12 +3584,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4171,17 +4112,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.6.tgz", - "integrity": "sha512-XZLZDFfXKM9U/Y/B4nNynfCRUqNyVZ4sBC/n9GDRCkq9vd2mIvKjKKsbIh1WPmHmNbg6ND7cTBY3Y2+u1G3/2Q==", - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/b4a": { "version": "1.6.4", @@ -4637,14 +4569,6 @@ "node": ">=0.10.0" } }, - "node_modules/byte-size": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", - "engines": { - "node": ">=12.17" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4654,12 +4578,17 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4732,15 +4661,9 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4753,6 +4676,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -4862,17 +4788,6 @@ "npm": ">=5.0.0" } }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cli-spinners": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", @@ -5014,6 +4929,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5272,9 +5188,9 @@ "devOptional": true }, "node_modules/cron": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.3.tgz", - "integrity": "sha512-KVxeKTKYj2eNzN4ElnT6nRSbjbfhyxR92O/Jdp6SH3pc05CDJws59jBrZWEMQlxevCiE6QUTrXy+Im3vC3oD3A==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz", + "integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==", "dependencies": { "@types/luxon": "~3.3.0", "luxon": "~3.4.0" @@ -5365,10 +5281,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.3.tgz", + "integrity": "sha512-h3GBouC+RPtNX2N0hHVLo2ZwPYurq8mLmXpOLTsw71gr7lHt5VaI4vVkDUNOfiWmm48JEXe3VM7PmLX45AMmmg==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -5676,6 +5610,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", @@ -5805,9 +5747,9 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -6469,25 +6411,6 @@ "which": "bin/which" } }, - "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -6535,6 +6458,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6647,9 +6571,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gauge": { "version": "3.0.2", @@ -6725,14 +6652,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6878,6 +6809,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6921,6 +6863,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -6945,6 +6888,17 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -6972,6 +6926,17 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -7046,9 +7011,9 @@ } }, "node_modules/i18n-iso-countries": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.8.0.tgz", - "integrity": "sha512-XNxPF0cpNFDxz/FA/uMoONFwvRoWFzrtU8OTOXKMXNpoY7FtqKSKF++/jxTKkMEew0J2MGB8Mf/f1j+BEW/hfw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.0.tgz", + "integrity": "sha512-Y4wkIS2MzYk7cvaV665qcHpBUK4FaMcAhSfsggu9SPV9VpWvmH8NklofWvPPFWG1ZXmxqZ0Ubgr+ZtqddxG4ag==", "dependencies": { "diacritics": "1.3.0" }, @@ -8163,13 +8128,13 @@ } }, "node_modules/joi": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.0.tgz", - "integrity": "sha512-HSLsmSmXz+PV9PYoi3p7cgIbj06WnEBNT28n+bbBNcPZXZFqCzzvGqpTBPujx/Z0nh1+KNQPDrNgdmQ8dq0qYw==", + "version": "17.12.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", + "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.4", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -9480,11 +9445,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -9622,9 +9587,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "bin": { "prettier": "bin/prettier.cjs" }, @@ -9773,11 +9738,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -10487,9 +10447,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -10514,6 +10474,22 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10823,9 +10799,9 @@ "dev": true }, "node_modules/sql-formatter": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.1.2.tgz", - "integrity": "sha512-zBrLBclCNurCsQaO6yMvkXzHvv7eJPjiF8LIEQ5HdBV/x6UuWIZwqss3mlZ/6HLj+VYhFKeHpQnyLuZWG2agKQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.2.0.tgz", + "integrity": "sha512-k1gDOblvmtzmrBT687Y167ElwQI/8KrlhfKeIUXsi6jw7Rp5n3G8TkMFZF0Z9NG7rAzHKXUlJ8kfmcIfMf5lFg==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -11103,9 +11079,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.0.tgz", - "integrity": "sha512-j0PIATqQSEFGOLmiJOJZj1X1Jt6bFIur3JpY7+ghliUnfZs0fpWDdHEkn9q7QUlBtKbkn6TepvSxTqnE8l3s0A==" + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" }, "node_modules/symbol-observable": { "version": "4.0.0", @@ -11132,12 +11108,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -11209,9 +11179,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/terser": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", - "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11227,16 +11197,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -11330,9 +11300,9 @@ } }, "node_modules/testcontainers": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.6.0.tgz", - "integrity": "sha512-FDJ3o3J8IMu1V7Uc6lNZ2MAD8+BV4HdpR/Vf5mHtgYHKdn6k1EbGFwtnvVNOxanJ99FCjf/EU8eA5ZQ4yjlsGA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", + "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", @@ -11811,9 +11781,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -11872,9 +11842,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typeorm": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.19.tgz", - "integrity": "sha512-OGelrY5qEoAU80mR1iyvmUHiKCPUydL6xp6bebXzS7jyv/X70Gp/jBWRAfF4qGOfy2A7orMiGRfwsBUNbEL65g==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", "dependencies": { "@sqltools/formatter": "^1.2.5", "app-root-path": "^3.1.0", @@ -11886,7 +11856,7 @@ "dotenv": "^16.0.3", "glob": "^10.3.10", "mkdirp": "^2.1.3", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.2.1", "sha.js": "^2.4.11", "tslib": "^2.5.0", "uuid": "^9.0.0", @@ -11898,7 +11868,7 @@ "typeorm-ts-node-esm": "cli-ts-node-esm.js" }, "engines": { - "node": ">= 12.9.0" + "node": ">=16.13.0" }, "funding": { "url": "https://opencollective.com/typeorm" @@ -12026,6 +11996,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/typeorm/node_modules/reflect-metadata": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + }, "node_modules/typeorm/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12217,9 +12192,13 @@ "dev": true }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -12312,19 +12291,19 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -12338,7 +12317,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -12534,6 +12513,7 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, "engines": { "node": ">= 14" } @@ -12624,9 +12604,9 @@ } }, "@angular-devkit/core": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.9.tgz", - "integrity": "sha512-r5jqwpWOgowqe9KSDqJ3iSbmsEt2XPjSvRG4DSI2T9s31bReoMtreo8b7wkRa2B3hbcDnstFbn8q27VvJDqRaQ==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.2.tgz", + "integrity": "sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==", "dev": true, "requires": { "ajv": "8.12.0", @@ -12635,15 +12615,23 @@ "picomatch": "3.0.1", "rxjs": "7.8.1", "source-map": "0.7.4" + }, + "dependencies": { + "picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true + } } }, "@angular-devkit/schematics": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.9.tgz", - "integrity": "sha512-5ti7g45F2KjDJS0DbgnOGI1GyKxGpn4XsKTYJFJrSAWj6VpuvPy/DINRrXNuRVo09VPEkqA+IW7QwaG9icptQg==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.2.tgz", + "integrity": "sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA==", "dev": true, "requires": { - "@angular-devkit/core": "17.0.9", + "@angular-devkit/core": "17.1.2", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -12651,15 +12639,15 @@ } }, "@angular-devkit/schematics-cli": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.0.9.tgz", - "integrity": "sha512-tznzzB26sy8jVUlV9HhXcbFYZcIIFMAiDMOuyLko2LZFjfoqW+OPvwa1mwAQwvVVSQZVAKvdndFhzwyl/axwFQ==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.1.2.tgz", + "integrity": "sha512-bvXykYzSST05qFdlgIzUguNOb3z0hCa8HaTwtqdmQo9aFPf+P+/AC56I64t1iTchMjQtf3JrBQhYM25gUdcGbg==", "dev": true, "requires": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", "ansi-colors": "4.1.3", - "inquirer": "9.2.11", + "inquirer": "9.2.12", "symbol-observable": "4.0.0", "yargs-parser": "21.1.1" }, @@ -12693,12 +12681,12 @@ } }, "inquirer": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", - "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, "requires": { - "@ljharb/through": "^2.3.9", + "@ljharb/through": "^2.3.11", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", @@ -13506,26 +13494,9 @@ "optional": true }, "@immich/cli": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.6.tgz", - "integrity": "sha512-RLl1w3+qijJgas6YoAKMVdE1OivjlWxu5s2myNbSU7EK9nCv7yntJiLzr6uTyCq52xoL9dlUwXfH5gkaTXqrOA==", - "requires": { - "axios": "^1.6.2", - "byte-size": "^8.1.1", - "cli-progress": "^3.12.0", - "commander": "^11.0.0", - "form-data": "^4.0.0", - "glob": "^10.3.1", - "graceful-fs": "^4.2.11", - "yaml": "^2.3.1" - }, - "dependencies": { - "commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" - } - } + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.7.tgz", + "integrity": "sha512-36bpL7SCnbWuaHwuvVmV0iw2dgxX6umk3DhQ5rThJ6C9vOVZs8WY2zMU0voTATlEPoJkpwcQTOMLKFLTPL5OJw==" }, "@ioredis/commands": { "version": "1.2.0", @@ -13949,9 +13920,9 @@ "devOptional": true }, "@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -13959,12 +13930,12 @@ } }, "@ljharb/through": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", - "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", + "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.5" } }, "@lukeed/csprng": { @@ -14011,6 +13982,11 @@ } } }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -14023,34 +13999,34 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.0.1.tgz", - "integrity": "sha512-8Td36l2i5x9+iQWjPB5Bd5+6u5Eangb5DclNcwrdwKqvd28xE92MSW97P4JV52C2kxrTjZwx8ck/wObAwtpQPw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", + "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", "requires": { - "tslib": "2.6.0" + "tslib": "2.6.2" } }, "@nestjs/bullmq": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.0.1.tgz", - "integrity": "sha512-YJtfJXfnQinN7OvGx/Qd6jlQFu56zVnI1SppftSS7gkthB2CbJQAjkrfCEPDjp11wbPptBhUnatIL2N+nH/3kA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", + "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", "requires": { - "@nestjs/bull-shared": "^10.0.1", - "tslib": "2.6.0" + "@nestjs/bull-shared": "^10.1.0", + "tslib": "2.6.2" } }, "@nestjs/cli": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.0.tgz", - "integrity": "sha512-37h+wSDItY0NE/x3a/M9yb2cXzfsD4qoE26rHgFn592XXLelDN12wdnfn7dTIaiRZT7WOCdQ+BYP9mQikR4AsA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", + "integrity": "sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg==", "dev": true, "requires": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", - "@angular-devkit/schematics-cli": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", + "@angular-devkit/schematics-cli": "17.1.2", "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", - "chokidar": "3.5.3", + "chokidar": "3.6.0", "cli-table3": "0.6.3", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.0.2", @@ -14065,7 +14041,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.89.0", + "webpack": "5.90.1", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -14119,44 +14095,30 @@ } }, "@nestjs/common": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.1.tgz", - "integrity": "sha512-YuxeIlVemVQCuXMkNbBpNlmwZgp/Cu6dwCOjki63mhyYHEFX48GNNA4zZn5MFRjF4h7VSceABsScROuzsxs9LA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.2.tgz", + "integrity": "sha512-yrtohmQlN5J/gFS/ui86SO+KIfUKeL39JPR3f/3AWgpz+duIfc9cGkfh7FGZQMfG9ZqXf7Zw+PRO9G+D4iEbPw==", "requires": { "iterare": "1.2.1", "tslib": "2.6.2", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@nestjs/config": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", - "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", + "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", "requires": { - "dotenv": "16.3.1", + "dotenv": "16.4.1", "dotenv-expand": "10.0.0", "lodash": "4.17.21", - "uuid": "9.0.0" - }, - "dependencies": { - "dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" - } + "uuid": "9.0.1" } }, "@nestjs/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.1.tgz", - "integrity": "sha512-mh6FwTKh2R3CmLRuB50BF5q/lzc+Mz+7qAlEvpgCiTSIfSXzbQ47vWpfgLirwkL3SlCvtFS8onxOeI69RpxvXA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.2.tgz", + "integrity": "sha512-JW3bQvDFY1gB+xXR6E5DzCdKftRszyWtd0YyDkdlKh1+44e2IGybFhSa5HcQBOiRqdVgPqAM5Vqc81rmhgeBnQ==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -14164,144 +14126,103 @@ "path-to-regexp": "3.2.0", "tslib": "2.6.2", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@nestjs/mapped-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.4.tgz", - "integrity": "sha512-xl+gUSp0B+ln1VSNoUftlglk8dfpUes3DHGxKZ5knuBxS5g2H/8p9/DSBOYWUfO5f4u9s6ffBPZ71WO+tbe5SA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", "requires": {} }, "@nestjs/platform-express": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.1.tgz", - "integrity": "sha512-Rj21quI5h4Lry7q9an+nO4ADQiQUy9A6XK74o5aTUHo3Ysm25ujqh2NgU4XbT3M2oXU9qzhE59OfhkQ7ZUvTAg==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.2.tgz", + "integrity": "sha512-jS0uQ5VCsOezhzW6xzlWquGEaDHQ9B+JTVsuxbac+3XOPt6sbKry8HHOWyjKss9wAKo0lDRSFvpV5oQAF/yl6w==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.18.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@nestjs/platform-socket.io": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.1.tgz", - "integrity": "sha512-ClGIQyGre7D8XBkOAsT/imDaLWpEfQFELnhZcZiIRI5T/p9mfVFrhvzK4srSFivmUqIQbIFGT1doOTNrkSU59Q==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.2.tgz", + "integrity": "sha512-BnU2tmH6cMSY6PUxen8QKmmLu7fZwe5b/IEx0kipBV9jGnidii+9VhW9sbdbZOQhrZVdGLByYEuaX7rdguq0lg==", "requires": { "socket.io": "4.7.4", "tslib": "2.6.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@nestjs/schedule": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.0.tgz", - "integrity": "sha512-zz4h54m/F/1qyQKvMJCRphmuwGqJltDAkFxUXCVqJBXEs5kbPt93Pza3heCQOcMH22MZNhGlc9DmDMLXVHmgVQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.1.tgz", + "integrity": "sha512-cz2FNjsuoma+aGsG0cMmG6Dqg/BezbBWet1UTHtAuu6d2mXNTVcmoEQM2DIVG5Lfwb2hfSE2yZt8Moww+7y+mA==", "requires": { - "cron": "3.1.3", + "cron": "3.1.6", "uuid": "9.0.1" - }, - "dependencies": { - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" - } } }, "@nestjs/schematics": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.0.tgz", - "integrity": "sha512-HQWvD3F7O0Sv3qHS2jineWxPLmBTLlyjT6VdSw2EAIXulitmV+ErxB3TCVQQORlNkl5p5cwRYWyBaOblDbNFIQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", + "integrity": "sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig==", "dev": true, "requires": { - "@angular-devkit/core": "17.0.9", - "@angular-devkit/schematics": "17.0.9", + "@angular-devkit/core": "17.1.2", + "@angular-devkit/schematics": "17.1.2", "comment-json": "4.2.3", - "jsonc-parser": "3.2.0", + "jsonc-parser": "3.2.1", "pluralize": "8.0.0" - } - }, - "@nestjs/swagger": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.2.0.tgz", - "integrity": "sha512-W7WPq561/79w27ZEgViXS7c5hqPwT7QXhsLsSeu2jeBROUhMM825QKDFKbMmtb643IW5dznJ4PjherlZZgtMvg==", - "requires": { - "@nestjs/mapped-types": "2.0.4", - "js-yaml": "4.1.0", - "lodash": "4.17.21", - "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.11.0" - } - }, - "@nestjs/testing": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.1.tgz", - "integrity": "sha512-74aSAugWT31jSPnStyRWDXgjHXWO3GYaUfAZ2T7Dml88UGkGy95iwaWgYy7aYM8/xVFKcDYkfL5FAYqZYce/yg==", - "dev": true, - "requires": { - "tslib": "2.6.2" }, "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true } } }, + "@nestjs/swagger": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", + "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "requires": { + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.11.2" + } + }, + "@nestjs/testing": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.2.tgz", + "integrity": "sha512-jetqEPqOPuxmhBinkizmJQg4UZ2IRFrUoMrBDSgg0ogQClokKjnLgkoC5de+Jfm2kub/VpqorHB0me8cCr5jEQ==", + "dev": true, + "requires": { + "tslib": "2.6.2" + } + }, "@nestjs/typeorm": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.1.tgz", - "integrity": "sha512-YVFYL7D25VAVp5/G+KLXIgsRfYomA+VaFZBpm2rtwrrBOmkXNrxr7kuI2bBBO/Xy4kKBDe6wbvIVVFeEA7/ngA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", + "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", "requires": { "uuid": "9.0.1" - }, - "dependencies": { - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" - } } }, "@nestjs/websockets": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.1.tgz", - "integrity": "sha512-4GckGRWQ6Ce0YnIoAysQof5a+/TZruLjbD8YHzWSbhykX33EJbK4mKYWSiL3pEI6w0RhwlpMU1cW7cFxV/gyjQ==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.2.tgz", + "integrity": "sha512-SMQM8hcRgngOabnluRLei46JUYvZ6j3DaZ2bDNYu57G3nlmkxXNktz7/pJG07NcetZL4CyF9pNyuICZql4Nhww==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", "tslib": "2.6.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@nodelib/fs.scandir": { @@ -14358,9 +14279,9 @@ "dev": true }, "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "requires": { "@hapi/hoek": "^9.0.0" } @@ -14421,12 +14342,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@testcontainers/postgresql": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.6.0.tgz", - "integrity": "sha512-gHYpsXkVLpCkJ0jg7xE5n8pogTNvw2LyhkXeJm04raH1yz020jcHAB2a1tRW1J2D8mM8WhFH+xH6pzH2ZggFdg==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", + "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", "dev": true, "requires": { - "testcontainers": "^10.6.0" + "testcontainers": "^10.7.1" } }, "@tsconfig/node10": { @@ -14628,9 +14549,9 @@ } }, "@types/estree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", - "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "@types/express": { @@ -14725,9 +14646,9 @@ } }, "@types/jest": { - "version": "29.5.11", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", - "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, "requires": { "expect": "^29.0.0", @@ -14791,9 +14712,9 @@ } }, "@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "requires": { "undici-types": "~5.26.4" } @@ -14880,9 +14801,9 @@ } }, "@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "@types/send": { @@ -14997,16 +14918,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -15016,54 +14937,54 @@ } }, "@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" } }, "@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -15093,27 +15014,27 @@ } }, "@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "requires": { - "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -15547,17 +15468,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.6.tgz", - "integrity": "sha512-XZLZDFfXKM9U/Y/B4nNynfCRUqNyVZ4sBC/n9GDRCkq9vd2mIvKjKKsbIh1WPmHmNbg6ND7cTBY3Y2+u1G3/2Q==", - "requires": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "b4a": { "version": "1.6.4", @@ -15893,23 +15805,20 @@ "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", "dev": true }, - "byte-size": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==" - }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" } }, "callsites": { @@ -15950,9 +15859,9 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -16040,14 +15949,6 @@ "yargs": "^16.0.0" } }, - "cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "requires": { - "string-width": "^4.2.3" - } - }, "cli-spinners": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", @@ -16152,6 +16053,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -16343,9 +16245,9 @@ "devOptional": true }, "cron": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.3.tgz", - "integrity": "sha512-KVxeKTKYj2eNzN4ElnT6nRSbjbfhyxR92O/Jdp6SH3pc05CDJws59jBrZWEMQlxevCiE6QUTrXy+Im3vC3oD3A==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz", + "integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==", "requires": { "@types/luxon": "~3.3.0", "luxon": "~3.4.0" @@ -16409,10 +16311,22 @@ "clone": "^1.0.2" } }, + "define-data-property": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.3.tgz", + "integrity": "sha512-h3GBouC+RPtNX2N0hHVLo2ZwPYurq8mLmXpOLTsw71gr7lHt5VaI4vVkDUNOfiWmm48JEXe3VM7PmLX45AMmmg==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true }, "delegates": { "version": "1.0.0", @@ -16652,6 +16566,11 @@ "is-arrayish": "^0.2.1" } }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", @@ -16767,9 +16686,9 @@ } }, "eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.22.20", @@ -17262,11 +17181,6 @@ } } }, - "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" - }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -17300,6 +17214,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -17386,9 +17301,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "gauge": { "version": "3.0.2", @@ -17446,14 +17361,15 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-package-type": { @@ -17547,6 +17463,14 @@ "slash": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -17581,6 +17505,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -17596,6 +17521,14 @@ "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", "dev": true }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -17611,6 +17544,14 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -17667,9 +17608,9 @@ "dev": true }, "i18n-iso-countries": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.8.0.tgz", - "integrity": "sha512-XNxPF0cpNFDxz/FA/uMoONFwvRoWFzrtU8OTOXKMXNpoY7FtqKSKF++/jxTKkMEew0J2MGB8Mf/f1j+BEW/hfw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.0.tgz", + "integrity": "sha512-Y4wkIS2MzYk7cvaV665qcHpBUK4FaMcAhSfsggu9SPV9VpWvmH8NklofWvPPFWG1ZXmxqZ0Ubgr+ZtqddxG4ag==", "requires": { "diacritics": "1.3.0" } @@ -18494,13 +18435,13 @@ } }, "joi": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.0.tgz", - "integrity": "sha512-HSLsmSmXz+PV9PYoi3p7cgIbj06WnEBNT28n+bbBNcPZXZFqCzzvGqpTBPujx/Z0nh1+KNQPDrNgdmQ8dq0qYw==", + "version": "17.12.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", + "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", "requires": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.4", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -19524,9 +19465,9 @@ "dev": true }, "picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==" }, "pirates": { "version": "4.0.6", @@ -19623,9 +19564,9 @@ "dev": true }, "prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==" + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==" }, "prettier-linter-helpers": { "version": "1.0.0", @@ -19727,11 +19668,6 @@ "ipaddr.js": "1.9.1" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -20253,9 +20189,9 @@ } }, "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -20277,6 +20213,19 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "requires": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -20532,9 +20481,9 @@ "dev": true }, "sql-formatter": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.1.2.tgz", - "integrity": "sha512-zBrLBclCNurCsQaO6yMvkXzHvv7eJPjiF8LIEQ5HdBV/x6UuWIZwqss3mlZ/6HLj+VYhFKeHpQnyLuZWG2agKQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.2.0.tgz", + "integrity": "sha512-k1gDOblvmtzmrBT687Y167ElwQI/8KrlhfKeIUXsi6jw7Rp5n3G8TkMFZF0Z9NG7rAzHKXUlJ8kfmcIfMf5lFg==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -20742,9 +20691,9 @@ "dev": true }, "swagger-ui-dist": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.0.tgz", - "integrity": "sha512-j0PIATqQSEFGOLmiJOJZj1X1Jt6bFIur3JpY7+ghliUnfZs0fpWDdHEkn9q7QUlBtKbkn6TepvSxTqnE8l3s0A==" + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" }, "symbol-observable": { "version": "4.0.0", @@ -20760,14 +20709,6 @@ "requires": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - } } }, "tapable": { @@ -20828,9 +20769,9 @@ } }, "terser": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", - "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", @@ -20848,16 +20789,16 @@ } }, "terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "dependencies": { "jest-worker": { @@ -20910,9 +20851,9 @@ } }, "testcontainers": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.6.0.tgz", - "integrity": "sha512-FDJ3o3J8IMu1V7Uc6lNZ2MAD8+BV4HdpR/Vf5mHtgYHKdn6k1EbGFwtnvVNOxanJ99FCjf/EU8eA5ZQ4yjlsGA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", + "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", @@ -21274,9 +21215,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tweetnacl": { "version": "0.14.5", @@ -21320,9 +21261,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typeorm": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.19.tgz", - "integrity": "sha512-OGelrY5qEoAU80mR1iyvmUHiKCPUydL6xp6bebXzS7jyv/X70Gp/jBWRAfF4qGOfy2A7orMiGRfwsBUNbEL65g==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", "requires": { "@sqltools/formatter": "^1.2.5", "app-root-path": "^3.1.0", @@ -21334,7 +21275,7 @@ "dotenv": "^16.0.3", "glob": "^10.3.10", "mkdirp": "^2.1.3", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.2.1", "sha.js": "^2.4.11", "tslib": "^2.5.0", "uuid": "^9.0.0", @@ -21365,6 +21306,11 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==" }, + "reflect-metadata": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -21485,9 +21431,9 @@ } }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "v8-compile-cache-lib": { "version": "3.0.1", @@ -21567,19 +21513,19 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -21593,7 +21539,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -21726,7 +21672,8 @@ "yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true }, "yargs": { "version": "16.2.0", diff --git a/server/package.json b/server/package.json index 481c878481..7ed2e551d3 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "description": "", "author": "", "private": true, - "license": "UNLICENSED", + "license": "GNU Affero General Public License version 3", "scripts": { "build": "nest build", "format": "prettier --check .", @@ -35,7 +35,7 @@ }, "dependencies": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.3", + "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -50,7 +50,6 @@ "@types/picomatch": "^2.3.3", "archiver": "^6.0.0", "async-lock": "^1.4.0", - "axios": "^1.5.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", "chokidar": "^3.5.3", @@ -72,7 +71,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^3.0.1", + "picomatch": "^4.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -93,7 +92,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.11", + "@types/jest": "29.5.12", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -108,7 +107,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -146,7 +145,7 @@ "coverageDirectory": "./coverage", "coverageThreshold": { "./src/domain/": { - "branches": 80, + "branches": 79, "functions": 80, "lines": 90, "statements": 90 diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index e73858c311..d46678b8b4 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -31,8 +31,6 @@ import { AssetBulkUpdateDto, AssetJobName, AssetJobsDto, - AssetOrder, - AssetSearchDto, AssetStatsDto, MapMarkerDto, MemoryLaneDto, @@ -92,34 +90,6 @@ export class AssetService { this.configCore = SystemConfigCore.create(configRepository); } - search(auth: AuthDto, dto: AssetSearchDto) { - let checksum: Buffer | undefined; - - if (dto.checksum) { - const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; - checksum = Buffer.from(dto.checksum, encoding); - } - - const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; - const order = dto.order ? enumToOrder[dto.order] : undefined; - - return this.assetRepository - .search({ - ...dto, - order, - checksum, - ownerId: auth.user.id, - }) - .then((assets) => - assets.map((asset) => - mapAsset(asset, { - stripMetadata: false, - withStack: true, - }), - ), - ); - } - canUploadFile({ auth, fieldName, file }: UploadRequest): true { this.access.requireUploadAccess(auth); diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index bd4cf93ba6..0244ecd90e 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -1,20 +1,16 @@ -import { AssetType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsBoolean, IsDateString, - IsEnum, IsInt, IsLatitude, IsLongitude, IsNotEmpty, IsPositive, IsString, - Min, ValidateIf, } from 'class-validator'; -import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util'; +import { Optional, ValidateUUID } from '../../domain.util'; import { BulkIdsDto } from '../response-dto'; export class DeviceIdDto { @@ -32,152 +28,6 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) => o.latitude !== undefined || o.longitude !== undefined; const ValidateGPS = () => ValidateIf(hasGPS); -export class AssetSearchDto { - @ValidateUUID({ optional: true }) - id?: string; - - @ValidateUUID({ optional: true }) - libraryId?: string; - - @IsString() - @Optional() - deviceAssetId?: string; - - @IsString() - @Optional() - deviceId?: string; - - @IsEnum(AssetType) - @Optional() - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) - type?: AssetType; - - @IsString() - @Optional() - checksum?: string; - - @QueryBoolean({ optional: true }) - isArchived?: boolean; - - @QueryBoolean({ optional: true }) - isEncoded?: boolean; - - @QueryBoolean({ optional: true }) - isExternal?: boolean; - - @QueryBoolean({ optional: true }) - isFavorite?: boolean; - - @QueryBoolean({ optional: true }) - isMotion?: boolean; - - @QueryBoolean({ optional: true }) - isOffline?: boolean; - - @QueryBoolean({ optional: true }) - isReadOnly?: boolean; - - @QueryBoolean({ optional: true }) - isVisible?: boolean; - - @QueryBoolean({ optional: true }) - withDeleted?: boolean; - - @QueryBoolean({ optional: true }) - withStacked?: boolean; - - @QueryBoolean({ optional: true }) - withExif?: boolean; - - @QueryBoolean({ optional: true }) - withPeople?: boolean; - - @QueryDate({ optional: true }) - createdBefore?: Date; - - @QueryDate({ optional: true }) - createdAfter?: Date; - - @QueryDate({ optional: true }) - updatedBefore?: Date; - - @QueryDate({ optional: true }) - updatedAfter?: Date; - - @QueryDate({ optional: true }) - trashedBefore?: Date; - - @QueryDate({ optional: true }) - trashedAfter?: Date; - - @QueryDate({ optional: true }) - takenBefore?: Date; - - @QueryDate({ optional: true }) - takenAfter?: Date; - - @IsString() - @Optional() - originalFileName?: string; - - @IsString() - @Optional() - originalPath?: string; - - @IsString() - @Optional() - resizePath?: string; - - @IsString() - @Optional() - webpPath?: string; - - @IsString() - @Optional() - encodedVideoPath?: string; - - @IsString() - @Optional() - city?: string; - - @IsString() - @Optional() - state?: string; - - @IsString() - @Optional() - country?: string; - - @IsString() - @Optional() - make?: string; - - @IsString() - @Optional() - model?: string; - - @IsString() - @Optional() - lensModel?: string; - - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) - order?: AssetOrder; - - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - size?: number; -} - export class AssetBulkUpdateDto extends BulkIdsDto { @Optional() @IsBoolean() diff --git a/server/src/domain/auth/auth.constant.ts b/server/src/domain/auth/auth.constant.ts index d237a19cd5..f29fc92741 100644 --- a/server/src/domain/auth/auth.constant.ts +++ b/server/src/domain/auth/auth.constant.ts @@ -1,6 +1,7 @@ export const MOBILE_REDIRECT = 'app.immich:/'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; +export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; export const IMMICH_API_KEY_NAME = 'api_key'; export const IMMICH_API_KEY_HEADER = 'x-api-key'; diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/domain/auth/auth.dto.ts index 854ac99963..2f6f4b4b72 100644 --- a/server/src/domain/auth/auth.dto.ts +++ b/server/src/domain/auth/auth.dto.ts @@ -106,15 +106,6 @@ export class OAuthConfigDto { redirectUri!: string; } -/** @deprecated use oauth authorize */ -export class OAuthConfigResponseDto { - enabled!: boolean; - passwordLoginEnabled!: boolean; - url?: string; - buttonText?: string; - autoLaunch?: boolean; -} - export class OAuthAuthorizeResponseDto { url!: string; } diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index 78462cb490..8ebb75857a 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -429,27 +429,6 @@ describe('AuthService', () => { }); }); - describe('generateConfig', () => { - it('should work when oauth is not configured', async () => { - configMock.load.mockResolvedValue(systemConfigStub.disabled); - await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({ - enabled: false, - passwordLoginEnabled: false, - }); - }); - - it('should generate the config', async () => { - configMock.load.mockResolvedValue(systemConfigStub.enabled); - await expect(sut.generateConfig({ redirectUri: 'http://redirect' })).resolves.toEqual({ - enabled: true, - buttonText: 'OAuth', - url: 'http://authorization-url', - autoLaunch: false, - passwordLoginEnabled: true, - }); - }); - }); - describe('callback', () => { it('should throw an error if OAuth is not enabled', async () => { await expect(sut.callback({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException); diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index 2acade6365..a2c0d2df93 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -29,6 +29,7 @@ import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_AUTH_TYPE_COOKIE, + IMMICH_IS_AUTHENTICATED, LOGIN_URL, MOBILE_REDIRECT, } from './auth.constant'; @@ -42,7 +43,6 @@ import { OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, - OAuthConfigResponseDto, SignUpDto, mapLoginResponse, mapUserToken, @@ -201,28 +201,6 @@ export class AuthService { return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`; } - async generateConfig(dto: OAuthConfigDto): Promise { - const config = await this.configCore.getConfig(); - const response = { - enabled: config.oauth.enabled, - passwordLoginEnabled: config.passwordLogin.enabled, - }; - - if (!response.enabled) { - return response; - } - - const { scope, buttonText, autoLaunch } = config.oauth; - const oauthClient = await this.getOAuthClient(config); - const url = oauthClient.authorizationUrl({ - redirect_uri: this.normalize(config, dto.redirectUri), - scope, - state: generators.state(), - }); - - return { ...response, buttonText, url, autoLaunch }; - } - async authorize(dto: OAuthConfigDto): Promise { const config = await this.configCore.getConfig(); if (!config.oauth.enabled) { @@ -452,14 +430,17 @@ export class AuthService { let authTypeCookie = ''; let accessTokenCookie = ''; + let isAuthenticatedCookie = ''; if (isSecure) { accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; + isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; } else { accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; + isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; } - return [accessTokenCookie, authTypeCookie]; + return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie]; } } diff --git a/server/src/domain/database/database.service.spec.ts b/server/src/domain/database/database.service.spec.ts index 7608f61111..703805b065 100644 --- a/server/src/domain/database/database.service.spec.ts +++ b/server/src/domain/database/database.service.spec.ts @@ -1,41 +1,65 @@ -import { DatabaseExtension, DatabaseService, IDatabaseRepository, Version } from '@app/domain'; +import { + DatabaseExtension, + DatabaseService, + IDatabaseRepository, + VectorIndex, + Version, + VersionType, +} from '@app/domain'; import { ImmichLogger } from '@app/infra/logger'; import { newDatabaseRepositoryMock } from '@test'; describe(DatabaseService.name, () => { let sut: DatabaseService; let databaseMock: jest.Mocked; - let fatalLog: jest.SpyInstance; beforeEach(async () => { databaseMock = newDatabaseRepositoryMock(); - fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal'); sut = new DatabaseService(databaseMock); - - sut.minVectorsVersion = new Version(0, 1, 1); - sut.maxVectorsVersion = new Version(0, 1, 11); - }); - - afterEach(() => { - fatalLog.mockRestore(); }); it('should work', () => { expect(sut).toBeDefined(); }); - describe('init', () => { - it('should resolve successfully if minimum supported PostgreSQL and vectors version are installed', async () => { + describe.each([ + [{ vectorExt: DatabaseExtension.VECTORS, extName: 'pgvecto.rs', minVersion: new Version(0, 1, 1) }], + [{ vectorExt: DatabaseExtension.VECTOR, extName: 'pgvector', minVersion: new Version(0, 5, 0) }], + ] as const)('init', ({ vectorExt, extName, minVersion }) => { + let fatalLog: jest.SpyInstance; + let errorLog: jest.SpyInstance; + let warnLog: jest.SpyInstance; + + beforeEach(async () => { + fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal'); + errorLog = jest.spyOn(ImmichLogger.prototype, 'error'); + warnLog = jest.spyOn(ImmichLogger.prototype, 'warn'); + databaseMock.getPreferredVectorExtension.mockReturnValue(vectorExt); + databaseMock.getExtensionVersion.mockResolvedValue(minVersion); + + sut = new DatabaseService(databaseMock); + + sut.minVectorVersion = minVersion; + sut.minVectorsVersion = minVersion; + sut.vectorVersionPin = VersionType.MINOR; + sut.vectorsVersionPin = VersionType.MINOR; + }); + + afterEach(() => { + fatalLog.mockRestore(); + warnLog.mockRestore(); + }); + + it(`should resolve successfully if minimum supported PostgreSQL and ${extName} version are installed`, async () => { databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(14, 0, 0)); - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 1)); await expect(sut.init()).resolves.toBeUndefined(); - expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(2); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS); + expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); + expect(databaseMock.createExtension).toHaveBeenCalledWith(vectorExt); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); + expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); expect(fatalLog).not.toHaveBeenCalled(); }); @@ -43,112 +67,162 @@ describe(DatabaseService.name, () => { it('should throw an error if PostgreSQL version is below minimum supported version', async () => { databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(13, 0, 0)); - await expect(sut.init()).rejects.toThrow(/PostgreSQL version is 13/s); + await expect(sut.init()).rejects.toThrow('PostgreSQL version is 13'); expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); - it('should resolve successfully if minimum supported vectors version is installed', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 1)); - + it(`should resolve successfully if minimum supported ${extName} version is installed`, async () => { await expect(sut.init()).resolves.toBeUndefined(); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS); + expect(databaseMock.createExtension).toHaveBeenCalledWith(vectorExt); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); expect(fatalLog).not.toHaveBeenCalled(); }); - it('should resolve successfully if maximum supported vectors version is installed', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 11)); + it(`should throw an error if ${extName} version is not installed even after createVectorExtension`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue(null); - await expect(sut.init()).resolves.toBeUndefined(); + await expect(sut.init()).rejects.toThrow(`Unexpected: ${extName} extension is not installed.`); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(fatalLog).not.toHaveBeenCalled(); - }); - - it('should throw an error if vectors version is not installed even after createVectors', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(null); - - await expect(sut.init()).rejects.toThrow('Unexpected: The pgvecto.rs extension is not installed.'); - - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - it('should throw an error if vectors version is below minimum supported version', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 1)); - - await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0.1.11')/s); - - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); - - it('should throw an error if vectors version is above maximum supported version', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 12)); - - await expect(sut.init()).rejects.toThrow( - /('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s, + it(`should throw an error if ${extName} version is below minimum supported version`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue( + new Version(minVersion.major, minVersion.minor - 1, minVersion.patch), ); - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); + await expect(sut.init()).rejects.toThrow(extName); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - it('should throw an error if vectors version is a nightly', async () => { - databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 0)); + it.each([ + { type: VersionType.EQUAL, max: 'no', actual: 'patch' }, + { type: VersionType.PATCH, max: 'patch', actual: 'minor' }, + { type: VersionType.MINOR, max: 'minor', actual: 'major' }, + ] as const)( + `should throw an error if $max upgrade from min version is allowed and ${extName} version is $actual`, + async ({ type, actual }) => { + const version = new Version(minVersion.major, minVersion.minor, minVersion.patch); + version[actual] = minVersion[actual] + 1; + databaseMock.getExtensionVersion.mockResolvedValue(version); + if (vectorExt === DatabaseExtension.VECTOR) { + sut.minVectorVersion = minVersion; + sut.vectorVersionPin = type; + } else { + sut.minVectorsVersion = minVersion; + sut.vectorsVersionPin = type; + } - await expect(sut.init()).rejects.toThrow( - /(nightly).*('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s, - ); + await expect(sut.init()).rejects.toThrow(extName); + + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }, + ); + + it(`should throw an error if ${extName} version is a nightly`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 0)); + + await expect(sut.init()).rejects.toThrow(extName); - expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - it('should throw error if vectors extension could not be created', async () => { - databaseMock.createExtension.mockRejectedValueOnce(new Error('Failed to create extension')); + it(`should throw error if ${extName} extension could not be created`, async () => { + databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); await expect(sut.init()).rejects.toThrow('Failed to create extension'); expect(fatalLog).toHaveBeenCalledTimes(1); - expect(fatalLog.mock.calls[0][0]).toMatch(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11').*(v1\.91\.0)/s); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - it.each([{ major: 14 }, { major: 15 }, { major: 16 }])( - `should suggest image with postgres $major if database is $major`, - async ({ major }) => { - databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1)); - databaseMock.getPostgresVersion.mockResolvedValue(new Version(major, 0, 0)); + it(`should update ${extName} if a newer version is available`, async () => { + const version = new Version(minVersion.major, minVersion.minor + 1, minVersion.patch); + databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - await expect(sut.init()).rejects.toThrow(new RegExp(`tensorchord\/pgvecto-rs:pg${major}-v0\\.1\\.11`, 's')); + await expect(sut.init()).resolves.toBeUndefined(); + + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(vectorExt, version); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); + }); + + it(`should not update ${extName} if a newer version is higher than the maximum`, async () => { + const version = new Version(minVersion.major + 1, minVersion.minor, minVersion.patch); + databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); + + await expect(sut.init()).resolves.toBeUndefined(); + + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); + }); + + it(`should warn if attempted to update ${extName} and failed`, async () => { + const version = new Version(minVersion.major, minVersion.minor, minVersion.patch + 1); + databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); + databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); + + await expect(sut.init()).resolves.toBeUndefined(); + + expect(warnLog).toHaveBeenCalledTimes(1); + expect(warnLog.mock.calls[0][0]).toContain(extName); + expect(errorLog).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(vectorExt, version); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + }); + + it(`should warn if ${extName} update requires restart`, async () => { + const version = new Version(minVersion.major, minVersion.minor, minVersion.patch + 1); + databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); + + await expect(sut.init()).resolves.toBeUndefined(); + + expect(warnLog).toHaveBeenCalledTimes(1); + expect(warnLog.mock.calls[0][0]).toContain(extName); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(vectorExt, version); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); + }); + + it.each([{ index: VectorIndex.CLIP }, { index: VectorIndex.FACE }])( + `should reindex $index if necessary`, + async ({ index }) => { + databaseMock.shouldReindex.mockImplementation((indexArg) => Promise.resolve(indexArg === index)); + + await expect(sut.init()).resolves.toBeUndefined(); + + expect(databaseMock.shouldReindex).toHaveBeenCalledWith(index); + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).toHaveBeenCalledWith(index); + expect(databaseMock.reindex).toHaveBeenCalledTimes(1); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); }, ); - it('should not suggest image if postgres version is not in 14, 15 or 16', async () => { - databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(17, 0, 0)); - databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(17, 0, 0)); + it.each([{ index: VectorIndex.CLIP }, { index: VectorIndex.FACE }])( + `should not reindex $index if not necessary`, + async () => { + databaseMock.shouldReindex.mockResolvedValue(false); - await expect(sut.init()).rejects.toThrow(/^(?:(?!tensorchord\/pgvecto-rs).)*$/s); - }); + await expect(sut.init()).resolves.toBeUndefined(); - it('should reject and suggest the maximum supported version when unsupported pgvecto.rs version is in use', async () => { - databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1)); - - await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s); - - sut.maxVectorsVersion = new Version(0, 1, 12); - await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.12')/s); - }); + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(fatalLog).not.toHaveBeenCalled(); + }, + ); }); }); diff --git a/server/src/domain/database/database.service.ts b/server/src/domain/database/database.service.ts index 5af576a73b..5ea9e1a474 100644 --- a/server/src/domain/database/database.service.ts +++ b/server/src/domain/database/database.service.ts @@ -1,74 +1,56 @@ import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { QueryFailedError } from 'typeorm'; -import { Version } from '../domain.constant'; -import { DatabaseExtension, IDatabaseRepository } from '../repositories'; +import { Version, VersionType } from '../domain.constant'; +import { + DatabaseExtension, + DatabaseLock, + IDatabaseRepository, + VectorExtension, + VectorIndex, + extName, +} from '../repositories'; @Injectable() export class DatabaseService { private logger = new ImmichLogger(DatabaseService.name); + private vectorExt: VectorExtension; minPostgresVersion = 14; - minVectorsVersion = new Version(0, 1, 1); - maxVectorsVersion = new Version(0, 1, 11); + minVectorsVersion = new Version(0, 2, 0); + vectorsVersionPin = VersionType.MINOR; + minVectorVersion = new Version(0, 5, 0); + vectorVersionPin = VersionType.MAJOR; - constructor(@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository) {} + constructor(@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository) { + this.vectorExt = this.databaseRepository.getPreferredVectorExtension(); + } async init() { await this.assertPostgresql(); - await this.createVectors(); - await this.assertVectors(); - await this.databaseRepository.runMigrations(); - } + await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { + await this.createVectorExtension(); + await this.updateVectorExtension(); + await this.assertVectorExtension(); - private async assertVectors() { - const version = await this.databaseRepository.getExtensionVersion(DatabaseExtension.VECTORS); - if (version == null) { - throw new Error('Unexpected: The pgvecto.rs extension is not installed.'); - } + try { + if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { + await this.databaseRepository.reindex(VectorIndex.CLIP); + } - const image = await this.getVectorsImage(); - const suggestion = image ? `, such as with the docker image '${image}'` : ''; + if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { + await this.databaseRepository.reindex(VectorIndex.FACE); + } + } catch (error) { + this.logger.warn( + 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', + ); + throw error; + } - if (version.isEqual(new Version(0, 0, 0))) { - throw new Error( - `The pgvecto.rs extension version is ${version}, which means it is a nightly release.` + - `Please run 'DROP EXTENSION IF EXISTS vectors' and switch to a release version${suggestion}.`, - ); - } - - if (version.isNewerThan(this.maxVectorsVersion)) { - throw new Error(` - The pgvecto.rs extension version is ${version} instead of ${this.maxVectorsVersion}. - Please run 'DROP EXTENSION IF EXISTS vectors' and switch to ${this.maxVectorsVersion}${suggestion}.`); - } - - if (version.isOlderThan(this.minVectorsVersion)) { - throw new Error(` - The pgvecto.rs extension version is ${version}, which is older than the minimum supported version ${this.minVectorsVersion}. - Please upgrade to this version or later${suggestion}.`); - } - } - - private async createVectors() { - await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (error: QueryFailedError) => { - const image = await this.getVectorsImage(); - this.logger.fatal(` - Failed to create pgvecto.rs extension. - If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so. - See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0' - `); - throw error; + await this.databaseRepository.runMigrations(); }); } - private async getVectorsImage() { - const { major } = await this.databaseRepository.getPostgresVersion(); - if (![14, 15, 16].includes(major)) { - return null; - } - return `tensorchord/pgvecto-rs:pg${major}-v${this.maxVectorsVersion}`; - } - private async assertPostgresql() { const { major } = await this.databaseRepository.getPostgresVersion(); if (major < this.minPostgresVersion) { @@ -77,4 +59,99 @@ export class DatabaseService { Please upgrade to this version or later.`); } } + + private async createVectorExtension() { + await this.databaseRepository.createExtension(this.vectorExt).catch(async (error: QueryFailedError) => { + const otherExt = + this.vectorExt === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; + this.logger.fatal(` + Failed to activate ${extName[this.vectorExt]} extension. + Please ensure the Postgres instance has ${extName[this.vectorExt]} installed. + + If the Postgres instance already has ${extName[this.vectorExt]} installed, Immich may not have the necessary permissions to activate it. + In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${this.vectorExt}' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database. + + Alternatively, if your Postgres instance has ${extName[otherExt]}, you may use this instead by setting the environment variable 'VECTOR_EXTENSION=${otherExt}'. + Note that switching between the two extensions after a successful startup is not supported. + The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. + In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup. + `); + throw error; + }); + } + + private async updateVectorExtension() { + const [version, availableVersion] = await Promise.all([ + this.databaseRepository.getExtensionVersion(this.vectorExt), + this.databaseRepository.getAvailableExtensionVersion(this.vectorExt), + ]); + if (version == null) { + throw new Error(`Unexpected: ${extName[this.vectorExt]} extension is not installed.`); + } + + if (availableVersion == null) { + return; + } + + const maxVersion = this.vectorExt === DatabaseExtension.VECTOR ? this.vectorVersionPin : this.vectorsVersionPin; + const isNewer = availableVersion.isNewerThan(version); + if (isNewer == null || isNewer > maxVersion) { + return; + } + + try { + this.logger.log(`Updating ${extName[this.vectorExt]} extension to ${availableVersion}`); + const { restartRequired } = await this.databaseRepository.updateVectorExtension(this.vectorExt, availableVersion); + if (restartRequired) { + this.logger.warn(` + The ${extName[this.vectorExt]} extension has been updated to ${availableVersion}. + Please restart the Postgres instance to complete the update.`); + } + } catch (error) { + this.logger.warn(` + The ${extName[this.vectorExt]} extension version is ${version}, but ${availableVersion} is available. + Immich attempted to update the extension, but failed to do so. + This may be because Immich does not have the necessary permissions to update the extension. + + Please run 'ALTER EXTENSION ${this.vectorExt} UPDATE' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database.`); + this.logger.error(error); + } + } + + private async assertVectorExtension() { + const version = await this.databaseRepository.getExtensionVersion(this.vectorExt); + if (version == null) { + throw new Error(`Unexpected: The ${extName[this.vectorExt]} extension is not installed.`); + } + + if (version.isEqual(new Version(0, 0, 0))) { + throw new Error(` + The ${extName[this.vectorExt]} extension version is ${version}, which means it is a nightly release. + + Please run 'DROP EXTENSION IF EXISTS ${this.vectorExt}' and switch to a release version. + See https://immich.app/docs/guides/database-queries for how to query the database.`); + } + + const minVersion = this.vectorExt === DatabaseExtension.VECTOR ? this.minVectorVersion : this.minVectorsVersion; + const maxVersion = this.vectorExt === DatabaseExtension.VECTOR ? this.vectorVersionPin : this.vectorsVersionPin; + + if (version.isOlderThan(minVersion) || version.isNewerThan(minVersion) > maxVersion) { + const allowedReleaseType = maxVersion === VersionType.MAJOR ? '' : ` ${VersionType[maxVersion].toLowerCase()}`; + const releases = + maxVersion === VersionType.EQUAL + ? minVersion.toString() + : `${minVersion} and later${allowedReleaseType} releases`; + + throw new Error(` + The ${extName[this.vectorExt]} extension version is ${version}, but Immich only supports ${releases}. + + If the Postgres instance already has a compatible version installed, Immich may not have the necessary permissions to activate it. + In this case, please run 'ALTER EXTENSION UPDATE ${this.vectorExt}' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database. + + Otherwise, please update the version of ${extName[this.vectorExt]} in the Postgres instance to a compatible version.`); + } + } } diff --git a/server/src/domain/domain.config.ts b/server/src/domain/domain.config.ts index 3a106bad2b..ed1283ec2f 100644 --- a/server/src/domain/domain.config.ts +++ b/server/src/domain/domain.config.ts @@ -24,5 +24,6 @@ export const immichAppConfig: ConfigModuleOptions = { MACHINE_LEARNING_PORT: Joi.number().optional(), MICROSERVICES_PORT: Joi.number().optional(), SERVER_PORT: Joi.number().optional(), + VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'), }), }; diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts index 4ec4b1124c..154128a1c2 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/domain/domain.constant.spec.ts @@ -1,4 +1,4 @@ -import { Version, mimeTypes } from './domain.constant'; +import { Version, VersionType, mimeTypes } from './domain.constant'; describe('mimeTypes', () => { for (const { mimetype, extension } of [ @@ -196,45 +196,37 @@ describe('mimeTypes', () => { }); }); -describe('ServerVersion', () => { +describe('Version', () => { const tests = [ - { this: new Version(0, 0, 1), other: new Version(0, 0, 0), expected: 1 }, - { this: new Version(0, 1, 0), other: new Version(0, 0, 0), expected: 1 }, - { this: new Version(1, 0, 0), other: new Version(0, 0, 0), expected: 1 }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 1), expected: -1 }, - { this: new Version(0, 0, 0), other: new Version(0, 1, 0), expected: -1 }, - { this: new Version(0, 0, 0), other: new Version(1, 0, 0), expected: -1 }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 0), expected: 0 }, - { this: new Version(0, 0, 1), other: new Version(0, 0, 1), expected: 0 }, - { this: new Version(0, 1, 0), other: new Version(0, 1, 0), expected: 0 }, - { this: new Version(1, 0, 0), other: new Version(1, 0, 0), expected: 0 }, - { this: new Version(1, 0), other: new Version(1, 0, 0), expected: 0 }, - { this: new Version(1, 0), other: new Version(1, 0, 1), expected: -1 }, - { this: new Version(1, 1), other: new Version(1, 0, 1), expected: 1 }, - { this: new Version(1), other: new Version(1, 0, 0), expected: 0 }, - { this: new Version(1), other: new Version(1, 0, 1), expected: -1 }, + { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH }, + { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR }, + { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR }, + { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR }, + { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, ]; - describe('compare', () => { - for (const { this: thisVersion, other: otherVersion, expected } of tests) { - it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.compare(otherVersion)).toEqual(expected); - }); - } - }); - describe('isOlderThan', () => { - for (const { this: thisVersion, other: otherVersion, expected } of tests) { - const bool = expected < 0; - it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isOlderThan(otherVersion)).toEqual(bool); + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare < 0 ? type : VersionType.EQUAL; + it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected); }); } }); describe('isEqual', () => { - for (const { this: thisVersion, other: otherVersion, expected } of tests) { - const bool = expected === 0; + for (const { this: thisVersion, other: otherVersion, compare } of tests) { + const bool = compare === 0; it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { expect(thisVersion.isEqual(otherVersion)).toEqual(bool); }); @@ -242,10 +234,10 @@ describe('ServerVersion', () => { }); describe('isNewerThan', () => { - for (const { this: thisVersion, other: otherVersion, expected } of tests) { - const bool = expected > 0; - it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isNewerThan(otherVersion)).toEqual(bool); + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare > 0 ? type : VersionType.EQUAL; + it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected); }); } }); diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 227595e04f..4e7c4d5524 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -12,11 +12,20 @@ export interface IVersion { patch: number; } +export enum VersionType { + EQUAL = 0, + PATCH = 1, + MINOR = 2, + MAJOR = 3, +} + export class Version implements IVersion { + public readonly types = ['major', 'minor', 'patch'] as const; + constructor( - public readonly major: number, - public readonly minor: number = 0, - public readonly patch: number = 0, + public major: number, + public minor: number = 0, + public patch: number = 0, ) {} toString() { @@ -39,27 +48,30 @@ export class Version implements IVersion { } } - compare(version: Version): number { - for (const key of ['major', 'minor', 'patch'] as const) { + private compare(version: Version): [number, VersionType] { + for (const [i, key] of this.types.entries()) { const diff = this[key] - version[key]; if (diff !== 0) { - return diff > 0 ? 1 : -1; + return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType]; } } - return 0; + return [0, VersionType.EQUAL]; } - isOlderThan(version: Version): boolean { - return this.compare(version) < 0; + isOlderThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool < 0 ? type : VersionType.EQUAL; } isEqual(version: Version): boolean { - return this.compare(version) === 0; + const [bool] = this.compare(version); + return bool === 0; } - isNewerThan(version: Version): boolean { - return this.compare(version) > 0; + isNewerThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool > 0 ? type : VersionType.EQUAL; } } diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index ba5942cea4..5fdfd7ec5b 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -137,6 +137,17 @@ export interface PaginationOptions { skip?: number; } +export enum PaginationMode { + LIMIT_OFFSET = 'limit-offset', + SKIP_TAKE = 'skip-take', +} + +export interface PaginatedBuilderOptions { + take: number; + skip?: number; + mode?: PaginationMode; +} + export interface PaginationResult { items: T[]; hasNextPage: boolean; @@ -178,23 +189,25 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = { } /** - * Chunks an array or set into smaller arrays of the specified size. + * Chunks an array or set into smaller collections of the same type and specified size. * * @param collection The collection to chunk. * @param size The size of each chunk. */ -export function chunks(collection: Array | Set, size: number): T[][] { +export function chunks(collection: Array, size: number): Array>; +export function chunks(collection: Set, size: number): Array>; +export function chunks(collection: Array | Set, size: number): Array> | Array> { if (collection instanceof Set) { const result = []; - let chunk = []; + let chunk = new Set(); for (const element of collection) { - chunk.push(element); - if (chunk.length === size) { + chunk.add(element); + if (chunk.size === size) { result.push(chunk); - chunk = []; + chunk = new Set(); } } - if (chunk.length > 0) { + if (chunk.size > 0) { result.push(chunk); } return result; diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index e12ca4c185..638841ec63 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -1,6 +1,6 @@ import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ValidateUUID } from '../domain.util'; export class CreateLibraryDto { @@ -20,11 +20,13 @@ export class CreateLibraryDto { @IsOptional() @IsString({ each: true }) @IsNotEmpty({ each: true }) + @ArrayUnique() importPaths?: string[]; @IsOptional() @IsString({ each: true }) @IsNotEmpty({ each: true }) + @ArrayUnique() exclusionPatterns?: string[]; @IsOptional() @@ -45,11 +47,13 @@ export class UpdateLibraryDto { @IsOptional() @IsString({ each: true }) @IsNotEmpty({ each: true }) + @ArrayUnique() importPaths?: string[]; @IsOptional() @IsNotEmpty({ each: true }) @IsString({ each: true }) + @ArrayUnique() exclusionPatterns?: string[]; } diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 1af343318b..c86aa4a4e7 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -1,11 +1,11 @@ import { AssetType, LibraryType, SystemConfig, SystemConfigKey, UserEntity } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; - import { IAccessRepositoryMock, assetStub, authStub, libraryStub, + makeMockWatcher, newAccessRepositoryMock, newAssetRepositoryMock, newCryptoRepositoryMock, @@ -17,8 +17,6 @@ import { systemConfigStub, userStub, } from '@test'; - -import { newFSWatcherMock } from '@test/mocks'; import { Stats } from 'node:fs'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { @@ -128,16 +126,6 @@ describe(LibraryService.name, () => { } }); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); - await sut.init(); expect(storageMock.watch.mock.calls).toEqual( @@ -718,21 +706,13 @@ describe(LibraryService.name, () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); + const mockClose = jest.fn(); + storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); await sut.init(); - await sut.delete(authStub.admin, libraryStub.externalLibraryWithImportPaths1.id); - expect(mockWatcher.close).toHaveBeenCalled(); + expect(mockClose).toHaveBeenCalled(); }); }); @@ -940,16 +920,6 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([]); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); - await sut.init(); await sut.create(authStub.admin, { type: LibraryType.EXTERNAL, @@ -959,6 +929,7 @@ describe(LibraryService.name, () => { expect(storageMock.watch).toHaveBeenCalledWith( libraryStub.externalLibraryWithImportPaths1.importPaths, expect.anything(), + expect.anything(), ); }); @@ -1133,16 +1104,6 @@ describe(LibraryService.name, () => { libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); - await expect(sut.update(authStub.admin, authStub.admin.user.id, { importPaths: ['/foo'] })).resolves.toEqual( mapLibrary(libraryStub.externalLibraryWithImportPaths1), ); @@ -1155,6 +1116,7 @@ describe(LibraryService.name, () => { expect(storageMock.watch).toHaveBeenCalledWith( libraryStub.externalLibraryWithImportPaths1.importPaths, expect.anything(), + expect.anything(), ); }); @@ -1163,16 +1125,6 @@ describe(LibraryService.name, () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); - await expect(sut.update(authStub.admin, authStub.admin.user.id, { exclusionPatterns: ['bar'] })).resolves.toEqual( mapLibrary(libraryStub.externalLibraryWithImportPaths1), ); @@ -1182,7 +1134,11 @@ describe(LibraryService.name, () => { id: authStub.admin.user.id, }), ); - expect(storageMock.watch).toHaveBeenCalledWith(expect.arrayContaining([expect.any(String)]), expect.anything()); + expect(storageMock.watch).toHaveBeenCalledWith( + expect.arrayContaining([expect.any(String)]), + expect.anything(), + expect.anything(), + ); }); }); @@ -1204,56 +1160,35 @@ describe(LibraryService.name, () => { }); describe('watching enabled', () => { - const mockWatcher = newFSWatcherMock(); beforeEach(async () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.getAll.mockResolvedValue([]); await sut.init(); - - storageMock.watch.mockReturnValue(mockWatcher); }); it('should watch library', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - const mockWatcher = newFSWatcherMock(); - - let isReady = false; - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - isReady = true; - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); - await sut.watchAll(); expect(storageMock.watch).toHaveBeenCalledWith( libraryStub.externalLibraryWithImportPaths1.importPaths, expect.anything(), + expect.anything(), ); - - expect(isReady).toBe(true); }); it('should watch and unwatch library', async () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); + const mockClose = jest.fn(); + storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); await sut.watchAll(); await sut.unwatch(libraryStub.externalLibraryWithImportPaths1.id); - expect(mockWatcher.close).toHaveBeenCalled(); + expect(mockClose).toHaveBeenCalled(); }); it('should not watch library without import paths', async () => { @@ -1277,14 +1212,7 @@ describe(LibraryService.name, () => { it('should handle a new file event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'add') { - callback('/foo/photo.jpg'); - } - }); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -1304,14 +1232,9 @@ describe(LibraryService.name, () => { it('should handle a file change event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'change') { - callback('/foo/photo.jpg'); - } - }); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), + ); await sut.watchAll(); @@ -1331,16 +1254,10 @@ describe(LibraryService.name, () => { it('should handle a file unlink event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'unlink') { - callback('/foo/photo.jpg'); - } - }); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }), + ); await sut.watchAll(); @@ -1351,34 +1268,19 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - - let didError = false; - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'error') { - didError = true; - callback('Error!'); - } - }); + storageMock.watch.mockImplementation( + makeMockWatcher({ + items: [{ event: 'error', value: 'Error!' }], + }), + ); await sut.watchAll(); - - expect(didError).toBe(true); }); it('should ignore unknown extensions', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'add') { - callback('/foo/photo.txt'); - } - }); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -1388,14 +1290,7 @@ describe(LibraryService.name, () => { it('should ignore excluded paths', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'add') { - callback('/dir1/photo.txt'); - } - }); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] })); await sut.watchAll(); @@ -1405,14 +1300,7 @@ describe(LibraryService.name, () => { it('should ignore excluded paths without case sensitivity', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } else if (event === 'add') { - callback('/DIR1/photo.txt'); - } - }); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] })); await sut.watchAll(); @@ -1445,20 +1333,13 @@ describe(LibraryService.name, () => { } }); - const mockWatcher = newFSWatcherMock(); - - mockWatcher.on.mockImplementation((event, callback) => { - if (event === 'ready') { - callback(); - } - }); - - storageMock.watch.mockReturnValue(mockWatcher); + const mockClose = jest.fn(); + storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); await sut.init(); await sut.unwatchAll(); - expect(mockWatcher.close).toHaveBeenCalledTimes(2); + expect(mockClose).toHaveBeenCalledTimes(2); }); }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index dc36c9d23b..3454f3547b 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -38,10 +38,8 @@ export class LibraryService extends EventEmitter { readonly logger = new ImmichLogger(LibraryService.name); private access: AccessCore; private configCore: SystemConfigCore; - private watchLibraries = false; - - private watchers: Record Promise> = {}; + private watchers: Record void> = {}; constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, @@ -116,63 +114,57 @@ export class LibraryService extends EventEmitter { this.logger.debug(`Settings for watcher: usePolling: ${usePolling}, interval: ${interval}`); - const watcher = this.storageRepository.watch(library.importPaths, { - usePolling, - interval, - binaryInterval: interval, - ignoreInitial: true, - }); + let _resolve: () => void; + const ready$ = new Promise((resolve) => (_resolve = resolve)); - this.watchers[id] = async () => { - await watcher.close(); - }; - - watcher.on('add', async (path) => { - this.logger.debug(`File add event received for ${path} in library ${library.id}}`); - if (matcher(path)) { - await this.scanAssets(library.id, [path], library.ownerId, false); - } - this.emit('add', path); - }); - - watcher.on('change', async (path) => { - this.logger.debug(`Detected file change for ${path} in library ${library.id}`); - - if (matcher(path)) { - // Note: if the changed file was not previously imported, it will be imported now. - await this.scanAssets(library.id, [path], library.ownerId, false); - } - this.emit('change', path); - }); - - watcher.on('unlink', async (path) => { - this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); - const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); - - if (existingAssetEntity && matcher(path)) { - await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true }); - } - - this.emit('unlink', path); - }); - - watcher.on('error', async (error) => { - // TODO: should we log, or throw an exception? - this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); - }); + this.watchers[id] = this.storageRepository.watch( + library.importPaths, + { + usePolling, + interval, + binaryInterval: interval, + ignoreInitial: true, + }, + { + onReady: () => _resolve(), + onAdd: async (path) => { + this.logger.debug(`File add event received for ${path} in library ${library.id}}`); + if (matcher(path)) { + await this.scanAssets(library.id, [path], library.ownerId, false); + } + this.emit('add', path); + }, + onChange: async (path) => { + this.logger.debug(`Detected file change for ${path} in library ${library.id}`); + if (matcher(path)) { + // Note: if the changed file was not previously imported, it will be imported now. + await this.scanAssets(library.id, [path], library.ownerId, false); + } + this.emit('change', path); + }, + onUnlink: async (path) => { + this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); + const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); + if (asset && matcher(path)) { + await this.assetRepository.save({ id: asset.id, isOffline: true }); + } + this.emit('unlink', path); + }, + onError: (error) => { + // TODO: should we log, or throw an exception? + this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); + }, + }, + ); // Wait for the watcher to initialize before returning - await new Promise((resolve) => { - watcher.on('ready', async () => { - resolve(); - }); - }); + await ready$; return true; } async unwatch(id: string) { - if (this.watchers.hasOwnProperty(id)) { + if (this.watchers[id]) { await this.watchers[id](); delete this.watchers[id]; } diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index 9a3a4bfed1..6eafc176bb 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -37,7 +37,6 @@ import { IStorageRepository, ISystemConfigRepository, ImmichTags, - WithProperty, WithoutProperty, } from '../repositories'; import { MetadataService, Orientation } from './metadata.service'; @@ -547,6 +546,22 @@ describe(MetadataService.name, () => { ); }); + it('should handle duration in ISO time string', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Duration: '00:00:08.41' }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalled(); + expect(assetMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: assetStub.image.id, + duration: '00:00:08.410', + }), + ); + }); + it('should handle duration as an object without Scale', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } }); @@ -582,11 +597,11 @@ describe(MetadataService.name, () => { describe('handleQueueSidecar', () => { it('should queue assets with sidecar files', async () => { - assetMock.getWith.mockResolvedValue({ items: [assetStub.sidecar], hasNextPage: false }); + assetMock.getAll.mockResolvedValue({ items: [assetStub.sidecar], hasNextPage: false }); await sut.handleQueueSidecar({ force: true }); - expect(assetMock.getWith).toHaveBeenCalledWith({ take: 1000, skip: 0 }, WithProperty.SIDECAR); + expect(assetMock.getAll).toHaveBeenCalledWith({ take: 1000, skip: 0 }); expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -602,7 +617,7 @@ describe(MetadataService.name, () => { await sut.handleQueueSidecar({ force: false }); expect(assetMock.getWithout).toHaveBeenCalledWith({ take: 1000, skip: 0 }, WithoutProperty.SIDECAR); - expect(assetMock.getWith).not.toHaveBeenCalled(); + expect(assetMock.getAll).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.SIDECAR_DISCOVERY, @@ -613,8 +628,46 @@ describe(MetadataService.name, () => { }); describe('handleSidecarSync', () => { - it('should not error', async () => { - await sut.handleSidecarSync(); + it('should do nothing if asset could not be found', async () => { + assetMock.getByIds.mockResolvedValue([]); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); + expect(assetMock.save).not.toHaveBeenCalled(); + }); + + it('should do nothing if asset has no sidecar path', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); + expect(assetMock.save).not.toHaveBeenCalled(); + }); + + it('should do nothing if asset has no sidecar path', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); + expect(assetMock.save).not.toHaveBeenCalled(); + }); + + it('should set sidecar path if exists', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true); + expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); + expect(assetMock.save).toHaveBeenCalledWith({ + id: assetStub.sidecar.id, + sidecarPath: assetStub.sidecar.sidecarPath, + }); + }); + + it('should unset sidecar path if file does not exist anymore', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + storageMock.checkFileExists.mockResolvedValue(false); + + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true); + expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); + expect(assetMock.save).toHaveBeenCalledWith({ + id: assetStub.sidecar.id, + sidecarPath: null, + }); }); }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 12ad57006d..94c3e9ae3f 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -12,7 +12,6 @@ import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, Job import { ClientEvent, DatabaseLock, - ExifDuration, IAlbumRepository, IAssetRepository, ICommunicationRepository, @@ -26,7 +25,6 @@ import { IStorageRepository, ISystemConfigRepository, ImmichTags, - WithProperty, WithoutProperty, } from '../repositories'; import { StorageCore } from '../storage'; @@ -268,7 +266,7 @@ export class MetadataService { const { force } = job; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getWith(pagination, WithProperty.SIDECAR) + ? this.assetRepository.getAll(pagination) : this.assetRepository.getWithout(pagination, WithoutProperty.SIDECAR); }); @@ -284,26 +282,12 @@ export class MetadataService { return true; } - async handleSidecarSync() { - // TODO: optimize to only queue assets with recent xmp changes - return true; + handleSidecarSync({ id }: IEntityJob) { + return this.processSidecar(id, true); } - async handleSidecarDiscovery({ id }: IEntityJob) { - const [asset] = await this.assetRepository.getByIds([id]); - if (!asset || !asset.isVisible || asset.sidecarPath) { - return false; - } - - const sidecarPath = `${asset.originalPath}.xmp`; - const exists = await this.storageRepository.checkFileExists(sidecarPath, constants.R_OK); - if (!exists) { - return false; - } - - await this.assetRepository.save({ id: asset.id, sidecarPath }); - - return true; + handleSidecarDiscovery({ id }: IEntityJob) { + return this.processSidecar(id, false); } async handleSidecarWrite(job: ISidecarWriteJob) { @@ -555,11 +539,47 @@ export class MetadataService { return bitsPerSample; } - private getDuration(seconds?: number | ExifDuration): string { + private getDuration(seconds?: ImmichTags['Duration']): string { let _seconds = seconds as number; + if (typeof seconds === 'object') { _seconds = seconds.Value * (seconds?.Scale || 1); + } else if (typeof seconds === 'string') { + _seconds = Duration.fromISOTime(seconds).as('seconds'); } + return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); } + + private async processSidecar(id: string, isSync: boolean) { + const [asset] = await this.assetRepository.getByIds([id]); + + if (!asset) { + return false; + } + + if (isSync && !asset.sidecarPath) { + return false; + } + + if (!isSync && (!asset.isVisible || asset.sidecarPath)) { + return false; + } + + const sidecarPath = `${asset.originalPath}.xmp`; + const exists = await this.storageRepository.checkFileExists(sidecarPath, constants.R_OK); + if (exists) { + await this.assetRepository.save({ id: asset.id, sidecarPath }); + return true; + } + + if (!isSync) { + return false; + } + + this.logger.debug(`Sidecar File '${sidecarPath}' was not found, removing sidecarPath for asset ${asset.id}`); + await this.assetRepository.save({ id: asset.id, sidecarPath: null }); + + return true; + } } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index e1937524af..5da8666016 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -13,7 +13,7 @@ import { newMediaRepositoryMock, newMoveRepositoryMock, newPersonRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, personStub, @@ -31,7 +31,7 @@ import { IMediaRepository, IMoveRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, IStorageRepository, ISystemConfigRepository, WithoutProperty, @@ -76,7 +76,7 @@ describe(PersonService.name, () => { let moveMock: jest.Mocked; let personMock: jest.Mocked; let storageMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let searchMock: jest.Mocked; let cryptoMock: jest.Mocked; let sut: PersonService; @@ -90,7 +90,7 @@ describe(PersonService.name, () => { mediaMock = newMediaRepositoryMock(); personMock = newPersonRepositoryMock(); storageMock = newStorageRepositoryMock(); - smartInfoMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); sut = new PersonService( accessMock, @@ -102,7 +102,7 @@ describe(PersonService.name, () => { configMock, storageMock, jobMock, - smartInfoMock, + searchMock, cryptoMock, ); @@ -752,7 +752,7 @@ describe(PersonService.name, () => { it('should create a face with no person and queue recognition job', async () => { personMock.createFaces.mockResolvedValue([faceStub.face1.id]); machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]); - smartInfoMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); + searchMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); assetMock.getByIds.mockResolvedValue([assetStub.image]); const face = { assetId: 'asset-id', @@ -823,7 +823,7 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(faceStub.primaryFace1.person); @@ -850,7 +850,7 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); @@ -866,13 +866,31 @@ describe(PersonService.name, () => { }); }); - it('should defer non-core faces to end of queue', async () => { + it('should not queue face with no matches', async () => { const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; + searchMock.searchFaces.mockResolvedValue(faces); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); + personMock.create.mockResolvedValue(personStub.withName); + + await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + + expect(jobMock.queue).not.toHaveBeenCalled(); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(1); + expect(personMock.create).not.toHaveBeenCalled(); + expect(personMock.reassignFaces).not.toHaveBeenCalled(); + }); + + it('should defer non-core faces to end of queue', async () => { + const faces = [ + { face: faceStub.noPerson1, distance: 0 }, + { face: faceStub.noPerson2, distance: 0.4 }, + ] as FaceSearchResult[]; + configMock.load.mockResolvedValue([ - { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 }, + { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); @@ -882,25 +900,28 @@ describe(PersonService.name, () => { name: JobName.FACIAL_RECOGNITION, data: { id: faceStub.noPerson1.id, deferred: true }, }); - expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(1); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); }); - it('should not assign person to non-core face with no matching person', async () => { - const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; + it('should not assign person to deferred non-core face with no matching person', async () => { + const faces = [ + { face: faceStub.noPerson1, distance: 0 }, + { face: faceStub.noPerson2, distance: 0.4 }, + ] as FaceSearchResult[]; configMock.load.mockResolvedValue([ - { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 }, + { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, ]); - smartInfoMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); + searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(2); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(2); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 576f94c491..359084bf21 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -20,7 +20,7 @@ import { IMediaRepository, IMoveRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, IStorageRepository, ISystemConfigRepository, JobItem, @@ -61,7 +61,7 @@ export class PersonService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, ) { this.access = AccessCore.create(accessRepository); @@ -285,15 +285,7 @@ export class PersonService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { - order: 'DESC', - withFaces: true, - withPeople: false, - withSmartInfo: false, - withSmartSearch: false, - withExif: false, - withStacked: false, - }) + ? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.FACES); }); @@ -417,7 +409,13 @@ export class PersonService { numResults: machineLearning.facialRecognition.minFaces, }); - this.logger.debug(`Face ${id} has ${matches.length} match${matches.length == 1 ? '' : 'es'}`); + // `matches` also includes the face itself + if (matches.length <= 1) { + this.logger.debug(`Face ${id} has no matches`); + return true; + } + + this.logger.debug(`Face ${id} has ${matches.length} matches`); const isCore = matches.length >= machineLearning.facialRecognition.minFaces; if (!isCore && !deferred) { @@ -426,7 +424,7 @@ export class PersonService { return true; } - let personId = matches.find((match) => match.face.personId)?.face.personId; // `matches` also includes the face itself + let personId = matches.find((match) => match.face.personId)?.face.personId; if (!personId) { const matchWithPerson = await this.smartInfoRepository.searchFaces({ userIds: [face.asset.ownerId], diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 00a74f8293..4ff58cdb1b 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,4 +1,4 @@ -import { SearchExploreItem } from '@app/domain'; +import { AssetSearchOptions, SearchExploreItem } from '@app/domain'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -11,64 +11,6 @@ export interface AssetStatsOptions { isTrashed?: boolean; } -export interface AssetSearchOptions { - id?: string; - libraryId?: string; - deviceAssetId?: string; - deviceId?: string; - ownerId?: string; - type?: AssetType; - checksum?: Buffer; - - isArchived?: boolean; - isEncoded?: boolean; - isExternal?: boolean; - isFavorite?: boolean; - isMotion?: boolean; - isOffline?: boolean; - isReadOnly?: boolean; - isVisible?: boolean; - - withDeleted?: boolean; - withStacked?: boolean; - withExif?: boolean; - withPeople?: boolean; - withSmartInfo?: boolean; - withSmartSearch?: boolean; - withFaces?: boolean; - - createdBefore?: Date; - createdAfter?: Date; - updatedBefore?: Date; - updatedAfter?: Date; - trashedBefore?: Date; - trashedAfter?: Date; - takenBefore?: Date; - takenAfter?: Date; - - originalFileName?: string; - originalPath?: string; - resizePath?: string; - webpPath?: string; - encodedVideoPath?: string; - - city?: string; - state?: string; - country?: string; - make?: string; - model?: string; - lensModel?: string; - - /** defaults to 'DESC' */ - order?: 'ASC' | 'DESC'; - - /** defaults to 1 */ - page?: number; - - /** defaults to 250 */ - size?: number; -} - export interface LivePhotoSearchOptions { ownerId: string; livePhotoCID: string; @@ -204,7 +146,6 @@ export interface IAssetRepository { getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise; upsertExif(exif: Partial): Promise; upsertJobStatus(jobStatus: Partial): Promise; - search(options: AssetSearchOptions): Promise; getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise; diff --git a/server/src/domain/repositories/database.repository.ts b/server/src/domain/repositories/database.repository.ts index 07d0afca6b..d32939fe61 100644 --- a/server/src/domain/repositories/database.repository.ts +++ b/server/src/domain/repositories/database.repository.ts @@ -3,21 +3,47 @@ import { Version } from '../domain.constant'; export enum DatabaseExtension { CUBE = 'cube', EARTH_DISTANCE = 'earthdistance', + VECTOR = 'vector', VECTORS = 'vectors', } +export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS; + +export enum VectorIndex { + CLIP = 'clip_index', + FACE = 'face_index', +} + export enum DatabaseLock { GeodataImport = 100, + Migrations = 200, StorageTemplateMigration = 420, CLIPDimSize = 512, } +export const extName: Record = { + cube: 'cube', + earthdistance: 'earthdistance', + vector: 'pgvector', + vectors: 'pgvecto.rs', +} as const; + +export interface VectorUpdateResult { + restartRequired: boolean; +} + export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { getExtensionVersion(extensionName: string): Promise; + getAvailableExtensionVersion(extension: DatabaseExtension): Promise; + getPreferredVectorExtension(): VectorExtension; getPostgresVersion(): Promise; createExtension(extension: DatabaseExtension): Promise; + updateExtension(extension: DatabaseExtension, version?: Version): Promise; + updateVectorExtension(extension: VectorExtension, version?: Version): Promise; + reindex(index: VectorIndex): Promise; + shouldReindex(name: VectorIndex): Promise; runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise; withLock(lock: DatabaseLock, callback: () => Promise): Promise; isBusy(lock: DatabaseLock): boolean; diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts index 48b1d7e8e2..636abd2bea 100644 --- a/server/src/domain/repositories/index.ts +++ b/server/src/domain/repositories/index.ts @@ -19,7 +19,6 @@ export * from './person.repository'; export * from './search.repository'; export * from './server-info.repository'; export * from './shared-link.repository'; -export * from './smart-info.repository'; export * from './storage.repository'; export * from './system-config.repository'; export * from './system-metadata.repository'; diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index 98df4517ae..aff74ef361 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -26,7 +26,7 @@ export interface ImmichTags extends Omit { MediaGroupUUID?: string; ImagePixelDepth?: string; FocalLength?: number; - Duration?: number | ExifDuration; + Duration?: number | string | ExifDuration; EmbeddedVideoType?: string; EmbeddedVideoFile?: BinaryField; MotionPhotoVideo?: BinaryField; @@ -39,4 +39,9 @@ export interface IMetadataRepository { readTags(path: string): Promise; writeTags(path: string, tags: Partial): Promise; extractBinaryTag(tagName: string, path: string): Promise; + getCountries(userId: string): Promise; + getStates(userId: string, country?: string): Promise; + getCities(userId: string, country?: string, state?: string): Promise; + getCameraMakes(userId: string, model?: string): Promise; + getCameraModels(userId: string, make?: string): Promise; } diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index 5c3497c8ef..4d720f98ad 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -1,4 +1,7 @@ -import { AssetType } from '@app/infra/entities'; +import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities'; +import { Paginated } from '../domain.util'; + +export const ISearchRepository = 'ISearchRepository'; export enum SearchStrategy { SMART = 'SMART', @@ -54,3 +57,122 @@ export interface SearchExploreItem { fieldName: string; items: SearchExploreItemSet; } + +export type Embedding = number[]; + +export interface SearchAssetIDOptions { + checksum?: Buffer; + deviceAssetId?: string; + id?: string; +} + +export interface SearchUserIDOptions { + deviceId?: string; + libraryId?: string; + ownerId?: string; +} + +export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions; + +export interface SearchStatusOptions { + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + type?: AssetType; + withArchived?: boolean; + withDeleted?: boolean; +} + +export interface SearchOneToOneRelationOptions { + withExif?: boolean; + withSmartInfo?: boolean; +} + +export interface SearchRelationOptions extends SearchOneToOneRelationOptions { + withFaces?: boolean; + withPeople?: boolean; + withStacked?: boolean; +} + +export interface SearchDateOptions { + createdBefore?: Date; + createdAfter?: Date; + takenBefore?: Date; + takenAfter?: Date; + trashedBefore?: Date; + trashedAfter?: Date; + updatedBefore?: Date; + updatedAfter?: Date; +} + +export interface SearchPathOptions { + encodedVideoPath?: string; + originalFileName?: string; + originalPath?: string; + resizePath?: string; + webpPath?: string; +} + +export interface SearchExifOptions { + city?: string; + country?: string; + lensModel?: string; + make?: string; + model?: string; + state?: string; +} + +export interface SearchEmbeddingOptions { + embedding: Embedding; + userIds: string[]; +} + +export interface SearchOrderOptions { + orderDirection?: 'ASC' | 'DESC'; +} + +export interface SearchPaginationOptions { + page: number; + size: number; +} + +export type AssetSearchOptions = SearchDateOptions & + SearchIDOptions & + SearchExifOptions & + SearchOrderOptions & + SearchPathOptions & + SearchRelationOptions & + SearchStatusOptions; + +export type AssetSearchBuilderOptions = Omit; + +export type SmartSearchOptions = SearchDateOptions & + SearchEmbeddingOptions & + SearchExifOptions & + SearchOneToOneRelationOptions & + SearchStatusOptions & + SearchUserIDOptions; + +export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { + hasPerson?: boolean; + numResults: number; + maxDistance?: number; +} + +export interface FaceSearchResult { + distance: number; + face: AssetFaceEntity; +} + +export interface ISearchRepository { + init(modelName: string): Promise; + searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated; + searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; + searchFaces(search: FaceEmbeddingSearch): Promise; + upsert(smartInfo: Partial, embedding?: Embedding): Promise; +} diff --git a/server/src/domain/repositories/smart-info.repository.ts b/server/src/domain/repositories/smart-info.repository.ts deleted file mode 100644 index 7b82e9d744..0000000000 --- a/server/src/domain/repositories/smart-info.repository.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AssetEntity, AssetFaceEntity, SmartInfoEntity } from '@app/infra/entities'; - -export const ISmartInfoRepository = 'ISmartInfoRepository'; - -export type Embedding = number[]; - -export interface EmbeddingSearch { - userIds: string[]; - embedding: Embedding; - numResults?: number; - withArchived?: boolean; -} - -export interface FaceEmbeddingSearch extends EmbeddingSearch { - maxDistance?: number; - hasPerson?: boolean; -} - -export interface FaceSearchResult { - face: AssetFaceEntity; - distance: number; -} - -export interface ISmartInfoRepository { - init(modelName: string): Promise; - searchCLIP(search: EmbeddingSearch): Promise; - searchFaces(search: FaceEmbeddingSearch): Promise; - upsert(smartInfo: Partial, embedding?: Embedding): Promise; -} diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/domain/repositories/storage.repository.ts index c55aaf7ecd..c88095b17b 100644 --- a/server/src/domain/repositories/storage.repository.ts +++ b/server/src/domain/repositories/storage.repository.ts @@ -1,4 +1,4 @@ -import { FSWatcher, WatchOptions } from 'chokidar'; +import { WatchOptions } from 'chokidar'; import { Stats } from 'node:fs'; import { FileReadOptions } from 'node:fs/promises'; import { Readable } from 'node:stream'; @@ -23,7 +23,13 @@ export interface DiskUsage { export const IStorageRepository = 'IStorageRepository'; -export interface ImmichWatcher extends FSWatcher {} +export interface WatchEvents { + onReady(): void; + onAdd(path: string): void; + onChange(path: string): void; + onUnlink(path: string): void; + onError(error: Error): void; +} export interface IStorageRepository { createZipStream(): ImmichZipStream; @@ -41,5 +47,6 @@ export interface IStorageRepository { crawl(crawlOptions: CrawlOptionsDto): Promise; copyFile(source: string, target: string): Promise; rename(source: string, target: string): Promise; - watch(paths: string[], options: WatchOptions): ImmichWatcher; + watch(paths: string[], options: WatchOptions, events: Partial): () => void; + utimes(filepath: string, atime: Date, mtime: Date): Promise; } diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts new file mode 100644 index 0000000000..36a7524587 --- /dev/null +++ b/server/src/domain/search/dto/search-suggestion.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', +} + +export class SearchSuggestionRequestDto { + @IsEnum(SearchSuggestionType) + @IsNotEmpty() + @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) + type!: SearchSuggestionType; + + @IsString() + @IsOptional() + country?: string; + + @IsString() + @IsOptional() + state?: string; + + @IsString() + @IsOptional() + make?: string; + + @IsString() + @IsOptional() + model?: string; +} diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 3ddcb3a32c..a4e0396688 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,8 +1,184 @@ +import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; import { AssetType } from '@app/infra/entities'; -import { Transform } from 'class-transformer'; -import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, toBoolean } from '../../domain.util'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { Optional, QueryBoolean, QueryDate, ValidateUUID, toBoolean } from '../../domain.util'; +class BaseSearchDto { + @ValidateUUID({ optional: true }) + libraryId?: string; + + @IsString() + @IsNotEmpty() + @Optional() + deviceId?: string; + + @IsEnum(AssetType) + @Optional() + @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + type?: AssetType; + + @QueryBoolean({ optional: true }) + isArchived?: boolean; + + @QueryBoolean({ optional: true }) + withArchived?: boolean; + + @QueryBoolean({ optional: true }) + isEncoded?: boolean; + + @QueryBoolean({ optional: true }) + isExternal?: boolean; + + @QueryBoolean({ optional: true }) + isFavorite?: boolean; + + @QueryBoolean({ optional: true }) + isMotion?: boolean; + + @QueryBoolean({ optional: true }) + isOffline?: boolean; + + @QueryBoolean({ optional: true }) + isReadOnly?: boolean; + + @QueryBoolean({ optional: true }) + isVisible?: boolean; + + @QueryBoolean({ optional: true }) + withDeleted?: boolean; + + @QueryBoolean({ optional: true }) + withExif?: boolean; + + @QueryDate({ optional: true }) + createdBefore?: Date; + + @QueryDate({ optional: true }) + createdAfter?: Date; + + @QueryDate({ optional: true }) + updatedBefore?: Date; + + @QueryDate({ optional: true }) + updatedAfter?: Date; + + @QueryDate({ optional: true }) + trashedBefore?: Date; + + @QueryDate({ optional: true }) + trashedAfter?: Date; + + @QueryDate({ optional: true }) + takenBefore?: Date; + + @QueryDate({ optional: true }) + takenAfter?: Date; + + @IsString() + @IsNotEmpty() + @Optional() + city?: string; + + @IsString() + @IsNotEmpty() + @Optional() + state?: string; + + @IsString() + @IsNotEmpty() + @Optional() + country?: string; + + @IsString() + @IsNotEmpty() + @Optional() + make?: string; + + @IsString() + @IsNotEmpty() + @Optional() + model?: string; + + @IsString() + @IsNotEmpty() + @Optional() + lensModel?: string; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; + + @IsInt() + @Min(1) + @Max(1000) + @Type(() => Number) + @Optional() + size?: number; +} + +export class MetadataSearchDto extends BaseSearchDto { + @ValidateUUID({ optional: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Optional() + deviceAssetId?: string; + + @IsString() + @IsNotEmpty() + @Optional() + checksum?: string; + + @QueryBoolean({ optional: true }) + withStacked?: boolean; + + @QueryBoolean({ optional: true }) + withPeople?: boolean; + + @IsString() + @IsNotEmpty() + @Optional() + originalFileName?: string; + + @IsString() + @IsNotEmpty() + @Optional() + originalPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + resizePath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + webpPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + encodedVideoPath?: string; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order?: AssetOrder; +} + +export class SmartSearchDto extends BaseSearchDto { + @IsString() + @IsNotEmpty() + query!: string; +} + +// TODO: remove after implementing new search filters +/** @deprecated */ export class SearchDto { @IsString() @IsNotEmpty() @@ -43,6 +219,19 @@ export class SearchDto { @Optional() @Transform(toBoolean) withArchived?: boolean; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; + + @IsInt() + @Min(1) + @Max(1000) + @Type(() => Number) + @Optional() + size?: number; } export class SearchPeopleDto { diff --git a/server/src/domain/search/response-dto/search-response.dto.ts b/server/src/domain/search/response-dto/search-response.dto.ts index 724cd5854b..9dd65e7cc3 100644 --- a/server/src/domain/search/response-dto/search-response.dto.ts +++ b/server/src/domain/search/response-dto/search-response.dto.ts @@ -29,6 +29,7 @@ class SearchAssetResponseDto { count!: number; items!: AssetResponseDto[]; facets!: SearchFacetResponseDto[]; + nextPage!: string | null; } export class SearchResponseDto { diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/domain/search/search.service.spec.ts index 86373ce2d2..de1d63c9d7 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/domain/search/search.service.spec.ts @@ -4,9 +4,10 @@ import { authStub, newAssetRepositoryMock, newMachineLearningRepositoryMock, + newMetadataRepositoryMock, newPartnerRepositoryMock, newPersonRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newSystemConfigRepositoryMock, personStub, } from '@test'; @@ -14,9 +15,10 @@ import { mapAsset } from '../asset'; import { IAssetRepository, IMachineLearningRepository, + IMetadataRepository, IPartnerRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, } from '../repositories'; import { SearchDto } from './dto'; @@ -30,17 +32,20 @@ describe(SearchService.name, () => { let configMock: jest.Mocked; let machineMock: jest.Mocked; let personMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let searchMock: jest.Mocked; let partnerMock: jest.Mocked; + let metadataMock: jest.Mocked; beforeEach(() => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); machineMock = newMachineLearningRepositoryMock(); personMock = newPersonRepositoryMock(); - smartInfoMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); partnerMock = newPartnerRepositoryMock(); - sut = new SearchService(configMock, machineMock, personMock, smartInfoMock, assetMock, partnerMock); + metadataMock = newMetadataRepositoryMock(); + + sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock, metadataMock); }); it('should work', () => { @@ -104,6 +109,7 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; @@ -111,13 +117,13 @@ describe(SearchService.name, () => { expect(result).toEqual(expectedResponse); expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 }); - expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled(); + expect(searchMock.searchSmart).not.toHaveBeenCalled(); }); it('should search archived photos if `withArchived` option is true', async () => { const dto: SearchDto = { q: 'test query', clip: true, withArchived: true }; const embedding = [1, 2, 3]; - smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); + searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false }); machineMock.encodeText.mockResolvedValueOnce(embedding); partnerMock.getAll.mockResolvedValueOnce([]); const expectedResponse = { @@ -132,25 +138,28 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ - userIds: [authStub.user1.user.id], - embedding, - numResults: 100, - withArchived: true, - }); + expect(searchMock.searchSmart).toHaveBeenCalledWith( + { page: 1, size: 100 }, + { + userIds: [authStub.user1.user.id], + embedding, + withArchived: true, + }, + ); expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); it('should search by CLIP if `clip` option is true', async () => { const dto: SearchDto = { q: 'test query', clip: true }; const embedding = [1, 2, 3]; - smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); + searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false }); machineMock.encodeText.mockResolvedValueOnce(embedding); partnerMock.getAll.mockResolvedValueOnce([]); const expectedResponse = { @@ -165,18 +174,21 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ - userIds: [authStub.user1.user.id], - embedding, - numResults: 100, - withArchived: false, - }); + expect(searchMock.searchSmart).toHaveBeenCalledWith( + { page: 1, size: 100 }, + { + userIds: [authStub.user1.user.id], + embedding, + withArchived: false, + }, + ); expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 932a865d04..49cca2ab48 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,21 +1,23 @@ import { AssetEntity } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { AssetResponseDto, mapAsset } from '../asset'; +import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; import { IAssetRepository, IMachineLearningRepository, + IMetadataRepository, IPartnerRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, SearchExploreItem, SearchStrategy, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config'; -import { SearchDto, SearchPeopleDto } from './dto'; +import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto'; +import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto'; import { SearchResponseDto } from './response-dto'; @Injectable() @@ -27,9 +29,10 @@ export class SearchService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, + @Inject(IMetadataRepository) private metadataRepository: IMetadataRepository, ) { this.configCore = SystemConfigCore.create(configRepository); } @@ -55,6 +58,53 @@ export class SearchService { })); } + async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { + let checksum: Buffer | undefined; + + if (dto.checksum) { + const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; + checksum = Buffer.from(dto.checksum, encoding); + } + + const page = dto.page ?? 1; + const size = dto.size || 250; + const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; + const { hasNextPage, items } = await this.searchRepository.searchMetadata( + { page, size }, + { + ...dto, + checksum, + ownerId: auth.user.id, + orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC', + }, + ); + + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + } + + async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { + await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH); + const { machineLearning } = await this.configCore.getConfig(); + const userIds = await this.getUserIdsToSearch(auth); + + const embedding = await this.machineLearning.encodeText( + machineLearning.url, + { text: dto.query }, + machineLearning.clip, + ); + + const page = dto.page ?? 1; + const size = dto.size || 100; + const { hasNextPage, items } = await this.searchRepository.searchSmart( + { page, size }, + { ...dto, userIds, embedding }, + ); + + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + } + + // TODO: remove after implementing new search filters + /** @deprecated */ async search(auth: AuthDto, dto: SearchDto): Promise { await this.configCore.requireFeature(FeatureFlag.SEARCH); const { machineLearning } = await this.configCore.getConfig(); @@ -70,10 +120,10 @@ export class SearchService { } const userIds = await this.getUserIdsToSearch(auth); - const withArchived = dto.withArchived || false; + const page = dto.page ?? 1; + let nextPage: string | null = null; let assets: AssetEntity[] = []; - switch (strategy) { case SearchStrategy.SMART: { const embedding = await this.machineLearning.encodeText( @@ -81,36 +131,30 @@ export class SearchService { { text: query }, machineLearning.clip, ); - assets = await this.smartInfoRepository.searchCLIP({ - userIds: userIds, - embedding, - numResults: 100, - withArchived, - }); + + const { hasNextPage, items } = await this.searchRepository.searchSmart( + { page, size: dto.size || 100 }, + { + userIds, + embedding, + withArchived: !!dto.withArchived, + }, + ); + if (hasNextPage) { + nextPage = (page + 1).toString(); + } + assets = items; break; } case SearchStrategy.TEXT: { - assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 }); + assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 }); } default: { break; } } - return { - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: assets.length, - count: assets.length, - items: assets.map((asset) => mapAsset(asset)), - facets: [], - }, - }; + return this.mapResponse(assets, nextPage); } private async getUserIdsToSearch(auth: AuthDto): Promise { @@ -122,4 +166,41 @@ export class SearchService { userIds.push(...partnersIds); return userIds; } + + private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise { + return { + albums: { total: 0, count: 0, items: [], facets: [] }, + assets: { + total: assets.length, + count: assets.length, + items: assets.map((asset) => mapAsset(asset)), + facets: [], + nextPage, + }, + }; + } + + async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise { + if (dto.type === SearchSuggestionType.COUNTRY) { + return this.metadataRepository.getCountries(auth.user.id); + } + + if (dto.type === SearchSuggestionType.STATE) { + return this.metadataRepository.getStates(auth.user.id, dto.country); + } + + if (dto.type === SearchSuggestionType.CITY) { + return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); + } + + if (dto.type === SearchSuggestionType.CAMERA_MAKE) { + return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); + } + + if (dto.type === SearchSuggestionType.CAMERA_MODEL) { + return this.metadataRepository.getCameraModels(auth.user.id, dto.make); + } + + return []; + } } diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts index 373f8da91c..5da7b7824b 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/domain/smart-info/smart-info.service.spec.ts @@ -5,7 +5,7 @@ import { newDatabaseRepositoryMock, newJobRepositoryMock, newMachineLearningRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newSystemConfigRepositoryMock, } from '@test'; import { JobName } from '../job'; @@ -14,7 +14,7 @@ import { IDatabaseRepository, IJobRepository, IMachineLearningRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, WithoutProperty, } from '../repositories'; @@ -31,18 +31,18 @@ describe(SmartInfoService.name, () => { let assetMock: jest.Mocked; let configMock: jest.Mocked; let jobMock: jest.Mocked; - let smartMock: jest.Mocked; + let searchMock: jest.Mocked; let machineMock: jest.Mocked; let databaseMock: jest.Mocked; beforeEach(async () => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - smartMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); jobMock = newJobRepositoryMock(); machineMock = newMachineLearningRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); - sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, smartMock, configMock); + sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock); assetMock.getByIds.mockResolvedValue([asset]); }); @@ -102,12 +102,12 @@ describe(SmartInfoService.name, () => { await sut.handleEncodeClip({ id: asset.id }); - expect(smartMock.upsert).not.toHaveBeenCalled(); + expect(searchMock.upsert).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { - smartMock.upsert.mockResolvedValue(); + searchMock.upsert.mockResolvedValue(); machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); await sut.handleEncodeClip({ id: asset.id }); @@ -117,7 +117,7 @@ describe(SmartInfoService.name, () => { { imagePath: 'path/to/resize.ext' }, { enabled: true, modelName: 'ViT-B-32__openai' }, ); - expect(smartMock.upsert).toHaveBeenCalledWith( + expect(searchMock.upsert).toHaveBeenCalledWith( { assetId: 'asset-1', }, diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index 55e4b7080f..d193b29b51 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -8,7 +8,7 @@ import { IDatabaseRepository, IJobRepository, IMachineLearningRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, WithoutProperty, } from '../repositories'; @@ -24,7 +24,7 @@ export class SmartInfoService { @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, - @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, + @Inject(ISearchRepository) private repository: ISearchRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, ) { this.configCore = SystemConfigCore.create(configRepository); diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts index 6e17ca64e9..67d2bd2226 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/domain/storage-template/storage-template.service.spec.ts @@ -534,6 +534,12 @@ describe(StorageTemplateService.name, () => { .mockResolvedValue({ size: 5000, } as Stats); + when(storageMock.stat) + .calledWith(assetStub.image.originalPath) + .mockResolvedValue({ + atime: new Date(), + mtime: new Date(), + } as Stats); when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(assetStub.image.checksum); await sut.handleMigration(); @@ -542,6 +548,8 @@ describe(StorageTemplateService.name, () => { expect(storageMock.rename).toHaveBeenCalledWith('/original/path.jpg', newPath); expect(storageMock.copyFile).toHaveBeenCalledWith('/original/path.jpg', newPath); expect(storageMock.stat).toHaveBeenCalledWith(newPath); + expect(storageMock.stat).toHaveBeenCalledWith(assetStub.image.originalPath); + expect(storageMock.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date)); expect(storageMock.unlink).toHaveBeenCalledWith(assetStub.image.originalPath); expect(storageMock.unlink).toHaveBeenCalledTimes(1); expect(assetMock.save).toHaveBeenCalledWith({ diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts index 9456fd66b1..30a6002be5 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/domain/storage/storage.core.ts @@ -222,6 +222,9 @@ export class StorageCore { return; } + const { atime, mtime } = await this.repository.stat(move.oldPath); + await this.repository.utimes(newPath, atime, mtime); + try { await this.repository.unlink(move.oldPath); } catch (error: any) { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 0516e04043..1591e87d63 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -230,8 +230,6 @@ export class SystemConfigCore { [FeatureFlag.SIDECAR]: true, [FeatureFlag.SEARCH]: true, [FeatureFlag.TRASH]: config.trash.enabled, - - // TODO: use these instead of `POST oauth/config` [FeatureFlag.OAUTH]: config.oauth.enabled, [FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch, [FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index e8fa3f62e8..8addc63a0f 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException } from '@nestjs/common'; import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test'; import { QueueName } from '../job'; -import { ICommunicationRepository, ISmartInfoRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; +import { ICommunicationRepository, ISearchRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; import { defaults, SystemConfigValidator } from './system-config.core'; import { SystemConfigService } from './system-config.service'; @@ -146,7 +146,7 @@ describe(SystemConfigService.name, () => { let sut: SystemConfigService; let configMock: jest.Mocked; let communicationMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let smartInfoMock: jest.Mocked; beforeEach(async () => { delete process.env.IMMICH_CONFIG_FILE; diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 5bf597e35d..39a3ea1dfb 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { ClientEvent, ICommunicationRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, ServerEvent, } from '../repositories'; @@ -32,7 +32,7 @@ export class SystemConfigService { constructor( @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, ) { this.core = SystemConfigCore.create(repository); this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate()); diff --git a/server/src/domain/trash/trash.service.ts b/server/src/domain/trash/trash.service.ts index b1a38f72c9..30fd6843e2 100644 --- a/server/src/domain/trash/trash.service.ts +++ b/server/src/domain/trash/trash.service.ts @@ -33,7 +33,9 @@ export class TrashService { async restore(auth: AuthDto): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + this.assetRepository.getByUserId(pagination, auth.user.id, { + trashedBefore: DateTime.now().toJSDate(), + }), ); for await (const assets of assetPagination) { @@ -44,7 +46,9 @@ export class TrashService { async empty(auth: AuthDto): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + this.assetRepository.getByUserId(pagination, auth.user.id, { + trashedBefore: DateTime.now().toJSDate(), + }), ); for await (const assets of assetPagination) { diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 7855b1ed6b..a5b3fb7dc7 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -1,6 +1,7 @@ import { UserEntity } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { AuthDto } from '../auth'; import { CacheControl, ImmichFileResponse } from '../domain.util'; @@ -188,12 +189,7 @@ export class UserService { return false; } - // TODO use luxon for date calculation - const msInDay = 86_400_000; - const msDeleteWait = msInDay * 7; - const msSinceDelete = Date.now() - (Date.parse(user.deletedAt.toString()) || 0); - - return msSinceDelete >= msDeleteWait; + return DateTime.now().minus({ days: 7 }) > DateTime.fromJSDate(user.deletedAt); } private async findOrFail(id: string, options: UserFindOptions) { diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index c8b76d9674..7d55fa790e 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,4 +1,3 @@ -import { AssetCreate } from '@app/domain'; import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { OptionalBetween } from '@app/infra/infra.utils'; import { Injectable } from '@nestjs/common'; @@ -22,8 +21,6 @@ export interface AssetOwnerCheck extends AssetCheck { export interface IAssetRepositoryV1 { get(id: string): Promise; - create(asset: AssetCreate): Promise; - upsertExif(exif: Partial): Promise; getAllByUserId(userId: string, dto: AssetSearchDto): Promise; getLocationsByUserId(userId: string): Promise; getDetectedObjectsByUserId(userId: string): Promise; @@ -132,14 +129,6 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { }); } - create(asset: AssetCreate): Promise { - return this.assetRepository.save(asset); - } - - async upsertExif(exif: Partial): Promise { - await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); - } - /** * Get assets by checksums on the database * @param ownerId diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index ddcb8417c8..bc59e2dfef 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto, AssetService, AuthDto } from '@app/domain'; +import { AssetResponseDto, AuthDto } from '@app/domain'; import { Body, Controller, @@ -18,7 +18,7 @@ import { import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../../app.guard'; -import { UseValidation, sendFile } from '../../app.utils'; +import { sendFile } from '../../app.utils'; import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors'; import FileNotEmptyValidator from '../validation/file-not-empty-validator'; @@ -45,10 +45,7 @@ interface UploadFiles { @Controller(Route.ASSET) @Authenticated() export class AssetController { - constructor( - private serviceV1: AssetServiceV1, - private service: AssetService, - ) {} + constructor(private serviceV1: AssetServiceV1) {} @SharedLinkRoute() @Post('upload') @@ -143,17 +140,6 @@ export class AssetController { return this.serviceV1.getAllAssets(auth, dto); } - /** - * Get a single asset's information - * @deprecated Use `/asset/:id` - */ - @SharedLinkRoute() - @UseValidation() - @Get('/assetById/:id') - getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.get(auth, id) as Promise; - } - /** * Checks if multiple assets exist on the server and returns all existing - used by background backup */ diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts deleted file mode 100644 index 0688a65dd6..0000000000 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain'; -import { AssetEntity } from '@app/infra/entities'; -import { BadRequestException } from '@nestjs/common'; -import { parse } from 'node:path'; -import { IAssetRepositoryV1 } from './asset-repository'; -import { CreateAssetDto } from './dto/create-asset.dto'; - -export class AssetCore { - constructor( - private repository: IAssetRepositoryV1, - private jobRepository: IJobRepository, - ) {} - - async create( - auth: AuthDto, - dto: CreateAssetDto & { libraryId: string }, - file: UploadFile, - livePhotoAssetId?: string, - sidecarPath?: string, - ): Promise { - const asset = await this.repository.create({ - ownerId: auth.user.id, - libraryId: dto.libraryId, - - checksum: file.checksum, - originalPath: file.originalPath, - - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - - fileCreatedAt: dto.fileCreatedAt, - fileModifiedAt: dto.fileModifiedAt, - localDateTime: dto.fileCreatedAt, - deletedAt: null, - - type: mimeTypes.assetType(file.originalPath), - isFavorite: dto.isFavorite, - isArchived: dto.isArchived ?? false, - duration: dto.duration || null, - isVisible: dto.isVisible ?? true, - livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), - resizePath: null, - webpPath: null, - thumbhash: null, - encodedVideoPath: null, - tags: [], - sharedLinks: [], - originalFileName: parse(file.originalName).name, - faces: [], - sidecarPath: sidecarPath || null, - isReadOnly: dto.isReadOnly ?? false, - isExternal: dto.isExternal ?? false, - isOffline: dto.isOffline ?? false, - }); - - await this.repository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); - - return asset; - } - - static requireQuota(auth: AuthDto, size: number) { - if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { - throw new BadRequestException('Quota has been exceeded!'); - } - } -} diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index d5fde4a625..9f0aa371e8 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -1,4 +1,11 @@ -import { IAssetRepository, IJobRepository, ILibraryRepository, IUserRepository, JobName } from '@app/domain'; +import { + IAssetRepository, + IJobRepository, + ILibraryRepository, + IStorageRepository, + IUserRepository, + JobName, +} from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { IAccessRepositoryMock, @@ -9,6 +16,7 @@ import { newAssetRepositoryMock, newJobRepositoryMock, newLibraryRepositoryMock, + newStorageRepositoryMock, newUserRepositoryMock, } from '@test'; import { when } from 'jest-when'; @@ -63,14 +71,12 @@ describe('AssetService', () => { let assetMock: jest.Mocked; let jobMock: jest.Mocked; let libraryMock: jest.Mocked; + let storageMock: jest.Mocked; let userMock: jest.Mocked; beforeEach(() => { assetRepositoryMockV1 = { get: jest.fn(), - create: jest.fn(), - upsertExif: jest.fn(), - getAllByUserId: jest.fn(), getDetectedObjectsByUserId: jest.fn(), getLocationsByUserId: jest.fn(), @@ -84,9 +90,10 @@ describe('AssetService', () => { assetMock = newAssetRepositoryMock(); jobMock = newJobRepositoryMock(); libraryMock = newLibraryRepositoryMock(); + storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, userMock); + sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock); when(assetRepositoryMockV1.get) .calledWith(assetStub.livePhotoStillAsset.id) @@ -109,13 +116,18 @@ describe('AssetService', () => { }; const dto = _getCreateAssetDto(); - assetRepositoryMockV1.create.mockResolvedValue(assetEntity); + assetMock.create.mockResolvedValue(assetEntity); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); - expect(assetRepositoryMockV1.create).toHaveBeenCalled(); + expect(assetMock.create).toHaveBeenCalled(); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size); + expect(storageMock.utimes).toHaveBeenCalledWith( + file.originalPath, + expect.any(Date), + new Date(dto.fileModifiedAt), + ); }); it('should handle a duplicate', async () => { @@ -131,7 +143,7 @@ describe('AssetService', () => { const error = new QueryFailedError('', [], new Error('unique key violation')); (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; - assetRepositoryMockV1.create.mockRejectedValue(error); + assetMock.create.mockRejectedValue(error); assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); @@ -149,8 +161,8 @@ describe('AssetService', () => { const error = new QueryFailedError('', [], new Error('unique key violation')); (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; - assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - assetRepositoryMockV1.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); + assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect( @@ -170,6 +182,16 @@ describe('AssetService', () => { [{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }], ]); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111); + expect(storageMock.utimes).toHaveBeenCalledWith( + fileStub.livePhotoStill.originalPath, + expect.any(Date), + new Date(dto.fileModifiedAt), + ); + expect(storageMock.utimes).toHaveBeenCalledWith( + fileStub.livePhotoMotion.originalPath, + expect.any(Date), + new Date(dto.fileModifiedAt), + ); }); }); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 6d59647cbf..f438e55e9c 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -7,6 +7,7 @@ import { IAssetRepository, IJobRepository, ILibraryRepository, + IStorageRepository, IUserRepository, ImmichFileResponse, JobName, @@ -18,10 +19,16 @@ import { } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; -import { Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { parse } from 'node:path'; import { QueryFailedError } from 'typeorm'; import { IAssetRepositoryV1 } from './asset-repository'; -import { AssetCore } from './asset.core'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; @@ -41,7 +48,6 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon @Injectable() export class AssetService { readonly logger = new ImmichLogger(AssetService.name); - private assetCore: AssetCore; private access: AccessCore; constructor( @@ -50,9 +56,9 @@ export class AssetService { @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository, + @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IUserRepository) private userRepository: IUserRepository, ) { - this.assetCore = new AssetCore(assetRepositoryV1, jobRepository); this.access = AccessCore.create(accessRepository); } @@ -75,19 +81,13 @@ export class AssetService { try { const libraryId = await this.getLibraryId(auth, dto.libraryId); await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId); - AssetCore.requireQuota(auth, file.size); + this.requireQuota(auth, file.size); if (livePhotoFile) { const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; - livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile); + livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile); } - const asset = await this.assetCore.create( - auth, - { ...dto, libraryId }, - file, - livePhotoAsset?.id, - sidecarFile?.originalPath, - ); + const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath); await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size); @@ -115,7 +115,7 @@ export class AssetService { const userId = dto.userId || auth.user.id; await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto); - return assets.map((asset) => mapAsset(asset)); + return assets.map((asset) => mapAsset(asset, { withStack: true })); } async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise { @@ -317,4 +317,62 @@ export class AssetService { return library.id; } + + private async create( + auth: AuthDto, + dto: CreateAssetDto & { libraryId: string }, + file: UploadFile, + livePhotoAssetId?: string, + sidecarPath?: string, + ): Promise { + const asset = await this.assetRepository.create({ + ownerId: auth.user.id, + libraryId: dto.libraryId, + + checksum: file.checksum, + originalPath: file.originalPath, + + deviceAssetId: dto.deviceAssetId, + deviceId: dto.deviceId, + + fileCreatedAt: dto.fileCreatedAt, + fileModifiedAt: dto.fileModifiedAt, + localDateTime: dto.fileCreatedAt, + deletedAt: null, + + type: mimeTypes.assetType(file.originalPath), + isFavorite: dto.isFavorite, + isArchived: dto.isArchived ?? false, + duration: dto.duration || null, + isVisible: dto.isVisible ?? true, + livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), + resizePath: null, + webpPath: null, + thumbhash: null, + encodedVideoPath: null, + tags: [], + sharedLinks: [], + originalFileName: parse(file.originalName).name, + faces: [], + sidecarPath: sidecarPath || null, + isReadOnly: dto.isReadOnly ?? false, + isExternal: dto.isExternal ?? false, + isOffline: dto.isOffline ?? false, + }); + + if (sidecarPath) { + await this.storageRepository.utimes(sidecarPath, new Date(), new Date(dto.fileModifiedAt)); + } + await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); + await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); + await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); + + return asset; + } + + private requireQuota(auth: AuthDto, size: number) { + if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) { + throw new BadRequestException('Quota has been exceeded!'); + } + } } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 86a2b155ab..57a67d33fe 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -1,24 +1,21 @@ import { AssetBulkDeleteDto, AssetBulkUpdateDto, - AssetIdsDto, AssetJobsDto, AssetResponseDto, - AssetSearchDto, AssetService, AssetStatsDto, AssetStatsResponseDto, AuthDto, - BulkIdsDto, DeviceIdDto, - DownloadInfoDto, - DownloadResponseDto, DownloadService, MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MemoryLaneResponseDto, + MetadataSearchDto, RandomAssetsDto, + SearchService, TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto, @@ -26,25 +23,10 @@ import { UpdateAssetDto as UpdateDto, UpdateStackParentDto, } from '@app/domain'; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Next, - Param, - Post, - Put, - Query, - Res, - StreamableFile, -} from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { NextFunction, Response } from 'express'; -import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../app.guard'; -import { UseValidation, asStreamableFile, sendFile } from '../app.utils'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; +import { UseValidation } from '../app.utils'; import { Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -53,11 +35,15 @@ import { UUIDParamDto } from './dto/uuid-param.dto'; @Authenticated() @UseValidation() export class AssetsController { - constructor(private service: AssetService) {} + constructor(private searchService: SearchService) {} @Get() - searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { - return this.service.search(auth, dto); + @ApiOperation({ deprecated: true }) + async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise { + const { + assets: { items }, + } = await this.searchService.searchMetadata(auth, dto); + return items; } } @@ -87,42 +73,6 @@ export class AssetController { return this.service.getRandom(auth, dto.count ?? 1); } - /** - * @deprecated use `/download/info` - */ - @SharedLinkRoute() - @Post('download/info') - getDownloadInfoOld(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise { - return this.downloadService.getDownloadInfo(auth, dto); - } - - /** - * @deprecated use `/download/archive` - */ - @SharedLinkRoute() - @Post('download/archive') - @HttpCode(HttpStatus.OK) - @FileResponse() - downloadArchiveOld(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { - return this.downloadService.downloadArchive(auth, dto).then(asStreamableFile); - } - - /** - * @deprecated use `/download/:id` - */ - @SharedLinkRoute() - @Post('download/:id') - @HttpCode(HttpStatus.OK) - @FileResponse() - async downloadFileOld( - @Res() res: Response, - @Next() next: NextFunction, - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - ) { - await sendFile(res, next, () => this.downloadService.downloadFile(auth, id)); - } - /** * Get all asset of a device that are in the database, ID only. */ @@ -166,33 +116,6 @@ export class AssetController { return this.service.deleteAll(auth, dto); } - /** - * @deprecated use `POST /trash/restore/assets` - */ - @Post('restore') - @HttpCode(HttpStatus.NO_CONTENT) - restoreAssetsOld(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { - return this.trashService.restoreAssets(auth, dto); - } - - /** - * @deprecated use `POST /trash/empty` - */ - @Post('trash/empty') - @HttpCode(HttpStatus.NO_CONTENT) - emptyTrashOld(@Auth() auth: AuthDto): Promise { - return this.trashService.empty(auth); - } - - /** - * @deprecated use `POST /trash/restore` - */ - @Post('trash/restore') - @HttpCode(HttpStatus.NO_CONTENT) - restoreTrashOld(@Auth() auth: AuthDto): Promise { - return this.trashService.restore(auth); - } - @Put('stack/parent') @HttpCode(HttpStatus.OK) updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise { diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index 15018c10de..c8ffd52fd1 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -5,6 +5,7 @@ import { ChangePasswordDto, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, + IMMICH_IS_AUTHENTICATED, LoginCredentialDto, LoginDetails, LoginResponseDto, @@ -84,6 +85,7 @@ export class AuthController { ): Promise { res.clearCookie(IMMICH_ACCESS_COOKIE); res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); + res.clearCookie(IMMICH_IS_AUTHENTICATED); return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); } diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts index 678e4a4f3c..a62458715b 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/immich/controllers/oauth.controller.ts @@ -6,7 +6,6 @@ import { OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, - OAuthConfigResponseDto, UserResponseDto, } from '@app/domain'; import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; @@ -32,13 +31,6 @@ export class OAuthController { }; } - /** @deprecated use feature flags and /oauth/authorize */ - @PublicRoute() - @Post('config') - generateOAuthConfig(@Body() dto: OAuthConfigDto): Promise { - return this.service.generateConfig(dto); - } - @PublicRoute() @Post('authorize') startOAuth(@Body() dto: OAuthConfigDto): Promise { diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index 51ee900eec..f8438b2e35 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -1,14 +1,17 @@ import { AuthDto, + MetadataSearchDto, PersonResponseDto, SearchDto, SearchExploreResponseDto, SearchPeopleDto, SearchResponseDto, SearchService, + SmartSearchDto, } from '@app/domain'; +import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto'; import { Controller, Get, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -19,7 +22,18 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} + @Get('metadata') + searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise { + return this.service.searchMetadata(auth, dto); + } + + @Get('smart') + searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise { + return this.service.searchSmart(auth, dto); + } + @Get() + @ApiOperation({ deprecated: true }) search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise { return this.service.search(auth, dto); } @@ -33,4 +47,9 @@ export class SearchController { searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise { return this.service.searchPerson(auth, dto); } + + @Get('suggestions') + getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { + return this.service.getSearchSuggestions(auth, dto); + } } diff --git a/server/src/infra/database.config.ts b/server/src/infra/database.config.ts index 9e6cccd198..93926e51cf 100644 --- a/server/src/infra/database.config.ts +++ b/server/src/infra/database.config.ts @@ -1,3 +1,4 @@ +import { DatabaseExtension } from '@app/domain/repositories/database.repository'; import { DataSource } from 'typeorm'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; @@ -27,3 +28,6 @@ export const databaseConfig: PostgresConnectionOptions = { // this export is used by TypeORM commands in package.json#scripts export const dataSource = new DataSource(databaseConfig); + +export const vectorExt = + process.env.VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 93cb8fb681..b36fdf6f0a 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -17,9 +17,9 @@ import { IMoveRepository, IPartnerRepository, IPersonRepository, + ISearchRepository, IServerInfoRepository, ISharedLinkRepository, - ISmartInfoRepository, IStorageRepository, ISystemConfigRepository, ISystemMetadataRepository, @@ -56,9 +56,9 @@ import { MoveRepository, PartnerRepository, PersonRepository, + SearchRepository, ServerInfoRepository, SharedLinkRepository, - SmartInfoRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -86,7 +86,7 @@ const providers: Provider[] = [ { provide: IPersonRepository, useClass: PersonRepository }, { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, - { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, + { provide: ISearchRepository, useClass: SearchRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 1036df2afa..89bd319662 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -1,7 +1,19 @@ -import { Paginated, PaginationOptions } from '@app/domain'; +import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/domain'; import _ from 'lodash'; -import { Between, FindManyOptions, LessThanOrEqual, MoreThanOrEqual, ObjectLiteral, Repository } from 'typeorm'; -import { chunks, setUnion } from '../domain/domain.util'; +import { + Between, + Brackets, + FindManyOptions, + IsNull, + LessThanOrEqual, + MoreThanOrEqual, + Not, + ObjectLiteral, + Repository, + SelectQueryBuilder, +} from 'typeorm'; +import { PaginatedBuilderOptions, PaginationMode, PaginationResult, chunks, setUnion } from '../domain/domain.util'; +import { AssetEntity } from './entities'; import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util'; /** @@ -18,9 +30,21 @@ export function OptionalBetween(from?: T, to?: T) { } } +export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { + const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; + return Number.isInteger(value) && value >= min && value <= max; +}; + +function paginationHelper(items: Entity[], take: number): PaginationResult { + const hasNextPage = items.length > take; + items.splice(take); + + return { items, hasNextPage }; +} + export async function paginate( repository: Repository, - paginationOptions: PaginationOptions, + { take, skip }: PaginationOptions, searchOptions?: FindManyOptions, ): Paginated { const items = await repository.find( @@ -28,27 +52,33 @@ export async function paginate( { ...searchOptions, // Take one more item to check if there's a next page - take: paginationOptions.take + 1, - skip: paginationOptions.skip, + take: take + 1, + skip, }, _.isUndefined, ), ); - const hasNextPage = items.length > paginationOptions.take; - items.splice(paginationOptions.take); + return paginationHelper(items, take); +} - return { items, hasNextPage }; +export async function paginatedBuilder( + qb: SelectQueryBuilder, + { take, skip, mode }: PaginatedBuilderOptions, +): Paginated { + if (mode === PaginationMode.LIMIT_OFFSET) { + qb.limit(take + 1).offset(skip); + } else { + qb.take(take + 1).skip(skip); + } + + const items = await qb.getMany(); + return paginationHelper(items, take); } export const asVector = (embedding: number[], quote = false) => quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; -export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { - const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; - return Number.isInteger(value) && value >= min && value <= max; -}; - /** * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection, * to overcome the maximum number of parameters allowed by the database driver. @@ -91,3 +121,79 @@ export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator { return Chunked({ ...options, mergeFn: setUnion }); } + +export function searchAssetBuilder( + builder: SelectQueryBuilder, + options: AssetSearchBuilderOptions, +): SelectQueryBuilder { + builder.andWhere( + _.omitBy( + { + createdAt: OptionalBetween(options.createdAfter, options.createdBefore), + updatedAt: OptionalBetween(options.updatedAfter, options.updatedBefore), + deletedAt: OptionalBetween(options.trashedAfter, options.trashedBefore), + fileCreatedAt: OptionalBetween(options.takenAfter, options.takenBefore), + }, + _.isUndefined, + ), + ); + + const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined); + if (Object.keys(exifInfo).length > 0) { + builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); + builder.andWhere({ exifInfo }); + } + + const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']); + builder.andWhere(_.omitBy(id, _.isUndefined)); + + const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']); + builder.andWhere(_.omitBy(path, _.isUndefined)); + + const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']); + const { isArchived, isEncoded, isMotion, withArchived } = options; + builder.andWhere( + _.omitBy( + { + ...status, + isArchived: isArchived ?? withArchived, + encodedVideoPath: isEncoded ? Not(IsNull()) : undefined, + livePhotoVideoId: isMotion ? Not(IsNull()) : undefined, + }, + _.isUndefined, + ), + ); + + if (options.withExif) { + builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo'); + } + + if (options.withFaces || options.withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.faces`, 'faces'); + } + + if (options.withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.person`, 'person'); + } + + if (options.withSmartInfo) { + builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo'); + } + + if (options.withStacked) { + builder + .leftJoinAndSelect(`${builder.alias}.stack`, 'stack') + .leftJoinAndSelect('stack.assets', 'stackedAssets') + .andWhere( + new Brackets((qb) => qb.where(`stack.primaryAssetId = ${builder.alias}.id`).orWhere('asset.stackId IS NULL')), + ); + } + + const withDeleted = + options.withDeleted ?? (options.trashedAfter !== undefined || options.trashedBefore !== undefined); + if (withDeleted) { + builder.withDeleted(); + } + + return builder; +} diff --git a/server/src/infra/logger.ts b/server/src/infra/logger.ts index 183ffb492f..8de149c409 100644 --- a/server/src/infra/logger.ts +++ b/server/src/infra/logger.ts @@ -5,7 +5,7 @@ import { LogLevel } from './entities'; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; export class ImmichLogger extends ConsoleLogger { - private static logLevels: LogLevel[] = [LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; + private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; constructor(context: string) { super(context); diff --git a/server/src/infra/migrations/1700713871511-UsePgVectors.ts b/server/src/infra/migrations/1700713871511-UsePgVectors.ts index a952f1646d..008d5eadc8 100644 --- a/server/src/infra/migrations/1700713871511-UsePgVectors.ts +++ b/server/src/infra/migrations/1700713871511-UsePgVectors.ts @@ -1,11 +1,13 @@ import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; import { MigrationInterface, QueryRunner } from 'typeorm'; +import { vectorExt } from '@app/infra/database.config'; export class UsePgVectors1700713871511 implements MigrationInterface { name = 'UsePgVectors1700713871511'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS vectors`); + await queryRunner.query(`SET search_path TO "$user", public, vectors`); + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${vectorExt}`); const faceDimQuery = await queryRunner.query(` SELECT CARDINALITY(embedding::real[]) as dimsize FROM asset_faces diff --git a/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts index 7a1a1144d6..c3716cc191 100644 --- a/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts +++ b/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts @@ -1,16 +1,20 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; +import { vectorExt } from '../database.config'; +import { DatabaseExtension } from '@app/domain/repositories/database.repository'; export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface { name = 'AddCLIPEmbeddingIndex1700713994428'; public async up(queryRunner: QueryRunner): Promise { + if (vectorExt === DatabaseExtension.VECTORS) { + await queryRunner.query(`SET vectors.pgvector_compatibility=on`); + } + await queryRunner.query(`SET search_path TO "$user", public, vectors`); + await queryRunner.query(` CREATE INDEX IF NOT EXISTS clip_index ON smart_search - USING vectors (embedding cosine_ops) WITH (options = $$ - [indexing.hnsw] - m = 16 - ef_construction = 300 - $$);`); + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts index 0ac7b0cd4c..066303530a 100644 --- a/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts +++ b/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts @@ -1,16 +1,20 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; +import { vectorExt } from '../database.config'; +import { DatabaseExtension } from '@app/domain/repositories/database.repository'; export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface { name = 'AddFaceEmbeddingIndex1700714033632'; public async up(queryRunner: QueryRunner): Promise { + if (vectorExt === DatabaseExtension.VECTORS) { + await queryRunner.query(`SET vectors.pgvector_compatibility=on`); + } + await queryRunner.query(`SET search_path TO "$user", public, vectors`); + await queryRunner.query(` CREATE INDEX IF NOT EXISTS face_index ON asset_faces - USING vectors (embedding cosine_ops) WITH (options = $$ - [indexing.hnsw] - m = 16 - ef_construction = 300 - $$);`); + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts b/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts new file mode 100644 index 0000000000..e83e4b4fb0 --- /dev/null +++ b/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddVectorsToSearchPath1707000751533 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const res = await queryRunner.query(`SELECT current_database() as db`); + const databaseName = res[0]['db']; + await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public, vectors`); + } + + public async down(queryRunner: QueryRunner): Promise { + const databaseName = await queryRunner.query(`SELECT current_database()`); + await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public`); + } +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index cb6469195e..fea1985ced 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -1,4 +1,4 @@ -import { IAccessRepository, chunks, setUnion } from '@app/domain'; +import { IAccessRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, In, Repository } from 'typeorm'; import { @@ -12,7 +12,8 @@ import { SharedLinkEntity, UserTokenEntity, } from '../entities'; -import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util'; +import { DummyValue, GenerateSql } from '../infra.util'; +import { ChunkedSet } from '../infra.utils'; type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; @@ -30,72 +31,63 @@ class ActivityAccess implements IActivityAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, activityIds: Set): Promise> { if (activityIds.size === 0) { return new Set(); } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - userId, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); + return this.activityRepository + .find({ + select: { id: true }, + where: { + id: In([...activityIds]), + userId, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkAlbumOwnerAccess(userId: string, activityIds: Set): Promise> { if (activityIds.size === 0) { return new Set(); } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - album: { - ownerId: userId, - }, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); + return this.activityRepository + .find({ + select: { id: true }, + where: { + id: In([...activityIds]), + album: { + ownerId: userId, + }, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkCreateAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .select('album.id') - .leftJoin('album.sharedUsers', 'sharedUsers') - .where('album.id IN (:...albumIds)', { albumIds: idChunk }) - .andWhere('album.isActivityEnabled = true') - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getMany() - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .createQueryBuilder('album') + .select('album.id') + .leftJoin('album.sharedUsers', 'sharedUsers') + .where('album.id IN (:...albumIds)', { albumIds: [...albumIds] }) + .andWhere('album.isActivityEnabled = true') + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .getMany() + .then((albums) => new Set(albums.map((album) => album.id))); } } @@ -106,71 +98,61 @@ class AlbumAccess implements IAlbumAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + ownerId: userId, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedAlbumAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - sharedUsers: { - id: userId, - }, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + sharedUsers: { + id: userId, + }, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.sharedLinkRepository - .find({ - select: { albumId: true }, - where: { - id: sharedLinkId, - albumId: In(idChunk), - }, - }) - .then( - (sharedLinks) => - new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), - ), - ), - ).then((results) => setUnion(...results)); + return this.sharedLinkRepository + .find({ + select: { albumId: true }, + where: { + id: sharedLinkId, + albumId: In([...albumIds]), + }, + }) + .then( + (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), + ); } } @@ -183,132 +165,120 @@ class AssetAccess implements IAssetAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkAlbumAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .innerJoin('album.assets', 'asset') - .leftJoin('album.sharedUsers', 'sharedUsers') - .select('asset.id', 'assetId') - .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') - .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { - assetIds: idChunk, - }) - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); - } - if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { - allowedIds.add(row.livePhotoVideoId); - } - } - return allowedIds; - }), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .createQueryBuilder('album') + .innerJoin('album.assets', 'asset') + .leftJoin('album.sharedUsers', 'sharedUsers') + .select('asset.id', 'assetId') + .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') + .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { + assetIds: [...assetIds], + }) + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { + allowedIds.add(row.livePhotoVideoId); + } + } + return allowedIds; + }); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - withDeleted: true, - }) - .then((assets) => new Set(assets.map((asset) => asset.id))), - ), - ).then((results) => setUnion(...results)); + return this.assetRepository + .find({ + select: { id: true }, + where: { + id: In([...assetIds]), + ownerId: userId, + }, + withDeleted: true, + }) + .then((assets) => new Set(assets.map((asset) => asset.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .innerJoin('partner.sharedBy', 'sharedBy') - .innerJoin('sharedBy.assets', 'asset') - .select('asset.id', 'assetId') - .where('partner.sharedWithId = :userId', { userId }) - .andWhere('asset.id IN (:...assetIds)', { assetIds: idChunk }) - .getRawMany() - .then((rows) => new Set(rows.map((row) => row.assetId))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .innerJoin('partner.sharedBy', 'sharedBy') + .innerJoin('sharedBy.assets', 'asset') + .select('asset.id', 'assetId') + .where('partner.sharedWithId = :userId', { userId }) + .andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] }) + .getRawMany() + .then((rows) => new Set(rows.map((row) => row.assetId))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.sharedLinkRepository - .createQueryBuilder('sharedLink') - .leftJoin('sharedLink.album', 'album') - .leftJoin('sharedLink.assets', 'assets') - .leftJoin('album.assets', 'albumAssets') - .select('assets.id', 'assetId') - .addSelect('albumAssets.id', 'albumAssetId') - .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') - .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') - .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) - .andWhere( - 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', - { - assetIds: idChunk, - }, - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); - } - if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { - allowedIds.add(row.assetLivePhotoVideoId); - } - if (row.albumAssetId && assetIds.has(row.albumAssetId)) { - allowedIds.add(row.albumAssetId); - } - if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { - allowedIds.add(row.albumAssetLivePhotoVideoId); - } - } - return allowedIds; - }), - ), - ).then((results) => setUnion(...results)); + return this.sharedLinkRepository + .createQueryBuilder('sharedLink') + .leftJoin('sharedLink.album', 'album') + .leftJoin('sharedLink.assets', 'assets') + .leftJoin('album.assets', 'albumAssets') + .select('assets.id', 'assetId') + .addSelect('albumAssets.id', 'albumAssetId') + .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') + .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') + .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) + .andWhere( + 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', + { + assetIds: [...assetIds], + }, + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { + allowedIds.add(row.assetLivePhotoVideoId); + } + if (row.albumAssetId && assetIds.has(row.albumAssetId)) { + allowedIds.add(row.albumAssetId); + } + if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { + allowedIds.add(row.albumAssetLivePhotoVideoId); + } + } + return allowedIds; + }); } } @@ -316,24 +286,21 @@ class AuthDeviceAccess implements IAuthDeviceAccess { constructor(private tokenRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, deviceIds: Set): Promise> { if (deviceIds.size === 0) { return new Set(); } - return Promise.all( - chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.tokenRepository - .find({ - select: { id: true }, - where: { - userId, - id: In(idChunk), - }, - }) - .then((tokens) => new Set(tokens.map((token) => token.id))), - ), - ).then((results) => setUnion(...results)); + return this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In([...deviceIds]), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))); } } @@ -344,43 +311,37 @@ class LibraryAccess implements ILibraryAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, libraryIds: Set): Promise> { if (libraryIds.size === 0) { return new Set(); } - return Promise.all( - chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.libraryRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((libraries) => new Set(libraries.map((library) => library.id))), - ), - ).then((results) => setUnion(...results)); + return this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In([...libraryIds]), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } @@ -388,22 +349,19 @@ class TimelineAccess implements ITimelineAccess { constructor(private partnerRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } @@ -414,47 +372,41 @@ class PersonAccess implements IPersonAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, personIds: Set): Promise> { if (personIds.size === 0) { return new Set(); } - return Promise.all( - chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.personRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((persons) => new Set(persons.map((person) => person.id))), - ), - ).then((results) => setUnion(...results)); + return this.personRepository + .find({ + select: { id: true }, + where: { + id: In([...personIds]), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkFaceOwnerAccess(userId: string, assetFaceIds: Set): Promise> { if (assetFaceIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetFaceRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - asset: { - ownerId: userId, - }, - }, - }) - .then((faces) => new Set(faces.map((face) => face.id))), - ), - ).then((results) => setUnion(...results)); + return this.assetFaceRepository + .find({ + select: { id: true }, + where: { + id: In([...assetFaceIds]), + asset: { + ownerId: userId, + }, + }, + }) + .then((faces) => new Set(faces.map((face) => face.id))); } } @@ -462,22 +414,19 @@ class PartnerAccess implements IPartnerAccess { constructor(private partnerRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkUpdateAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } diff --git a/server/src/infra/repositories/api-key.repository.ts b/server/src/infra/repositories/api-key.repository.ts index 71226a5376..b7ebc303dc 100644 --- a/server/src/infra/repositories/api-key.repository.ts +++ b/server/src/infra/repositories/api-key.repository.ts @@ -42,7 +42,7 @@ export class ApiKeyRepository implements IKeyRepository { return this.repository.findOne({ where: { userId, id } }); } - @GenerateSql({ params: [DummyValue.STRING] }) + @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string): Promise { return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 95a227b693..215d280f4c 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -12,6 +12,7 @@ import { MetadataSearchOptions, MonthDay, Paginated, + PaginationMode, PaginationOptions, SearchExploreItem, TimeBucketItem, @@ -22,26 +23,21 @@ import { } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import _ from 'lodash'; import { DateTime } from 'luxon'; import path from 'node:path'; import { - And, Brackets, FindOptionsRelations, FindOptionsSelect, FindOptionsWhere, In, IsNull, - LessThan, Not, Repository, } from 'typeorm'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; -import { Chunked, ChunkedArray, OptionalBetween, paginate } from '../infra.utils'; - -const DEFAULT_SEARCH_SIZE = 250; +import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; const truncateMap: Record = { [TimeBucketSize.DAY]: 'day', @@ -70,142 +66,6 @@ export class AssetRepository implements IAssetRepository { await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] }); } - search(options: AssetSearchOptions): Promise { - const { - id, - libraryId, - deviceAssetId, - type, - checksum, - ownerId, - - isVisible, - isFavorite, - isExternal, - isReadOnly, - isOffline, - isArchived, - isMotion, - isEncoded, - - createdBefore, - createdAfter, - updatedBefore, - updatedAfter, - trashedBefore, - trashedAfter, - takenBefore, - takenAfter, - - originalFileName, - originalPath, - resizePath, - webpPath, - encodedVideoPath, - - city, - state, - country, - make, - model, - lensModel, - - withDeleted: _withDeleted, - withExif: _withExif, - withStacked, - withPeople, - withSmartInfo, - - order, - } = options; - - const withDeleted = _withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); - - const page = Math.max(options.page || 1, 1); - const size = Math.min(options.size || DEFAULT_SEARCH_SIZE, DEFAULT_SEARCH_SIZE); - - const exifWhere = _.omitBy( - { - city, - state, - country, - make, - model, - lensModel, - }, - _.isUndefined, - ); - - const withExif = Object.keys(exifWhere).length > 0 || _withExif; - - const where: FindOptionsWhere = _.omitBy( - { - ownerId, - id, - libraryId, - deviceAssetId, - type, - checksum, - isVisible, - isFavorite, - isExternal, - isReadOnly, - isOffline, - isArchived, - livePhotoVideoId: isMotion && Not(IsNull()), - originalFileName, - originalPath, - resizePath, - webpPath, - encodedVideoPath: encodedVideoPath ?? (isEncoded && Not(IsNull())), - createdAt: OptionalBetween(createdAfter, createdBefore), - updatedAt: OptionalBetween(updatedAfter, updatedBefore), - deletedAt: OptionalBetween(trashedAfter, trashedBefore), - fileCreatedAt: OptionalBetween(takenAfter, takenBefore), - exifInfo: Object.keys(exifWhere).length > 0 ? exifWhere : undefined, - }, - _.isUndefined, - ); - - const builder = this.repository.createQueryBuilder('asset'); - - if (withExif) { - if (_withExif) { - builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo'); - } else { - builder.leftJoin('asset.exifInfo', 'exifInfo'); - } - } - - if (withPeople) { - builder.leftJoinAndSelect('asset.faces', 'faces'); - builder.leftJoinAndSelect('faces.person', 'person'); - } - - if (withSmartInfo) { - builder.leftJoinAndSelect('asset.smartInfo', 'smartInfo'); - } - - if (withDeleted) { - builder.withDeleted(); - } - - builder.where(where); - - if (withStacked) { - builder - .leftJoinAndSelect('asset.stack', 'stack') - .leftJoinAndSelect('stack.assets', 'stackedAssets') - .andWhere(new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL'))); - } - - return builder - .skip(size * (page - 1)) - .take(size) - .orderBy('asset.fileCreatedAt', order ?? 'DESC') - .getMany(); - } - create(asset: AssetCreate): Promise { return this.repository.save(asset); } @@ -316,17 +176,7 @@ export class AssetRepository implements IAssetRepository { } getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated { - return paginate(this.repository, pagination, { - where: { - ownerId: userId, - isVisible: options.isVisible, - deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined, - }, - relations: { - exifInfo: true, - }, - withDeleted: !!options.trashedBefore, - }); + return this.getAll(pagination, { ...options, id: userId }); } @GenerateSql({ params: [[DummyValue.UUID]] }) @@ -345,24 +195,13 @@ export class AssetRepository implements IAssetRepository { } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { - return paginate(this.repository, pagination, { - where: { - isVisible: options.isVisible, - type: options.type, - deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined, - }, - relations: { - exifInfo: options.withExif !== false, - smartInfo: options.withSmartInfo !== false, - tags: options.withSmartInfo !== false, - faces: options.withFaces !== false, - smartSearch: options.withSmartInfo === true, - }, - withDeleted: options.withDeleted ?? !!options.trashedBefore, - order: { - // Ensures correct order when paginating - createdAt: options.order ?? 'ASC', - }, + let builder = this.repository.createQueryBuilder('asset'); + builder = searchAssetBuilder(builder, options); + builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); + return paginatedBuilder(builder, { + mode: PaginationMode.SKIP_TAKE, + skip: pagination.skip, + take: pagination.take, }); } @@ -435,7 +274,7 @@ export class AssetRepository implements IAssetRepository { await this.repository.remove(asset); } - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] }) + @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) getByChecksum(userId: string, checksum: Buffer): Promise { return this.repository.findOne({ where: { ownerId: userId, checksum } }); } diff --git a/server/src/infra/repositories/database.repository.ts b/server/src/infra/repositories/database.repository.ts index af595057e2..b0e4623af5 100644 --- a/server/src/infra/repositories/database.repository.ts +++ b/server/src/infra/repositories/database.repository.ts @@ -1,21 +1,60 @@ -import { DatabaseExtension, DatabaseLock, IDatabaseRepository, Version } from '@app/domain'; +import { + DatabaseExtension, + DatabaseLock, + IDatabaseRepository, + VectorExtension, + VectorIndex, + VectorUpdateResult, + Version, + VersionType, + extName, +} from '@app/domain'; +import { vectorExt } from '@app/infra/database.config'; import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import AsyncLock from 'async-lock'; -import { DataSource, QueryRunner } from 'typeorm'; +import { DataSource, EntityManager, QueryRunner } from 'typeorm'; +import { isValidInteger } from '../infra.utils'; +import { ImmichLogger } from '../logger'; @Injectable() export class DatabaseRepository implements IDatabaseRepository { + private logger = new ImmichLogger(DatabaseRepository.name); readonly asyncLock = new AsyncLock(); constructor(@InjectDataSource() private dataSource: DataSource) {} async getExtensionVersion(extension: DatabaseExtension): Promise { const res = await this.dataSource.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extension]); - const version = res[0]?.['extversion']; + const extVersion = res[0]?.['extversion']; + if (extVersion == null) { + return null; + } + + const version = Version.fromString(extVersion); + if (version.isEqual(new Version(0, 1, 1))) { + return new Version(0, 1, 11); + } + + return version; + } + + async getAvailableExtensionVersion(extension: DatabaseExtension): Promise { + const res = await this.dataSource.query( + ` + SELECT version FROM pg_available_extension_versions + WHERE name = $1 AND installed = false + ORDER BY version DESC`, + [extension], + ); + const version = res[0]?.['version']; return version == null ? null : Version.fromString(version); } + getPreferredVectorExtension(): VectorExtension { + return vectorExt; + } + async getPostgresVersion(): Promise { const res = await this.dataSource.query(`SHOW server_version`); return Version.fromString(res[0]['server_version']); @@ -25,6 +64,129 @@ export class DatabaseRepository implements IDatabaseRepository { await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`); } + async updateExtension(extension: DatabaseExtension, version?: Version): Promise { + await this.dataSource.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`); + } + + async updateVectorExtension(extension: VectorExtension, version?: Version): Promise { + const curVersion = await this.getExtensionVersion(extension); + if (!curVersion) { + throw new Error(`${extName[extension]} extension is not installed`); + } + + const minorOrMajor = version && curVersion.isOlderThan(version) >= VersionType.MINOR; + const isVectors = extension === DatabaseExtension.VECTORS; + let restartRequired = false; + await this.dataSource.manager.transaction(async (manager) => { + await this.setSearchPath(manager); + if (minorOrMajor && isVectors) { + await this.updateVectorsSchema(manager, curVersion); + } + + await manager.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`); + + if (!minorOrMajor) { + return; + } + + if (isVectors) { + await manager.query('SELECT pgvectors_upgrade()'); + restartRequired = true; + } else { + await this.reindex(VectorIndex.CLIP); + await this.reindex(VectorIndex.FACE); + } + }); + + return { restartRequired }; + } + + async reindex(index: VectorIndex): Promise { + try { + await this.dataSource.query(`REINDEX INDEX ${index}`); + } catch (error) { + if (vectorExt === DatabaseExtension.VECTORS) { + this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); + const table = index === VectorIndex.CLIP ? 'smart_search' : 'asset_faces'; + const dimSize = await this.getDimSize(table); + await this.dataSource.manager.transaction(async (manager) => { + await this.setSearchPath(manager); + await manager.query(`DROP INDEX IF EXISTS ${index}`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE real[]`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); + await manager.query(`SET vectors.pgvector_compatibility=on`); + await manager.query(` + CREATE INDEX IF NOT EXISTS ${index} ON ${table} + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`); + }); + } else { + throw error; + } + } + } + + async shouldReindex(name: VectorIndex): Promise { + if (vectorExt !== DatabaseExtension.VECTORS) { + return false; + } + + try { + const res = await this.dataSource.query( + ` + SELECT idx_status + FROM pg_vector_index_stat + WHERE indexname = $1`, + [name], + ); + return res[0]?.['idx_status'] === 'UPGRADE'; + } catch (error) { + const message: string = (error as any).message; + if (message.includes('index is not existing')) { + return true; + } else if (message.includes('relation "pg_vector_index_stat" does not exist')) { + return false; + } + throw error; + } + } + + private async setSearchPath(manager: EntityManager): Promise { + await manager.query(`SET search_path TO "$user", public, vectors`); + } + + private async updateVectorsSchema(manager: EntityManager, curVersion: Version): Promise { + await manager.query('CREATE SCHEMA IF NOT EXISTS vectors'); + await manager.query(`UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`, [ + curVersion.toString(), + DatabaseExtension.VECTORS, + ]); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [ + DatabaseExtension.VECTORS, + ]); + await manager.query('ALTER EXTENSION vectors SET SCHEMA vectors'); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = $1', [ + DatabaseExtension.VECTORS, + ]); + } + + private async getDimSize(table: string, column = 'embedding'): Promise { + const res = await this.dataSource.query(` + SELECT atttypmod as dimsize + FROM pg_attribute f + JOIN pg_class c ON c.oid = f.attrelid + WHERE c.relkind = 'r'::char + AND f.attnum > 0 + AND c.relname = '${table}' + AND f.attname = '${column}'`); + + const dimSize = res[0]['dimsize']; + if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { + throw new Error(`Could not retrieve dimension size`); + } + return dimSize; + } + async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise { await this.dataSource.runMigrations(options); } diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts index 2ae18432b2..ed009da76f 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/infra/repositories/filesystem.provider.ts @@ -2,17 +2,17 @@ import { CrawlOptionsDto, DiskUsage, ImmichReadStream, - ImmichWatcher, ImmichZipStream, IStorageRepository, mimeTypes, + WatchEvents, } from '@app/domain'; import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; import chokidar, { WatchOptions } from 'chokidar'; import { glob } from 'glob'; import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; -import fs, { copyFile, readdir, rename, writeFile } from 'node:fs/promises'; +import fs, { copyFile, readdir, rename, utimes, writeFile } from 'node:fs/promises'; import path from 'node:path'; export class FilesystemProvider implements IStorageRepository { @@ -56,6 +56,8 @@ export class FilesystemProvider implements IStorageRepository { copyFile = copyFile; + utimes = utimes; + async checkFileExists(filepath: string, mode = constants.F_OK): Promise { try { await fs.access(filepath, mode); @@ -134,8 +136,15 @@ export class FilesystemProvider implements IStorageRepository { }); } - watch(paths: string[], options: WatchOptions): ImmichWatcher { - return chokidar.watch(paths, options); + watch(paths: string[], options: WatchOptions, events: Partial) { + const watcher = chokidar.watch(paths, options); + + watcher.on('ready', () => events.onReady?.()); + watcher.on('add', (path) => events.onAdd?.(path)); + watcher.on('change', (path) => events.onChange?.(path)); + watcher.on('unlink', (path) => events.onUnlink?.(path)); + + return () => watcher.close(); } readdir = readdir; diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 21703ec8c8..d684f6b004 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -17,9 +17,9 @@ export * from './metadata.repository'; export * from './move.repository'; export * from './partner.repository'; export * from './person.repository'; +export * from './search.repository'; export * from './server-info.repository'; export * from './shared-link.repository'; -export * from './smart-info.repository'; export * from './system-config.repository'; export * from './system-metadata.repository'; export * from './tag.repository'; diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 83c05597a2..6a90ad1081 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -10,7 +10,13 @@ import { ISystemMetadataRepository, ReverseGeocodeResult, } from '@app/domain'; -import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; +import { + ExifEntity, + GeodataAdmin1Entity, + GeodataAdmin2Entity, + GeodataPlacesEntity, + SystemMetadataKey, +} from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Inject } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; @@ -21,12 +27,14 @@ import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import * as readLine from 'node:readline'; import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm'; +import { DummyValue, GenerateSql } from '../infra.util'; type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity; type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity; export class MetadataRepository implements IMetadataRepository { constructor( + @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository, @InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository, @InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository, @@ -213,4 +221,106 @@ export class MetadataRepository implements IMetadataRepository { this.logger.warn(`Error writing exif data (${path}): ${error}`); } } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getCountries(userId: string): Promise { + const entity = await this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId = :userId', { userId }) + .andWhere('exif.country IS NOT NULL') + .select('exif.country') + .distinctOn(['exif.country']) + .getMany(); + + return entity.map((e) => e.country ?? '').filter((c) => c !== ''); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getStates(userId: string, country: string | undefined): Promise { + let result: ExifEntity[] = []; + + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId = :userId', { userId }) + .andWhere('exif.state IS NOT NULL') + .select('exif.state') + .distinctOn(['exif.state']); + + if (country) { + query.andWhere('exif.country = :country', { country }); + } + + result = await query.getMany(); + + return result.map((entity) => entity.state ?? '').filter((s) => s !== ''); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] }) + async getCities(userId: string, country: string | undefined, state: string | undefined): Promise { + let result: ExifEntity[] = []; + + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId = :userId', { userId }) + .andWhere('exif.city IS NOT NULL') + .select('exif.city') + .distinctOn(['exif.city']); + + if (country) { + query.andWhere('exif.country = :country', { country }); + } + + if (state) { + query.andWhere('exif.state = :state', { state }); + } + + result = await query.getMany(); + + return result.map((entity) => entity.city ?? '').filter((c) => c !== ''); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getCameraMakes(userId: string, model: string | undefined): Promise { + let result: ExifEntity[] = []; + + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId = :userId', { userId }) + .andWhere('exif.make IS NOT NULL') + .select('exif.make') + .distinctOn(['exif.make']); + + if (model) { + query.andWhere('exif.model = :model', { model }); + } + + result = await query.getMany(); + + return result.map((entity) => entity.make ?? '').filter((m) => m !== ''); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getCameraModels(userId: string, make: string | undefined): Promise { + let result: ExifEntity[] = []; + + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId = :userId', { userId }) + .andWhere('exif.model IS NOT NULL') + .select('exif.model') + .distinctOn(['exif.model']); + + if (make) { + query.andWhere('exif.make = :make', { make }); + } + + result = await query.getMany(); + + return result.map((entity) => entity.model ?? '').filter((m) => m !== ''); + } } diff --git a/server/src/infra/repositories/smart-info.repository.ts b/server/src/infra/repositories/search.repository.ts similarity index 56% rename from server/src/infra/repositories/smart-info.repository.ts rename to server/src/infra/repositories/search.repository.ts index ab43ff6f91..7d0421b05d 100644 --- a/server/src/infra/repositories/smart-info.repository.ts +++ b/server/src/infra/repositories/search.repository.ts @@ -1,16 +1,29 @@ -import { Embedding, EmbeddingSearch, FaceEmbeddingSearch, FaceSearchResult, ISmartInfoRepository } from '@app/domain'; +import { + AssetSearchOptions, + DatabaseExtension, + Embedding, + FaceEmbeddingSearch, + FaceSearchResult, + ISearchRepository, + Paginated, + PaginationMode, + PaginationResult, + SearchPaginationOptions, + SmartSearchOptions, +} from '@app/domain'; import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { vectorExt } from '../database.config'; import { DummyValue, GenerateSql } from '../infra.util'; -import { asVector, isValidInteger } from '../infra.utils'; +import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; @Injectable() -export class SmartInfoRepository implements ISmartInfoRepository { - private logger = new ImmichLogger(SmartInfoRepository.name); +export class SearchRepository implements ISearchRepository { + private logger = new ImmichLogger(SearchRepository.name); private faceColumns: string[]; constructor( @@ -27,50 +40,74 @@ export class SmartInfoRepository implements ISmartInfoRepository { async init(modelName: string): Promise { const { dimSize } = getCLIPModelInfo(modelName); - if (dimSize == null) { - throw new Error(`Invalid CLIP model name: ${modelName}`); - } + const curDimSize = await this.getDimSize(); + this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`); - const currentDimSize = await this.getDimSize(); - this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`); - - if (dimSize != currentDimSize) { - this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`); + if (dimSize != curDimSize) { + this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`); await this.updateDimSize(dimSize); } } @GenerateSql({ - params: [{ userIds: [DummyValue.UUID], embedding: Array.from({ length: 512 }, Math.random), numResults: 100 }], + params: [ + { page: 1, size: 100 }, + { + takenAfter: DummyValue.DATE, + lensModel: DummyValue.STRING, + ownerId: DummyValue.UUID, + withStacked: true, + isFavorite: true, + }, + ], }) - async searchCLIP({ userIds, embedding, numResults, withArchived }: EmbeddingSearch): Promise { - let results: AssetEntity[] = []; + async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { + let builder = this.assetRepository.createQueryBuilder('asset'); + builder = searchAssetBuilder(builder, options); + + builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC'); + + return paginatedBuilder(builder, { + mode: PaginationMode.SKIP_TAKE, + skip: (pagination.page - 1) * pagination.size, + take: pagination.size, + }); + } + + @GenerateSql({ + params: [ + { page: 1, size: 100 }, + { + takenAfter: DummyValue.DATE, + embedding: Array.from({ length: 512 }, Math.random), + lensModel: DummyValue.STRING, + withStacked: true, + isFavorite: true, + userIds: [DummyValue.UUID], + }, + ], + }) + async searchSmart( + pagination: SearchPaginationOptions, + { embedding, userIds, ...options }: SmartSearchOptions, + ): Paginated { + let results: PaginationResult = { items: [], hasNextPage: false }; + await this.assetRepository.manager.transaction(async (manager) => { - await manager.query(`SET LOCAL vectors.enable_prefilter = on`); - - let query = manager - .createQueryBuilder(AssetEntity, 'a') - .innerJoin('a.smartSearch', 's') - .leftJoinAndSelect('a.exifInfo', 'e') - .where('a.ownerId IN (:...userIds )') - - .orderBy('s.embedding <=> :embedding') + let builder = manager.createQueryBuilder(AssetEntity, 'asset'); + builder = searchAssetBuilder(builder, options); + builder + .innerJoin('asset.smartSearch', 'search') + .andWhere('asset.ownerId IN (:...userIds )') + .orderBy('search.embedding <=> :embedding') .setParameters({ userIds, embedding: asVector(embedding) }); - if (!withArchived) { - query.andWhere('a.isArchived = false'); - } - query.andWhere('a.isVisible = true').andWhere('a.fileCreatedAt < NOW()'); - - if (numResults) { - if (!isValidInteger(numResults, { min: 1 })) { - throw new Error(`Invalid value for 'numResults': ${numResults}`); - } - query = query.limit(numResults); - await manager.query(`SET LOCAL vectors.k = '${numResults}'`); - } - - results = await query.getMany(); + await manager.query(this.getRuntimeConfig(pagination.size)); + results = await paginatedBuilder(builder, { + mode: PaginationMode.LIMIT_OFFSET, + skip: (pagination.page - 1) * pagination.size, + take: pagination.size, + }); }); return results; @@ -93,36 +130,34 @@ export class SmartInfoRepository implements ISmartInfoRepository { maxDistance, hasPerson, }: FaceEmbeddingSearch): Promise { + if (!isValidInteger(numResults, { min: 1 })) { + throw new Error(`Invalid value for 'numResults': ${numResults}`); + } + + // setting this too low messes with prefilter recall + numResults = Math.max(numResults, 64); + let results: Array = []; await this.assetRepository.manager.transaction(async (manager) => { - await manager.query(`SET LOCAL vectors.enable_prefilter = on`); - let cte = manager + const cte = manager .createQueryBuilder(AssetFaceEntity, 'faces') - .select('1 + (faces.embedding <=> :embedding)', 'distance') + .select('faces.embedding <=> :embedding', 'distance') .innerJoin('faces.asset', 'asset') .where('asset.ownerId IN (:...userIds )') - .orderBy('1 + (faces.embedding <=> :embedding)') + .orderBy('faces.embedding <=> :embedding') .setParameters({ userIds, embedding: asVector(embedding) }); - if (numResults) { - if (!isValidInteger(numResults, { min: 1 })) { - throw new Error(`Invalid value for 'numResults': ${numResults}`); - } - cte = cte.limit(numResults); - if (numResults > 64) { - // setting k too low messes with prefilter recall - await manager.query(`SET LOCAL vectors.k = '${numResults}'`); - } - } + cte.limit(numResults); if (hasPerson) { - cte = cte.andWhere('faces."personId" IS NOT NULL'); + cte.andWhere('faces."personId" IS NOT NULL'); } for (const col of this.faceColumns) { cte.addSelect(`faces.${col}`, col); } + await manager.query(this.getRuntimeConfig(numResults)); results = await manager .createQueryBuilder() .select('res.*') @@ -131,7 +166,6 @@ export class SmartInfoRepository implements ISmartInfoRepository { .where('res.distance <= :maxDistance', { maxDistance }) .getRawMany(); }); - return results.map((row) => ({ face: this.assetFaceRepository.create(row), distance: row.distance, @@ -159,8 +193,8 @@ export class SmartInfoRepository implements ISmartInfoRepository { throw new Error(`Invalid CLIP dimension size: ${dimSize}`); } - const currentDimSize = await this.getDimSize(); - if (currentDimSize === dimSize) { + const curDimSize = await this.getDimSize(); + if (curDimSize === dimSize) { return; } @@ -176,14 +210,14 @@ export class SmartInfoRepository implements ISmartInfoRepository { await manager.query(` CREATE INDEX clip_index ON smart_search - USING vectors (embedding cosine_ops) WITH (options = $$ + USING vectors (embedding vector_cos_ops) WITH (options = $$ [indexing.hnsw] m = 16 ef_construction = 300 $$)`); }); - this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`); + this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); } private async getDimSize(): Promise { @@ -202,4 +236,17 @@ export class SmartInfoRepository implements ISmartInfoRepository { } return dimSize; } + + private getRuntimeConfig(numResults?: number): string { + if (vectorExt === DatabaseExtension.VECTOR) { + return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall + } + + let runtimeConfig = 'SET LOCAL vectors.enable_prefilter=on; SET LOCAL vectors.search_mode=vbase;'; + if (numResults) { + runtimeConfig += ` SET LOCAL vectors.hnsw_ef_search = ${numResults};`; + } + + return runtimeConfig; + } } diff --git a/server/src/infra/repositories/server-info.repository.ts b/server/src/infra/repositories/server-info.repository.ts index 61fa448f97..8c917cb63c 100644 --- a/server/src/infra/repositories/server-info.repository.ts +++ b/server/src/infra/repositories/server-info.repository.ts @@ -1,12 +1,19 @@ import { GitHubRelease, IServerInfoRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; -import axios from 'axios'; @Injectable() export class ServerInfoRepository implements IServerInfoRepository { - getGitHubRelease(): Promise { - return axios - .get('https://api.github.com/repos/immich-app/immich/releases/latest') - .then((response) => response.data); + async getGitHubRelease(): Promise { + try { + const response = await fetch('https://api.github.com/repos/immich-app/immich/releases/latest'); + + if (!response.ok) { + throw new Error(`GitHub API request failed with status ${response.status}: ${await response.text()}`); + } + + return response.json(); + } catch (error) { + throw new Error(`Failed to fetch GitHub release: ${error}`); + } } } diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts index 82d0b8c8be..1e9242e479 100644 --- a/server/src/infra/repositories/system-config.repository.ts +++ b/server/src/infra/repositories/system-config.repository.ts @@ -1,6 +1,5 @@ import { ISystemConfigRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; -import axios from 'axios'; import { readFile } from 'node:fs/promises'; import { In, Repository } from 'typeorm'; import { SystemConfigEntity } from '../entities'; @@ -13,7 +12,17 @@ export class SystemConfigRepository implements ISystemConfigRepository { private repository: Repository, ) {} async fetchStyle(url: string) { - return axios.get(url).then((response) => response.data); + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`); + } + + return response.json(); + } catch (error) { + throw new Error(`Failed to fetch data from ${url}: ${error}`); + } } @GenerateSql() diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts index 348762d957..0b10c018c1 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/infra/sql-generator/index.ts @@ -19,8 +19,8 @@ import { MoveRepository, PartnerRepository, PersonRepository, + SearchRepository, SharedLinkRepository, - SmartInfoRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -41,7 +41,7 @@ const repositories = [ PartnerRepository, PersonRepository, SharedLinkRepository, - SmartInfoRepository, + SearchRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -142,7 +142,7 @@ class SqlGenerator { this.sqlLogger.clear(); // errors still generate sql, which is all we care about - await target.apply(instance, params).catch(() => null); + await target.apply(instance, params).catch((error: Error) => console.error(`${queryLabel} error: ${error}`)); if (this.sqlLogger.queries.length === 0) { console.warn(`No queries recorded for ${queryLabel}`); diff --git a/server/src/infra/sql/access.repository.sql b/server/src/infra/sql/access.repository.sql index f2ed3f9a38..638be9f90b 100644 --- a/server/src/infra/sql/access.repository.sql +++ b/server/src/infra/sql/access.repository.sql @@ -7,8 +7,8 @@ FROM "activity" "ActivityEntity" WHERE ( - "ActivityEntity"."id" IN ($1) - AND "ActivityEntity"."userId" = $2 + ("ActivityEntity"."id" IN ($1)) + AND ("ActivityEntity"."userId" = $2) ) -- AccessRepository.activity.checkAlbumOwnerAccess @@ -22,8 +22,14 @@ FROM ) WHERE ( - "ActivityEntity"."id" IN ($1) - AND "ActivityEntity__ActivityEntity_album"."ownerId" = $2 + ("ActivityEntity"."id" IN ($1)) + AND ( + ( + ( + "ActivityEntity__ActivityEntity_album"."ownerId" = $2 + ) + ) + ) ) -- AccessRepository.activity.checkCreateAccess @@ -53,8 +59,8 @@ FROM WHERE ( ( - "AlbumEntity"."id" IN ($1) - AND "AlbumEntity"."ownerId" = $2 + ("AlbumEntity"."id" IN ($1)) + AND ("AlbumEntity"."ownerId" = $2) ) ) AND ("AlbumEntity"."deletedAt" IS NULL) @@ -72,8 +78,12 @@ FROM WHERE ( ( - "AlbumEntity"."id" IN ($1) - AND "AlbumEntity__AlbumEntity_sharedUsers"."id" = $2 + ("AlbumEntity"."id" IN ($1)) + AND ( + ( + ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $2) + ) + ) ) ) AND ("AlbumEntity"."deletedAt" IS NULL) @@ -86,8 +96,8 @@ FROM "shared_links" "SharedLinkEntity" WHERE ( - "SharedLinkEntity"."id" = $1 - AND "SharedLinkEntity"."albumId" IN ($2) + ("SharedLinkEntity"."id" = $1) + AND ("SharedLinkEntity"."albumId" IN ($2)) ) -- AccessRepository.asset.checkAlbumAccess @@ -119,8 +129,8 @@ FROM "assets" "AssetEntity" WHERE ( - "AssetEntity"."id" IN ($1) - AND "AssetEntity"."ownerId" = $2 + ("AssetEntity"."id" IN ($1)) + AND ("AssetEntity"."ownerId" = $2) ) -- AccessRepository.asset.checkPartnerAccess @@ -168,8 +178,8 @@ FROM "user_token" "UserTokenEntity" WHERE ( - "UserTokenEntity"."userId" = $1 - AND "UserTokenEntity"."id" IN ($2) + ("UserTokenEntity"."userId" = $1) + AND ("UserTokenEntity"."id" IN ($2)) ) -- AccessRepository.library.checkOwnerAccess @@ -180,8 +190,8 @@ FROM WHERE ( ( - "LibraryEntity"."id" IN ($1) - AND "LibraryEntity"."ownerId" = $2 + ("LibraryEntity"."id" IN ($1)) + AND ("LibraryEntity"."ownerId" = $2) ) ) AND ("LibraryEntity"."deletedAt" IS NULL) @@ -203,8 +213,8 @@ FROM "person" "PersonEntity" WHERE ( - "PersonEntity"."id" IN ($1) - AND "PersonEntity"."ownerId" = $2 + ("PersonEntity"."id" IN ($1)) + AND ("PersonEntity"."ownerId" = $2) ) -- AccessRepository.person.checkFaceOwnerAccess @@ -218,8 +228,14 @@ FROM ) WHERE ( - "AssetFaceEntity"."id" IN ($1) - AND "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" = $2 + ("AssetFaceEntity"."id" IN ($1)) + AND ( + ( + ( + "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" = $2 + ) + ) + ) ) -- AccessRepository.partner.checkUpdateAccess diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql index a5f32f57a7..5e104bc1a2 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/infra/sql/album.repository.sql @@ -72,7 +72,7 @@ FROM ) LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" WHERE - (("AlbumEntity"."id" = $1)) + ((("AlbumEntity"."id" = $1))) AND ("AlbumEntity"."deletedAt" IS NULL) ) "distinctAlias" ORDER BY @@ -135,7 +135,7 @@ FROM "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL ) WHERE - (("AlbumEntity"."id" IN ($1))) + ((("AlbumEntity"."id" IN ($1)))) AND ("AlbumEntity"."deletedAt" IS NULL) -- AlbumRepository.getByAssetId @@ -201,12 +201,20 @@ WHERE ( ( ( - "AlbumEntity"."ownerId" = $1 - AND "AlbumEntity__AlbumEntity_assets"."id" = $2 + ( + ("AlbumEntity"."ownerId" = $1) + AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2))) + ) ) OR ( - "AlbumEntity__AlbumEntity_sharedUsers"."id" = $3 - AND "AlbumEntity__AlbumEntity_assets"."id" = $4 + ( + ( + ( + ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $3) + ) + ) + AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4))) + ) ) ) ) @@ -328,7 +336,7 @@ FROM "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL ) WHERE - (("AlbumEntity"."ownerId" = $1)) + ((("AlbumEntity"."ownerId" = $1))) AND ("AlbumEntity"."deletedAt" IS NULL) ORDER BY "AlbumEntity"."createdAt" DESC @@ -403,14 +411,38 @@ FROM WHERE ( ( - ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $1) - OR ( - "AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2 + ( + ( + ( + ( + ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $1) + ) + ) + ) ) OR ( - "AlbumEntity"."ownerId" = $3 - AND NOT ( - "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL + ( + ( + ( + ( + "AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2 + ) + ) + ) + ) + ) + OR ( + ( + ("AlbumEntity"."ownerId" = $3) + AND ( + ( + ( + NOT ( + "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL + ) + ) + ) + ) ) ) ) @@ -489,9 +521,21 @@ FROM WHERE ( ( - "AlbumEntity"."ownerId" = $1 - AND "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL - AND "AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL + ("AlbumEntity"."ownerId" = $1) + AND ( + ( + ( + "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL + ) + ) + ) + AND ( + ( + ( + "AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL + ) + ) + ) ) ) AND ("AlbumEntity"."deletedAt" IS NULL) @@ -586,8 +630,8 @@ WHERE WHERE ( ( - "AlbumEntity"."id" = $1 - AND "AlbumEntity__AlbumEntity_assets"."id" = $2 + ("AlbumEntity"."id" = $1) + AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2))) ) ) AND ("AlbumEntity"."deletedAt" IS NULL) diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/infra/sql/api.key.repository.sql index 71d8022ca5..78eab13b30 100644 --- a/server/src/infra/sql/api.key.repository.sql +++ b/server/src/infra/sql/api.key.repository.sql @@ -32,7 +32,7 @@ FROM "APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL ) WHERE - ("APIKeyEntity"."key" = $1) + (("APIKeyEntity"."key" = $1)) ) "distinctAlias" ORDER BY "APIKeyEntity_id" ASC @@ -50,8 +50,8 @@ FROM "api_keys" "APIKeyEntity" WHERE ( - "APIKeyEntity"."userId" = $1 - AND "APIKeyEntity"."id" = $2 + ("APIKeyEntity"."userId" = $1) + AND ("APIKeyEntity"."id" = $2) ) LIMIT 1 @@ -66,6 +66,6 @@ SELECT FROM "api_keys" "APIKeyEntity" WHERE - ("APIKeyEntity"."userId" = $1) + (("APIKeyEntity"."userId" = $1)) ORDER BY "APIKeyEntity"."createdAt" DESC diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index a2e30c3e3e..88f17eb920 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -65,11 +65,11 @@ FROM WHERE ( ( - "AssetEntity"."ownerId" = $1 - AND "AssetEntity"."isVisible" = $2 - AND "AssetEntity"."isArchived" = $3 - AND NOT ("AssetEntity"."resizePath" IS NULL) - AND "AssetEntity"."fileCreatedAt" BETWEEN $4 AND $5 + ("AssetEntity"."ownerId" = $1) + AND ("AssetEntity"."isVisible" = $2) + AND ("AssetEntity"."isArchived" = $3) + AND (NOT ("AssetEntity"."resizePath" IS NULL)) + AND ("AssetEntity"."fileCreatedAt" BETWEEN $4 AND $5) ) ) AND ("AssetEntity"."deletedAt" IS NULL) @@ -286,7 +286,7 @@ FROM LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id" WHERE - ("AssetEntity"."id" IN ($1)) + (("AssetEntity"."id" IN ($1))) -- AssetRepository.deleteAll DELETE FROM "assets" @@ -331,7 +331,13 @@ FROM "AssetEntity__AssetEntity_library"."deletedAt" IS NULL ) WHERE - (("AssetEntity__AssetEntity_library"."id" IN ($1))) + ( + ( + ( + (("AssetEntity__AssetEntity_library"."id" IN ($1))) + ) + ) + ) AND ("AssetEntity"."deletedAt" IS NULL) -- AssetRepository.getByLibraryIdAndOriginalPath @@ -378,8 +384,8 @@ FROM WHERE ( ( - "AssetEntity__AssetEntity_library"."id" = $1 - AND "AssetEntity"."originalPath" = $2 + ((("AssetEntity__AssetEntity_library"."id" = $1))) + AND ("AssetEntity"."originalPath" = $2) ) ) AND ("AssetEntity"."deletedAt" IS NULL) @@ -397,9 +403,9 @@ FROM "assets" "AssetEntity" WHERE ( - "AssetEntity"."ownerId" = $1 - AND "AssetEntity"."deviceId" = $2 - AND "AssetEntity"."isVisible" = $3 + ("AssetEntity"."ownerId" = $1) + AND ("AssetEntity"."deviceId" = $2) + AND ("AssetEntity"."isVisible" = $3) ) -- AssetRepository.getById @@ -436,7 +442,7 @@ SELECT FROM "assets" "AssetEntity" WHERE - ("AssetEntity"."id" = $1) + (("AssetEntity"."id" = $1)) LIMIT 1 @@ -484,8 +490,8 @@ FROM WHERE ( ( - "AssetEntity"."ownerId" = $1 - AND "AssetEntity"."checksum" = $2 + ("AssetEntity"."ownerId" = $1) + AND ("AssetEntity"."checksum" = $2) ) ) AND ("AssetEntity"."deletedAt" IS NULL) @@ -529,12 +535,16 @@ WHERE ( ( ( - "AssetEntity"."sidecarPath" IS NULL - AND "AssetEntity"."isVisible" = $1 + ( + ("AssetEntity"."sidecarPath" IS NULL) + AND ("AssetEntity"."isVisible" = $1) + ) ) OR ( - "AssetEntity"."sidecarPath" = $2 - AND "AssetEntity"."isVisible" = $3 + ( + ("AssetEntity"."sidecarPath" = $2) + AND ("AssetEntity"."isVisible" = $3) + ) ) ) ) diff --git a/server/src/infra/sql/library.repository.sql b/server/src/infra/sql/library.repository.sql index 8dd7828f0b..c791b2c8a5 100644 --- a/server/src/infra/sql/library.repository.sql +++ b/server/src/infra/sql/library.repository.sql @@ -40,7 +40,7 @@ FROM "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL ) WHERE - (("LibraryEntity"."id" = $1)) + ((("LibraryEntity"."id" = $1))) AND ("LibraryEntity"."deletedAt" IS NULL) ) "distinctAlias" ORDER BY @@ -63,7 +63,7 @@ WHERE FROM "libraries" "LibraryEntity" WHERE - (("LibraryEntity"."name" = $1)) + ((("LibraryEntity"."name" = $1))) AND ("LibraryEntity"."deletedAt" IS NULL) ) LIMIT @@ -75,7 +75,7 @@ SELECT FROM "libraries" "LibraryEntity" WHERE - (("LibraryEntity"."ownerId" = $1)) + ((("LibraryEntity"."ownerId" = $1))) AND ("LibraryEntity"."deletedAt" IS NULL) -- LibraryRepository.getDefaultUploadLibrary @@ -96,8 +96,8 @@ FROM WHERE ( ( - "LibraryEntity"."ownerId" = $1 - AND "LibraryEntity"."type" = $2 + ("LibraryEntity"."ownerId" = $1) + AND ("LibraryEntity"."type" = $2) ) ) AND ("LibraryEntity"."deletedAt" IS NULL) @@ -114,8 +114,8 @@ FROM WHERE ( ( - "LibraryEntity"."ownerId" = $1 - AND "LibraryEntity"."type" = $2 + ("LibraryEntity"."ownerId" = $1) + AND ("LibraryEntity"."type" = $2) ) ) AND ("LibraryEntity"."deletedAt" IS NULL) @@ -158,8 +158,8 @@ FROM WHERE ( ( - "LibraryEntity"."ownerId" = $1 - AND "LibraryEntity"."isVisible" = $2 + ("LibraryEntity"."ownerId" = $1) + AND ("LibraryEntity"."isVisible" = $2) ) ) AND ("LibraryEntity"."deletedAt" IS NULL) @@ -240,8 +240,8 @@ FROM LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" WHERE ( - "LibraryEntity"."isVisible" = $1 - AND NOT ("LibraryEntity"."deletedAt" IS NULL) + ("LibraryEntity"."isVisible" = $1) + AND (NOT ("LibraryEntity"."deletedAt" IS NULL)) ) ORDER BY "LibraryEntity"."createdAt" ASC diff --git a/server/src/infra/sql/move.repository.sql b/server/src/infra/sql/move.repository.sql index b83175bd3f..3ce8c0ccdd 100644 --- a/server/src/infra/sql/move.repository.sql +++ b/server/src/infra/sql/move.repository.sql @@ -11,8 +11,8 @@ FROM "move_history" "MoveEntity" WHERE ( - "MoveEntity"."entityId" = $1 - AND "MoveEntity"."pathType" = $2 + ("MoveEntity"."entityId" = $1) + AND ("MoveEntity"."pathType" = $2) ) LIMIT 1 diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql index 781e68d9ae..bd4a523e86 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/infra/sql/person.repository.sql @@ -83,7 +83,7 @@ FROM "asset_faces" "AssetFaceEntity" LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId" WHERE - ("AssetFaceEntity"."assetId" = $1) + (("AssetFaceEntity"."assetId" = $1)) -- PersonRepository.getFaceById SELECT DISTINCT @@ -113,7 +113,7 @@ FROM "asset_faces" "AssetFaceEntity" LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId" WHERE - ("AssetFaceEntity"."id" = $1) + (("AssetFaceEntity"."id" = $1)) ) "distinctAlias" ORDER BY "AssetFaceEntity_id" ASC @@ -181,7 +181,7 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL ) WHERE - ("AssetFaceEntity"."id" = $1) + (("AssetFaceEntity"."id" = $1)) ) "distinctAlias" ORDER BY "AssetFaceEntity_id" ASC @@ -325,9 +325,13 @@ FROM WHERE ( ( - "AssetEntity__AssetEntity_faces"."personId" = $1 - AND "AssetEntity"."isVisible" = $2 - AND "AssetEntity"."isArchived" = $3 + ( + ( + ("AssetEntity__AssetEntity_faces"."personId" = $1) + ) + ) + AND ("AssetEntity"."isVisible" = $2) + AND ("AssetEntity"."isArchived" = $3) ) ) AND ("AssetEntity"."deletedAt" IS NULL) @@ -395,8 +399,10 @@ FROM WHERE ( ( - "AssetFaceEntity"."assetId" = $1 - AND "AssetFaceEntity"."personId" = $2 + ( + ("AssetFaceEntity"."assetId" = $1) + AND ("AssetFaceEntity"."personId" = $2) + ) ) ) @@ -414,6 +420,6 @@ SELECT FROM "asset_faces" "AssetFaceEntity" WHERE - ("AssetFaceEntity"."personId" = $1) + (("AssetFaceEntity"."personId" = $1)) LIMIT 1 diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql new file mode 100644 index 0000000000..538a854094 --- /dev/null +++ b/server/src/infra/sql/search.repository.sql @@ -0,0 +1,234 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SearchRepository.searchMetadata +SELECT DISTINCT + "distinctAlias"."asset_id" AS "ids_asset_id", + "distinctAlias"."asset_fileCreatedAt" +FROM + ( + SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."resizePath" AS "asset_resizePath", + "asset"."webpPath" AS "asset_webpPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "stack"."id" AS "stack_id", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."resizePath" AS "stackedAssets_resizePath", + "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" + FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND "asset"."ownerId" = $3 + AND 1 = 1 + AND "asset"."isFavorite" = $4 + AND ( + "stack"."primaryAssetId" = "asset"."id" + OR "asset"."stackId" IS NULL + ) + ) + AND ("asset"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "distinctAlias"."asset_fileCreatedAt" DESC, + "asset_id" ASC +LIMIT + 101 + +-- SearchRepository.searchSmart +START TRANSACTION +SET + LOCAL vectors.enable_prefilter = on; + +SET + LOCAL vectors.search_mode = vbase; + +SET + LOCAL vectors.hnsw_ef_search = 100; +SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."resizePath" AS "asset_resizePath", + "asset"."webpPath" AS "asset_webpPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "stack"."id" AS "stack_id", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."resizePath" AS "stackedAssets_resizePath", + "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" +FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + INNER JOIN "smart_search" "search" ON "search"."assetId" = "asset"."id" +WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND 1 = 1 + AND 1 = 1 + AND "asset"."isFavorite" = $3 + AND ( + "stack"."primaryAssetId" = "asset"."id" + OR "asset"."stackId" IS NULL + ) + AND "asset"."ownerId" IN ($4) + ) + AND ("asset"."deletedAt" IS NULL) +ORDER BY + "search"."embedding" <= > $5 ASC +LIMIT + 101 +COMMIT + +-- SearchRepository.searchFaces +START TRANSACTION +SET + LOCAL vectors.enable_prefilter = on; + +SET + LOCAL vectors.search_mode = vbase; + +SET + LOCAL vectors.hnsw_ef_search = 100; +WITH + "cte" AS ( + SELECT + "faces"."id" AS "id", + "faces"."assetId" AS "assetId", + "faces"."personId" AS "personId", + "faces"."imageWidth" AS "imageWidth", + "faces"."imageHeight" AS "imageHeight", + "faces"."boundingBoxX1" AS "boundingBoxX1", + "faces"."boundingBoxY1" AS "boundingBoxY1", + "faces"."boundingBoxX2" AS "boundingBoxX2", + "faces"."boundingBoxY2" AS "boundingBoxY2", + "faces"."embedding" <= > $1 AS "distance" + FROM + "asset_faces" "faces" + INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId" + AND ("asset"."deletedAt" IS NULL) + WHERE + "asset"."ownerId" IN ($2) + ORDER BY + "faces"."embedding" <= > $1 ASC + LIMIT + 100 + ) +SELECT + res.* +FROM + "cte" "res" +WHERE + res.distance <= $3 +COMMIT diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql index ee3563b8b7..5f43210685 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/infra/sql/shared.link.repository.sql @@ -184,8 +184,8 @@ FROM ) WHERE ( - "SharedLinkEntity"."id" = $1 - AND "SharedLinkEntity"."userId" = $2 + ("SharedLinkEntity"."id" = $1) + AND ("SharedLinkEntity"."userId" = $2) ) ) "distinctAlias" ORDER BY @@ -280,7 +280,7 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" IS NULL ) WHERE - ("SharedLinkEntity"."userId" = $1) + (("SharedLinkEntity"."userId" = $1)) ORDER BY "SharedLinkEntity"."createdAt" DESC @@ -325,7 +325,7 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" IS NULL ) WHERE - ("SharedLinkEntity"."key" = $1) + (("SharedLinkEntity"."key" = $1)) ) "distinctAlias" ORDER BY "SharedLinkEntity_id" ASC diff --git a/server/src/infra/sql/smart.info.repository.sql b/server/src/infra/sql/smart.info.repository.sql deleted file mode 100644 index afb120bade..0000000000 --- a/server/src/infra/sql/smart.info.repository.sql +++ /dev/null @@ -1,121 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- SmartInfoRepository.searchCLIP -START TRANSACTION -SET - LOCAL vectors.enable_prefilter = on -SET - LOCAL vectors.k = '100' -SELECT - "a"."id" AS "a_id", - "a"."deviceAssetId" AS "a_deviceAssetId", - "a"."ownerId" AS "a_ownerId", - "a"."libraryId" AS "a_libraryId", - "a"."deviceId" AS "a_deviceId", - "a"."type" AS "a_type", - "a"."originalPath" AS "a_originalPath", - "a"."resizePath" AS "a_resizePath", - "a"."webpPath" AS "a_webpPath", - "a"."thumbhash" AS "a_thumbhash", - "a"."encodedVideoPath" AS "a_encodedVideoPath", - "a"."createdAt" AS "a_createdAt", - "a"."updatedAt" AS "a_updatedAt", - "a"."deletedAt" AS "a_deletedAt", - "a"."fileCreatedAt" AS "a_fileCreatedAt", - "a"."localDateTime" AS "a_localDateTime", - "a"."fileModifiedAt" AS "a_fileModifiedAt", - "a"."isFavorite" AS "a_isFavorite", - "a"."isArchived" AS "a_isArchived", - "a"."isExternal" AS "a_isExternal", - "a"."isReadOnly" AS "a_isReadOnly", - "a"."isOffline" AS "a_isOffline", - "a"."checksum" AS "a_checksum", - "a"."duration" AS "a_duration", - "a"."isVisible" AS "a_isVisible", - "a"."livePhotoVideoId" AS "a_livePhotoVideoId", - "a"."originalFileName" AS "a_originalFileName", - "a"."sidecarPath" AS "a_sidecarPath", - "a"."stackId" AS "a_stackId", - "e"."assetId" AS "e_assetId", - "e"."description" AS "e_description", - "e"."exifImageWidth" AS "e_exifImageWidth", - "e"."exifImageHeight" AS "e_exifImageHeight", - "e"."fileSizeInByte" AS "e_fileSizeInByte", - "e"."orientation" AS "e_orientation", - "e"."dateTimeOriginal" AS "e_dateTimeOriginal", - "e"."modifyDate" AS "e_modifyDate", - "e"."timeZone" AS "e_timeZone", - "e"."latitude" AS "e_latitude", - "e"."longitude" AS "e_longitude", - "e"."projectionType" AS "e_projectionType", - "e"."city" AS "e_city", - "e"."livePhotoCID" AS "e_livePhotoCID", - "e"."autoStackId" AS "e_autoStackId", - "e"."state" AS "e_state", - "e"."country" AS "e_country", - "e"."make" AS "e_make", - "e"."model" AS "e_model", - "e"."lensModel" AS "e_lensModel", - "e"."fNumber" AS "e_fNumber", - "e"."focalLength" AS "e_focalLength", - "e"."iso" AS "e_iso", - "e"."exposureTime" AS "e_exposureTime", - "e"."profileDescription" AS "e_profileDescription", - "e"."colorspace" AS "e_colorspace", - "e"."bitsPerSample" AS "e_bitsPerSample", - "e"."fps" AS "e_fps" -FROM - "assets" "a" - INNER JOIN "smart_search" "s" ON "s"."assetId" = "a"."id" - LEFT JOIN "exif" "e" ON "e"."assetId" = "a"."id" -WHERE - ( - "a"."ownerId" IN ($1) - AND "a"."isArchived" = false - AND "a"."isVisible" = true - AND "a"."fileCreatedAt" < NOW() - ) - AND ("a"."deletedAt" IS NULL) -ORDER BY - "s"."embedding" <= > $2 ASC -LIMIT - 100 -COMMIT - --- SmartInfoRepository.searchFaces -START TRANSACTION -SET - LOCAL vectors.enable_prefilter = on -SET - LOCAL vectors.k = '100' -WITH - "cte" AS ( - SELECT - "faces"."id" AS "id", - "faces"."assetId" AS "assetId", - "faces"."personId" AS "personId", - "faces"."imageWidth" AS "imageWidth", - "faces"."imageHeight" AS "imageHeight", - "faces"."boundingBoxX1" AS "boundingBoxX1", - "faces"."boundingBoxY1" AS "boundingBoxY1", - "faces"."boundingBoxX2" AS "boundingBoxX2", - "faces"."boundingBoxY2" AS "boundingBoxY2", - 1 + ("faces"."embedding" <= > $1) AS "distance" - FROM - "asset_faces" "faces" - INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId" - AND ("asset"."deletedAt" IS NULL) - WHERE - "asset"."ownerId" IN ($2) - ORDER BY - 1 + ("faces"."embedding" <= > $1) ASC - LIMIT - 100 - ) -SELECT - res.* -FROM - "cte" "res" -WHERE - res.distance <= $3 -COMMIT diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql index 40cf14077a..29a2ee5301 100644 --- a/server/src/infra/sql/user.repository.sql +++ b/server/src/infra/sql/user.repository.sql @@ -21,7 +21,7 @@ SELECT FROM "users" "UserEntity" WHERE - (("UserEntity"."isAdmin" = $1)) + ((("UserEntity"."isAdmin" = $1))) AND ("UserEntity"."deletedAt" IS NULL) LIMIT 1 @@ -41,7 +41,7 @@ WHERE FROM "users" "UserEntity" WHERE - (("UserEntity"."isAdmin" = $1)) + ((("UserEntity"."isAdmin" = $1))) AND ("UserEntity"."deletedAt" IS NULL) ) LIMIT @@ -92,7 +92,7 @@ SELECT FROM "users" "UserEntity" WHERE - (("UserEntity"."storageLabel" = $1)) + ((("UserEntity"."storageLabel" = $1))) AND ("UserEntity"."deletedAt" IS NULL) LIMIT 1 @@ -118,7 +118,7 @@ SELECT FROM "users" "UserEntity" WHERE - (("UserEntity"."oauthId" = $1)) + ((("UserEntity"."oauthId" = $1))) AND ("UserEntity"."deletedAt" IS NULL) LIMIT 1 diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/infra/sql/user.token.repository.sql index 576dea5c4f..0f496504f5 100644 --- a/server/src/infra/sql/user.token.repository.sql +++ b/server/src/infra/sql/user.token.repository.sql @@ -35,7 +35,7 @@ FROM "UserTokenEntity__UserTokenEntity_user"."deletedAt" IS NULL ) WHERE - ("UserTokenEntity"."token" = $1) + (("UserTokenEntity"."token" = $1)) ) "distinctAlias" ORDER BY "UserTokenEntity_id" ASC diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 3109b7f782..df1d9938b5 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -72,7 +72,7 @@ export class AppService { [JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(), [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data), [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data), - [JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(), + [JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data), [JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data), [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 3dbbdcbf12..4b0c06baf3 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -145,6 +145,7 @@ export const loginResponseStub = { cookie: [ 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', 'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', + 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', ], }, user1password: { @@ -160,6 +161,7 @@ export const loginResponseStub = { cookie: [ 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', 'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', + 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', ], }, user1insecure: { @@ -175,6 +177,7 @@ export const loginResponseStub = { cookie: [ 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', 'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', + 'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;', ], }, }; diff --git a/server/test/mocks/fswatcher.mock.ts b/server/test/mocks/fswatcher.mock.ts deleted file mode 100644 index 5699005964..0000000000 --- a/server/test/mocks/fswatcher.mock.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const newFSWatcherMock = () => { - return { - options: {}, - on: jest.fn(), - add: jest.fn(), - unwatch: jest.fn(), - getWatched: jest.fn(), - close: jest.fn(), - addListener: jest.fn(), - removeListener: jest.fn(), - removeAllListeners: jest.fn(), - eventNames: jest.fn(), - rawListeners: jest.fn(), - listeners: jest.fn(), - emit: jest.fn(), - listenerCount: jest.fn(), - off: jest.fn(), - once: jest.fn(), - prependListener: jest.fn(), - prependOnceListener: jest.fn(), - setMaxListeners: jest.fn(), - getMaxListeners: jest.fn(), - }; -}; diff --git a/server/test/mocks/index.ts b/server/test/mocks/index.ts deleted file mode 100644 index e2611ef304..0000000000 --- a/server/test/mocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './fswatcher.mock'; diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 9d778883de..1c98b78c9e 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -32,7 +32,6 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getTimeBuckets: jest.fn(), restoreAll: jest.fn(), softDeleteAll: jest.fn(), - search: jest.fn(), getAssetIdByCity: jest.fn(), getAssetIdByTag: jest.fn(), searchMetadata: jest.fn(), diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index f34e6b06b5..f5a4d39a67 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -3,8 +3,14 @@ import { IDatabaseRepository, Version } from '@app/domain'; export const newDatabaseRepositoryMock = (): jest.Mocked => { return { getExtensionVersion: jest.fn(), + getAvailableExtensionVersion: jest.fn(), + getPreferredVectorExtension: jest.fn(), getPostgresVersion: jest.fn().mockResolvedValue(new Version(14, 0, 0)), createExtension: jest.fn().mockImplementation(() => Promise.resolve()), + updateExtension: jest.fn(), + updateVectorExtension: jest.fn(), + reindex: jest.fn(), + shouldReindex: jest.fn(), runMigrations: jest.fn(), withLock: jest.fn().mockImplementation((_, function_: () => Promise) => function_()), isBusy: jest.fn(), diff --git a/server/test/repositories/index.ts b/server/test/repositories/index.ts index e31a3a1c45..90fd1326b4 100644 --- a/server/test/repositories/index.ts +++ b/server/test/repositories/index.ts @@ -15,8 +15,8 @@ export * from './metadata.repository.mock'; export * from './move.repository.mock'; export * from './partner.repository.mock'; export * from './person.repository.mock'; +export * from './search.repository.mock'; export * from './shared-link.repository.mock'; -export * from './smart-info.repository.mock'; export * from './storage.repository.mock'; export * from './system-config.repository.mock'; export * from './system-info.repository.mock'; diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 6771060fc2..e47120ac9f 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -8,5 +8,10 @@ export const newMetadataRepositoryMock = (): jest.Mocked => readTags: jest.fn(), writeTags: jest.fn(), extractBinaryTag: jest.fn(), + getCameraMakes: jest.fn(), + getCameraModels: jest.fn(), + getCities: jest.fn(), + getCountries: jest.fn(), + getStates: jest.fn(), }; }; diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts new file mode 100644 index 0000000000..e0bdab269a --- /dev/null +++ b/server/test/repositories/search.repository.mock.ts @@ -0,0 +1,11 @@ +import { ISearchRepository } from '@app/domain'; + +export const newSearchRepositoryMock = (): jest.Mocked => { + return { + init: jest.fn(), + searchMetadata: jest.fn(), + searchSmart: jest.fn(), + searchFaces: jest.fn(), + upsert: jest.fn(), + }; +}; diff --git a/server/test/repositories/smart-info.repository.mock.ts b/server/test/repositories/smart-info.repository.mock.ts deleted file mode 100644 index c7bc4f5c56..0000000000 --- a/server/test/repositories/smart-info.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ISmartInfoRepository } from '@app/domain'; - -export const newSmartInfoRepositoryMock = (): jest.Mocked => { - return { - init: jest.fn(), - searchCLIP: jest.fn(), - searchFaces: jest.fn(), - upsert: jest.fn(), - }; -}; diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index f4dbc0c5b1..1ee57b78dd 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -1,4 +1,36 @@ -import { IStorageRepository, StorageCore } from '@app/domain'; +import { IStorageRepository, StorageCore, WatchEvents } from '@app/domain'; +import { WatchOptions } from 'chokidar'; + +interface MockWatcherOptions { + items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; + close?: () => void; +} + +export const makeMockWatcher = + ({ items, close }: MockWatcherOptions) => + (paths: string[], options: WatchOptions, events: Partial) => { + events.onReady?.(); + for (const item of items || []) { + switch (item.event) { + case 'add': { + events.onAdd?.(item.value); + break; + } + case 'change': { + events.onChange?.(item.value); + break; + } + case 'unlink': { + events.onUnlink?.(item.value); + break; + } + case 'error': { + events.onError?.(new Error(item.value)); + } + } + } + return () => close?.(); + }; export const newStorageRepositoryMock = (reset = true): jest.Mocked => { if (reset) { @@ -21,6 +53,7 @@ export const newStorageRepositoryMock = (reset = true): jest.Mocked= 8" } }, + "node_modules/@photo-sphere-viewer/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.7.0.tgz", + "integrity": "sha512-9IIvs4P8LWr/lTYQLEuhdGvkoK6dGCRLiVD54QybxIG7dt5dudC8Jq/GIQQb8G/QcbjmdW49ezA9LPxHTNYIgg==", + "dependencies": { + "three": "^0.161.0" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", @@ -1416,12 +1390,12 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.1.tgz", - "integrity": "sha512-CGURX6Ps+TkOovK6xV+Y2rn8JKa8ZPUHPZ/NKgCxAmgBrXReavzFl8aOSCj3kQ1xqT7yGJj53hjcV/gqwDAaWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", + "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", "dev": true, "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0-next.0 || ^2.0.0", + "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", @@ -1553,9 +1527,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.3.0.tgz", - "integrity": "sha512-hJVIrkFizEQxoWsGBlycTcQhrpoCH4DhXfrnHFFXgkx3Xdm15zycsq5Ep+vpw4W8S0NJa8cxDHcuJib+1tEbhg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", + "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.3.2", @@ -1671,9 +1645,9 @@ } }, "node_modules/@testing-library/svelte": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-4.0.6.tgz", - "integrity": "sha512-4Niit+F/V3/AC03+/hsTrUJjXeoLFgd2KYjlz9nKFVELZR0dApHtc+tILe7NHiBctTAKTEzoM2VDLn1mBOqn3w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-4.1.0.tgz", + "integrity": "sha512-MJqe7x9WowkiAVdk9mvazEC2ktFZdmK2OqFVoO557PC37aBemQ4ozqdK3yrG34Zg9kuln3qgTVeLSh08e69AMw==", "dev": true, "dependencies": { "@testing-library/dom": "^9.3.1" @@ -1793,9 +1767,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "node_modules/@types/supercluster": { @@ -1806,22 +1780,17 @@ "@types/geojson": "*" } }, - "node_modules/@types/webxr": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.5.tgz", - "integrity": "sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==" - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1859,9 +1828,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1880,15 +1849,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -1908,13 +1877,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1925,13 +1894,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1952,9 +1921,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1965,13 +1934,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2029,9 +1998,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2050,17 +2019,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -2087,9 +2056,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2108,12 +2077,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2164,9 +2133,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.1.tgz", - "integrity": "sha512-fJEhKaDwGMZtJUX7BRcGxooGwg1Hl0qt53mVup/ZJeznhvL5EodteVnb/mcByhEcvVWbK83ZF31c7nPEDi4LOQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz", + "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -2191,13 +2160,13 @@ } }, "node_modules/@vitest/expect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz", - "integrity": "sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", + "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.1", - "@vitest/utils": "1.2.1", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", "chai": "^4.3.10" }, "funding": { @@ -2205,12 +2174,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.1.tgz", - "integrity": "sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", + "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.1", + "@vitest/utils": "1.2.2", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2246,9 +2215,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.1.tgz", - "integrity": "sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", + "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2292,9 +2261,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.1.tgz", - "integrity": "sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", + "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2304,9 +2273,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", + "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2940,6 +2909,20 @@ "node": ">=4" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3229,6 +3212,15 @@ "node": ">= 0.4" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -3344,6 +3336,12 @@ "integrity": "sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/engine.io-client": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", @@ -3637,9 +3635,9 @@ "dev": true }, "node_modules/eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -3694,9 +3692,9 @@ } }, "node_modules/eslint-plugin-unicorn/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4196,6 +4194,15 @@ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -4819,6 +4826,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -4836,6 +4858,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5033,6 +5064,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5893,6 +5936,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6137,9 +6197,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -6324,9 +6384,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -6629,6 +6689,15 @@ "jsesc": "bin/jsesc" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6719,6 +6788,41 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", @@ -7135,6 +7239,20 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7270,9 +7388,9 @@ } }, "node_modules/svelte": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz", - "integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.10.tgz", + "integrity": "sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -7294,9 +7412,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.3.tgz", - "integrity": "sha512-Q2nGnoysxUnB9KjnjpQLZwdjK62DHyW6nuH/gm2qteFnDk0lCehe/6z8TsIvYeKjC6luKaWxiNGyOcWiLLPSwA==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz", + "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -7366,9 +7484,9 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.7.6.tgz", - "integrity": "sha512-ntJaEz58Pas4CrCsr/YQ+0DJXoRkJTQj+oc6BckEwGrzBM9lcsJgKyMzQlNFwuMboIUBaou+aeiJOyqCSv4oFg==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.7.7.tgz", + "integrity": "sha512-fnv8L3tA4EMePp9BGKAc8AvXCsg34z56NBMGjYkz6qkl90qSTY4vUhIu1KXbwjGfQmHBmPkIl9VSdnnHCMnaRA==", "dependencies": { "d3-geo": "^3.1.0", "just-compare": "^2.3.0", @@ -7596,6 +7714,11 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.161.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.161.0.tgz", + "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==" + }, "node_modules/thumbhash": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz", @@ -7618,9 +7741,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", - "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", "dev": true, "engines": { "node": ">=14.0.0" @@ -7909,13 +8032,13 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", + "integrity": "sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" }, "bin": { @@ -7964,9 +8087,9 @@ } }, "node_modules/vite-node": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.1.tgz", - "integrity": "sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", + "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -8000,16 +8123,16 @@ } }, "node_modules/vitest": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.1.tgz", - "integrity": "sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", + "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", "dev": true, "dependencies": { - "@vitest/expect": "1.2.1", - "@vitest/runner": "1.2.1", - "@vitest/snapshot": "1.2.1", - "@vitest/spy": "1.2.1", - "@vitest/utils": "1.2.1", + "@vitest/expect": "1.2.2", + "@vitest/runner": "1.2.2", + "@vitest/snapshot": "1.2.2", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", "acorn-walk": "^8.3.2", "cac": "^6.7.14", "chai": "^4.3.10", @@ -8022,9 +8145,9 @@ "std-env": "^3.5.0", "strip-literal": "^1.3.0", "tinybench": "^2.5.1", - "tinypool": "^0.8.1", + "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.2.1", + "vite-node": "1.2.2", "why-is-node-running": "^2.2.2" }, "bin": { @@ -8226,6 +8349,56 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8282,6 +8455,15 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8299,6 +8481,33 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 9e4ccf460a..eaafeef25e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,7 @@ { "name": "immich-web", "version": "1.0.0", + "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", "build": "vite build", @@ -39,26 +40,27 @@ "eslint": "^8.34.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.30.0", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.0", "factory.ts": "^1.3.0", "identity-obj-proxy": "^3.0.0", "postcss": "^8.4.21", "prettier": "^3.1.0", "prettier-plugin-svelte": "^3.1.2", + "rollup-plugin-visualizer": "^5.12.0", "svelte": "^4.0.5", "svelte-check": "^3.4.3", "svelte-preprocess": "^5.0.3", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", "typescript": "^5.3.3", - "vite": "^5.0.10", + "vite": "^5.1.1", "vitest": "^1.0.4" }, "type": "module", "dependencies": { - "@egjs/svelte-view360": "^4.0.0-beta.7", "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.3.67", + "@photo-sphere-viewer/core": "^5.7.0", "@zoom-image/svelte": "^0.2.0", "axios": "^1.6.7", "buffer": "^6.0.3", @@ -68,7 +70,6 @@ "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.2.1", - "maplibre-gl": "^3.6.0", "socket.io-client": "^4.6.1", "svelte-local-storage-store": "^0.6.0", "svelte-maplibre": "^0.7.0", diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 78228aee1f..c11ac66534 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -1,53 +1,25 @@ import { - APIKeyApi, - ActivityApi, - AlbumApi, AssetApi, AssetApiFp, AssetJobName, - AuditApi, - AuthenticationApi, DownloadApi, - FaceApi, - JobApi, JobName, - LibraryApi, - OAuthApi, - PartnerApi, PersonApi, SearchApi, - ServerInfoApi, SharedLinkApi, - SystemConfigApi, - TrashApi, - UserApi, UserApiFp, base, common, configuration, -} from '@immich/sdk'; +} from '@immich/sdk/axios'; import type { ApiParams as ApiParameters } from './types'; class ImmichApi { - public activityApi: ActivityApi; - public albumApi: AlbumApi; public downloadApi: DownloadApi; - public libraryApi: LibraryApi; public assetApi: AssetApi; - public auditApi: AuditApi; - public authenticationApi: AuthenticationApi; - public faceApi: FaceApi; - public jobApi: JobApi; - public keyApi: APIKeyApi; - public oauthApi: OAuthApi; - public partnerApi: PartnerApi; public searchApi: SearchApi; - public serverInfoApi: ServerInfoApi; public sharedLinkApi: SharedLinkApi; public personApi: PersonApi; - public systemConfigApi: SystemConfigApi; - public userApi: UserApi; - public trashApi: TrashApi; private config: configuration.Configuration; private key?: string; @@ -59,25 +31,11 @@ class ImmichApi { constructor(parameters: configuration.ConfigurationParameters) { this.config = new configuration.Configuration(parameters); - this.activityApi = new ActivityApi(this.config); - this.albumApi = new AlbumApi(this.config); - this.auditApi = new AuditApi(this.config); this.downloadApi = new DownloadApi(this.config); - this.libraryApi = new LibraryApi(this.config); this.assetApi = new AssetApi(this.config); - this.authenticationApi = new AuthenticationApi(this.config); - this.faceApi = new FaceApi(this.config); - this.jobApi = new JobApi(this.config); - this.keyApi = new APIKeyApi(this.config); - this.oauthApi = new OAuthApi(this.config); - this.partnerApi = new PartnerApi(this.config); this.searchApi = new SearchApi(this.config); - this.serverInfoApi = new ServerInfoApi(this.config); this.sharedLinkApi = new SharedLinkApi(this.config); this.personApi = new PersonApi(this.config); - this.systemConfigApi = new SystemConfigApi(this.config); - this.userApi = new UserApi(this.config); - this.trashApi = new TrashApi(this.config); } private createUrl(path: string, parameters?: Record) { diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 4d50cff538..886f439d8e 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,3 +1,3 @@ export * from './api'; -export * from '@immich/sdk'; +export * from '@immich/sdk/axios'; export * from './utils'; diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 96baf2f3aa..7a2ae17f67 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -1,4 +1,4 @@ -import type { Configuration } from '@immich/sdk'; +import type { Configuration } from '@immich/sdk/axios'; /* eslint-disable @typescript-eslint/no-explicit-any */ export type ApiFp = (configuration: Configuration) => Record any>; diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts index a3fed43d32..0367e7905a 100644 --- a/web/src/api/utils.ts +++ b/web/src/api/utils.ts @@ -1,11 +1,11 @@ -import type { AxiosError, AxiosPromise } from 'axios'; +import { finishOAuth, linkOAuthAccount, startOAuth, unlinkOAuthAccount } from '@immich/sdk'; +import type { UserResponseDto } from '@immich/sdk/axios'; +import type { AxiosError } from 'axios'; import { - notificationController, NotificationType, + notificationController, } from '../lib/components/shared-components/notification/notification'; import { handleError } from '../lib/utils/handle-error'; -import { api } from './api'; -import type { UserResponseDto } from '@immich/sdk'; export type ApiError = AxiosError<{ message: string }>; @@ -43,8 +43,8 @@ export const oauth = { authorize: async (location: Location) => { try { const redirectUri = location.href.split('?')[0]; - const { data } = await api.oauthApi.startOAuth({ oAuthConfigDto: { redirectUri } }); - window.location.href = data.url; + const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } }); + window.location.href = url; return true; } catch (error) { handleError(error, 'Unable to login with OAuth'); @@ -52,12 +52,12 @@ export const oauth = { } }, login: (location: Location) => { - return api.oauthApi.finishOAuth({ oAuthCallbackDto: { url: location.href } }); + return finishOAuth({ oAuthCallbackDto: { url: location.href } }); }, - link: (location: Location): AxiosPromise => { - return api.oauthApi.linkOAuthAccount({ oAuthCallbackDto: { url: location.href } }); + link: (location: Location): Promise => { + return linkOAuthAccount({ oAuthCallbackDto: { url: location.href } }); }, unlink: () => { - return api.oauthApi.unlinkOAuthAccount(); + return unlinkOAuthAccount(); }, }; diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte index dfb93f7033..1046b7ef67 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -1,8 +1,8 @@ - +

diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 2efd2c1bf6..36d973d35a 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -21,6 +21,7 @@ import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte'; import JobTile from './job-tile.svelte'; import StorageMigrationDescription from './storage-migration-description.svelte'; + import { sendJobCommand } from '@immich/sdk'; export let jobs: AllJobStatusResponseDto; @@ -127,8 +128,7 @@ const title = jobDetails[jobId]?.title; try { - const { data } = await api.jobApi.sendJobCommand({ id: jobId, jobCommandDto: jobCommand }); - jobs[jobId] = data; + jobs[jobId] = await sendJobCommand({ id: jobId, jobCommandDto: jobCommand }); switch (jobCommand.command) { case JobCommand.Empty: { diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index 95525ed9d3..c585d60e93 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -1,7 +1,7 @@ - +

{user.name}'s account will be restored.

diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 3f7ddf7614..1ad962b0de 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -1,15 +1,15 @@ diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index f24a9cad65..c4f9f9ea2b 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -235,6 +235,7 @@ /> @@ -296,7 +297,11 @@
- +
- +
- +
- +
- +
- +
- +

Manage + import { page } from '$app/stores'; + import { QueryParameter } from '$lib/constants'; + import { hasParamValue, updateParamList } from '$lib/utils'; import { slide } from 'svelte/transition'; + export let title: string; export let subtitle = ''; - + export let key: string; export let isOpen = false; - const toggle = () => (isOpen = !isOpen); + + const syncFromUrl = () => (isOpen = hasParamValue(QueryParameter.IS_OPEN, key)); + const syncToUrl = (isOpen: boolean) => updateParamList({ param: QueryParameter.IS_OPEN, value: key, add: isOpen }); + + isOpen ? syncToUrl(true) : syncFromUrl(); + $: $page.url && syncFromUrl(); + + const toggle = () => { + isOpen = !isOpen; + syncToUrl(isOpen); + };

diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 5e2d7b781b..475c3a65cb 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -1,7 +1,12 @@ diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index d05760c390..39cfddfa6e 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -1,16 +1,17 @@
- {#await loadAssetData()} + + {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])} - {:then assetData} - {#if assetData} - - {:else} -

{errorMessage}

- {/if} + {:then [data, module]} + + {:catch} + Failed to load asset {/await}
diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte new file mode 100644 index 0000000000..540c783648 --- /dev/null +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -0,0 +1,27 @@ + + +
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index cf7c16b3d8..f957341fbf 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -11,6 +11,7 @@ import { photoViewer } from '$lib/stores/assets.store'; import { getBoundingBox } from '$lib/utils/people-utils'; import { boundingBoxesArray } from '$lib/stores/people.store'; + import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; export let asset: AssetResponseDto; export let element: HTMLDivElement | undefined = undefined; @@ -114,7 +115,7 @@ zoomImageWheelState.subscribe((state) => { photoZoomState.set(state); - if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed) { + if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) { hasZoomed = true; loadAssetData({ loadOriginal: true }); @@ -129,7 +130,7 @@ transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} class="flex h-full select-none place-content-center place-items-center" > - {#await loadAssetData({ loadOriginal: false })} + {#await loadAssetData({ loadOriginal: $alwaysLoadOriginalFile ? isWebCompatibleImage(asset) : false })} {:then}
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index c7aa0b6d83..9c82ad77b7 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -47,6 +47,7 @@ class:rounded-xl={curve} class:shadow-lg={shadow} class:rounded-full={circle} + class:aspect-square={circle || !heightStyle} class:opacity-0={!thumbhash && !complete} draggable="false" /> diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index de540b3208..1df6a0d9d5 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -37,6 +37,7 @@ export let readonly = false; export let showArchiveIcon = false; export let showStackedIcon = true; + export let intersecting = false; let className = ''; export { className as class }; @@ -85,7 +86,7 @@ }; - +
onMouseEnter()} - on:mouseleave={() => onMouseLeave()} + on:mouseenter={onMouseEnter} + on:mouseleave={onMouseLeave} on:click={thumbnailClickedHandler} on:keydown={thumbnailKeyDownHandler} > diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte index b057d2da9a..dccecccd36 100644 --- a/web/src/lib/components/elements/buttons/button.svelte +++ b/web/src/lib/components/elements/buttons/button.svelte @@ -4,6 +4,7 @@ | 'primary' | 'secondary' | 'transparent-primary' + | 'text-primary' | 'light-red' | 'red' | 'green' @@ -26,6 +27,7 @@ export let fullwidth = false; export let border = false; export let title: string | undefined = ''; + let className = ''; export { className as class }; @@ -36,6 +38,8 @@ 'bg-gray-500 dark:bg-gray-200 text-white dark:text-immich-dark-gray enabled:hover:bg-gray-500/90 enabled:dark:hover:bg-gray-200/90', 'transparent-primary': 'text-gray-500 dark:text-immich-dark-primary enabled:hover:bg-gray-100 enabled:dark:hover:bg-gray-700', + 'text-primary': + 'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10', 'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50', red: 'bg-red-500 text-white enabled:hover:bg-red-400', green: 'bg-green-500 text-gray-800 enabled:hover:bg-green-400/90', diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index 57757dafdb..58eeef0e4d 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -9,7 +9,7 @@ import { mdiCheck } from '@mdi/js'; - import _ from 'lodash'; + import { isEqual } from 'lodash-es'; import LinkButton from './buttons/link-button.svelte'; import { clickOutside } from '$lib/utils/click-outside'; import { fly } from 'svelte/transition'; @@ -92,7 +92,7 @@ class="grid grid-cols-[20px,1fr] place-items-center p-2 transition-all hover:bg-gray-300 dark:hover:bg-gray-800" on:click={() => handleSelectOption(option)} > - {#if _.isEqual(selectedOption, option)} + {#if isEqual(selectedOption, option)}
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index ab77a0df03..8f6bbc1f5c 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -8,12 +8,6 @@ import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; - const dispatch = createEventDispatcher<{ - reject: void; - confirm: [PersonResponseDto, PersonResponseDto]; - close: void; - }>(); - export let personMerge1: PersonResponseDto; export let personMerge2: PersonResponseDto; export let potentialMergePeople: PersonResponseDto[]; @@ -22,6 +16,21 @@ const title = personMerge2.name; + const dispatch = createEventDispatcher<{ + reject: void; + confirm: [PersonResponseDto, PersonResponseDto]; + close: void; + }>(); + + const handleKeyboardPress = (event: KeyboardEvent) => { + switch (event.key) { + case 'Escape': { + dispatch('close'); + return; + } + } + }; + const changePersonToMerge = (newperson: PersonResponseDto) => { const index = potentialMergePeople.indexOf(newperson); [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]]; @@ -29,6 +38,8 @@ }; + + dispatch('close')}>
They will be merged together

- +
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 04fd47c273..a0bb503516 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -14,6 +14,7 @@ import AssignFaceSidePanel from './assign-face-side-panel.svelte'; import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { timeBeforeShowLoadingSpinner } from '$lib/constants'; + import { getFaces, reassignFacesById } from '@immich/sdk'; export let assetId: string; export let assetType: AssetTypeEnum; @@ -70,8 +71,7 @@ try { const { data } = await api.personApi.getAllPeople({ withHidden: true }); allPeople = data.people; - const result = await api.faceApi.getFaces({ id: assetId }); - peopleWithFaces = result.data; + peopleWithFaces = await getFaces({ id: assetId }); selectedPersonToCreate = Array.from({ length: peopleWithFaces.length }); selectedPersonToReassign = Array.from({ length: peopleWithFaces.length }); } catch (error) { @@ -110,14 +110,14 @@ const personId = selectedPersonToReassign[index]?.id; if (personId) { - await api.faceApi.reassignFacesById({ + await reassignFacesById({ id: personId, faceDto: { id: peopleWithFace.id }, }); } else if (selectedPersonToCreate[index]) { const { data } = await api.personApi.createPerson(); numberOfPersonToCreate.push(data.id); - await api.faceApi.reassignFacesById({ + await reassignFacesById({ id: data.id, faceDto: { id: peopleWithFace.id }, }); diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte index 912c8f1da1..144e1711da 100644 --- a/web/src/lib/components/forms/admin-registration-form.svelte +++ b/web/src/lib/components/forms/admin-registration-form.svelte @@ -1,27 +1,28 @@ @@ -71,12 +66,12 @@ type="password" autocomplete="current-password" required - bind:value={confirmPassowrd} + bind:value={passwordConfirm} />
- {#if error} -

{error}

+ {#if errorMessage} +

{errorMessage}

{/if} {#if success} diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index b0434648c0..70daa1ef97 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -1,11 +1,11 @@ {#if !oauthLoading && $featureFlags.passwordLogin} - + {#if errorMessage}

{errorMessage} diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index a4dba7da5b..49fd685e95 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -1,14 +1,15 @@ diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte index e89e6376d8..b34abac266 100644 --- a/web/src/lib/components/photos-page/actions/add-to-album.svelte +++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte @@ -6,11 +6,12 @@ NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; + import { AppRoute } from '$lib/constants'; import { addAssetsToAlbum } from '$lib/utils/asset-utils'; - import { type AlbumResponseDto, api } from '@api'; + import { type AlbumResponseDto } from '@api'; + import { createAlbum } from '@immich/sdk'; import { getMenuContext } from '../asset-select-context-menu.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { AppRoute } from '$lib/constants'; export let shared = false; let showAlbumPicker = false; @@ -27,8 +28,8 @@ showAlbumPicker = false; const assetIds = [...getAssets()].map((asset) => asset.id); - api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => { - const { id, albumName } = response.data; + createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => { + const { id, albumName } = response; notificationController.show({ message: `Added ${assetIds.length} to ${albumName}`, diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index 48b33719e5..a2a10297d6 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -5,10 +5,10 @@ NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; - import { type AlbumResponseDto, api } from '@api'; + import { getAlbumInfo, removeAssetFromAlbum, type AlbumResponseDto } from '@immich/sdk'; + import { mdiDeleteOutline } from '@mdi/js'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { mdiDeleteOutline } from '@mdi/js'; export let album: AlbumResponseDto; export let onRemove: ((assetIds: string[]) => void) | undefined; @@ -21,13 +21,12 @@ const removeFromAlbum = async () => { try { const ids = [...getAssets()].map((a) => a.id); - const { data: results } = await api.albumApi.removeAssetFromAlbum({ + const results = await removeAssetFromAlbum({ id: album.id, bulkIdsDto: { ids }, }); - const { data } = await api.albumApi.getAlbumInfo({ id: album.id }); - album = data; + album = await getAlbumInfo({ id: album.id }); onRemove?.(ids); diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte index 5121be9ce3..e6570eafe2 100644 --- a/web/src/lib/components/photos-page/actions/restore-assets.svelte +++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte @@ -1,15 +1,15 @@ - +

-
@@ -68,10 +83,11 @@
{#each filteredOptions as option (option.label)} -
-
- {/if} - - {#each $savedSearchTerms as savedSearchTerm, index (index)} -
-
- -
- -
-
-
- {/each} -
+ {#if showFilter} + {/if}
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte new file mode 100644 index 0000000000..b4665f3a21 --- /dev/null +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -0,0 +1,476 @@ + + +
+

FILTERS

+
+ +
+ +
+
+
+

PEOPLE

+
+
+ + {#if suggestions.people.length > 0} +
+ {#each peopleList as person (person.id)} + + {/each} +
+ +
+ +
+ {/if} +
+ +
+ +
+ + +
+ +
+ +
+

PLACE

+ +
+
+

Country

+ updateSuggestion(SearchSuggestionType.Country, {})} + /> +
+ +
+

State

+ updateSuggestion(SearchSuggestionType.State, { country: filter.location.country?.value })} + /> +
+ +
+

City

+ + updateSuggestion(SearchSuggestionType.City, { + country: filter.location.country?.value, + state: filter.location.state?.value, + })} + /> +
+
+
+ +
+ +
+

CAMERA

+ +
+
+

Make

+ + updateSuggestion(SearchSuggestionType.CameraMake, { cameraModel: filter.camera.model?.value })} + /> +
+ +
+

Model

+ + updateSuggestion(SearchSuggestionType.CameraModel, { cameraMake: filter.camera.make?.value })} + /> +
+
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
+ +
+

MEDIA TYPE

+ +
+ + + + + +
+
+ + +
+

DISPLAY OPTIONS

+ +
+ + + + + +
+
+
+ +
+ + +
+
+
diff --git a/web/src/lib/components/shared-components/search-bar/search-history-box.svelte b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte new file mode 100644 index 0000000000..cb22513019 --- /dev/null +++ b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte @@ -0,0 +1,62 @@ + + +
+
+

+ Smart search is enabled by default, to search for metadata use the syntax m:your-search-term +

+
+ + {#if $savedSearchTerms.length > 0} +
+

RECENT SEARCHES

+
+ +
+
+ {/if} + + {#each $savedSearchTerms as savedSearchTerm, i (i)} +
+
+ +
+ +
+
+
+ {/each} +
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index d39add7f56..f2a26be543 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -2,7 +2,8 @@ import { page } from '$app/stores'; import { locale, sidebarSettings } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; - import { type AssetApiGetAssetStatisticsRequest, api } from '@api'; + import { api, type AssetApiGetAssetStatisticsRequest } from '@api'; + import { getAlbumCount } from '@immich/sdk'; import { mdiAccount, mdiAccountMultiple, @@ -28,10 +29,9 @@ return stats; }; - const getAlbumCount = async () => { + const handleAlbumCount = async () => { try { - const { data: albumCount } = await api.albumApi.getAlbumCount(); - return albumCount; + return await getAlbumCount(); } catch { return { owned: 0, shared: 0, notShared: 0 }; } @@ -85,7 +85,7 @@ isSelected={isSharingSelected} > - {#await getAlbumCount()} + {#await handleAlbumCount()} {:then data}
@@ -127,7 +127,7 @@ isSelected={$page.route.id === '/(user)/albums'} > - {#await getAlbumCount()} + {#await handleAlbumCount()} {:then data}
diff --git a/web/src/lib/components/user-settings-page/change-password-settings.svelte b/web/src/lib/components/user-settings-page/change-password-settings.svelte index c4f4b14947..c90eaef833 100644 --- a/web/src/lib/components/user-settings-page/change-password-settings.svelte +++ b/web/src/lib/components/user-settings-page/change-password-settings.svelte @@ -3,7 +3,8 @@ notificationController, NotificationType, } from '$lib/components/shared-components/notification/notification'; - import { api, type ApiError } from '@api'; + import { type ApiError } from '@api'; + import { changePassword } from '@immich/sdk'; import { fade } from 'svelte/transition'; import SettingInputField, { SettingInputFieldType } from '../admin-page/settings/setting-input-field.svelte'; import Button from '../elements/buttons/button.svelte'; @@ -14,12 +15,7 @@ const handleChangePassword = async () => { try { - await api.authenticationApi.changePassword({ - changePasswordDto: { - password, - newPassword, - }, - }); + await changePassword({ changePasswordDto: { password, newPassword } }); notificationController.show({ message: 'Updated password', diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index b0f1be38c0..a933c12d67 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -1,5 +1,6 @@ + +
+
+
+
+ +
+
+
+
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 90a776cc09..72aa06f314 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -1,15 +1,16 @@ - + - - + + - + - + - + - + {#if $featureFlags.loaded && $featureFlags.oauth} {/if} - + - + + + + + - + - + diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index a1ac68c216..295bd99433 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -63,14 +63,15 @@ export const dateFormats = { export enum QueryParameter { ACTION = 'action', ASSET_INDEX = 'assetIndex', - SMART_SEARCH = 'smartSearch', + IS_OPEN = 'isOpen', MEMORY_INDEX = 'memoryIndex', ONBOARDING_STEP = 'step', OPEN_SETTING = 'openSetting', - QUERY = 'query', PREVIOUS_ROUTE = 'previousRoute', + QUERY = 'query', SEARCHED_PEOPLE = 'searchedPeople', SEARCH_TERM = 'q', + SMART_SEARCH = 'smartSearch', } export enum OpenSettingQueryParameterValue { diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 09154f5b12..d23c3a3ac3 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -92,3 +92,5 @@ export const albumViewSettings = persisted('album-view-settin }); export const showDeleteModal = persisted('delete-confirm-dialog', true, {}); + +export const alwaysLoadOriginalFile = persisted('always-load-original-file', false, {}); diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 3f9df3a458..9c48f4faf2 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -1,4 +1,5 @@ -import { api, type ServerConfigDto, type ServerFeaturesDto } from '@api'; +import { type ServerConfigDto, type ServerFeaturesDto } from '@api'; +import { getServerConfig, getServerFeatures } from '@immich/sdk'; import { writable } from 'svelte/store'; export type FeatureFlags = ServerFeaturesDto & { loaded: boolean }; @@ -31,10 +32,7 @@ export const serverConfig = writable({ }); export const loadConfig = async () => { - const [{ data: flags }, { data: config }] = await Promise.all([ - api.serverInfoApi.getServerFeatures(), - api.serverInfoApi.getServerConfig(), - ]); + const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); serverConfig.update(() => ({ ...config, loaded: true })); diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 8659506cc5..7f737e9fcc 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,16 +1,8 @@ -import { get, writable } from 'svelte/store'; +import { writable } from 'svelte/store'; import type { UserResponseDto } from '@api'; export let user = writable(); -export const setUser = (value: UserResponseDto) => { - user.set(value); -}; - -export const getSavedUser = () => { - return get(user); -}; - export const resetSavedUser = () => { user = writable(); }; diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 2062eddb34..5b59e2c823 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -1,8 +1,8 @@ import type { AssetResponseDto, ServerVersionResponseDto } from '@api'; -import { Socket, io } from 'socket.io-client'; -import { writable } from 'svelte/store'; +import { type Socket, io } from 'socket.io-client'; +import { get, writable } from 'svelte/store'; import { loadConfig } from './server-config.store'; -import { getAuthUser } from '$lib/utils/auth'; +import { user } from './user.store'; export interface ReleaseEvent { isAvailable: boolean; @@ -30,8 +30,7 @@ export const openWebsocketConnection = async () => { return; } - const user = await getAuthUser(); - if (!user) { + if (!get(user)) { return; } diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000000..059b526508 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,33 @@ +import { goto } from '$app/navigation'; +import { page } from '$app/stores'; +import { get } from 'svelte/store'; + +interface UpdateParamAction { + param: string; + value: string; + add: boolean; +} + +const getParamValues = (param: string) => + new Set((get(page).url.searchParams.get(param) || '').split(' ').filter((x) => x !== '')); + +export const hasParamValue = (param: string, value: string) => getParamValues(param).has(value); + +export const updateParamList = async ({ param, value, add }: UpdateParamAction) => { + const values = getParamValues(param); + + if (add) { + values.add(value); + } else { + values.delete(value); + } + + const searchParams = new URLSearchParams(get(page).url.searchParams); + searchParams.set(param, [...values.values()].join(' ')); + + if (values.size === 0) { + searchParams.delete(param); + } + + await goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }); +}; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 04bbf0b04d..785d370bb9 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,34 +1,33 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { downloadManager } from '$lib/stores/download'; +import { api } from '@api'; import { - api, - type BulkIdResponseDto, + addAssetsToAlbum as addAssets, type AssetResponseDto, - type DownloadResponseDto, + type AssetTypeEnum, + type BulkIdResponseDto, type DownloadInfoDto, - AssetTypeEnum, + type DownloadResponseDto, type UserResponseDto, -} from '@api'; -import { handleError } from './handle-error'; +} from '@immich/sdk'; import { DateTime } from 'luxon'; +import { handleError } from './handle-error'; export const addAssetsToAlbum = async (albumId: string, assetIds: Array): Promise => - api.albumApi - .addAssetsToAlbum({ - id: albumId, - bulkIdsDto: { ids: assetIds }, - key: api.getKey(), - }) - .then(({ data: results }) => { - const count = results.filter(({ success }) => success).length; - notificationController.show({ - type: NotificationType.Info, - message: `Added ${count} asset${count === 1 ? '' : 's'}`, - }); - - return results; + addAssets({ + id: albumId, + bulkIdsDto: { ids: assetIds }, + key: api.getKey(), + }).then((results) => { + const count = results.filter(({ success }) => success).length; + notificationController.show({ + type: NotificationType.Info, + message: `Added ${count} asset${count === 1 ? '' : 's'}`, }); + return results; + }); + export const downloadBlob = (data: Blob, filename: string) => { const url = URL.createObjectURL(data); diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index a47adbcbed..dc7bb53db1 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,53 +1,64 @@ -import { api } from '@api'; -import { redirect } from '@sveltejs/kit'; -import { AppRoute } from '../constants'; -import { getSavedUser, setUser } from '$lib/stores/user.store'; +import { browser } from '$app/environment'; import { serverInfo } from '$lib/stores/server-info.store'; +import { user } from '$lib/stores/user.store'; +import { getMyUserInfo, getServerInfo } from '@immich/sdk'; +import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; +import { AppRoute } from '../constants'; export interface AuthOptions { admin?: true; + public?: true; } -export const getAuthUser = async () => { +export const loadUser = async () => { try { - const { data: user } = await api.userApi.getMyUserInfo(); - return user; + let loaded = get(user); + if (!loaded && hasAuthCookie()) { + loaded = await getMyUserInfo(); + user.set(loaded); + } + return loaded; } catch { return null; } }; -export const authenticate = async (options?: AuthOptions) => { - options = options || {}; +const hasAuthCookie = (): boolean => { + if (!browser) { + return false; + } - const savedUser = getSavedUser(); - const user = savedUser || (await getAuthUser()); + for (const cookie of document.cookie.split('; ')) { + const [name] = cookie.split('='); + if (name === 'immich_is_authenticated') { + return true; + } + } + + return false; +}; + +export const authenticate = async (options?: AuthOptions) => { + const { public: publicRoute, admin: adminRoute } = options || {}; + const user = await loadUser(); + + if (publicRoute) { + return; + } if (!user) { redirect(302, AppRoute.AUTH_LOGIN); } - if (options.admin && !user.isAdmin) { + if (adminRoute && !user.isAdmin) { redirect(302, AppRoute.PHOTOS); } - - if (!savedUser) { - setUser(user); - } }; export const requestServerInfo = async () => { - if (getSavedUser()) { - const { data } = await api.serverInfoApi.getServerInfo(); + if (get(user)) { + const data = await getServerInfo(); serverInfo.set(data); } }; - -export const isLoggedIn = async () => { - const savedUser = getSavedUser(); - const user = savedUser || (await getAuthUser()); - if (!savedUser) { - setUser(user); - } - return user; -}; diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 2e039abbd4..761215b5f6 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -1,8 +1,9 @@ +import { UploadState } from '$lib/models/upload-asset'; import { uploadAssetsStore } from '$lib/stores/upload'; import { addAssetsToAlbum } from '$lib/utils/asset-utils'; -import { api, type AssetFileUploadResponseDto } from '@api'; -import { UploadState } from '$lib/models/upload-asset'; import { ExecutorQueue } from '$lib/utils/executor-queue'; +import { api, type AssetFileUploadResponseDto } from '@api'; +import { getSupportedMediaTypes } from '@immich/sdk'; import { getServerErrorMessage, handleError } from './handle-error'; let _extensions: string[]; @@ -11,8 +12,8 @@ export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 }); const getExtensions = async () => { if (!_extensions) { - const { data } = await api.serverInfoApi.getSupportedMediaTypes(); - _extensions = [...data.image, ...data.video]; + const { image, video } = await getSupportedMediaTypes(); + _extensions = [...image, ...video]; } return _extensions; }; diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index c9dd89db53..e6e33d0b2f 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,5 +1,5 @@ import type { AssetResponseDto } from '@api'; -import lodash from 'lodash-es'; +import { groupBy, sortBy } from 'lodash-es'; import { DateTime, Interval } from 'luxon'; export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC' }); @@ -45,9 +45,8 @@ export function splitBucketIntoDateGroups( assets: AssetResponseDto[], locale: string | undefined, ): AssetResponseDto[][] { - return lodash - .chain(assets) - .groupBy((asset) => fromLocalDateTime(asset.localDateTime).toLocaleString(groupDateFormat, { locale })) - .sortBy((group) => assets.indexOf(group[0])) - .value(); + const grouped = groupBy(assets, (asset) => + fromLocalDateTime(asset.localDateTime).toLocaleString(groupDateFormat, { locale }), + ); + return sortBy(grouped, (group) => assets.indexOf(group[0])); } diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte new file mode 100644 index 0000000000..c0e957d3bf --- /dev/null +++ b/web/src/routes/(user)/+layout.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/web/src/routes/(user)/albums/+page.ts b/web/src/routes/(user)/albums/+page.ts index 5b12ad2601..5f2ae4abbb 100644 --- a/web/src/routes/(user)/albums/+page.ts +++ b/web/src/routes/(user)/albums/+page.ts @@ -1,10 +1,10 @@ import { authenticate } from '$lib/utils/auth'; -import { api } from '@api'; +import { getAllAlbums } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate(); - const { data: albums } = await api.albumApi.getAllAlbums(); + const albums = await getAllAlbums({}); return { albums, diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 2769aeb806..23497b45d9 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -1,17 +1,21 @@ + {#if viewMode === ViewMode.UNASSIGN_ASSETS} a.id)} @@ -499,24 +546,24 @@
{:else} {#each suggestedPeople as person, index (person.id)} -
handleSuggestPeople(person)} > - -
+ +

{person.name}

+ {/each} {/if}
diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 42f9af0f0f..ea068a7137 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -14,7 +14,6 @@ import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; - import type { AssetResponseDto } from '@api'; import type { PageData } from './$types'; import Icon from '$lib/components/elements/icon.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; @@ -27,15 +26,20 @@ import { preventRaceConditionSearchBar } from '$lib/stores/search.store'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; + import type { AssetResponseDto, SearchResponseDto } from '@immich/sdk'; + import { authenticate } from '$lib/utils/auth'; + import { api } from '@api'; export let data: PageData; + const MAX_ASSET_COUNT = 5000; let { isViewing: showAssetViewer } = assetViewingStore; // The GalleryViewer pushes it's own history state, which causes weird // behavior for history.back(). To prevent that we store the previous page // manually and navigate back to that. let previousRoute = AppRoute.EXPLORE as string; + $: curPage = data.results?.assets.nextPage; $: albums = data.results?.albums.items; const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); @@ -107,6 +111,33 @@ const handleSelectAll = () => { selectedAssets = new Set(searchResultAssets); }; + + export const loadNextPage = async () => { + if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) { + return; + } + + await authenticate(); + let results: SearchResponseDto | null = null; + $page.url.searchParams.set('page', curPage.toString()); + const res = await api.searchApi.search({}, { params: $page.url.searchParams }); + if (searchResultAssets) { + searchResultAssets.push(...res.data.assets.items); + } else { + searchResultAssets = res.data.assets.items; + } + + const assets = { + ...res.data.assets, + items: searchResultAssets, + }; + results = { + assets, + albums: res.data.albums, + }; + + data.results = results; + };
@@ -164,7 +195,12 @@
{#if searchResultAssets && searchResultAssets.length > 0}
- +
{:else}
diff --git a/web/src/routes/(user)/search/+page.ts b/web/src/routes/(user)/search/+page.ts index b6cbac101a..75b9df487f 100644 --- a/web/src/routes/(user)/search/+page.ts +++ b/web/src/routes/(user)/search/+page.ts @@ -1,5 +1,5 @@ import { authenticate } from '$lib/utils/auth'; -import { type SearchResponseDto, api } from '@api'; +import { type AssetResponseDto, type SearchResponseDto, api } from '@api'; import type { PageLoad } from './$types'; import { QueryParameter } from '$lib/constants'; @@ -10,8 +10,18 @@ export const load = (async (data) => { url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined; let results: SearchResponseDto | null = null; if (term) { - const { data } = await api.searchApi.search({}, { params: url.searchParams }); - results = data; + const res = await api.searchApi.search({}, { params: data.url.searchParams }); + let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items; + if (items) { + items.push(...res.data.assets.items); + } else { + items = res.data.assets.items; + } + const assets = { ...res.data.assets, items }; + results = { + assets, + albums: res.data.albums, + }; } return { diff --git a/web/src/routes/(user)/share/[key]/+page.ts b/web/src/routes/(user)/share/[key]/+page.ts index 47b52f1f88..e43f81f914 100644 --- a/web/src/routes/(user)/share/[key]/+page.ts +++ b/web/src/routes/(user)/share/[key]/+page.ts @@ -1,4 +1,4 @@ -import { getAuthUser } from '$lib/utils/auth'; +import { authenticate } from '$lib/utils/auth'; import { api, ThumbnailFormat } from '@api'; import type { AxiosError } from 'axios'; import type { PageLoad } from './$types'; @@ -6,7 +6,7 @@ import { error as throwError } from '@sveltejs/kit'; export const load = (async ({ params }) => { const { key } = params; - await getAuthUser(); + await authenticate({ public: true }); try { const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key }); diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index c244d5b815..602a3a2e01 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -3,38 +3,25 @@ import empty2Url from '$lib/assets/empty-2.svg'; import AlbumCard from '$lib/components/album-page/album-card.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; - import { - notificationController, - NotificationType, - } from '$lib/components/shared-components/notification/notification'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import { AppRoute } from '$lib/constants'; - import { api } from '@api'; - import { flip } from 'svelte/animate'; - import type { PageData } from './$types'; + import { createAlbum } from '@immich/sdk'; import { mdiLink, mdiPlusBoxOutline } from '@mdi/js'; - import Icon from '$lib/components/elements/icon.svelte'; + import { flip } from 'svelte/animate'; + import { handleError } from '../../../lib/utils/handle-error'; + import type { PageData } from './$types'; export let data: PageData; const createSharedAlbum = async () => { try { - const { data: newAlbum } = await api.albumApi.createAlbum({ - createAlbumDto: { - albumName: '', - }, - }); - + const newAlbum = await createAlbum({ createAlbumDto: { albumName: '' } }); goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); } catch (error) { - notificationController.show({ - message: 'Error creating album, check console for more details', - type: NotificationType.Error, - }); - - console.log('Error [createAlbum]', error); + handleError(error, 'Unable to create album'); } }; diff --git a/web/src/routes/(user)/sharing/+page.ts b/web/src/routes/(user)/sharing/+page.ts index 5f0291ed33..2fa8415e7c 100644 --- a/web/src/routes/(user)/sharing/+page.ts +++ b/web/src/routes/(user)/sharing/+page.ts @@ -1,11 +1,11 @@ import { authenticate } from '$lib/utils/auth'; -import { api } from '@api'; +import { getAllAlbums, getPartners } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate(); - const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true }); - const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' }); + const sharedAlbums = await getAllAlbums({ shared: true }); + const partners = await getPartners({ direction: 'shared-with' }); return { sharedAlbums, diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 7cd37e328f..3dc126d7b6 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -1,29 +1,29 @@ @@ -154,7 +134,3 @@ {#if $user?.isAdmin} {/if} - -{#if $page.route.id?.includes('(user)')} - -{/if} diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index 38c5bff013..3f66cc9f41 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -1,20 +1,20 @@ import { AppRoute } from '$lib/constants'; +import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; -import { api } from '../api'; -import { isLoggedIn } from '../lib/utils/auth'; +import { loadUser } from '../lib/utils/auth'; import type { PageLoad } from './$types'; export const ssr = false; export const csr = true; export const load = (async () => { - const authenticated = await isLoggedIn(); + const authenticated = await loadUser(); if (authenticated) { redirect(302, AppRoute.PHOTOS); } - const { data } = await api.serverInfoApi.getServerConfig(); - if (data.isInitialized) { + const { isInitialized } = await getServerConfig(); + if (isInitialized) { // Redirect to login page if there exists an admin account (i.e. server is initialized) redirect(302, AppRoute.AUTH_LOGIN); } diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index e71278fd42..6e9582a5ec 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -1,13 +1,14 @@ @@ -158,8 +176,8 @@
- {#each settings as { item, title, subtitle, isOpen }} - + {#each settings as { item, title, subtitle, key }} + handleSave(detail)} diff --git a/web/src/routes/admin/system-settings/+page.ts b/web/src/routes/admin/system-settings/+page.ts index d86b4c3688..144ac2af90 100644 --- a/web/src/routes/admin/system-settings/+page.ts +++ b/web/src/routes/admin/system-settings/+page.ts @@ -1,10 +1,10 @@ import { authenticate } from '$lib/utils/auth'; -import { api } from '@api'; +import { getConfig } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); - const { data: configs } = await api.systemConfigApi.getConfig(); + const configs = await getConfig(); return { configs, diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 812814c835..73237915b6 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -1,20 +1,20 @@ diff --git a/web/src/routes/admin/user-management/+page.ts b/web/src/routes/admin/user-management/+page.ts index 6fbb435bf5..e93cdc3c44 100644 --- a/web/src/routes/admin/user-management/+page.ts +++ b/web/src/routes/admin/user-management/+page.ts @@ -1,11 +1,11 @@ import { authenticate, requestServerInfo } from '$lib/utils/auth'; -import { api } from '@api'; +import { getAllUsers } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); await requestServerInfo(); - const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false }); + const allUsers = await getAllUsers({ isAll: false }); return { allUsers, diff --git a/web/src/routes/auth/change-password/+page.ts b/web/src/routes/auth/change-password/+page.ts index 749c884bf6..37ab1415e3 100644 --- a/web/src/routes/auth/change-password/+page.ts +++ b/web/src/routes/auth/change-password/+page.ts @@ -2,11 +2,12 @@ import { AppRoute } from '$lib/constants'; import { authenticate } from '$lib/utils/auth'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -import { getSavedUser } from '$lib/stores/user.store'; +import { get } from 'svelte/store'; +import { user } from '$lib/stores/user.store'; export const load = (async () => { await authenticate(); - if (!getSavedUser().shouldChangePassword) { + if (!get(user).shouldChangePassword) { redirect(302, AppRoute.PHOTOS); } diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 7ead2e2d93..2c19b5550a 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -5,7 +5,7 @@ import { AppRoute } from '$lib/constants'; import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { resetSavedUser } from '$lib/stores/user.store'; - import { api } from '@api'; + import { logout } from '@immich/sdk'; import type { PageData } from './$types'; export let data: PageData; @@ -13,7 +13,7 @@ afterNavigate(async ({ from }) => { if (from?.url?.pathname === AppRoute.AUTH_CHANGE_PASSWORD) { resetSavedUser(); - await api.authenticationApi.logout(); + await logout(); } }); diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 25d92f605a..c24eea46bc 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,11 +1,11 @@ import { AppRoute } from '$lib/constants'; -import { api } from '@api'; +import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; export const load = (async () => { - const { data } = await api.serverInfoApi.getServerConfig(); - if (!data.isInitialized) { + const { isInitialized } = await getServerConfig(); + if (!isInitialized) { // Admin not registered redirect(302, AppRoute.AUTH_REGISTER); } diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index fbdd24fcc9..4cf42d4d3d 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -1,11 +1,11 @@