From c0f4e0337709b4162155a1e192ad916313b7b6bf Mon Sep 17 00:00:00 2001 From: skarrok Date: Thu, 19 Sep 2024 21:18:27 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=89=20initial=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 283 ++++++ .gitignore | 2 + CHANGELOG.md | 7 + Cargo.lock | 1625 +++++++++++++++++++++++++++++++++ Cargo.toml | 102 +++ LICENSE.txt | 21 + README.md | 137 +++ rustfmt.toml | 8 + src/config.rs | 192 ++++ src/logger.rs | 39 + src/main.rs | 26 + src/zvuk.rs | 582 ++++++++++++ 12 files changed, 3024 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 src/config.rs create mode 100644 src/logger.rs create mode 100644 src/main.rs create mode 100644 src/zvuk.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5df0c7b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,283 @@ +# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/ +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with cargo-dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (cargo-dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'cargo dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh" + - name: Cache cargo-dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/cargo-dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by cargo-dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + run: ${{ matrix.install_dist }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached cargo-dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/cargo-dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached cargo-dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/cargo-dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fedaa2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e439665 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Unreleased + +## v0.1.0 + +🎉 Initial release diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6d74bfa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1625 @@ +# 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 = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "audiotags" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e797ce0164cf599c71f2c3849b56301d96a3dc033544588e875686b050ed39" +dependencies = [ + "audiotags-macro", + "id3", + "metaflac", + "mp4ameta", + "readme-rustdocifier", + "thiserror", +] + +[[package]] +name = "audiotags-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa9b2312fc01f7291f3b7b0f52ed08b1c0177c96a2e696ab55695cc4d06889" + +[[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", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[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 = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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-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 = "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" + +[[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 = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[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", + "webpki-roots", +] + +[[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 = "id3" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f4e785f2c700217ee82a1c727c720449421742abd5fcb2f1df04e1244760e9" +dependencies = [ + "bitflags", + "byteorder", + "flate2", +] + +[[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 = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[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" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "metaflac" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f083edae4a21f5acb1fda8220d1c14fa31f725bfd4e21005a14c2d8944db9b" +dependencies = [ + "byteorder", + "hex", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mp4ameta" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81" +dependencies = [ + "lazy_static", + "mp4ameta_proc", +] + +[[package]] +name = "mp4ameta_proc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[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 = "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 = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[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 = "readme-rustdocifier" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac" + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "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 = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +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 = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[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_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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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" + +[[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" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77" +dependencies = [ + "is_ci", +] + +[[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 = "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 = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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-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-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 = "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", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[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", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[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 = "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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[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-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[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", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[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" + +[[package]] +name = "zvuk-dl" +version = "0.1.0" +dependencies = [ + "anyhow", + "audiotags", + "clap", + "dotenvy", + "humantime", + "metaflac", + "reqwest", + "serde", + "serde_json", + "supports-color", + "tracing", + "tracing-subscriber", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..73c78a2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,102 @@ +[package] +name = "zvuk-dl" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/skarrok/zvuk-dl-rs" +license = "MIT" + +[dependencies] +anyhow = "1.0.89" +audiotags = "0.5.0" +clap = { version = "4.5.18", features = ["derive", "env"] } +dotenvy = "0.15.7" +humantime = "2.1.0" +metaflac = "0.2.7" +reqwest = { version = "0.12.7", default-features = false, features = [ + "blocking", + "http2", + "json", + "rustls-tls", +] } +serde_json = "1.0.128" +serde = { version = "1.0.210", features = ["derive"] } +supports-color = "3.0.1" +tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } +tracing = { version = "0.1.40", features = ["log"] } + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } + +clone_on_ref_ptr = "deny" +disallowed_script_idents = "deny" +empty_enum_variants_with_brackets = "deny" +empty_structs_with_brackets = "deny" +enum_glob_use = "deny" +error_impl_error = "deny" +exit = "deny" +explicit_into_iter_loop = "deny" +explicit_iter_loop = "deny" +float_cmp_const = "deny" +if_then_some_else_none = "deny" +indexing_slicing = "deny" +lossy_float_literal = "deny" +map_err_ignore = "deny" +multiple_inherent_impl = "deny" +needless_raw_strings = "deny" +partial_pub_fields = "deny" +rc_buffer = "deny" +rc_mutex = "deny" +rest_pat_in_fully_bound_structs = "deny" +self_named_module_files = "deny" +semicolon_inside_block = "deny" +semicolon_outside_block = "deny" +string_slice = "deny" +string_to_string = "deny" +tests_outside_test_module = "deny" +try_err = "deny" +unnecessary_self_imports = "deny" +unneeded_field_pattern = "deny" +unseparated_literal_suffix = "deny" +verbose_file_reads = "deny" + +complexity = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } + +similar_names = "allow" +single_match_else = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" + +# The profile that 'cargo dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" + +# Config for 'cargo dist' +[workspace.metadata.dist] +# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.22.1" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell"] +# Target platforms to build apps for (Rust target-triple syntax) +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", +] +# Path that installers should place binaries in +install-path = "CARGO_HOME" +# Whether to install an updater program +install-updater = false diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c411d6b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 skarrok.h@gmail.com + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d58d067 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# zvuk-dl ![Release](https://github.com/skarrok/zvuk-dl-rs/actions/workflows/release.yml/badge.svg) + +Download albums and tracks in high quality (FLAC) from [zvuk.com](https://zvuk.com) + +NOTICE: You must have zvuk.com account and paid subscription to use this tool. + +## Quickstart + +```sh +# write you token to config +echo "TOKEN=YOUR_TOKEN" > .env + +# or provide it as an environment variable +export TOKEN=YOUR_TOKEN + +# or provide it as an argument to command +# zvuk-dl --token YOUR_TOKEN ... + +# download track and album +zvuk-dl https://zvuk.com/track/128672726 https://zvuk.com/release/29970563 +``` + +Tracks are downloaded to current directory with +`Author - Album (Year)/## - Title.flac` format and tags are added +automatically. + +By default, zvuk-dl downloads and embeds lyrics and downloads album cover. +You can enable cover embedding with `--embed-cover` option. +Album cover is resized to be less than 1MB using imagemagick. + +NOTICE: If you don't have [imagemagick](https://imagemagick.org) installed, disable +cover resizing with `--resize-cover=false` or command will fail. + +## Getting your personal token + +Token looks like hexadecimal string with 32 symbols. +Simplest way to get it is to visit zvuk.com and log in. +Make sure you have paid subscription. +Open your browser's developer tools and view cookies for `https://zvuk.com` domain. +Your token will be in there under `auth` name. + +For example in Chrome: + +1. Click the Three-dot menu button to the right of the address bar and select +More Tools > Developer Tools. +2. In the top bar select Application tab. +3. In the left sidebar under Storage -> Cookies select `https://zvuk.com` +4. In the right pane select `auth` cookie and copy it. +5. Write it to `.env` file in the current directory with +`echo TOKEN=YOUR_TOKEN > .env` + +## Configuration + +You can pass configuration parameters as command line arguments or environment +variables or write it to `.env` file in the current directory. + +```txt +Download albums and tracks in high quality (FLAC) from Zvuk.com + +Usage: zvuk-dl [OPTIONS] --token ... + +Arguments: + ... + URLs of releases or tracks + + URLs must look like https://zvuk.com/track/128672726 or https://zvuk.com/release/29970563 + +Options: + --token + Zvuk Token + + [env: TOKEN] + + --embed-cover[=] + Embed album cover into tracks + + [env: EMBED_COVER=] + [default: false] + [possible values: true, false] + + --resize-cover[=] + Resize album cover + + [env: RESIZE_COVER=] + [default: true] + [possible values: true, false] + + --resize-cover-limit + Resize if cover size in bytes bigger than this value + + [env: RESIZE_COVER_LIMIT=] + [default: 2000000] + + --download-lyrics[=] + Download and embed lyrics + + [env: DOWNLOAD_LYRICS=] + [default: true] + [possible values: true, false] + + --resize-command + Resize cover command. By default uses imagemagick + + [env: RESIZE_COMMAND=] + [default: "magick {source} -define jpeg:extent=1MB {target}"] + + --log-level + Verbosity of logging + + [env: LOG_LEVEL=] + [default: debug] + [possible values: off, trace, debug, info, warn, error] + + --log-format + Format of logs + + [env: LOG_FORMAT=] + [default: console] + + Possible values: + - console: Pretty logs for debugging + - json: JSON logs + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version +``` + +## Building + +It is as simple as cloning this repository and running + +```bash +cargo build --release +``` diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3e9b5ea --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +hard_tabs = false +match_block_trailing_comma = true +max_width = 79 +newline_style = "Unix" +reorder_imports = true +reorder_modules = true +use_field_init_shorthand = true +use_small_heuristics = "Default" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..83884ff --- /dev/null +++ b/src/config.rs @@ -0,0 +1,192 @@ +use std::time::Duration; + +use anyhow::anyhow; +use clap::ArgAction; +use clap::Parser; +use clap::ValueEnum; +use serde::Serialize; +use serde::Serializer; +use serde_json::to_value; +use tracing::level_filters::LevelFilter; + +use crate::zvuk::ZVUK_DEFAULT_COVER_RESIZE_COMMAND; + +/// Download albums and tracks in high quality (FLAC) from Zvuk.com +#[derive(Debug, Parser, Serialize)] +#[command(author, version, about, long_about = None)] +pub struct Config { + #[allow(clippy::doc_markdown)] + /// URLs of releases or tracks + /// + /// URLs must look like https://zvuk.com/track/128672726 or https://zvuk.com/release/29970563 + #[arg(required = true, num_args = 1..)] + pub urls: Vec, + + /// Zvuk Token + #[serde(serialize_with = "mask")] + #[arg(long, env, hide_env_values = true)] + pub token: String, + + /// Embed album cover into tracks + #[arg( + long, + env, + action = ArgAction::Set, + default_value_t = false, + default_missing_value = "true", + require_equals = true, + num_args=0..=1, + )] + pub embed_cover: bool, + + /// Resize album cover + #[arg( + long, + env, + action = ArgAction::Set, + default_value_t = true, + default_missing_value = "true", + require_equals = true, + num_args=0..=1, + )] + pub resize_cover: bool, + + /// Resize if cover size in bytes bigger than this value + #[arg(long, env, default_value_t = 2 * 1000 * 1000)] + pub resize_cover_limit: u64, + + /// Download and embed lyrics + #[arg( + long, + env, + action = ArgAction::Set, + default_value_t = true, + default_missing_value = "true", + require_equals = true, + num_args=0..=1, + )] + pub download_lyrics: bool, + + /// Resize cover command. + /// By default uses imagemagick + #[arg( + long, + env, + value_parser = resize_command_validator, + default_value_t = ZVUK_DEFAULT_COVER_RESIZE_COMMAND.to_string(), + )] + pub resize_command: String, + + /// How long to wait between getting track links + #[arg( + long, + env, + hide = true, + default_value = "1s", + value_parser = humantime::parse_duration, + )] + pub pause_between_getting_track_links: Duration, + + /// Verbosity of logging + #[arg(long, value_enum, env, default_value_t = LogLevel::Debug)] + pub log_level: LogLevel, + + /// Format of logs + #[arg(long, value_enum, env, default_value_t = LogFormat::Console)] + pub log_format: LogFormat, +} + +#[derive(ValueEnum, Debug, Clone, Copy, Serialize)] +pub enum LogFormat { + /// Pretty logs for debugging + Console, + /// JSON logs + Json, +} + +#[derive(ValueEnum, Debug, Clone, Copy, Serialize)] +pub enum LogLevel { + Off, + Trace, + Debug, + Info, + Warn, + Error, +} + +impl From for LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Off => Self::OFF, + LogLevel::Trace => Self::TRACE, + LogLevel::Debug => Self::DEBUG, + LogLevel::Info => Self::INFO, + LogLevel::Warn => Self::WARN, + LogLevel::Error => Self::ERROR, + } + } +} + +pub fn mask(_: &T, s: S) -> Result +where + S: Serializer, +{ + static MASK: &str = "******"; + s.serialize_str(MASK) +} + +pub trait LogStruct { + fn log(&self); +} + +impl LogStruct for T +where + T: Serialize, +{ + fn log(&self) { + if let Ok(json_obj) = to_value(self) { + if let Ok(json_obj) = + json_obj.as_object().ok_or_else(|| anyhow!("WTF")) + { + for (key, value) in json_obj { + tracing::debug!("Config {}={}", key, value); + } + } + } + } +} + +fn resize_command_validator(value: &str) -> anyhow::Result { + if value.contains("{source}") && value.contains("{target}") { + return Ok(String::from(value)); + } + Err(anyhow!( + "command is required to have {{source}} and {{target}} placeholders" + )) +} + +#[cfg(test)] +mod tests { + use super::resize_command_validator; + use super::Config; + + #[test] + fn validate_resize_command() { + let successes = &["cmd {source} {target}"]; + let fails = &["cmd {target}", "cmd {source}", "cmd", ""]; + + for case in successes { + assert!(resize_command_validator(case).is_ok()); + } + + for case in fails { + assert!(resize_command_validator(case).is_err()); + } + } + + #[test] + fn verify_cli() { + use clap::CommandFactory; + Config::command().debug_assert(); + } +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..54468bd --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,39 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::EnvFilter; + +use crate::config::{LogFormat, LogLevel}; + +pub fn setup( + log_level: LogLevel, + log_format: LogFormat, + bin_name: Option<&str>, +) { + let log_level: LevelFilter = log_level.into(); + + let with_color = supports_color::on(supports_color::Stream::Stderr) + .filter(|s| s.has_basic) + .is_some(); + + let mut default_filter = + format!("{}={log_level}", env!("CARGO_PKG_NAME").replace('-', "_")); + if let Some(bin_name) = bin_name { + default_filter + .push_str(&format!(",{}={log_level}", bin_name.replace('-', "_"))); + } + + let filter = EnvFilter::builder().try_from_env().unwrap_or_else(|_| { + EnvFilter::builder() + .parse(default_filter) + .expect("hardcoded filter should be correct") + }); + + let builder = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(std::io::stderr) + .with_ansi(with_color); + + match log_format { + LogFormat::Console => builder.init(), + LogFormat::Json => builder.json().flatten_event(true).init(), + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..de3a92e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +mod config; +mod logger; +mod zvuk; + +use clap::Parser; +use dotenvy::dotenv; + +use config::Config; +use config::LogStruct; + +fn main() -> anyhow::Result<()> { + dotenv().ok(); + let config = Config::parse(); + + logger::setup( + config.log_level, + config.log_format, + option_env!("CARGO_BIN_NAME"), + ); + + config.log(); + + zvuk::download(&config)?; + + Ok(()) +} diff --git a/src/zvuk.rs b/src/zvuk.rs new file mode 100644 index 0000000..000bca7 --- /dev/null +++ b/src/zvuk.rs @@ -0,0 +1,582 @@ +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, + time::Duration, +}; + +use anyhow::Context; +use audiotags::{ + traits::AudioTagEdit, traits::AudioTagWrite, FlacTag, MimeType, Picture, +}; +use reqwest::header::COOKIE; + +use crate::config::Config; + +const ZVUK_RELEASE_PREFIX: &str = "https://zvuk.com/release/"; +const ZVUK_TRACKS_PREFIX: &str = "https://zvuk.com/track/"; +const ZVUK_RELEASES_URL: &str = "https://zvuk.com/api/tiny/releases"; +const ZVUK_LABELS_URL: &str = "https://zvuk.com/api/tiny/labels"; +const ZVUK_TRACKS_URL: &str = "https://zvuk.com/api/tiny/tracks"; +const ZVUK_DOWNLOAD_URL: &str = "https://zvuk.com/api/tiny/track/stream"; +const ZVUK_LYRICS_URL: &str = "https://zvuk.com/api/tiny/lyrics"; + +pub const ZVUK_DEFAULT_COVER_RESIZE_COMMAND: &str = + "magick {source} -define jpeg:extent=1MB {target}"; + +#[derive(Debug)] +struct ReleaseInfo { + track_ids: Vec, + track_count: u32, + label: String, + date: String, + album: String, + author: String, +} + +#[expect(unused)] +#[derive(Debug)] +struct TrackInfo { + author: String, + name: String, + album: String, + release_id: String, + track_id: String, + genre: String, + number: u32, + image: String, + lyrics: bool, +} + +struct Client { + embed_cover: bool, + resize_cover: bool, + resize_cover_limit: u64, + download_lyrics: bool, + resize_command: String, + + pause_between_getting_track_links: Duration, + cookie_token: String, + http: reqwest::blocking::Client, +} + +impl Client { + fn new(config: &Config) -> Self { + Self { + embed_cover: config.embed_cover, + resize_cover: config.resize_cover, + resize_cover_limit: config.resize_cover_limit, + download_lyrics: config.download_lyrics, + resize_command: config.resize_command.clone(), + + pause_between_getting_track_links: Duration::from_secs(1), + cookie_token: format!("auth={}", config.token), + http: reqwest::blocking::Client::new(), + } + } + + fn get_labels_info( + &self, + label_ids: &[String], + ) -> anyhow::Result> { + tracing::info!("Downloading label metadata"); + let response = self + .http + .get(ZVUK_LABELS_URL) + .query(&[("ids", label_ids.join(","))]) + .header(COOKIE, &self.cookie_token) + .send() + .context("Failed to download labels metadata")?; + let body = response + .json::() + .context("Failed to parse labels metadata")?; + + tracing::trace!("{ZVUK_LABELS_URL} response: {body:#?}"); + + let mut labels = HashMap::new(); + + for (label_id, label_info) in body + .get("result") + .and_then(|x| x.get("labels")) + .and_then(|x| x.as_object()) + .context("No labels in labels metadata")? + { + labels.insert( + label_id.clone(), + label_info + .get("title") + .and_then(|x| x.as_str()) + .context("Label title is not a string")? + .to_string(), + ); + } + + Ok(labels) + } + + fn get_releases_info( + &self, + release_ids: &[String], + ) -> anyhow::Result> { + tracing::info!("Downloading releases metadata"); + let client = reqwest::blocking::Client::new(); + let response = client + .get(ZVUK_RELEASES_URL) + .query(&[("ids", release_ids.join(","))]) + .header(COOKIE, &self.cookie_token) + .send() + .context("Failed to download releases metadata")?; + + let body = response + .json::() + .context("Failed to parse releses metadata")?; + + tracing::trace!("{ZVUK_RELEASES_URL} response: {body:#?}"); + + let mut label_ids = Vec::new(); + for (_release_id, release_info) in body + .get("result") + .and_then(|x| x.get("releases")) + .and_then(|x| x.as_object()) + .context("No releases in releases metadata")? + { + label_ids.push( + release_info + .get("label_id") + .and_then(|x| x.as_number()) + .context("Label id is not a number")? + .to_string(), + ); + } + + let labels = self.get_labels_info(&label_ids)?; + + let mut releases = HashMap::new(); + + for (release_id, release_info) in body + .get("result") + .and_then(|x| x.get("releases")) + .and_then(|x| x.as_object()) + .context("No releases in releases metadata")? + { + let track_ids: Vec<_> = release_info + .get("track_ids") + .and_then(|x| x.as_array()) + .context("track_ids is not an array")? + .iter() + .filter_map(|x| Some(x.as_number()?.to_string())) + .collect(); + let track_count: u32 = track_ids.len().try_into()?; + + releases.insert( + release_id.clone(), + ReleaseInfo { + track_ids, + track_count, + label: labels + .get( + &release_info + .get("label_id") + .and_then(|x| x.as_number()) + .context("label_id is not a number")? + .to_string(), + ) + .context("no label info")? + .as_str() + .to_string(), + date: release_info + .get("date") + .context("no date")? + .to_string(), + album: release_info + .get("title") + .and_then(|x| x.as_str()) + .context("no title")? + .to_string(), + author: release_info + .get("credits") + .and_then(|x| x.as_str()) + .context("credits is not a string")? + .to_string(), + }, + ); + } + + Ok(releases) + } + + fn download_albums(&self, release_ids: &[String]) -> anyhow::Result<()> { + let mut track_ids = Vec::new(); + let releases = self + .get_releases_info(release_ids) + .context("Failed to get releases metadata")?; + + for release_info in releases.values() { + track_ids.extend(release_info.track_ids.clone()); + } + + self.download_tracks(&track_ids, &releases) + .context("Failed to download tracks")?; + Ok(()) + } + + fn download_tracks( + &self, + track_ids: &[String], + releases: &HashMap, + ) -> anyhow::Result<()> { + let metadata = self + .get_tracks_metadata(track_ids) + .context("Failed to get tracks metadata")?; + let links = self + .get_tracks_links(track_ids) + .context("Failed to get tracks download links")?; + + if metadata.len() != links.len() { + return Err(anyhow::anyhow!( + "metadata and links have different length" + )); + } + let releases_ = if releases.is_empty() { + let mut release_ids = HashSet::new(); + for track_info in metadata.values() { + release_ids.insert(track_info.release_id.clone()); + } + let release_ids = release_ids.into_iter().collect::>(); + &self + .get_releases_info(&release_ids) + .context("Failed to get releases metadata")? + } else { + releases + }; + + for (track_id, track_info) in metadata { + let result = self.get_and_save_track( + links.get(&track_id).context("no link")?, + &track_info, + releases_ + .get(&track_info.release_id) + .context("no release info")?, + ); + if let Err(e) = result { + tracing::warn!( + "Failed to download and process track id={track_id}: {e}" + ); + } + } + Ok(()) + } + + fn get_tracks_metadata( + &self, + track_ids: &[String], + ) -> anyhow::Result> { + tracing::info!("Downloading tracks metadata"); + let response = self + .http + .get(ZVUK_TRACKS_URL) + .query(&[("ids", track_ids.join(","))]) + .header(COOKIE, &self.cookie_token) + .send() + .context("Failed to donwload tracks metadata")?; + + let body = response + .json::() + .context("Failed to parse tracks metadata")?; + tracing::trace!("{ZVUK_TRACKS_URL} response: {body:#?}"); + + let mut tracks = HashMap::new(); + + for (track_id, track_info) in body + .get("result") + .and_then(|x| x.get("tracks")) + .and_then(|x| x.as_object()) + .context("tracks is not an object")? + { + if !track_info + .get("has_flac") + .and_then(serde_json::Value::as_bool) + .context("has_flac is not bool")? + { + tracing::warn!( + "track id {track_id} doesn't have FLAC quality available" + ); + continue; + } + tracks.insert( + track_id.clone(), + TrackInfo { + author: track_info + .get("credits") + .and_then(|x| x.as_str()) + .context("credits is not a string")? + .to_string(), + name: track_info + .get("title") + .and_then(|x| x.as_str()) + .context("title is not a string")? + .to_string(), + album: track_info + .get("release_title") + .and_then(|x| x.as_str()) + .context("release_title is not a string")? + .to_string(), + release_id: track_info + .get("release_id") + .and_then(|x| x.as_number()) + .context("release_id is not a number")? + .to_string(), + track_id: track_info + .get("id") + .context("no id")? + .to_string(), + genre: track_info + .get("genres") + .and_then(|x| x.as_array()) + .context("genre is not an array")? + .iter() + .filter_map(|x| x.as_str()) + .collect::>() + .join(", "), + number: track_info + .get("position") + .and_then(serde_json::Value::as_u64) + .context("position is not a number")? + .try_into()?, + image: track_info + .get("image") + .and_then(|x| x.get("src")) + .and_then(|x| x.as_str()) + .context("image src is not a string")? + .replace("&size={size}&ext=jpg", ""), + lyrics: track_info + .get("lyrics") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + }, + ); + } + + Ok(tracks) + } + + fn get_tracks_links( + &self, + track_ids: &[String], + ) -> anyhow::Result> { + tracing::info!("Downloading tracks FLAC urls"); + let mut urls = HashMap::new(); + + for track_id in track_ids { + let response = self + .http + .get(ZVUK_DOWNLOAD_URL) + .query(&[("quality", "flac"), ("id", track_id)]) + .header(COOKIE, &self.cookie_token) + .send() + .context("Failed to download track links")?; + + let body = response + .json::() + .context("Failed to prase track links")?; + tracing::trace!("{ZVUK_DOWNLOAD_URL} response: {body:#?}"); + + urls.insert( + track_id.clone(), + body.get("result") + .and_then(|x| x.get("stream")) + .and_then(|x| x.as_str()) + .context("stream is not a string")? + .to_string(), + ); + + std::thread::sleep(self.pause_between_getting_track_links); + } + Ok(urls) + } + + fn get_lyrics(&self, track_id: &str) -> anyhow::Result { + tracing::debug!("Downloading lyrics for track id={track_id}"); + let response = self + .http + .get(ZVUK_LYRICS_URL) + .query(&[("track_id", track_id)]) + .header(COOKIE, &self.cookie_token) + .send() + .context("Failed to download lyrics")?; + let body = response + .json::() + .context("Failed to parse lyrics")?; + tracing::trace!("{ZVUK_LYRICS_URL} response: {body:#?}"); + + let lyrics = body + .get("result") + .and_then(|x| x.get("lyrics")) + .and_then(|x| x.as_str()) + .context("lyrics is not a string")? + .to_string(); + + Ok(lyrics) + } + + fn download_cover(&self, url: &str, path: &Path) -> anyhow::Result<()> { + if !path.try_exists()? { + tracing::info!("Downloading cover {}", path.display()); + let response = self.http.get(url).send()?; + std::fs::write(path, response.bytes()?)?; + } + + if self.resize_cover + && std::fs::metadata(path)?.len() > self.resize_cover_limit + { + tracing::debug!("Resizing cover {}", path.display()); + + let path_str = + path.to_str().context("Failed to convert path to str")?; + let command_str = self + .resize_command + .split_whitespace() + .map(|x| { + x.replace("{source}", path_str) + .replace("{target}", path_str) + }) + .collect::>(); + let (command, args) = command_str + .split_first() + .context("Failed to parse resize command")?; + + let status = std::process::Command::new(command) + .args(args) + .status() + .context("Failed to run resize command")?; + if !status.success() { + return Err(anyhow::anyhow!("Failed to resize cover")); + } + } + Ok(()) + } + + fn get_and_save_track( + &self, + url: &str, + track_info: &TrackInfo, + release_info: &ReleaseInfo, + ) -> anyhow::Result<()> { + let folder = PathBuf::from(format!( + "{} - {} ({})", + release_info.author, + release_info.album, + release_info.date.chars().take(4).collect::() + )); + + std::fs::create_dir_all(&folder).with_context(|| { + format!("Failed to create folder {}", folder.display()) + })?; + + let cover_path = folder.join("cover.jpg"); + self.download_cover(&track_info.image, &cover_path) + .context("Failed to download and process album cover")?; + + let filename = PathBuf::from(format!( + "{:02} - {}.flac", + track_info.number, track_info.name + )); + let filepath = folder.join(filename); + + tracing::info!("Downloading {}", filepath.display()); + + let response = self + .http + .get(url) + .send() + .context("Failed to download track")?; + std::fs::write( + &filepath, + response.bytes().context("Failed to read track data")?, + ) + .context("Failed to write track")?; + + let mut flac = FlacTag::read_from_path(&filepath) + .context("Failed to read tags from track")?; + + flac.set_artist(&track_info.author); + flac.set_title(&track_info.name); + flac.set_album_title(&release_info.album); + flac.set_track_number(track_info.number.try_into()?); + flac.set_total_tracks(release_info.track_count.try_into()?); + flac.set_genre(&track_info.genre); + + if self.embed_cover { + let cover = Picture { + mime_type: MimeType::Jpeg, + data: &std::fs::read(cover_path) + .context("Failed to read cover file for embedding")?, + }; + flac.set_album_cover(cover); + } + + let mut flactag: metaflac::Tag = flac.into(); + let vorbis_tags = flactag.vorbis_comments_mut(); + + vorbis_tags.set("DATE", vec![&release_info.date]); + vorbis_tags.set( + "YEAR", + vec![&release_info.date.chars().take(4).collect::()], + ); + vorbis_tags.set("COPYRIGHT", vec![&release_info.label]); + + vorbis_tags.set("RELEASE_ID", vec![&track_info.release_id]); + vorbis_tags.set("TRACK_ID", vec![&track_info.track_id]); + + if self.download_lyrics && track_info.lyrics { + let lyrics = self + .get_lyrics(&track_info.track_id) + .context("Failed to get lyrics")?; + if lyrics.is_empty() { + tracing::warn!("No lyrics for {}", filepath.display()); + } else { + vorbis_tags.set_lyrics(vec![lyrics]); + } + } + + let mut flac: FlacTag = flactag.into(); + flac.write_to_path( + filepath.to_str().context("filepath is not valid string")?, + ) + .context("Failed to write tags to file")?; + + Ok(()) + } +} + +pub fn download(config: &Config) -> anyhow::Result<()> { + let mut release_ids = Vec::new(); + let mut track_ids = Vec::new(); + + for url in &config.urls { + if url.starts_with(ZVUK_RELEASE_PREFIX) { + if let Some(url) = url.strip_prefix(ZVUK_RELEASE_PREFIX) { + release_ids.push(url.to_owned()); + } + } else if url.starts_with(ZVUK_TRACKS_PREFIX) { + if let Some(url) = url.strip_prefix(ZVUK_TRACKS_PREFIX) { + track_ids.push(url.to_owned()); + } + } else { + tracing::warn!( + "This doens't look like zvuk.com URL, skipping: {}", + url + ); + } + } + + let client = Client::new(config); + + if !release_ids.is_empty() { + client.download_albums(&release_ids)?; + } + if !track_ids.is_empty() { + client.download_tracks(&track_ids, &HashMap::new())?; + } + + Ok(()) +}