diff --git a/.dockerignore b/.dockerignore index 0743f25..60ff264 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,13 @@ *.exe *.log -*.pyc *.token -.hypothesis/ -.pytest_cache/ -config/ -doc_templates/ -log/ +/config/ +/db/ +/doc_templates/ +/hentai/ +/log/ +/target/ -.env docker-image.tar -tests/test.py \ No newline at end of file +rustfmt.toml \ No newline at end of file diff --git a/.github/workflows/on tag deploy on GitHub.yaml b/.github/workflows/on tag deploy on GitHub.yaml new file mode 100644 index 0000000..e7449e9 --- /dev/null +++ b/.github/workflows/on tag deploy on GitHub.yaml @@ -0,0 +1,237 @@ +name: "On Tag Deploy on GitHub" +env: + REPO_NAME: "nhentai_archivist" + RUN_TESTS: true + RUST_VERSION: "1.80" +on: + push: + tags: + # - "[0-9]+.[0-9]+.[0-9]+" + - "*" # execute every time tag is pushed + + +jobs: + datetime: + name: "Get Current Datetime" + env: + working-directory: ${{github.workspace}} + runs-on: "ubuntu-latest" + + steps: + - name: "NOW" + id: "now" + run: "echo \"NOW=$(date +'%Y-%m-%dT%H:%M:%S')\" >> $GITHUB_OUTPUT" # get datetime, save in NOW, push to output + + - name: "TODAY" + id: "today" + run: "echo \"TODAY=$(date +'%Y-%m-%d')\" >> $GITHUB_OUTPUT" # get date, save in TODAY, push to output + + outputs: # set step output as job output so other jobs can access + NOW: ${{steps.now.outputs.NOW}} + TODAY: ${{steps.today.outputs.TODAY}} + + + test: + name: "Run Tests" + env: + working-directory: ${{github.workspace}} + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout Repository" + uses: "actions/checkout@v4" # makes repository structure available + + - name: "Install Rust" + uses: "actions-rust-lang/setup-rust-toolchain@v1" + with: + toolchain: ${{env.RUST_VERSION}} + + - name: "Check Project Version and Tag Match" + run: | + project_version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') + tag=${GITHUB_REF#refs/tags/*} + if [ "$project_version" == "$tag" ]; then + exit 0 + else + exit 1 + fi + + - name: "Run Tests" + if: ${{env.RUN_TESTS == 'true'}} + run: "cargo test" + + + create_release: + name: "Create Release on GitHub" + env: + working-directory: ${{github.workspace}} + needs: ["datetime", "test"] + runs-on: "ubuntu-latest" + + steps: + - name: "Create Release" + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + id: "create_release" + uses: "actions/create-release@v1" # function that creates release + with: # parameters + body: # release text + draft: false + prerelease: false + release_name: "${{needs.datetime.outputs.TODAY}} ${{env.REPO_NAME}} ${{github.ref}}" # release title + tag_name: ${{github.ref}} # release tag + + outputs: + github_release: ${{steps.create_release.outputs.upload_url}} + + + build_executables: + name: "Build Executable for ${{matrix.os}}" + env: + working-directory: ${{github.workspace}} + runs-on: "ubuntu-latest" + strategy: + matrix: + os: ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-gnu"] + + steps: + - name: "Checkout Repository" + uses: "actions/checkout@v4" # makes repository structure available + + - name: "Install Rust" + uses: "actions-rust-lang/setup-rust-toolchain@v1" + with: + target: ${{matrix.os}} + toolchain: ${{env.RUST_VERSION}} + + - name: "Install cross" # install cross for cross-compilation + run: "cargo install cross" + + - name: "Compile" + run: "cross build --release --target ${{matrix.os}}" + + - name: "Cache Executable" + if: ${{matrix.os != 'x86_64-pc-windows-gnu'}} + uses: "actions/cache/save@v4" + with: + key: ${{matrix.os}} + path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}" + + - name: "Cache Executable" + if: ${{matrix.os == 'x86_64-pc-windows-gnu'}} + uses: "actions/cache/save@v4" + with: + key: ${{matrix.os}} + path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}.exe" # windows executable has .exe extension + + + build_docker_image: + name: "Build Docker Image" + env: + working-directory: ${{github.workspace}} + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout Repository" + uses: "actions/checkout@v4" # makes repository structure available + + - name: "Install Docker" + uses: "docker/setup-buildx-action@v1" + + - name: "Create \"./target/\" Directory" + run: "mkdir -p \"./target/\"" + + - name: "Compile" + run: "IMAGE_NAME=\"9-FS/${{env.REPO_NAME}}:latest\" && docker build -t \"${IMAGE_NAME@L}\" --no-cache ." + + - name: "Save Docker Image" + run: "IMAGE_NAME=\"9-FS/${{env.REPO_NAME}}:latest\" && docker save \"${IMAGE_NAME@L}\" > \"./target/docker-image.tar\"" + + - name: "Cache Docker Image" + uses: "actions/cache/save@v4" + with: + key: "docker" + path: "./target/docker-image.tar" + + + deploy_executables: + name: "Deploy Executable for ${{matrix.os}} on GitHub" + env: + working-directory: ${{github.workspace}} + needs: ["build_executables", "create_release", "datetime", "test"] + runs-on: "ubuntu-latest" + strategy: + matrix: + os: ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-gnu"] + + steps: + - name: "Load Executable" + if: ${{matrix.os != 'x86_64-pc-windows-gnu'}} + uses: "actions/cache/restore@v4" + with: + key: ${{matrix.os}} + path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}" + + - name: "Load Executable" + if: ${{matrix.os == 'x86_64-pc-windows-gnu'}} + uses: "actions/cache/restore@v4" + with: + key: ${{matrix.os}} + path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}.exe" + + - name: "Parse Tag" + id: "parse_tag" + run: "echo \"tag=${GITHUB_REF#refs/tags/*}\" >> $GITHUB_OUTPUT" # parse tag because github.ref provides tag as f"refs/tags/{tag}", in create_release it is parsed automatically idk + shell: "bash" # must be bash even on windows, because command to apply value to variable works differently in powershell + + - name: "Attach Executable to Release" + if: ${{matrix.os != 'x86_64-pc-windows-gnu'}} + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + uses: "actions/upload-release-asset@v1" + with: + asset_content_type: "application" + asset_name: "${{needs.datetime.outputs.TODAY}} ${{env.REPO_NAME}} ${{steps.parse_tag.outputs.tag}} ${{matrix.os}}" + asset_path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}" + upload_url: ${{needs.create_release.outputs.github_release}} + + - name: "Attach Executable to Release" + if: ${{matrix.os == 'x86_64-pc-windows-gnu'}} + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + uses: "actions/upload-release-asset@v1" + with: + asset_content_type: "application" + asset_name: "${{needs.datetime.outputs.TODAY}} ${{env.REPO_NAME}} ${{steps.parse_tag.outputs.tag}} ${{matrix.os}}.exe" + asset_path: "./target/${{matrix.os}}/release/${{env.REPO_NAME}}.exe" + upload_url: ${{needs.create_release.outputs.github_release}} + + + deploy_docker_image: + name: "Deploy Docker Image on GitHub" + env: + working-directory: ${{github.workspace}} + needs: ["build_docker_image", "create_release", "datetime", "test"] + runs-on: "ubuntu-latest" + + steps: + - name: "Load Docker Image" + uses: "actions/cache/restore@v4" + with: + key: "docker" + path: "./target/docker-image.tar" + + - name: "Parse Tag" + id: "parse_tag" + run: "echo \"tag=${GITHUB_REF#refs/tags/*}\" >> $GITHUB_OUTPUT" # parse tag because github.ref provides tag as f"refs/tags/{tag}", in create_release it is parsed automatically idk + shell: "bash" # must be bash even on windows, because command to apply value to variable works differently in powershell + + - name: "Attach Docker Image to Release" + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + uses: "actions/upload-release-asset@v1" + with: + asset_content_type: "application" + asset_name: "${{needs.datetime.outputs.TODAY}} ${{env.REPO_NAME}} ${{steps.parse_tag.outputs.tag}} docker.tar" + asset_path: "./target/docker-image.tar" + upload_url: ${{needs.create_release.outputs.github_release}} \ No newline at end of file diff --git a/.github/workflows/on tag deploy on Github.yaml b/.github/workflows/on tag deploy on Github.yaml deleted file mode 100644 index f755bd7..0000000 --- a/.github/workflows/on tag deploy on Github.yaml +++ /dev/null @@ -1,277 +0,0 @@ -name: On Tag Deploy on Github -env: - PROJECT_NAME: nHentai to PDF - PYTHON_VERSION: ^3.12.0 - REPO_NAME: 2021-11-15-nHentai-to-PDF - RUN_TESTS: false -on: - push: - tags: - # - "[0-9]+.[0-9]+.[0-9]+" - - "*" # execute every time tag is pushed - -jobs: - datetime: - name: Get Current Datetime - env: - working-directory: ${{github.workspace}} - runs-on: ubuntu-latest - - steps: - - name: NOW - id: now - run: echo "NOW=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_OUTPUT # get datetime, save in NOW, push to output - - - name: TODAY - id: today - run: echo "TODAY=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT # get date, save in TODAY, push to output - - outputs: # set step output as job output so other jobs can access - NOW: ${{steps.now.outputs.NOW}} - TODAY: ${{steps.today.outputs.TODAY}} - - - test: - name: Run Tests - env: - working-directory: ${{github.workspace}} - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 # makes repository structure available - - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: ${{env.PYTHON_VERSION}} - - - name: Update Pip - run: pip install --upgrade pip - - - name: Install Poetry - run: | - pip install poetry - poetry config virtualenvs.in-project true - poetry config repositories.test-pypi https://test.pypi.org/legacy/ - poetry install - - - name: Check Project Version and Tag Match - run: | - project_version=$(poetry version --no-ansi | awk '{print $NF}') - tag=${GITHUB_REF#refs/tags/*} - if [ "$project_version" == "$tag" ]; then - exit 0 - else - exit 1 - fi - - - name: Run Tests - if: ${{env.RUN_TESTS=='true'}} - run: poetry run pytest - - - create_release: - name: Create Release on Github - env: - working-directory: ${{github.workspace}} - needs: [datetime, test] - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 # makes repository structure available - - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: ${{env.PYTHON_VERSION}} - - - name: Update Pip - run: pip install --upgrade pip - - - name: Install Poetry - run: | - pip install poetry - poetry config virtualenvs.in-project true - poetry config repositories.test-pypi https://test.pypi.org/legacy/ - poetry install - - - name: Create Release - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - id: create_release - uses: actions/create-release@v1 # function that creates release - with: # parameters - body: # release text - draft: false - prerelease: false - release_name: ${{needs.datetime.outputs.TODAY}} ${{env.PROJECT_NAME}} ${{github.ref}} # release title - tag_name: ${{github.ref}} # release tag - - outputs: - github_release: ${{steps.create_release.outputs.upload_url}} - - - build_executables: - name: Build Executable for ${{matrix.os}} - env: - working-directory: ${{github.workspace}} - runs-on: ${{matrix.os}} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 # makes repository structure available - - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: ${{env.PYTHON_VERSION}} - - - name: Update Pip - run: pip install --upgrade pip - - - name: Install Poetry - run: | - pip install poetry - poetry config virtualenvs.in-project true - poetry config repositories.test-pypi https://test.pypi.org/legacy/ - poetry install - - - name: Install Pyinstaller - run: poetry run pip install pyinstaller # not `poetry add pyinstaller` because poetry will complain about pyinstaller's python dependency not being met - - - name: Create "./dist/" Directory - run: mkdir -p "./dist/" - - - name: Compile - run: poetry run pyinstaller --onefile "./src/main_outer.py" --clean --name "program" - - - name: Cache Ubuntu Executable - if: ${{matrix.os=='ubuntu-latest'}} - uses: actions/cache/save@v3 - with: - key: program.sh - path: ./dist/program - - - name: Cache Windows Executable - if: ${{matrix.os=='windows-latest'}} - uses: actions/cache/save@v3 - with: - key: program.exe - path: ./dist/program.exe - - - build_docker_image: - name: Build Docker Image - env: - working-directory: ${{github.workspace}} - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 # makes repository structure available - - - name: Install Docker - uses: docker/setup-buildx-action@v1 - - - name: Create "./dist/" Directory - run: mkdir -p "./dist/" - - - name: Compile - run: IMAGE_NAME="9-FS/${{env.REPO_NAME}}:latest" && docker build -t "${IMAGE_NAME@L}" --no-cache . - - - name: Save Docker Image - run: IMAGE_NAME="9-FS/${{env.REPO_NAME}}:latest" && docker save "${IMAGE_NAME@L}" > "./dist/docker-image.tar" - - - name: Cache Docker Image - uses: actions/cache/save@v3 - with: - key: docker-image.tar - path: ./dist/docker-image.tar - - - deploy_executables: - name: Deploy Executable for ${{matrix.os}} on Github - env: - working-directory: ${{github.workspace}} - needs: [build_executables, create_release, datetime, test] - runs-on: ${{matrix.os}} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - - steps: - - name: Load Ubuntu Executable - if: ${{matrix.os=='ubuntu-latest'}} - uses: actions/cache/restore@v3 - with: - key: program.sh - path: ./dist/program - - - name: Load Windows Executable - if: ${{matrix.os=='windows-latest'}} - uses: actions/cache/restore@v3 - with: - key: program.exe - path: ./dist/program.exe - - - name: Parse Tag - id: parse_tag - run: echo "tag=${GITHUB_REF#refs/tags/*}" >> $GITHUB_OUTPUT # parse tag because github.ref provides tag as f"refs/tags/{tag}", in create_release it is parsed automatically idk - shell: bash # must be bash even on windows, because command to apply value to variable works differently in powershell - - - name: Attach Ubuntu Executable to Release - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - if: ${{matrix.os=='ubuntu-latest'}} - uses: actions/upload-release-asset@v1 - with: - asset_content_type: application - asset_name: ${{needs.datetime.outputs.TODAY}} ${{env.PROJECT_NAME}} ${{steps.parse_tag.outputs.tag}}.sh - asset_path: ./dist/program - upload_url: ${{needs.create_release.outputs.github_release}} - - - name: Attach Windows Executable to Release - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - if: ${{matrix.os=='windows-latest'}} - uses: actions/upload-release-asset@v1 - with: - asset_content_type: application - asset_name: ${{needs.datetime.outputs.TODAY}} ${{env.PROJECT_NAME}} ${{steps.parse_tag.outputs.tag}}.exe - asset_path: ./dist/program.exe - upload_url: ${{needs.create_release.outputs.github_release}} - - - deploy_docker_image: - name: Deploy Docker Image on Github - env: - working-directory: ${{github.workspace}} - needs: [build_docker_image, create_release, datetime, test] - runs-on: ubuntu-latest - - steps: - - name: Load Docker Image - uses: actions/cache/restore@v3 - with: - key: docker-image.tar - path: ./dist/docker-image.tar - - - name: Parse Tag - id: parse_tag - run: echo "tag=${GITHUB_REF#refs/tags/*}" >> $GITHUB_OUTPUT # parse tag because github.ref provides tag as f"refs/tags/{tag}", in create_release it is parsed automatically idk - shell: bash # must be bash even on windows, because command to apply value to variable works differently in powershell - - - name: Attach Docker Image to Release - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - uses: actions/upload-release-asset@v1 - with: - asset_content_type: application - asset_name: ${{needs.datetime.outputs.TODAY}} ${{env.PROJECT_NAME}} ${{steps.parse_tag.outputs.tag}}.tar - asset_path: ./dist/docker-image.tar - upload_url: ${{needs.create_release.outputs.github_release}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0743f25..60ff264 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,13 @@ *.exe *.log -*.pyc *.token -.hypothesis/ -.pytest_cache/ -config/ -doc_templates/ -log/ +/config/ +/db/ +/doc_templates/ +/hentai/ +/log/ +/target/ -.env docker-image.tar -tests/test.py \ No newline at end of file +rustfmt.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dda12b7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2920 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colored" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" +dependencies = [ + "is-terminal", + "lazy_static", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie", + "idna 0.5.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "colored", + "log", +] + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "load_config" +version = "1.0.0" +source = "git+https://github.com/9-FS/load_config?tag=1.0.0#aeafea6bbe56d755ff68130ea6a403d6eebb11db" +dependencies = [ + "figment", + "log", + "serde", + "toml", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nhentai_archivist" +version = "3.0.0" +dependencies = [ + "chrono", + "load_config", + "log", + "reqwest", + "scaler", + "serde", + "serde-xml-rs", + "serde_json", + "setup_logging", + "sqlx", + "thiserror", + "tokio", + "zip", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scaler" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e160894bc80fee786ed90b4d2a96178894dc037957ac15f47f799ae99646242" +dependencies = [ + "log", +] + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "setup_logging" +version = "2.1.0" +source = "git+https://github.com/9-FS/setup_logging?tag=2.1.0#692cfdcb6422427e55d04170b065f7c0dd5bad95" +dependencies = [ + "chrono", + "fern", + "log", + "unicode-segmentation", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..25d3505 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = ["9-FS "] +categories = [] +description = "" +edition = "2021" +exclude = [".github/", "readme.pdf"] # additional to .gitignore +keywords = [] +license = "MIT" +name = "nhentai_archivist" +readme = "readme.md" +repository = "https://github.com/9-FS/nhentai_archivist" +version = "3.0.0" + +[dependencies] +chrono = { version = "^0.4.0", features = ["serde"] } +load_config = { git = "https://github.com/9-FS/load_config", tag = "1.0.0", features = [ + "toml_file", +] } +log = "^0.4.0" +reqwest = { version = "^0.12.0", features = ["cookies"] } +scaler = "^1.0.0" +serde = { version = "^1.0.0", features = ["derive"] } +serde-xml-rs = "^0.6.0" +serde_json = "^1.0.0" +setup_logging = { git = "https://github.com/9-FS/setup_logging", tag = "2.1.0" } +sqlx = { version = "^0.8.0", features = [ + "chrono", + "runtime-tokio-rustls", + "sqlite", +] } +thiserror = "^1.0.0" +tokio = { version = "^1.0.0", features = ["rt-multi-thread"] } +zip = "^2.0.0" diff --git a/Dockerfile b/Dockerfile index 87e053b..de33600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,22 @@ -ARG PYTHON_VERSION="3.12" -FROM python:$PYTHON_VERSION +ARG RUST_VERSION="1.80" +FROM rust:$RUST_VERSION -ENV PYTHON_VERSION="3.12" +ENV RUST_VERSION="1.80" WORKDIR "/app/" COPY . . -RUN python${PYTHON_VERSION} -m pip install poetry -RUN poetry config virtualenvs.in-project true -RUN poetry config repositories.test-pypi "https://test.pypi.org/legacy/" -RUN poetry install +RUN cargo build --release -CMD poetry run python${PYTHON_VERSION} "./src/main_outer.py" +CMD "./target/release/nhentai_archivist" # MANUAL BUILD: # build docker image, save in tar, remove image so only tar remains, @L to lowercase -# IMAGE_NAME="9-FS/2021-11-15-nHentai-to-PDF:latest" && docker build -t "${IMAGE_NAME@L}" --no-cache . && docker save "${IMAGE_NAME@L}" > "image.tar" && docker rmi "${IMAGE_NAME@L}" +# IMAGE_NAME="9-FS/nhentai_archivist:latest" && docker build -t "${IMAGE_NAME@L}" --no-cache . && docker save "${IMAGE_NAME@L}" > "docker-image.tar" && docker rmi "${IMAGE_NAME@L}" # on deployment environment load docker image from tar file -# docker load < "/mnt/user/appdata/image.tar" \ No newline at end of file +# docker load < "/mnt/user/appdata/docker-image.tar" \ No newline at end of file diff --git a/db/schema.png b/db/schema.png new file mode 100644 index 0000000..9318fef Binary files /dev/null and b/db/schema.png differ diff --git a/db/schema.txt b/db/schema.txt new file mode 100644 index 0000000..81246ee --- /dev/null +++ b/db/schema.txt @@ -0,0 +1,34 @@ +entity "Hentai" +{ + + id: u32 + + cover_type: String + + media_id: u32 + + num_favorites: u32 + + num_pages: u16 + + page_types: String + + scanlator: Option + + title_english: Option + + title_japanese: Option + + title_pretty: Option + + upload_date: chrono::DateTime +} + + +entity "Tag" +{ + + id: u32 + + name: String + + type: String + + url: String +} + + +entity "Hentai_Tag" +{ + - hentai_id: u32 + - tag_id: u32 +} + + +"Hentai" <-- "Hentai_Tag" +"Tag" <-- "Hentai_Tag" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index c05f993..34ff50c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,24 +1,25 @@ version: "3" services: - 2021-11-15-nhentai-to-pdf: - container_name: "2021-11-15-nHentai-to-PDF" - image: "9-fs/2021-11-15-nhentai-to-pdf:latest" + nhentai_archivist: + container_name: "nhentai_archivist" + image: "9-fs/nhentai_archivist:latest" environment: HOST_OS: "Unraid" + PGID: 100 + PUID: 99 TZ: "UTC" - CF_CLEARANCE: "" - CSRFTOKEN: "" - USER_AGENT: "" + UMASK: 000 volumes: - - "/mnt/user/appdata/2021-11-15-nhentai-to-pdf/config/:/app/config/:rw" - - "/mnt/user/appdata/2021-11-15-nhentai-to-pdf/log/:/app/log/:rw" + - "/mnt/user/appdata/nhentai_archivist/config/:/app/config/:rw" + - "/mnt/user/appdata/nhentai_archivist/db/:/app/db/:rw" + - "/mnt/user/appdata/nhentai_archivist/log/:/app/log/:rw" - "/mnt/user/media/hentai/:/app/hentai/:rw" network_mode: "bridge" deploy: resources: - limits: - memory: "20G" + limits: + memory: "1G" user: "99:100" networks: {} \ No newline at end of file diff --git a/licence.md b/licence.md index 4e0866a..0b32f48 100644 --- a/licence.md +++ b/licence.md @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 구FS - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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 +MIT License + +Copyright (c) 2024 구FS + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index d6c35b7..0000000 --- a/poetry.lock +++ /dev/null @@ -1,491 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "certifi" -version = "2024.7.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "hypothesis" -version = "6.108.2" -description = "A library for property-based testing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "hypothesis-6.108.2-py3-none-any.whl", hash = "sha256:2341d21d0e956bad8bd6269aa7d4f3233507f3ed52380c60ceb2f8b71f87a8e5"}, - {file = "hypothesis-6.108.2.tar.gz", hash = "sha256:62cf1c16bd98548b6a84007c5fb8cf6d9cb358dad870adb4f236c795ef162fdd"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -sortedcontainers = ">=2.1.0,<3.0.0" - -[package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.61)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.7)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] -cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] -codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.61)", "hypothesis-crosshair (>=0.0.7)"] -dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=3.2)"] -dpcontracts = ["dpcontracts (>=0.4)"] -ghostwriter = ["black (>=19.10b0)"] -lark = ["lark (>=0.10.1)"] -numpy = ["numpy (>=1.17.3)"] -pandas = ["pandas (>=1.1)"] -pytest = ["pytest (>=4.6)"] -pytz = ["pytz (>=2014.1)"] -redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "kfsconfig" -version = "2.3.1" -description = "" -optional = false -python-versions = "<4.0.0,>=3.12.0" -files = [ - {file = "kfsconfig-2.3.1-py3-none-any.whl", hash = "sha256:2c665396f081aeab3938b3a28d0c0c8b54c4543a522685a0a893a78b93e2eaaf"}, - {file = "kfsconfig-2.3.1.tar.gz", hash = "sha256:66e7885d2b6b28c4fe8983fe65085ad9f2365dd973b4b1b113efc7295243246b"}, -] - -[package.dependencies] -kfslog = ">=2.0.0,<3.0.0" - -[[package]] -name = "kfsfstr" -version = "1.2.0" -description = "" -optional = false -python-versions = "<4.0.0,>=3.12.0" -files = [ - {file = "kfsfstr-1.2.0-py3-none-any.whl", hash = "sha256:9980387ceb46aa3560b5d890dca886232f1e0502a75fd650fae33d411842912d"}, - {file = "kfsfstr-1.2.0.tar.gz", hash = "sha256:a819f170459ce69d43c2257df0b1347a2b210fb4a57d2df55fd45b79b16b8081"}, -] - -[package.dependencies] -colorama = ">=0.4.0,<0.5.0" - -[[package]] -name = "kfslog" -version = "2.0.1" -description = "" -optional = false -python-versions = "<4.0.0,>=3.12.0" -files = [ - {file = "kfslog-2.0.1-py3-none-any.whl", hash = "sha256:142a934a01eaeca786b446c16b725bf0388ff3087184ceefe807dfe82a4cecc5"}, - {file = "kfslog-2.0.1.tar.gz", hash = "sha256:df2f0f852a92a4db8316c4d4f7208fc9d6cd9b68e4b44111887bc602d4328500"}, -] - -[package.dependencies] -colorama = ">=0.4.6,<0.5.0" -kfsfstr = ">=1.1.0,<2.0.0" -result = ">=0.16.0,<0.17.0" - -[[package]] -name = "kfsmedia" -version = "2.5.2" -description = "" -optional = false -python-versions = "<4.0.0,>=3.12.0" -files = [ - {file = "kfsmedia-2.5.2-py3-none-any.whl", hash = "sha256:7c12018d0f002554cef128a32fc22dcb419cd7f6d9b6f28b3b1c3a20aca1f875"}, - {file = "kfsmedia-2.5.2.tar.gz", hash = "sha256:9058819a574faf25e076caeaf77390aa78838f77789ab10bba4a07efc68cb332"}, -] - -[package.dependencies] -kfsfstr = ">=1.1.0,<2.0.0" -kfslog = ">=2.0.0,<3.0.0" -olefile = ">=0.47,<0.48" -pebble = ">=5.0.0,<6.0.0" -pillow = ">=10.2.0,<11.0.0" -requests = ">=2.31.0,<3.0.0" - -[[package]] -name = "olefile" -version = "0.47" -description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f"}, - {file = "olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pebble" -version = "5.0.7" -description = "Threading and multiprocessing eye-candy." -optional = false -python-versions = ">=3.6" -files = [ - {file = "Pebble-5.0.7-py3-none-any.whl", hash = "sha256:f1742f2a62e8544e722c7b387211fb1a06038ca8cda322e5d55c84c793fd8d7d"}, - {file = "Pebble-5.0.7.tar.gz", hash = "sha256:2784c147766f06388cea784084b14bec93fdbaa793830f1983155aa330a2a6e4"}, -] - -[[package]] -name = "pillow" -version = "10.4.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pytest" -version = "8.2.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2.0" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "result" -version = "0.16.1" -description = "A Rust-like result type for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "result-0.16.1-py3-none-any.whl", hash = "sha256:47598bb9bde342251f5b0f49e3637cd30dc144c086b29dc58fff458477650669"}, - {file = "result-0.16.1.tar.gz", hash = "sha256:379f0233cdf8cf157588c77bb2ab1ac367431d9bf6e8456a336b026d528cee0d"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.12.0" -content-hash = "636a77f1e93bf4a62d9817a2a5752f0e264c80210ef50e0218f92e96e6897bb7" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index adf5414..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[tool.poetry] -authors = ["9FS "] -description = "" -license = "MIT" -name = "" -package-mode = false # this is an application, not library -readme = "readme.typ" -repository = "https://github.com/9-FS/nhentai_to_pdf" -version = "2.3.1" - -[tool.poetry.dependencies] -kfsconfig = "^2.0.0" -kfsfstr = "^1.0.0" -kfslog = "^2.0.0" -kfsmedia = "^2.5.0" -python = "^3.12.0" - -[tool.poetry.group.dev.dependencies] -hypothesis = "^6.0.0" -pytest = "^8.0.0" - -[build-system] -build-backend = "poetry.core.masonry.api" -requires = ["poetry-core"] diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a1ae80c --- /dev/null +++ b/readme.md @@ -0,0 +1,77 @@ +# nhentai_archivist +## Introduction + +NHentai Archivist is a tool to download hentai from https://nhentai.net and convert them to CBZ files. It can be used from quickly downloading a few hentai specified in the console, downloading a few hundred hentai specified in a downloadme.txt, up to automatically keeping a massive self hosted library up-to-date by automatically generating a downloadme.txt from a search by tag. (For that use-case it has been optimised to tag the CBZ files in a way that [Komga](https://komga.org/) in [oneshot mode](https://komga.org/docs/guides/oneshots) interprets everything correctly.) + +Why CBZ? CBZ is a widespread standard and basically just a ZIP file containing images and a metadata file. This enables NHentai Archivist to **keep the tags** which wouldn't be possible with PDF as far as I know. + +Big thanks go out to [h3nTa1m4st3r_xx69](https://github.com/sam-k0), who helped me using nhentai's completely undocumented API. Without him this project could not have been reactivated. +I'm happy about anyone who finds my software useful and feedback is also always welcome. Happy downloading~ + +## Installation + +1. Execute the program once and create a default `./config/.env`. +1. Confirm the database directory in `DATABASE_URL` exists already, which is `./db/` by default. It is not created automatically because remote URL are supported. The database file will be created automatically. +1. If you have problems with nhentai's bot protection (error 403), set `CF_CLEARANCE`, `CSRFTOKEN`, and `USER_AGENT`. + + ### Firefox + + 1. Go to https://nhentai.net/. Clear the cloudflare prompt. + 1. Open the developer console with F12. + 1. Go to the tab "Storage". On the left side expand "Cookies". Click on "https://nhentai.net". + 1. Copy the cookie values into `./config/.env`. + 1. Go to https://www.whatismybrowser.com/detect/what-is-my-user-agent/ and copy your user agent into `./config/.env`. + + ### Google Chrome + + 1. Go to https://nhentai.net/. Clear the cloudflare prompt. + 1. Open the developer console with F12. + 1. Go to the tab "Application". On the left side under "Storage", expand "Cookies". Click on "https://nhentai.net". + 1. Copy the cookie values into `./config/.env`. + 1. Go to https://www.whatismybrowser.com/detect/what-is-my-user-agent/ and copy your user agent into `./config/.env`. + + > [!NOTE] + > If nhentai has "under attack" mode enabled, setting `CF_CLEARANCE` seems to be required daily. + +Further settings: + +- `NHENTAI_TAG` + + Setting this will trigger "server mode". If no file at `DOWNLOADME_FILEPATH` is found, it will generate one by searching for the tag specified. After all hentai on the downloadme have been downloaded, it will wait for `SLEEP_INTERVAL` seconds and restart the search. This is useful to keep a self-hosted library up-to-date with the latest releases from the specified tag. + + Examples: + + - "NHENTAI_TAG = language:english": all english hentai + - "NHENTAI_TAG = tag:big-breasts": all hentai with the tag "big breasts" + - "NHENTAI_TAG = parody:kono-subarashii-sekai-ni-syukufuku-o": all hentai from the anime "Kono Subarashii Sekai ni Syukufuku o" + - "NHENTAI_TAG = artist:shindol": all hentai by Shindol + + More information can be found [here](https://nhentai.net/info/). + +- `LIBRARY_PATH` + + This is the directory temporary images and finished CBZ files are download to. By default, it will download to `./hentai/`. + +- `LIBRARY_SPLIT` + + Setting this to a value other than 0 splits the library at `LIBRARY_PATH` into sub-directories with a maximum number of `LIBRARY_SPLIT` hentai allowed per sub-directory. It is recommended if the number of hentai in 1 directory starts to affect file explorer performance. This _should_ not affect you if you plan to keep less than 10.000 files in your `LIBRARY_PATH` directory, otherwise the recommended setting is "LIBRARY_SPLIT = 10000". + + + +## Usage +### Download a Few Quickly + +1. Run the program as is. Do not specifiy `NHENTAI_TAG`, and make sure there is no file at `DOWNLOADME_FILEPATH`. +1. Enter the nhentai id you want to download separated by spaces. + +### Download a Bit More From a File + +1. Do not specifiy `NHENTAI_TAG`. +1. Create a file at `DOWNLOADME_FILEPATH` and enter the nhentai id you want to download separated by linebreaks. + +### Ich mein's ernst: Keeping a Self-Hosted Library Up-to-Date + +1. Set `NHENTAI_TAG` to the tag you want to keep up-to-date. For a very comprehensive library, set it to "NHENTAI_TAG = language:english". +1. Make sure there is no file at `DOWNLOADME_FILEPATH` otherwise it will be downloaded first. +1. Consider setting `LIBRARY_SPLIT` to a value other than 0 if you plan to keep more than 10.000 files in your `LIBRARY_PATH` directory. +1. Consider setting `SLEEP_INTERVAL` to wait a bit between searches. \ No newline at end of file diff --git a/readme.pdf b/readme.pdf deleted file mode 100644 index dff259d..0000000 Binary files a/readme.pdf and /dev/null differ diff --git a/readme.typ b/readme.typ deleted file mode 100644 index 4ee05df..0000000 --- a/readme.typ +++ /dev/null @@ -1,63 +0,0 @@ -#import "@preview/wrap-it:0.1.0": wrap-content // https://github.com/ntjess/wrap-it/blob/main/docs/manual.pdf -#import "./doc_templates/src/note.typ": * -#import "./doc_templates/src/style.typ": set_style - - -#show: doc => set_style( - topic: "nhentai_to_pdf", - author: "구FS", - language: "EN", - doc -) -#set text(size: 3.5mm) - - -#align(center, text(size: 2em, weight: "bold")[nhentai_to_pdf]) -#line(length: 100%, stroke: 0.3mm) -\ -\ -= Introduction - -This is the nHentai downloader I wrote to archive as much of the #link("https://nhentai.net/language/english/popular")[english nHentai library] as I can. That's why from the beginning it has been designed with big data sizes in mind and, for example, uses multithreaded downloads to download more than 1 image at once. Still, I wanted to keep it as simple code-wise and as easy to use as I can; hope I succeeded with that. - -Big thanks go out to #link("https://github.com/sam-k0")[h3nTa1m4st3r_xx69], who helped me using nhentai's completely undocumented API. Without him this project could not have been reactivated. -I'm happy about anyone who finds my software useful and feedback is also always welcome. Happy downloading~ - -= Table of Contents - -#outline() - -#pagebreak(weak: true) - -= Installation -== Firefox - -+ Execute the program once. This will create a default `./config/config.json`. - + Set `LIBRARY_PATH` to the directory you want to download to. By default, it will download to `./hentai/`. - + Set `LIBRARY_SPLIT` if you want to split your library into sub-directories. The number specifies the maximum number of hentai to allow per sub-directory. Set "0" if you want to disable splitting your library into sub-directories. It is disabled by default and only recommended if the number of hentai in 1 directory starts to affect file explorer performance. This _should_ not affect you if you have 10.000 files or less in 1 directory. -+ Execute the program again. This will create a default `./.env`. - + Go to https://nhentai.net/. Clear the cloudflare prompt. - + Open the developer console with F12. - + Go to the tab "Storage". On the left side expand "Cookies". Click on "https://nhentai.net". - + Copy the cookie values into the `./.env`. - + Go to https://www.whatismybrowser.com/detect/what-is-my-user-agent/ and copy your user agent into `./.env`. - -== Google Chrome - -+ Execute the program once. This will create a default `./config/config.json`. - + Set `LIBRARY_PATH` to the directory you want to download to. By default, it will download to `./hentai/`. - + Set `LIBRARY_SPLIT` if you want to split your library into sub-directories. The number specifies the maximum number of hentai to allow per sub-directory. Set "0" if you want to disable splitting your library into sub-directories. It is disabled by default and only recommended if the number of hentai in 1 directory starts to affect file explorer performance. This _should_ not affect you if you have 10.000 files or less in 1 directory. -+ Execute the program again. This will create a default `./.env`. - + Go to https://nhentai.net/. Clear the cloudflare prompt. - + Open the developer console with F12. - + Go to the tab "Application". On the left side under "Storage", expand "Cookies". Click on "https://nhentai.net". - + Copy the cookie values into the `./.env`. - + Go to https://www.whatismybrowser.com/detect/what-is-my-user-agent/ and copy your user agent into `./.env`. - -#info()[Setting cookies seems to be required daily nowadays.] - -#pagebreak(weak: true) - -= Usage - -Choose hentai by nHentai ID to download and convert to PDF. You can either load ID from a `./config/downloadme.txt` seperated by linebreaks or directly enter ID into the console separated by spaces. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a3c7981..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -certifi==2024.7.4 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -charset-normalizer==3.3.2 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -colorama==0.4.6 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -idna==3.7 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -kfsconfig==2.3.1 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -kfsfstr==1.2.0 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -kfslog==2.0.1 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -kfsmedia==2.5.2 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -olefile==0.47 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -pebble==5.0.7 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -pillow==10.4.0 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -requests==2.32.3 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -result==0.16.1 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" -urllib3==2.2.2 ; python_full_version >= "3.12.0" and python_full_version < "4.0.0" diff --git a/src/Hentai.py b/src/Hentai.py deleted file mode 100644 index 7ae81af..0000000 --- a/src/Hentai.py +++ /dev/null @@ -1,288 +0,0 @@ -# Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. -import dataclasses -import inspect -import json -from KFSfstr import KFSfstr -from KFSmedia import KFSmedia -import logging -import os -import random -import re -import requests -import time -import typing - - -@dataclasses.dataclass -class Hentai: - """ - represents an individual hentai from nhentai.net - """ - - galleries: typing.ClassVar[dict[int, list[dict]]]={} # list of already downloaded galleries - galleries_modified: typing.ClassVar[dict[int, bool]]={} # has galleries been modified since last save? - GALLERIES_PATH: typing.ClassVar[str]="./config/" # path to save galleries to - GALLERIES_SPLIT: typing.ClassVar[int]=100000 # split galleries into separate files every 100000 hentai - - - def __init__(self, nhentai_ID: int, cookies: dict[str, str], headers: dict[str, str]): - """ - Constructs a hentai object. Downloads data from the nhentai API. - - Arguments: - - nhentai_ID: the hentai from nhentai.net found here: https://nhentai.net/g/{hentai_ID} - - cookies: cookies to send with the request to bypass bot protection - - headers: user agent to send with the request to bypass bot protection - - Raises: - - requests.HTTPError: Downloading gallery from \"{NHENTAI_GALLERY_API_URL}/{self.ID}\" failed multiple times. - - ValueError: Hentai with ID \"{self.ID}\" does not exist. - """ - - self._fails: list[int] # list of how many times individual page has failed to be downloaded or converted to PDF - self._gallery: dict # gallery from nhentai API, saved to extract data for download later - self._give_up: bool=False # give this hentai up? after failing to download or convert numerous times - self.ID: int # nhentai ID - self.page_amount: int # number of pages - self.title: str # title (unchanged) - - - logging.debug(f"Creating hentai object...") - self.ID=nhentai_ID - self._gallery=self._get_gallery(self.ID, cookies, headers) - self.page_amount=int(self._gallery["num_pages"]) - self.title=self._gallery["title"]["pretty"] - self._fails=[0 for _ in range(self.page_amount)] # initialise with amount of pages number of zeros - logging.debug(f"Created hentai object.") - logging.debug(self.__repr__()) - - return - - - def __str__(self) -> str: - return f"{self.ID}: \"{self.title}\"" - - - @classmethod - def _get_gallery(cls, nhentai_ID: int, cookies: dict[str, str], headers: dict[str, str]) -> dict: - """ - Tries to load nhentai API gallery from class variable first, if unsuccessful from files, if unsuccesful again downloads from nhentai API. - - Arguments: - - nhentai_ID: the hentai from nhentai.net found here: https://nhentai.net/g/{hentai_ID} - - cookies: cookies to send with the request to bypass bot protection - - headers: user agent to send with the request to bypass bot protection - - Returns: - - gallery: gallery from nhentai API - - Raises: - - requests.HTTPError: Downloading gallery from \"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}\" failed multiple times. - - ValueError: Hentai with ID \"{nhentai_ID}\" does not exist. - """ - - gallery: dict # gallery to return - gallery_list_filepath: str=os.path.join(cls.GALLERIES_PATH, f"galleries{nhentai_ID//cls.GALLERIES_SPLIT}.json") # appropiate gallery filepath - gallery_page: requests.Response - NHENTAI_GALLERY_API_URL: str="https://nhentai.net/api/gallery" # URL to nhentai API - - - logging.info(f"Loading gallery {nhentai_ID}...") - if nhentai_ID//cls.GALLERIES_SPLIT in cls.galleries: # if class variable has appropiate gallery list: try to load from class variable - gallery=next((gallery for gallery in cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT] if str(gallery["id"])==str(nhentai_ID)), {}) # try to find gallery with same ID in appropiate gallery list - if gallery!={}: # if gallery found: return - logging.info(f"\rLoaded gallery {nhentai_ID}.") - return gallery - - elif os.path.isfile(gallery_list_filepath)==True: # if gallery could not be loaded from class variable and appropiate galleries file exists: try to load from file - with open(gallery_list_filepath, "rt") as galleries_file: - try: - cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT]=json.loads(galleries_file.read()) # load already downloaded galleries, overwrite or create entry class variable - cls.galleries_modified[nhentai_ID//cls.GALLERIES_SPLIT]=False # overwrite or create netry in modified variable, galleries have not been modified, don't save during next save turn - except ValueError as e: # if file is corrupted: - logging.critical(f"Parsing galleries from \"{gallery_list_filepath}\" failed with {KFSfstr.full_class_name(e)}. Check it for errors.") - raise RuntimeError(f"Error in {Hentai._get_gallery.__name__}{inspect.signature(Hentai._get_gallery)}: Parsing galleries from \"{gallery_list_filepath}\" failed with {KFSfstr.full_class_name(e)}. Check it for errors.") from e - gallery=next((gallery for gallery in cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT] if str(gallery["id"])==str(nhentai_ID)), {}) # try to find gallery with same ID - if gallery!={}: # if gallery found: return - logging.info(f"\rLoaded gallery {nhentai_ID} from \"{gallery_list_filepath}\".") - return gallery - - - logging.info(f"\rDownloading gallery {nhentai_ID} from \"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}\"...") - attempt_no: int=1 - while True: # if nothing locally worked: try to download gallery - try: - gallery_page=requests.get(f"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}", cookies=cookies, headers=headers, timeout=10) - except (requests.exceptions.ConnectionError, requests.Timeout): # if connection error: try again - time.sleep(1) - if attempt_no<3: # try 3 times - continue - else: # if failed 3 times: give up - raise - if gallery_page.status_code==403: # if status code 403 (forbidden): probably cookies and headers not set correctly - logging.critical(f"Downloading gallery {nhentai_ID} from \"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}\" resulted in status code {gallery_page.status_code}. Have you set cookies and headers correctly?") - raise requests.HTTPError(f"Error in {Hentai._get_gallery.__name__}{inspect.signature(Hentai._get_gallery)}: Downloading gallery {nhentai_ID} from \"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}\" resulted in status code {gallery_page.status_code}. Have you set cookies and headers correctly?", response=gallery_page) - if gallery_page.status_code==404: # if status code 404 (not found): hentai does not exist (anymore?) - logging.error(f"Hentai with ID \"{nhentai_ID}\" does not exist.") - raise ValueError(f"Error in {Hentai._get_gallery.__name__}{inspect.signature(Hentai._get_gallery)}: Hentai with ID \"{nhentai_ID}\" does not exist.") - if gallery_page.ok==False: # if status code not ok: try again - time.sleep(1) - if attempt_no<3: # try 3 times - continue - else: # if failed 3 times: give up - raise - - gallery=json.loads(gallery_page.text) - if nhentai_ID//cls.GALLERIES_SPLIT not in cls.galleries: # if gallery list not initialised yet: - cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT]=[] # initialise - if str(gallery["id"]) in [str(gallery["id"]) for gallery in cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT]]: # if gallery already downloaded but adding to class variable requested: something went wrong - logging.critical(f"Gallery {nhentai_ID} has been requested to be added to galleries even though it would result in a duplicate entry.") - raise RuntimeError(f"Error in {Hentai._get_gallery.__name__}{inspect.signature(Hentai._get_gallery)}: Gallery {nhentai_ID} has been requested to be added to galleries even though it would result in a duplicate entry.") - cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT].append(gallery) # append new gallery - cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT]=sorted(cls.galleries[nhentai_ID//cls.GALLERIES_SPLIT], key=lambda gallery: int(gallery["id"])) # sort galleries by ID - cls.galleries_modified[nhentai_ID//cls.GALLERIES_SPLIT]=True # galleries have been modified, save during next save turn - break - logging.info(f"\rDownloaded gallery {nhentai_ID} from \"{NHENTAI_GALLERY_API_URL}/{nhentai_ID}\".") - - return gallery - - - def _increment_fails(self, image_list: list[str]) -> None: - """ - Takes list of filepaths that could not be downloaded or converted and increments appropiate failure counter. - """ - - FAILS_MAX: int=5 # maximum amount of fails before giving up - PATTERNS: list[str]=[ - r"^((.*?[/])?(?P[0-9]+)\.[a-z]+)$", # page URL pattern ending - r"^((.*?[/\\])?[0-9]+-(?P[0-9]+)\.[a-z]+)$", # image filepath pattern ending - ] - re_match: re.Match|None - - - for image in image_list: # for each image: - for pattern in PATTERNS: # with each pattern: - re_match=re.search(pattern, image) # try to parse page number - if re_match!=None: # if page number could be parsed: - self._fails[int(re_match.groupdict()["page_no"])-1]+=1 # increment appropiate fails counter - if FAILS_MAX<=self._fails[int(re_match.groupdict()["page_no"])-1]: # if any counter at maximum fails: - self._give_up=True # give hentai up - break - else: # if page number can't be parsed: - logging.critical(f"Incrementing fails counter of \"{image}\" failed.") # don't know which counter to increment, critical error because should not happen - raise RuntimeError(f"Error in {self._increment_fails.__name__}{inspect.signature(self._increment_fails)}: Incrementing fails counter of \"{image}\" failed.") - - return - - - def download(self, library_path: str, library_split: int) -> bytes: - """ - Downloads the hentai and saves it as PDF at f"./{library_path}/", and also returns it in case needed. If library_split is set, library will be split into subdirectories of maximum library_split many hentai, set 0 to disable. - - Arguments: - - library_path: path to download hentai to - - library_split: split library into subdirectories of maximum this many hentai, 0 to disable - - Returns: - - PDF: finished PDF - - Raises: - - FileExistsError: File \"{PDF_filepath}\" already exists. - - Hentai.DownloadError: - - \"{PDF_filepath}\" already exists as directory. - - Can't generate page URL for {self} page {i+1}, because media type \"{page["t"]}\" is unknown. - - Tried to download and convert hentai {self} several times, but failed. - """ - - images_filepath: list[str]=[] # where to cache downloaded images - MEDIA_TYPES: dict[str, str]={ # parsed image type to file extension - "g": ".gif", - "j": ".jpg", - "p": ".png", - } - pages_URL: list[str]=[] # URL to individual pages to download - PDF: bytes # finished PDF - PDF_filepath: str # where to save downloaded result, ID title pdf, but title maximum 140 characters and without illegal filename characters - TIMEOUT=100 # timeout for downloading images - TITLE_CHARACTERS_FORBIDDEN: str="\\/:*?\"<>|\t\n" # in title forbidden characters - - - for i, page in enumerate(self._gallery["images"]["pages"]): - if page["t"] not in MEDIA_TYPES.keys(): # if media type unknown: - logging.error(f"Can't generate page URL for {self} page {i+1}, because media type \"{page["t"]}\" is unknown.") - raise KFSmedia.DownloadError(f"Error in {self.download.__name__}{inspect.signature(self.download)}: Can't generate page URL for {self} page {i+1}, because media type \"{page["t"]}\" is unknown.") - - pages_URL.append(f"https://i{random.choice(["", "2", "3", "5", "7"])}.nhentai.net/galleries/{self._gallery["media_id"]}/{i+1}{MEDIA_TYPES[page["t"]]}") # URL, use random image server instance to distribute load - images_filepath.append(os.path.join(library_path, str(self.ID), f"{self.ID}-{i+1}{MEDIA_TYPES[page["t"]]}")) # media filepath, but usually image filepath - - PDF_filepath=self.title - for c in TITLE_CHARACTERS_FORBIDDEN: # remove forbidden characters for filenames - PDF_filepath=PDF_filepath.replace(c, "") - PDF_filepath=PDF_filepath[:140] # limit title length to 140 characters - match library_split: - case 0: - PDF_filepath=os.path.join(library_path, f"{self.ID} {PDF_filepath}.pdf") # PDF filepath, splitting library into subdirectories disabled - case library_split if 0 None: - """ - Saves galleries to file. - """ - - for gallery_list_id, gallery_list in cls.galleries.items(): - if cls.galleries_modified[gallery_list_id]==False: # if gallery list not modified since last save: skip - continue - - gallery_list_filepath: str=os.path.join(cls.GALLERIES_PATH, f"galleries{gallery_list_id}.json") # appropiate gallery filepath - - logging.info(f"Saving galleries in \"{gallery_list_filepath}\"...") - with open(gallery_list_filepath, "wt") as galleries_file: - galleries_file.write(json.dumps(gallery_list, indent=4)) - logging.info(f"\rSaved galleries in \"{gallery_list_filepath}\".") - - cls.galleries_modified[gallery_list_id]=False # reset modified flag - - return \ No newline at end of file diff --git a/src/api_response.rs b/src/api_response.rs new file mode 100644 index 0000000..1212c1b --- /dev/null +++ b/src/api_response.rs @@ -0,0 +1,328 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use std::str::FromStr; + + +/// # Summary +/// Hentai search response from "nhentai.net/api/gallery/{id}". +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct HentaiSearchResponse +{ + #[serde(deserialize_with = "try_str_to_u32")] + pub id: u32, + pub images: Images, + #[serde(deserialize_with = "try_str_to_u32")] + pub media_id: u32, + pub num_favorites: u32, + pub num_pages: u16, + pub scanlator: Option, + pub tags: Vec, + pub title: Title, + #[serde(with = "chrono::serde::ts_seconds")] + pub upload_date: chrono::DateTime, +} + + +impl HentaiSearchResponse +{ + /// # Summary + /// Write hentai search response to database. Either creates new entries or updates existing ones with same primary key. + /// + /// # Arguments + /// - `db`: SQLite database + /// + /// # Returns + /// - nothing or sqlx::Error + pub async fn write_to_db(&self, db: &sqlx::sqlite::SqlitePool) -> Result<(), sqlx::Error> + { + let mut query: sqlx::query::Query<'_, _, _>; // query to update all tables + let query_string: String; // sql query string + + + let hentai_query_string: String = // query string for Hentai table + "INSERT OR REPLACE INTO Hentai (id, cover_type, media_id, num_favorites, num_pages, page_types, scanlator, title_english, title_japanese, title_pretty, upload_date) VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);".to_owned(); + let tag_query_string: String = format! // query string for Tag table + ( + "INSERT OR REPLACE INTO Tag (id, name, type, url) VALUES\n{};", + self.tags.iter().map(|_| "(?, ?, ?, ?)").collect::>().join(",\n") + ); + let hentai_tag_query_string: String = format! // query string for Hentai_Tag table + ( + "DELETE FROM Hentai_Tag WHERE hentai_id = ?;\nINSERT INTO Hentai_Tag (hentai_id, tag_id) VALUES\n{};", // delete all Hentai_Tag entries with same hentai_id before in case hentai had some tags untagged + self.tags.iter().map(|_| "(?, ?)").collect::>().join(",\n") + ); + query_string = format!("PRAGMA foreign_keys = OFF;\nBEGIN TRANSACTION;\n{}\n{}\n{}\nCOMMIT;\nPRAGMA foreign_keys = ON;", hentai_query_string, tag_query_string, hentai_tag_query_string); // combine all tables into one transaction, foreign key validation is too slow for inserts at this scale + + query = sqlx::query(query_string.as_str()); + query = query // bind Hentai values to placeholders + .bind(self.id) + .bind(format!("{:?}", self.images.cover.t)) + .bind(self.media_id) + .bind(self.num_favorites) + .bind(self.num_pages) + .bind(self.images.pages.iter().map(|page| format!("{:?}", page.t)).collect::>().join("")) // collapse all page types into 1 string, otherwise have to create huge Hentai_Pages table or too many Hentai_{id}_Pages tables + .bind(self.scanlator.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) // convert Some("") to None, otherwise forward unchanged + .bind(self.title.english.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(self.title.japanese.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(self.title.pretty.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(self.upload_date); + + for tag in self.tags.iter() // bind Tag values to placeholders + { + query = query + .bind(tag.id) + .bind(tag.name.clone()) + .bind(tag.r#type.clone()) + .bind(tag.url.clone()); + } + + query = query.bind(self.id); // bind hentai id to placeholder + for tag in self.tags.iter() // bind Hentai_Tag values to placeholders + { + query = query + .bind(self.id) + .bind(tag.id); + } + + query.execute(db).await?; + return Ok(()); + } +} + + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Image +{ + pub h: u32, + pub t: ImageType, + pub w: u32, +} + + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Images +{ + pub cover: Image, + pub pages: Vec, + pub thumbnail: Image, +} + + +#[derive(Clone, Eq, PartialEq)] +pub enum ImageType +{ + Gif, + Jpg, + Png, +} + +impl<'de> serde::Deserialize<'de> for ImageType +{ + fn deserialize(deserializer: D) -> Result // str -> ImageType + where + D: serde::Deserializer<'de>, + { + let s_de: String = String::deserialize(deserializer)?; + match Self::from_str(s_de.as_str()) + { + Ok(o) => return Ok(o), + _ => return Err(serde::de::Error::custom(format!("Invalid image type: \"{s_de}\""))), + }; + } +} + +impl serde::Serialize for ImageType +{ + fn serialize(&self, serializer: S) -> Result // ImageType -> str + where + S: serde::Serializer, + { + let s: String = format!("{:?}", self); + return serializer.serialize_str(s.as_str()); + } +} + +impl std::fmt::Debug for ImageType +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result // ImageType -> str + { + return write!(f, "{}", + match self + { + Self::Gif => "g", // only short form in program context (database) + Self::Jpg => "j", + Self::Png => "p", + } + ); + } +} + +impl std::fmt::Display for ImageType +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result // ImageType -> str + { + return write!(f, "{}", + match self + { + Self::Gif => "gif", // long form for output + Self::Jpg => "jpg", + Self::Png => "png", + } + ); + } +} + +impl std::str::FromStr for ImageType +{ + type Err = String; + fn from_str(s: &str) -> Result // str -> ImageType + { + let image_type: ImageType = match s.to_lowercase().trim() + { + "g" | "gif" => Self::Gif, + "j" | "jpg" => Self::Jpg, + "p" | "png" => Self::Png, + _ => return Err(format!("Invalid image type: \"{s}\"")), + }; + return Ok(image_type); + } +} + + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, sqlx::FromRow)] +pub struct Tag +{ + pub id: u32, + pub name: String, + pub r#type: String, // type is a reserved keyword, r#type resolves to type + pub url: String, +} + + +/// # Summary +/// Tag search response from "nhentai.net/api/galleries/search?query={tag}". +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct TagSearchResponse +{ + pub num_pages: u32, + pub per_page: u16, + pub result: Vec, +} + +impl TagSearchResponse +{ + /// # Summary + /// Write tag search response to database. Either creates new entries or updates existing ones with same primary key. + /// + /// # Arguments + /// - `db`: SQLite database + /// + /// # Returns + /// - nothing or sqlx::Error + pub async fn write_to_db(&self, db: &sqlx::sqlite::SqlitePool) -> Result<(), sqlx::Error> // separate function to create 1 big transaction instead of 1 transaction per Hentai, tag search requires this performance + { + let mut query: sqlx::query::Query<'_, _, _>; // query to update all tables + let query_string: String; // sql query string + + + let hentai_query_string: String = format! // query string for Hentai table + ( + "INSERT OR REPLACE INTO Hentai (id, cover_type, media_id, num_favorites, num_pages, page_types, scanlator, title_english, title_japanese, title_pretty, upload_date) VALUES\n{};", + self.result.iter().map(|_| "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").collect::>().join(",\n") + ); + let tag_query_string: String = format! + ( + "INSERT OR REPLACE INTO Tag (id, name, type, url) VALUES\n{};", // query string for Tag table + self.result.iter().flat_map(|hentai| hentai.tags.iter()).map(|_| "(?, ?, ?, ?)").collect::>().join(",\n") + ); + let mut hentai_tag_query_string: String = String::new(); // query string for Hentai_Tag table + for hentai in self.result.iter() + { + hentai_tag_query_string += format! + ( + "DELETE FROM Hentai_Tag WHERE hentai_id = ?;\nINSERT INTO Hentai_Tag (hentai_id, tag_id) VALUES\n{};", // delete all Hentai_Tag entries with same hentai_id before in case hentai had some tags untagged + hentai.tags.iter().map(|_| "(?, ?)").collect::>().join(",\n") + ).as_str(); + } + query_string = format!("PRAGMA foreign_keys = OFF;\nBEGIN TRANSACTION;\n{}\n{}\n{}\nCOMMIT;\nPRAGMA foreign_keys = ON;", hentai_query_string, tag_query_string, hentai_tag_query_string); // combine all tables into one transaction, foreign key validation is too slow for inserts at this scale + + query = sqlx::query(query_string.as_str()); + + for hentai in self.result.iter() // bind Hentai values to placeholders + { + query = query + .bind(hentai.id) + .bind(format!("{:?}", hentai.images.cover.t)) + .bind(hentai.media_id) + .bind(hentai.num_favorites) + .bind(hentai.num_pages) + .bind(hentai.images.pages.iter().map(|page| format!("{:?}", page.t)).collect::>().join("")) // collapse all page types into 1 string, otherwise have to create huge Hentai_Pages table or too many Hentai_{id}_Pages tables + .bind(hentai.scanlator.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) // convert Some("") to None, otherwise forward unchanged + .bind(hentai.title.english.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(hentai.title.japanese.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(hentai.title.pretty.clone().map(|s| if s.is_empty() {None} else {Some(s)}).flatten()) + .bind(hentai.upload_date); + } + + for hentai in self.result.iter() // bind Tag values to placeholders + { + for tag in hentai.tags.iter() + { + query = query + .bind(tag.id) + .bind(tag.name.clone()) + .bind(tag.r#type.clone()) + .bind(tag.url.clone()); + } + } + + for hentai in self.result.iter() // bind Hentai_Tag values to placeholders + { + query = query.bind(hentai.id); // bind hentai id to placeholder + for tag in hentai.tags.iter() + { + query = query + .bind(hentai.id) + .bind(tag.id); + } + } + + query.execute(db).await?; + return Ok(()); + } +} + + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Title +{ + pub english: Option, + pub japanese: Option, + pub pretty: Option, +} + + +/// # Summary +/// Tries to parse a string or number into a u32. +/// +/// # Arguments +/// - `deserializer`: serde deserializer +/// +/// # Returns +/// - u32 or serde::Error +fn try_str_to_u32<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; + let number: u32; + + match value + { + serde_json::Value::Number(n) => number = n.as_u64().ok_or_else(|| serde::de::Error::custom(format!("Converting number {n} to u64 failed. Subsequent converion to u32 has been aborted.")))? as u32, + serde_json::Value::String(s) => number = s.parse::().map_err(|e| serde::de::Error::custom(format!("Parsing string {s} to u32 failed with: {e}")))?, + _ => return Err(serde::de::Error::custom(format!("Value \"{value}\" is neither a number nor a string.")))?, + }; + + return Ok(number); +} \ No newline at end of file diff --git a/src/comicinfoxml.rs b/src/comicinfoxml.rs new file mode 100644 index 0000000..e615138 --- /dev/null +++ b/src/comicinfoxml.rs @@ -0,0 +1,75 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +#![allow(non_snake_case)] // non snake case because XML does this convention +use crate::api_response::*; +use crate::hentai::*; + + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, sqlx::FromRow)] +pub struct ComicInfoXml +{ + pub Title: String, // pretty title + pub Year: i16, // upload year + pub Month: u8, // upload month + pub Day: u8, // upload day + pub Writer: Option, // tag type: artist + pub Translator: Option, // scanlator + pub Publisher: Option, // tag type: group + pub Genre: Option, // tag type: category + pub Tags: Option, // tag types: character, language, parody, tag; language does not get own field "LanguageISO" because it only interprets 1 language as code properly, exhaustive language code list and only keeping 1 language if multiple present is janky + pub Web: String, // nhentai gallery + +} +// ComicInfo.xml schema: https://anansi-project.github.io/docs/comicinfo/documentation +// Komga interpretation: https://komga.org/docs/guides/scan-analysis-refresh + + +impl From for ComicInfoXml +{ + fn from(hentai: Hentai) -> Self // Hentai -> ComicInfo + { + return Self + { + Title: format!("{} {}", hentai.id, hentai.title_pretty.unwrap_or_default()), // id and actual title, because can't search for field "Number" in komga + Year: hentai.upload_date.format("%Y").to_string().parse::().expect(format!("Converting year \"{}\" to i16 failed even though it comes directly from chrono::DateTime.", hentai.upload_date.format("%Y").to_string()).as_str()), + Month: hentai.upload_date.format("%m").to_string().parse::().expect(format!("Converting month \"{}\" to u8 failed even though it comes directly from chrono::DateTime.", hentai.upload_date.format("%m").to_string()).as_str()), + Day: hentai.upload_date.format("%d").to_string().parse::().expect(format!("Converting day \"{}\" to u8 failed even though it comes directly from chrono::DateTime.", hentai.upload_date.format("%d").to_string()).as_str()), + Writer: filter_and_combine_tags(&hentai.tags, &vec!["artist"], false), + Translator: hentai.scanlator, + Publisher: filter_and_combine_tags(&hentai.tags, &vec!["group"], false), + Genre: filter_and_combine_tags(&hentai.tags, &vec!["category"], false), + Web: format!("https://nhentai.net/g/{id}/", id=hentai.id), + Tags: filter_and_combine_tags(&hentai.tags, &vec!["character", "language", "parody", "tag"], true), + } + } +} + + +/// # Summary +/// Filters tags by type and combines the remaining into a single string. If no tags are found, returns None. +/// +/// # Arguments +/// - `tags`: tag list to combine +/// - `types`: tag types to keep +/// - `display_type`: whether to display the tag type in the output in form of "type: name" +/// +/// # Returns +/// - filtered and combined tags or None +fn filter_and_combine_tags(tags: &Vec, types: &Vec<&str>, display_type: bool) -> Option +{ + let mut tags_filtered: Vec = tags.iter() + .filter(|tag| types.contains(&tag.r#type.as_str())) // only keep tags with type in types + .map + ( + |tag| + { + if display_type {format!("{}: {}", tag.r#type, tag.name)} + else {tag.name.clone()} + } + ) // change either to "{name}" or "{type}: {name}", because ComicInfo.xml + Komga don't have proper fields for all tag types + .collect(); + tags_filtered.sort(); // sort alphabetically + let tags_filtered_combined: Option = Some(tags_filtered.join(",")) // join at "," + .map(|s| if s.is_empty() {None} else {Some(s)}).flatten(); // convert Some("") to None, otherwise forward unchanged + + return tags_filtered_combined; +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f292a3c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. + + +/// # Summary +/// Collection of settings making up the configuration of the application. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[allow(non_snake_case)] +pub struct Config +{ + pub CF_CLEARANCE: String, // bypass bot protection + pub CSRFTOKEN: String, // bypass bot protection + pub DATABASE_URL: String, // url to database file + pub DOWNLOADME_FILEPATH: String, // path to file containing hentai ID to download + pub LIBRARY_PATH: String, // path to download hentai to + pub LIBRARY_SPLIT: u32, // split library into subdirectories of maximum this many hentai, 0 to disable + pub NHENTAI_TAG: Option, // keep creating downloadme.txt from this tag and keep downloading (server mode), normal tags are in format "tag:{tag}" for example "tag:ffm-threesome"; if None: don't generate downloadme.txt, download hentai once (client mode) + pub SLEEP_INTERVAL: Option, // sleep interval in seconds between checking for new hentai to download (server mode) + pub USER_AGENT: String, // bypass bot protection +} + +impl Default for Config +{ + fn default() -> Self + { + Config + { + CF_CLEARANCE: "".to_string(), + CSRFTOKEN: "".to_string(), + DATABASE_URL: "sqlite://./db/db.sqlite".to_owned(), + DOWNLOADME_FILEPATH: "./config/downloadme.txt".to_owned(), + LIBRARY_PATH: "./hentai/".to_string(), + LIBRARY_SPLIT: 0, + NHENTAI_TAG: None, + SLEEP_INTERVAL: None, + USER_AGENT: "".to_string(), + } + } +} \ No newline at end of file diff --git a/src/connect_to_db.rs b/src/connect_to_db.rs new file mode 100644 index 0000000..24880ca --- /dev/null +++ b/src/connect_to_db.rs @@ -0,0 +1,80 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use sqlx::migrate::MigrateDatabase; + + +/// # Summary +/// Connects to database at `database_url` and returns a connection pool. If database does not exist, creates a new database and initialises it with the instructions in `./db/create_db.sql`. +/// +/// # Arguments +/// - `database_url`: path to database file +/// +/// # Returns +/// - connection pool to database or error +pub async fn connect_to_db(database_url: &str) -> Result +{ + const CREATE_DB_QUERY_STRING: &str = // query string to create all tables except the dynamically created Hentai_{id}_Pages + "CREATE TABLE Hentai + ( + id INTEGER NOT NULL, + cover_type TEXT NOT NULL, + media_id INTEGER NOT NULL, + num_favorites INTEGER NOT NULL, + num_pages INTEGER NOT NULL, + page_types TEXT NOT NULL, + scanlator TEXT, + title_english TEXT, + title_japanese TEXT, + title_pretty TEXT, + upload_date TEXT NOT NULL, + PRIMARY KEY(id) + ); + CREATE TABLE Tag + ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + url TEXT NOT NULL, + PRIMARY KEY(id) + ); + CREATE TABLE Hentai_Tag + ( + hentai_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY(hentai_id, tag_id), + FOREIGN KEY(hentai_id) REFERENCES Hentai(id), + FOREIGN KEY(tag_id) REFERENCES Tag(id) + );"; + let db: sqlx::sqlite::SqlitePool; // database containing all metadata from nhentai.net api + + + if !sqlx::sqlite::Sqlite::database_exists(database_url).await? // if database does not exist + { + sqlx::sqlite::Sqlite::create_database(database_url).await?; // create new database + log::info!("Created new database at \"{}\".", database_url); + + db = sqlx::sqlite::SqlitePoolOptions::new() + .max_connections(1) // only 1 connection to database at the same time, otherwise concurrent writers fail + .connect(database_url).await?; // connect to database + db.set_connect_options(sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) // use write-ahead journal for better performance + .locking_mode(sqlx::sqlite::SqliteLockingMode::Exclusive) // do not release file lock until all transactions are complete + .synchronous(sqlx::sqlite::SqliteSynchronous::Normal)); + log::info!("Connected to database at \"{}\".", database_url); + + sqlx::query(CREATE_DB_QUERY_STRING).execute(&db).await?; // initialise database by creating tables + log::info!("Created database tables."); + } + else // if database already exists + { + db = sqlx::sqlite::SqlitePoolOptions::new() + .max_connections(1) // only 1 connection to database at the same time, otherwise concurrent writers fail + .connect(database_url).await?; // connect to database + db.set_connect_options(sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) // use write-ahead journal for better performance + .locking_mode(sqlx::sqlite::SqliteLockingMode::Exclusive) // do not release file lock until all transactions are complete + .synchronous(sqlx::sqlite::SqliteSynchronous::Normal)); + log::info!("Connected to database at \"{}\".", database_url); + } + + return Ok(db); +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..496f5c0 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. + + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("{directory_path}")] + BlockedByDirectory {directory_path: String}, + + #[error("")] + Download {}, + + #[error("")] + HentaiLengthInconsistency {page_types: u16, num_pages: u16}, + + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + + #[error("{status}")] + ReqwestStatus {url: String, status: reqwest::StatusCode}, + + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + + #[error(transparent)] + SerdeXml(#[from] serde_xml_rs::Error), + + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + + #[error(transparent)] + StdIo(#[from] std::io::Error), + + #[error(transparent)] + Zip(#[from] zip::result::ZipError), +} + + +pub type Result = std::result::Result; // strict error handling, only takes pre defined Error type \ No newline at end of file diff --git a/src/get_hentai_ID_list.py b/src/get_hentai_ID_list.py deleted file mode 100644 index efbd8ce..0000000 --- a/src/get_hentai_ID_list.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. -import logging -import os - - -def get_hentai_ID_list(downloadme_filepath: str) -> list[int]: - """ - Tries to return hentai ID list to download by trying to load "./config/downloadme.txt" or getting hentai ID by user input. - - Arguments: - - downloadme_filepath: path to file containing hentai ID to download - - Returns: - - hentai_ID_list: list of hentai ID to download - """ - - file_tried: bool=False # tried to load from file? - hentai_ID_list: list[int]=[] # hentai ID list to download - - - while True: - if os.path.isfile(downloadme_filepath)==True and file_tried==False: # if ID list in file and not tried to load from file yet: load from file, only try once - file_tried=True - with open(downloadme_filepath, "rt") as downloadme_file: - hentai_ID_list=_convert_hentai_ID_list_str_to_hentai_ID_list_int(downloadme_file.read().split("\n")) # read all hentai ID from file, list[int] -> list[str], clean up data - else: # if ID list file not available: ask user for input - logging.info("Enter the holy numbers: ") - hentai_ID_list=_convert_hentai_ID_list_str_to_hentai_ID_list_int(input().split(" ")) # user input seperated at whitespace, list[int] -> list[str], clean up data - - if len(hentai_ID_list)==0: # if file or user input empty: retry - continue - - break - - return hentai_ID_list - - -def _convert_hentai_ID_list_str_to_hentai_ID_list_int(hentai_ID_list_str: list[str]) -> list[int]: - """ - Converts list of hentai ID from list[str] to list[int], cleans up entries. Does not sort to respect input order. - - Arguments: - - hentai_ID_list_str: list of hentai ID in str to convert - - Returns: - - hentai_ID_list: list of hentai ID in int - """ - - hentai_ID_list: list[int]=[] # list of hentai ID in int - - - hentai_ID_list_str=[hentai_ID for hentai_ID in hentai_ID_list_str if len(hentai_ID)!=0] # throw out emtpy entries - hentai_ID_list_str=list(dict.fromkeys(hentai_ID_list_str)) # remove duplicates - - for hentai_ID in hentai_ID_list_str: # list[str] -> list[int] - try: - hentai_ID_list.append(int(hentai_ID)) - except ValueError: # if input invalid: discard that, keep rest - logging.error(f"Converting input \"{hentai_ID}\" to int failed. Skipping ID.") - - return hentai_ID_list \ No newline at end of file diff --git a/src/get_hentai_id_list.rs b/src/get_hentai_id_list.rs new file mode 100644 index 0000000..90c4a36 --- /dev/null +++ b/src/get_hentai_id_list.rs @@ -0,0 +1,149 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use crate::error::*; +use crate::search_api::*; +use tokio::io::AsyncWriteExt; + + +/// # Summary +/// Tries to return hentai ID list to download from the following sources with respective descending priority: +/// 1. if it exists: load from `downloadme_filepath` +/// 1. if `nhentai_tag` set: by searching on nhentai.net for all hentai ID with tag `nhentai_tag` +/// 1. manual user input, separated by spaces +/// +/// # Arguments +/// - `downloadme_filepath`: path to file containing hentai ID list +/// - `http_client`: reqwest http client +/// - `nhentai_tag_search_url`: nhentai.net tag search API URL +/// - `nhentai_tag`: tag to search for +/// - `db`: database connection +/// +/// # Returns +/// - list of hentai ID to download +pub async fn get_hentai_id_list(downloadme_filepath: &std::path::Path, http_client: &reqwest::Client, nhentai_tag_search_url: &str, nhentai_tag: &Option, db: &sqlx::sqlite::SqlitePool) -> Vec +{ + let mut hentai_id_list: Vec = Vec::new(); // list of hentai id to download + + + if std::path::Path::new(downloadme_filepath).exists() // only try loading if downloadme_filepath actually exists, so only non trivial errors are logged with log::error! + { + match tokio::fs::read_to_string(downloadme_filepath).await // try to load downloadme + { + Ok(content) => + { + hentai_id_list = content.lines().filter_map(|line| line.parse::().ok()).collect(); // String -> Vec, discard unparseable lines + log::info!("Loaded hentai ID list from {downloadme_filepath:?}."); + }, + Err(e) => log::error!("Loading hentai ID list from {downloadme_filepath:?} failed with: {e}"), + }; + } + else + { + log::info!("No hentai ID list found at {downloadme_filepath:?}."); + } + if !hentai_id_list.is_empty() // if hentai_id_list is not empty: work is done + { + log::debug!("{hentai_id_list:?}"); + return hentai_id_list; + } + + if nhentai_tag.is_some() // if nhentai_tag is set: search nhentai.net for hentai ID with tag + { + log::info!("\"NHENTAI_TAG\" is set."); + let nhentai_tag_unwrapped: &str = nhentai_tag.as_deref().expect("nhentai_tag lifting crashed even though previous line ensured Option is Some."); + match search_by_tag + ( + http_client, + nhentai_tag_search_url, + nhentai_tag_unwrapped, + db, + ).await + { + Ok(o) => hentai_id_list = o, + Err(e) => + { + match e + { + Error::Reqwest(e) => log::error! + ( + "Downloading hentai \"{}\" metadata page 1 from \"{}\" failed with: {e}", + nhentai_tag_unwrapped, + e.url().map_or_else(|| "", |o| o.as_str()) + ), + Error::ReqwestStatus {url, status} => log::error! + ( + "Downloading hentai \"{}\" metadata page 1 from \"{url}\" failed with status code {status}.", + nhentai_tag_unwrapped, + ), + Error::SerdeJson(e) => log::error! + ( + "Saving hentai \"{}\" metadata page 1 in database failed with: {e}", + nhentai_tag_unwrapped, + ), + _ => panic!("Unhandled error: {e}"), + }; + } + } + } + else // if nhentai_tag is not set: request manual user input + { + log::info!("\"NHENTAI_TAG\" is not set."); + } + if !hentai_id_list.is_empty() // if hentai_id_list is not empty: save tag search in downloadme.txt, work is done + { + #[cfg(target_family = "unix")] + match tokio::fs::OpenOptions::new().create_new(true).mode(0o666).write(true).open(downloadme_filepath).await + { + Ok(mut file) => + { + match file.write_all(hentai_id_list.iter().map(|id| id.to_string()).collect::>().join("\n").as_bytes()).await + { + Ok(_) => log::info!("Saved hentai ID list from tag search in {downloadme_filepath:?}."), + Err(e) => log::error!("Writing hentai ID list to {downloadme_filepath:?} failed with: {e}"), + } + }, + Err(e) => log::error!("Saving hentai ID list at {downloadme_filepath:?} failed with: {e}"), + } + #[cfg(not(target_family = "unix"))] + match tokio::fs::OpenOptions::new().create_new(true).write(true).open(downloadme_filepath).await + { + Ok(mut file) => + { + match file.write_all(hentai_id_list.iter().map(|id| id.to_string()).collect::>().join("\n").as_bytes()).await + { + Ok(_) => log::info!("Saved hentai ID list from tag search in {downloadme_filepath:?}."), + Err(e) => log::error!("Writing hentai ID list to {downloadme_filepath:?} failed with: {e}"), + } + }, + Err(e) => log::error!("Saving hentai ID list at {downloadme_filepath:?} failed with: {e}"), + } + log::debug!("{hentai_id_list:?}"); + return hentai_id_list; + } + + loop // if everything else fails: request manual user input + { + log::info!("Enter the holy numbers: "); + let mut input: String = String::new(); + _ = std::io::stdin().read_line(&mut input); + log::debug!("{input}"); + hentai_id_list = input.trim() + .split_whitespace() + .filter_map(|line| + { + match line.parse::() + { + Ok(o) => Some(o), + Err(e) => + { + log::warn!("Parsing entry \"{line}\" to u32 failed with: {e}. Discarding..."); + None + } + } + }) + .collect(); // String -> Vec, discard unparseable lines with warning + + if !hentai_id_list.is_empty() {break;} // if hentai_id_list is not empty: work is done + } + log::debug!("{hentai_id_list:?}"); + return hentai_id_list; +} \ No newline at end of file diff --git a/src/hentai.rs b/src/hentai.rs new file mode 100644 index 0000000..78eb306 --- /dev/null +++ b/src/hentai.rs @@ -0,0 +1,330 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use crate::api_response::*; +use crate::comicinfoxml::*; +use crate::error::*; +use crate::search_api::*; +use std::io::Read; +use std::io::Write; +#[cfg(target_family = "unix")] +use std::os::unix::fs::OpenOptionsExt; +#[cfg(target_family = "unix")] +use std::os::unix::fs::PermissionsExt; +use std::str::FromStr; +use tokio::io::AsyncWriteExt; + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Hentai +{ + pub id: u32, // nhentai.net hentai id + pub cbz_filepath: String, // filepath to final cbz + pub gallery_url: String, // nhentai.net gallery url + pub images_filename: Vec, // filenames of images, not filepath because needed at temporary image location and final zip location + pub images_url: Vec, // urls to images to download + pub library_path: String, // path to local hentai library, relevant to generate filepaths of temporary images + pub num_pages: u16, + pub scanlator: Option, + pub tags: Vec, // tags from Tag table, must be broken up by type + pub title_pretty: Option, + pub upload_date: chrono::DateTime +} + + +impl Hentai +{ + /// # Summary + /// Tries to build a Hentai with the metadata from the following sources with respective descending priority: + /// 1. if entry exists in database: load from database + /// 1. by searching on nhentai.net for a hentai with ID `id` + /// + /// # Arguments + /// - `id`: hentai ID + /// - `db`: database connection + /// - `http_client`: reqwest http client + /// - `nhentai_hentai_search_url`: nhentai.net hentai search API URL + /// - `library_path`: path to local hentai library + /// - `library_split`: split library into subdirectories with maximum this number of hentai, 0 for no split + /// + /// # Returns + /// - created hentai or error + pub async fn new(id: u32, db: &sqlx::sqlite::SqlitePool, http_client: &reqwest::Client, nhentai_hentai_search_url: &str, library_path: &str, library_split: u32) -> Result + { + const TITLE_CHARACTERS_FORBIDDEN: &str = "\\/:*?\"<>|\t\n"; // forbidden characters in Windows file names + let mut cbz_filepath: String; + let hentai_table_row: HentaiTableRow; + let mut images_filename: Vec = Vec::new(); + let mut images_url: Vec = Vec::new(); + let tags: Vec; + + + if let Ok(Some(s)) = sqlx::query_as("SELECT id, media_id, num_pages, page_types, scanlator, title_english, title_pretty, upload_date FROM Hentai WHERE id = ?") + .bind(id) + .fetch_optional(db).await // load hentai metadata from database + { + hentai_table_row = s; + log::info!("Loaded hentai {id} metadata from database."); + } + else // if any step to load from database failed + { + log::info!("Hentai {id} metadata could not be loaded from database. Downloading from nhentai.net API..."); + hentai_table_row = search_by_id(http_client, nhentai_hentai_search_url, id, db).await?; // load hentai metadata from api + log::info!("Downloaded hentai {id} metadata."); + } + + tags = sqlx::query_as("SELECT Tag.* FROM Tag JOIN (SELECT tag_id FROM Hentai_Tag WHERE hentai_id = ?) AS tags_attached_to_hentai_desired ON Tag.id = tags_attached_to_hentai_desired.tag_id") + .bind(id) + .fetch_all(db).await?; // load tags from database + log::info!("Loaded hentai {id} tags from database."); + + + for (i, page_type) in hentai_table_row.page_types.char_indices() + { + images_url.push(format!("https://i.nhentai.net/galleries/{}/{}.{}", hentai_table_row.media_id, i+1, ImageType::from_str(page_type.to_string().as_str()).expect("Invalid image type even though it was loaded from the database."))); + images_filename.push(format!("{id}-{:05}.{}", i+1, ImageType::from_str(page_type.to_string().as_str()).expect("Invalid image type even though it was loaded from the database."))); + } + if hentai_table_row.page_types.len() != hentai_table_row.num_pages as usize // if number of pages does not match number of page types: inconsistency + { + return Err(Error::HentaiLengthInconsistency {page_types: hentai_table_row.page_types.len() as u16, num_pages: hentai_table_row.num_pages}); + } + + cbz_filepath = hentai_table_row.title_english.clone().unwrap_or_default(); + cbz_filepath.retain(|c| !TITLE_CHARACTERS_FORBIDDEN.contains(c)); // remove forbidden characters + if library_split == 0 // no library split + { + cbz_filepath = format!("{}{id} {}.cbz", library_path.to_owned(), cbz_filepath); + } + if 0 < library_split // with library split + { + cbz_filepath = format! + ( + "{}{}~{}/{} {}.cbz", + library_path.to_owned(), + id.div_euclid(library_split) * library_split, + (id.div_euclid(library_split) + 1) * library_split - 1, + id, + cbz_filepath + ); + } + + return Ok(Self + { + id, + cbz_filepath, + gallery_url: format!("https://nhentai.net/g/{id}/"), + images_filename, + images_url, + library_path: library_path.to_owned(), + num_pages: hentai_table_row.num_pages, + scanlator: hentai_table_row.scanlator, + tags, + title_pretty: hentai_table_row.title_pretty, + upload_date: hentai_table_row.upload_date, + }); + } + + + /// # Summary + /// Downloads all images of the hentai and combines them into a cbz file. + /// + /// # Arguments + /// - `http_client`: reqwest http client + /// - `db`: database connectionc + /// + /// # Returns + /// - nothing or error + pub async fn download(&self, http_client: &reqwest::Client) -> Result<()> + { + const WORKERS: usize = 5; // number of parallel workers + let f = scaler::Formatter::new() + .set_scaling(scaler::Scaling::None) + .set_rounding(scaler::Rounding::Magnitude(0)); // formatter + let mut image_download_success: bool = true; // if all images were downloaded successfully, redundant initialisation here because of stupid error message + let mut handles: Vec>>; // list of handles to download_image + let worker_sem: std::sync::Arc = std::sync::Arc::new(tokio::sync::Semaphore::new(WORKERS)); // limit number of concurrent workers otherwise api enforces rate limit + let mut zip_writer: zip::ZipWriter; // write to zip file + + + if let Ok(o) = tokio::fs::metadata(self.cbz_filepath.as_str()).await + { + if o.is_file() // if cbz already exists + { + log::info!("Hentai {} already exists. Skipped download.", self.id); + return Ok(()); // skip download + } + if o.is_dir() // if cbz filepath blocked by directory + { + log::error!("\"{}\" already exists as directory. Skipped download.", self.cbz_filepath); + return Err(Error::BlockedByDirectory {directory_path: self.cbz_filepath.clone()}); // give up + } + } + + + for _ in 0..5 // try to download hentai maximum 5 times + { + image_download_success = true; // assume success + handles = Vec::new(); // reset handles + + for i in 0..self.images_url.len() // for each page + { + let f_clone: scaler::Formatter = f.clone(); + let http_client_clone: reqwest::Client = http_client.clone(); + let id_clone: u32 = self.id; + let image_filepath: String = format!("{}{}/{}.", self.library_path, self.id, self.images_filename.get(i).expect("Index out of bounds even though should have same size as images_url.")); + let image_url_clone: String = self.images_url.get(i).expect("Index out of bounds even though checked before that it fits.").clone(); + let num_pages_clone: u16 = self.num_pages; + + let permit: tokio::sync::OwnedSemaphorePermit = worker_sem.clone().acquire_owned().await.expect("Something closed semaphore even though it should never be closed."); // acquire semaphore + handles.push(tokio::spawn(async move + { + let result: Option<()>; + match Self::download_image(&http_client_clone, &image_url_clone, &image_filepath).await // download image + { + Ok(_) => + { + log::debug!("Downloaded hentai {id_clone} image {} / {}.", f_clone.format((i+1) as f64), f_clone.format(num_pages_clone as f64)); + result = Some(()); // success + } + Err(e) => + { + match e + { + Error::BlockedByDirectory {directory_path} => log::error! + ( + "Saving hentai {id_clone} image {} / {} failed, because \"{directory_path}\" already is a directory.", + f_clone.format((i+1) as f64), + f_clone.format(num_pages_clone as f64), + ), + Error::Reqwest(e) => log::error! + ( + "Downloading hentai {id_clone} image {} / {} from \"{}\" failed with: {e}", + f_clone.format((i+1) as f64), + f_clone.format(num_pages_clone as f64), + e.url().map_or_else(|| "", |o| o.as_str()), + ), + Error::ReqwestStatus {url, status} => log::error! + ( + "Downloading hentai {id_clone} image {} / {} from \"{url}\" failed with status code {status}.", + f_clone.format((i+1) as f64), + f_clone.format(num_pages_clone as f64), + ), + Error::StdIo(e) => log::error! + ( + "Saving hentai {id_clone} image {} / {} failed with: {e}", + f_clone.format((i+1) as f64), + f_clone.format(num_pages_clone as f64), + ), + _ => panic!("Unhandled error: {e}"), + } + result = None; // failure + } + } + drop(permit); // release semaphore + result // return result into handle + })); // search all pages in parallel + } + for handle in handles + { + if let None = handle.await.unwrap() {image_download_success = false;} // collect results, forward panics, if any image download failed: set flag and abandon creation of cbz later but continue downloading other images + } + if image_download_success {break;} // if all images were downloaded successfully: continue with cbz creation + } + if !image_download_success {return Err(Error::Download {})}; // if after 5 attempts still not all images downloaded successfully: give up + log::info!("Downloaded hentai {} images.", self.id); + + + let zip_file: std::fs::File; + #[cfg(target_family = "unix")] + { + if let Some(parent) = std::path::Path::new(&self.cbz_filepath).parent() {tokio::fs::DirBuilder::new().recursive(true).mode(0o777).create(parent).await?;} // create all parent directories with permissions "drwxrwxrwx" + zip_file = std::fs::OpenOptions::new().create_new(true).mode(0o666).write(true).open(self.cbz_filepath.clone())?; // create zip file with permissions "rw-rw-rw-" + if let Err(e) = zip_file.set_permissions(std::fs::Permissions::from_mode(0o666)) // set permissions + { + log::warn!("Setting permissions \"rw-rw-rw-\"for hentai {} failed with: {e}", self.id); + } + } + #[cfg(not(target_family = "unix"))] + { + if let Some(parent) = std::path::Path::new(&self.cbz_filepath).parent() {tokio::fs::DirBuilder::new().recursive(true).create(parent).await?;} // create all parent directories + zip_file = std::fs::OpenOptions::new().create_new(true).write(true).open(self.cbz_filepath.clone())?; // create zip file with permissions "rw-rw-rw-" + } + + zip_writer = zip::ZipWriter::new(zip_file); // create zip writer + for (i, image_filename) in self.images_filename.iter().enumerate() // load images into zip + { + let mut image: Vec = Vec::new(); + std::fs::File::open(format!("{}{}/{image_filename}.", self.library_path, self.id))?.read_to_end(&mut image)?; // open image file, read image into memory + zip_writer.start_file(image_filename, zip::write::SimpleFileOptions::default().unix_permissions(0o666))?; // create image file in zip with permissions "rw-rw-rw-" + zip_writer.write_all(&image)?; // write image into zip + log::debug!("Saved hentai {} image {} / {} in cbz.", self.id, f.format((i+1) as f64), f.format(self.num_pages)); + } + #[cfg(target_family = "unix")] + zip_writer.start_file("ComicInfo.xml", zip::write::SimpleFileOptions::default().unix_permissions(0o666))?; // create metadata ffile in zip with permissions "rw-rw-rw-" + #[cfg(not(target_family = "unix"))] + zip_writer.start_file("ComicInfo.xml", zip::write::SimpleFileOptions::default())?; // create metadata file in zip without permissions + zip_writer.write_all(serde_xml_rs::to_string(&ComicInfoXml::from(self.clone()))?.as_bytes())?; // write metadata into zip + zip_writer.finish()?; // finish zip + log::info!("Saved hentai {} cbz.", self.id); + + + if let Err(e) = tokio::fs::remove_dir_all(format!("{}{}", self.library_path, self.id)).await // cleanup, delete image directory + { + log::warn!("Deleting \"{}/\" failed with: {e}", format!("{}{}", self.library_path, self.id)); + } + + return Ok(()); + } + + + /// # Summary + /// Downloads an image from `image_url` and saves it to `image_filepath`. + /// + /// # Arguments + /// - `http_client`: reqwest http client + /// - `image_url`: url of the image to download + /// - `image_filepath`: path to save the image to + /// + /// # Returns + /// - nothing or error + async fn download_image(http_client: &reqwest::Client, image_url: &str, image_filepath: &str) -> Result<()> + { + if let Ok(o) = tokio::fs::metadata(image_filepath).await + { + if o.is_file() {return Ok(());} // if image already exists: skip download + if o.is_dir() {return Err(Error::BlockedByDirectory {directory_path: image_filepath.to_owned()});} // if image filepath blocked by directory: give up + } + + + let r: reqwest::Response = http_client.get(image_url).send().await?; // tag search, page + if r.status() != reqwest::StatusCode::OK {return Err(Error::ReqwestStatus {url: r.url().to_string(), status: r.status()});} // if status is not ok: something went wrong + + + let mut file: tokio::fs::File; + #[cfg(target_family = "unix")] + { + if let Some(parent) = std::path::Path::new(image_filepath).parent() {tokio::fs::DirBuilder::new().recursive(true).mode(0o777).create(parent).await?;} // create all parent directories with permissions "drwxrwxrwx" + file = tokio::fs::OpenOptions::new().create_new(true).mode(0o666).write(true).open(image_filepath).await?; + } + #[cfg(not(target_family = "unix"))] + { + if let Some(parent) = std::path::Path::new(image_filepath).parent() {tokio::fs::DirBuilder::new().recursive(true).create(parent).await?;} // create all parent directories with permissions "drwxrwxrwx" + file = tokio::fs::OpenOptions::new().create_new(true).write(true).open(image_filepath).await?; + } + file.write_all_buf(&mut r.bytes().await?).await?; // save image with permissions "rw-rw-rw-" + + return Ok(()); + } +} + + +#[derive(Clone, Debug, Eq, PartialEq, sqlx::FromRow)] +pub struct HentaiTableRow +{ + pub id: u32, + pub media_id: u32, + pub num_pages: u16, + pub page_types: String, + pub scanlator: Option, + pub title_english: Option, + pub title_pretty: Option, + pub upload_date: chrono::DateTime, +} \ No newline at end of file diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 0b4a3f6..0000000 --- a/src/main.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. -import gc # garbage collector, explicitly free memory -from KFSconfig import KFSconfig -from KFSfstr import KFSfstr -from KFSlog import KFSlog -from KFSmedia import KFSmedia -import logging -import os -import typing -from get_hentai_ID_list import get_hentai_ID_list -from Hentai import Hentai - - -@KFSlog.timeit() -def main(DEBUG: bool): - cleanup_success: bool=True # cleanup successful - config: dict[str, typing.Any] # config - CONFIG_DEFAULT: dict[str, typing.Any]=\ - { - "DOWNLOADME_FILEPATH": "./config/downloadme.txt", # path to file containing hentai ID to download - "LIBRARY_PATH": "./hentai/", # path to download hentai to - "LIBRARY_SPLIT": 0, # split library into subdirectories of maximum this many hentai, 0 to disable - } - env: dict[str, str] # environment variables - ENV_DEFAULT: dict[str, str]=\ - { - "CF_CLEARANCE": "", # for requests.get to bypass bot protection - "CSRFTOKEN": "", - "USER_AGENT": "", - } - hentai: Hentai # individual hentai - hentai_ID_list: list[int] # hentai ID to download - - - try: - config=KFSconfig.load_config(env=False, config_filepaths=["./config/config.json"], config_default=CONFIG_DEFAULT) # load configuration - env =KFSconfig.load_config( config_filepaths=["./.env"], config_default=ENV_DEFAULT) # load environment variables - except ValueError: - return - hentai_ID_list=get_hentai_ID_list(config["DOWNLOADME_FILEPATH"]) # get desired hentai ID - - - for i, hentai_ID in enumerate(hentai_ID_list): # work through all desired hentai - logging.info("--------------------------------------------------") - logging.info(f"{KFSfstr.notation_abs(i+1, 0, round_static=True)}/{KFSfstr.notation_abs(len(hentai_ID_list), 0, round_static=True)} ({KFSfstr.notation_abs((i+1)/(len(hentai_ID_list)), 2, round_static=True)})") - - if (i+1)%100==0: # save galleries to file, only every 100 hentai to save time - Hentai.save_galleries() - - try: - hentai=Hentai(hentai_ID, {"cf_clearance": env["CF_CLEARANCE"], "csrftoken": env["CSRFTOKEN"]}, {"User-Agent": env["USER_AGENT"]}) # create hentai object - except ValueError: # if hentai does not exist: - continue # skip to next hentai - else: - logging.info(hentai) - - try: - _=hentai.download(config["LIBRARY_PATH"], config["LIBRARY_SPLIT"]) # download hentai - except FileExistsError: # if hentai already exists: - continue # skip to next hentai - except KFSmedia.DownloadError: - with open("./log/FAILURES.txt", "at") as fails_file: # append in failure file - fails_file.write(f"{hentai.ID}\n") - continue # skip to next hentai - del _ - gc.collect() # explicitly free memory, otherwise PDF may clutter memory - logging.info("--------------------------------------------------") - - - Hentai.save_galleries() # save all galleries to file - - logging.info("Deleting leftover image directories...") - for hentai_ID in hentai_ID_list: # attempt final cleanup - if os.path.isdir(os.path.join(config["LIBRARY_PATH"], str(hentai_ID))) and len(os.listdir(os.path.join(config["LIBRARY_PATH"], str(hentai_ID))))==0: # if cache folder still exists and is empty: - try: - os.rmdir(os.path.join(config["LIBRARY_PATH"], str(hentai_ID))) # try to clean up - except PermissionError as e: # may fail if another process is still using directory like dropbox - logging.warning(f"Deleting \"{os.path.join(config["LIBRARY_PATH"], str(hentai_ID))}/\" failed with {KFSfstr.full_class_name(e)}.") - cleanup_success=False # cleanup unsuccessful - if cleanup_success==True: - logging.info("\rDeleted leftover image directories.") - - return \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..da4c5d0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +mod api_response; +mod comicinfoxml; +mod config; +use config::*; +mod connect_to_db; +mod error; +use error::*; +mod get_hentai_id_list; +mod hentai; +mod main_inner; +use main_inner::*; +mod search_api; + + +fn main() -> std::process::ExitCode +{ + const DEBUG: bool = false; // debug mode? + let mut crate_logging_level: std::collections::HashMap = std::collections::HashMap::new(); // logging level for individual crates + let config: Config; // config, settings + let tokio_rt: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("Creating tokio runtime failed."); // async runtime + + + crate_logging_level.insert("hyper_util".to_owned(), log::Level::Info); // shut up + crate_logging_level.insert("serde_xml_rs".to_owned(), log::Level::Error); // shut up + crate_logging_level.insert("sqlx::query".to_owned(), log::Level::Error); // shut up + if DEBUG == true // setup logging + { + setup_logging::setup_logging(log::Level::Debug, Some(crate_logging_level), "./log/%Y-%m-%dT%H_%M.log"); + } + else + { + setup_logging::setup_logging(log::Level::Info, Some(crate_logging_level), "./log/%Y-%m-%d.log"); + } + + std::panic::set_hook(Box::new(|panic_info: &std::panic::PanicInfo| // override panic behaviour + { + log::error!("{}", panic_info); // log panic source and reason + log::error!("{}", std::backtrace::Backtrace::capture()); // log backtrace + })); + + match load_config::load_config + ( + vec! + [ + load_config::Source::Env, + load_config::Source::File(load_config::SourceFile::Toml("./config/.env".to_string())), + ], + Some(load_config::SourceFile::Toml("./config/.env".to_string())) + ) + { + Ok(o) => {config = o;} // loaded config successfully + Err(_) => {return std::process::ExitCode::FAILURE;} // loading config failed + } + + + match std::panic::catch_unwind(|| tokio_rt.block_on(main_inner(config.clone()))) // execute main_inner, catch panic + { + Ok(result) => // no panic + { + match result + { + Ok(()) => {return std::process::ExitCode::SUCCESS;} // program executed successfully + Err(e) => // program failed in a controlled manner + { + match e // log error + { + Error::Reqwest(e) => log::error!("Test connecting to \"{}\" failed with: {e}", e.url().map_or_else(|| "", |o| o.as_str())), + Error::ReqwestStatus { url, status } => + { + if status == reqwest::StatusCode::FORBIDDEN + { + log::error!("Test connecting to \"{url}\" failed with status code {status}. Check if cookies \"cf_clearance\" and \"csrftoken\" and user agent are set and current."); + } + else + { + log::error!("Test connecting to \"{url}\" failed with status code {status}."); + } + } + Error::Sqlx(e) => log::error!("Connecting to database at \"{}\" failed with: {e}\nIf you're creating a new database, ensure all parent directories already exist.", config.DATABASE_URL), + _ => panic!("Unhandled error: {e}"), + } + return std::process::ExitCode::FAILURE; + } + } + } + Err(_) => {return std::process::ExitCode::FAILURE;} // program crashed with panic, dis not good + }; +} \ No newline at end of file diff --git a/src/main_inner.rs b/src/main_inner.rs new file mode 100644 index 0000000..bb31b6c --- /dev/null +++ b/src/main_inner.rs @@ -0,0 +1,129 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use crate::config::*; +use crate::connect_to_db::*; +use crate::error::*; +use crate::get_hentai_id_list::*; +use crate::hentai::*; + + +pub async fn main_inner(config: Config) -> Result<()> +{ + const NHENTAI_HENTAI_SEARCH_URL: &str="https://nhentai.net/api/gallery/"; // nhentai search by id api url + const NHENTAI_TAG_SEARCH_URL: &str="https://nhentai.net/api/galleries/search"; // nhentai search by tag api url + let db: sqlx::sqlite::SqlitePool; // database containing all metadata from nhentai.net api + let f0 = scaler::Formatter::new() + .set_scaling(scaler::Scaling::None) + .set_rounding(scaler::Rounding::Magnitude(0)); // formatter + let fm2 = scaler::Formatter::new() + .set_scaling(scaler::Scaling::None) + .set_rounding(scaler::Rounding::Magnitude(-2)); // formatter + let f4 = scaler::Formatter::new(); // formatter + let mut hentai_id_list: Vec; // list of hentai id to download + let http_client: reqwest::Client; // http client + let timeout: std::time::Duration = std::time::Duration::from_secs(30); // connection timeout + + + db = connect_to_db(&config.DATABASE_URL).await?; // connect to database + + { + let mut headers: reqwest::header::HeaderMap = reqwest::header::HeaderMap::new(); // headers + headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&config.USER_AGENT).unwrap_or_else + ( + |e| + { + log::warn!("Adding user agent to HTTP client headers failed with: {e}\nUsing empty user agent instead."); + reqwest::header::HeaderValue::from_str("").expect("Creating empty user agent failed.") + } + )); + headers.insert(reqwest::header::COOKIE, reqwest::header::HeaderValue::from_str(format!("cf_clearance={}; csrftoken={}", config.CF_CLEARANCE, config.CSRFTOKEN).as_str()).unwrap_or_else + ( + |e| + { + log::warn!("Adding cookies \"cf_clearance\" and \"csrftoken\" to HTTP client headers failed with: {e}\nUsing no cookies instead."); + reqwest::header::HeaderValue::from_str("").expect("Creating empty cookies failed.") + } + )); + http_client = reqwest::Client::builder() // create http client + .connect_timeout(timeout) + .cookie_store(true) // enable cookies + .default_headers(headers) + .read_timeout(timeout) + .build().expect("Creating HTTP client failed."); + let r: reqwest::Response = http_client.get(NHENTAI_TAG_SEARCH_URL).query(&[("query", "language:english"), ("page", "1")]).send().await?; // send test request + if r.status() != reqwest::StatusCode::OK // if status is not ok: something went wrong + { + return Err(Error::ReqwestStatus {url: r.url().to_string(), status: r.status()}); + } + } + + + loop // keep running for server mode + { + hentai_id_list = get_hentai_id_list + ( + std::path::Path::new(config.DOWNLOADME_FILEPATH.as_str()), + &http_client, + NHENTAI_TAG_SEARCH_URL, + &config.NHENTAI_TAG, + &db, + ).await; + + + for (i, hentai_id) in hentai_id_list.iter().enumerate() + { + log::info!("--------------------------------------------------"); + log::info!("{} / {} ({})", f0.format((i+1) as f64), f0.format(hentai_id_list.len() as f64), fm2.format((i+1) as f64 / hentai_id_list.len() as f64)); + let hentai: Hentai; // hentai to download + + + match Hentai::new(*hentai_id, &db, &http_client, NHENTAI_HENTAI_SEARCH_URL, &config.LIBRARY_PATH, config.LIBRARY_SPLIT).await + { + Ok(o) => hentai = o, // hentai created successfully + Err(e) => // hentai creation failed + { + match e + { + Error::HentaiLengthInconsistency { page_types, num_pages } => log::error!("Hentai {hentai_id} has {} page types specified, but {} pages were expected.", f0.format(page_types), f0.format(num_pages)), + Error::Reqwest(e) => log::error!("Hentai {hentai_id} metadata could not be loaded from database and downloading from \"{}\" failed with: {e}", e.url().map_or_else(|| "", |o| o.as_str())), + Error::ReqwestStatus {url, status} => log::error!("Hentai {hentai_id} metadata could not be loaded from database and downloading from \"{url}\" failed with status code {status}."), + Error::SerdeJson(e) => log::error!("Hentai {hentai_id} metadata could not be loaded from database and after downloading, deserialising API response failed with: {e}"), + Error::Sqlx(e) => log::error!("Loading hentai {hentai_id} tags from database failed with: {e}"), + _ => panic!("Unhandled error: {e}"), + } + continue; // skip download + } + } + + + if let Err(e) = hentai.download(&http_client).await + { + match e + { + Error::BlockedByDirectory {directory_path} => {log::error!("Downloading hentai {hentai_id} failed, because \"{directory_path}\" already is a directory.");} // directory blocked + Error::Download {} => log::error!("Downloading hentai {hentai_id} failed multiple times. Giving up..."), // download failed multiple times, more specific error messages already in download logged + Error::SerdeXml(e) => log::error!("Serialising hentai {hentai_id} metadata failed with: {e}"), // serde xml error + Error::StdIo(e) => log::error!("Saving hentai {hentai_id} failed with: {e}"), // std io error + Error::Zip(e) => log::error!("Saving hentai {hentai_id} failed with: {e}"), // zip error + _ => panic!("Unhandled error: {e}"), + } + } + } + log::info!("--------------------------------------------------"); + + + if config.NHENTAI_TAG.is_none() {break;} // if tag not set: client mode, exit + + if let Err(e) = tokio::fs::remove_file(&config.DOWNLOADME_FILEPATH).await // server mode cleanup, delete downloadme + { + log::error!("Deleting \"{}\" failed with: {e}", config.DOWNLOADME_FILEPATH); + } + + log::info!("Sleeping for {}s...", f4.format(config.SLEEP_INTERVAL.unwrap_or_default() as f64)); + tokio::time::sleep(std::time::Duration::from_secs(config.SLEEP_INTERVAL.unwrap_or_default())).await; // if in server mode: sleep for interval until next check + log::info!("--------------------------------------------------"); + } + + return Ok(()); +} + +// https://nhentai.net/api/galleries/search?query=language%3Aenglish \ No newline at end of file diff --git a/src/main_outer.py b/src/main_outer.py deleted file mode 100644 index 9e5d608..0000000 --- a/src/main_outer.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. -from KFSlog import KFSlog -import logging -import multiprocessing -import traceback -from main import main - - -if __name__=="__main__": - DEBUG: bool=False # debug mode? - - - if DEBUG==True: - KFSlog.setup_logging("", logging.DEBUG, filepath_format="./log/%Y-%m-%dT%H_%M.log", rotate_filepath_when="M") - else: - KFSlog.setup_logging("", logging.INFO) - multiprocessing.freeze_support() # for multiprocessing to work on windows executables - - - try: - main(DEBUG) - except: - logging.critical(traceback.format_exc()) - print("\nPress enter to close program.", flush=True) - input() # pause \ No newline at end of file diff --git a/src/search_api.rs b/src/search_api.rs new file mode 100644 index 0000000..bda9ac4 --- /dev/null +++ b/src/search_api.rs @@ -0,0 +1,192 @@ +// Copyright (c) 2024 구FS, all rights reserved. Subject to the MIT licence in `licence.md`. +use crate::api_response::*; +use crate::error::*; +use crate::hentai::*; + + +/// # Summary +/// Searches nhentai.net for hentai with ID `hentai_id` and returns a corresponding HentaiTableRow entry. Updates database while doing so. +/// +/// # Arguments +/// - `http_client`: reqwest http client +/// - `nhentai_hentai_search_url`: nhentai.net hentai search API URL +/// - `id`: hentai ID +/// - `db`: database connection +/// +/// # Returns +/// - HentaiTableRow entry or error +pub async fn search_by_id(http_client: &reqwest::Client, nhentai_hentai_search_url: &str, id: u32, db: &sqlx::sqlite::SqlitePool) -> Result +{ + let r_serialised: HentaiSearchResponse; // response in json format + + + let r: reqwest::Response = http_client.get(format!("{nhentai_hentai_search_url}{id}").as_str()).send().await?; // search hentai + if r.status() != reqwest::StatusCode::OK {return Err(Error::ReqwestStatus {url: r.url().to_string(), status: r.status()});} // if status is not ok: something went wrong + r_serialised = serde_json::from_str(r.text().await?.as_str())?; // deserialise json, get this response here to get number of pages before starting parallel workers + if let Err(e) = r_serialised.write_to_db(&db).await // save data to database, if unsuccessful: warning + { + log::warn!("Saving hentai \"{id}\" metadata in database failed with: {e}"); + } + + return Ok(HentaiTableRow + { + id: r_serialised.id, + media_id: r_serialised.media_id, + num_pages: r_serialised.num_pages, + page_types: r_serialised.images.pages.iter().map(|page| format!("{:?}", page.t)).collect::>().join(""), + scanlator: r_serialised.scanlator, + title_english: r_serialised.title.english, + title_pretty: r_serialised.title.pretty, + upload_date: r_serialised.upload_date, + }); +} + + +/// # Summary +/// Searches nhentai.net for all hentai ID with tag `nhentai_tag` and returns them in a sorted list. Updates database while doing so. +/// +/// # Arguments +/// - `http_client`: reqwest http client +/// - `nhentai_tag_search_url`: nhentai.net tag search API URL +/// - `nhentai_tag`: tag to search for +/// - `db`: database connection +/// +/// # Returns +/// - list of hentai ID to download or error +pub async fn search_by_tag(http_client: &reqwest::Client, nhentai_tag_search_url: &str, nhentai_tag: &str, db: &sqlx::sqlite::SqlitePool) -> Result> +{ + const WORKERS: usize = 2; // number of concurrent workers + let f = scaler::Formatter::new() + .set_scaling(scaler::Scaling::None) + .set_rounding(scaler::Rounding::Magnitude(0)); // formatter + let mut handles: Vec>>> = Vec::new(); // list of handles to tag_search_page + let mut hentai_id_list: Vec = Vec::new(); // list of hentai id to download + let r_serialised: TagSearchResponse; // response in json format + let worker_sem: std::sync::Arc = std::sync::Arc::new(tokio::sync::Semaphore::new(WORKERS)); // limit number of concurrent workers otherwise api enforces rate limit + + + { + let r: reqwest::Response = http_client.get(nhentai_tag_search_url).query(&[("query", nhentai_tag), ("page", "1")]).send().await?; // tag search, page + if r.status() != reqwest::StatusCode::OK {return Err(Error::ReqwestStatus {url: r.url().to_string(), status: r.status()});} // if status is not ok: something went wrong + r_serialised = serde_json::from_str(r.text().await?.as_str())?; // deserialise json, get this response here to get number of pages before starting parallel workers + if let Err(e) = r_serialised.write_to_db(&db).await // save data to database, if unsuccessful: warning + { + log::warn!("Saving hentai \"{nhentai_tag}\" metadata page 1 / {} in database failed with: {e}", f.format(r_serialised.num_pages)); + } + log::info!("Downloaded hentai \"{nhentai_tag}\" metadata page 1 / {}.", f.format(r_serialised.num_pages)); + } + + for hentai in r_serialised.result // collect hentai id + { + hentai_id_list.push(hentai.id); + } + + + for page_no in 2..=r_serialised.num_pages // for each page, search in parallel + { + let db_clone: sqlx::Pool = db.clone(); + let f_clone: scaler::Formatter = f.clone(); + let http_client_clone: reqwest::Client = http_client.clone(); + let nhentai_tag: String = nhentai_tag.to_owned(); + let nhentai_tag_search_url_clone: String = nhentai_tag_search_url.to_owned(); + + let permit: tokio::sync::OwnedSemaphorePermit = worker_sem.clone().acquire_owned().await.expect("Something closed semaphore even though it should never be closed."); // acquire semaphore + handles.push(tokio::spawn(async move + { + let result: Option>; + match search_by_tag_on_page(http_client_clone, nhentai_tag_search_url_clone.clone(), nhentai_tag.clone(), page_no, r_serialised.num_pages, db_clone).await + { + Ok(o) => + { + log::info!("Downloaded hentai \"{nhentai_tag}\" metadata page {} / {}.", f_clone.format(page_no), f_clone.format(r_serialised.num_pages)); + result = Some(o); + } + Err(e) => + { + match e + { + Error::Reqwest(e) => log::error! + ( + "Downloading hentai \"{nhentai_tag}\" metadata page {} / {} from \"{}\" failed with: {e}", + f_clone.format(page_no), + f_clone.format(r_serialised.num_pages), + e.url().map_or_else(|| "", |o| o.as_str()), + ), + Error::ReqwestStatus {url, status} => log::error! + ( + "Downloading hentai \"{nhentai_tag}\" metadata page {} / {} from \"{url}\" failed with status code {status}.", + f_clone.format(page_no), + f_clone.format(r_serialised.num_pages), + ), + Error::SerdeJson(e) => log::error! + ( + "Deserialising hentai \"{nhentai_tag}\" metadata page {} / {} failed with: {e}", + f_clone.format(page_no), + f_clone.format(r_serialised.num_pages), + ), + _ => panic!("Unhandled error: {e}"), + }; + result = None; + } + } + drop(permit); // release semaphore + result // return result into handle + })); // search all pages in parallel + } + for handle in handles + { + if let Some(s) = handle.await.unwrap() {hentai_id_list.extend(s);} // collect results, forward panics + } + hentai_id_list.sort(); // sort hentai id ascending + + return Ok(hentai_id_list); +} + + +/// # Summary +/// Searches nhentai.net for all hentai ID with tag `nhentai_tag` on page `page_no` and returns them in a list. Updates database while doing so. +/// +/// # Arguments +/// - `http_client`: reqwest http client +/// - `nhentai_tag_search_url`: nhentai.net tag search api url +/// - `nhentai_tag`: tag to search for +/// - `page_no`: page number +/// - `db`: database connection +/// +/// # Returns +/// - list of hentai ID to download or error +async fn search_by_tag_on_page(http_client: reqwest::Client, nhentai_tag_search_url: String, nhentai_tag: String, page_no: u32, num_pages: u32, db: sqlx::sqlite::SqlitePool) -> Result> +{ + let f = scaler::Formatter::new() + .set_scaling(scaler::Scaling::None) + .set_rounding(scaler::Rounding::Magnitude(0)); // formatter + let mut hentai_id_list: Vec = Vec::new(); // list of hentai id to download + let mut r: reqwest::Response; // nhentai.net api response + let r_serialised: TagSearchResponse; // response in json format + + + loop + { + r = http_client.get(nhentai_tag_search_url.clone()).query(&[("query", nhentai_tag.clone()), ("page", page_no.to_string())]).send().await?; // tag search, page + if r.status() == reqwest::StatusCode::TOO_MANY_REQUESTS // if status is too many requests: wait and retry + { + log::debug!("Downloading hentai \"{nhentai_tag}\" metadata page {} from \"{}\" failed with status code {}. Waiting 2 s and retrying...", f.format(page_no), r.url().to_string(), r.status()); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + continue; + } + if r.status() != reqwest::StatusCode::OK {return Err(Error::ReqwestStatus {url: r.url().to_string(), status: r.status()});} // if status is not ok: something went wrong + break; // everything went well, continue with processing + } + r_serialised = serde_json::from_str(r.text().await?.as_str())?; // deserialise json + if let Err(e) = r_serialised.write_to_db(&db).await // save data to database + { + log::warn!("Saving hentai \"{nhentai_tag}\" metadata page {} / {} in database failed with: {e}", f.format(page_no), f.format(num_pages)); + } + + for hentai in r_serialised.result // collect hentai id + { + hentai_id_list.push(hentai.id); + } + + return Ok(hentai_id_list); +} \ No newline at end of file diff --git a/update dependencies.bat b/update dependencies.bat deleted file mode 100644 index 575cde8..0000000 --- a/update dependencies.bat +++ /dev/null @@ -1,5 +0,0 @@ -poetry cache clear --all pypi -poetry update -poetry export -f requirements.txt -o requirements.txt --without-hashes - -pause \ No newline at end of file diff --git a/update dependencies.sh b/update dependencies.sh deleted file mode 100644 index a131dc0..0000000 --- a/update dependencies.sh +++ /dev/null @@ -1,4 +0,0 @@ -# !/bin/bash -poetry cache clear --all pypi -poetry update -poetry export -f requirements.txt -o requirements.txt --without-hashes \ No newline at end of file