commit 616c6b1c6225cf77edceeb368d62a304efdd9109 Author: nkozobrod Date: Sat May 30 12:07:11 2026 +0000 Initial commit: docker compose config diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2261801 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +web/node_modules +web/build +web/dist +web/.env_example \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..11f839f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Linux Ubuntu 22.04] + - TorrServer Version [e.g. MatriX.129] + +**Smartphone or tvbox on Android (please complete the following information):** + - Device: [e.g. Ugoos am6] + - OS: [e.g. Android 9] + - TorrServer Version [e.g. MatriX.129] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6269982 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/scripts/test-install-script.sh b/.github/scripts/test-install-script.sh new file mode 100755 index 0000000..9294f8f --- /dev/null +++ b/.github/scripts/test-install-script.sh @@ -0,0 +1,412 @@ +#!/usr/bin/env bash +# Test script for installTorrServerLinux.sh +# This script runs inside Docker containers to test the installation script + +set -e + +# Colors for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' # No Color + +# Test configuration +readonly SCRIPT_NAME="installTorrServerLinux.sh" +readonly INSTALL_DIR="/opt/torrserver" +readonly GLIBC_LIMITED_VERSION="135" +readonly MIN_GLIBC_VERSION="2.32" +readonly MAX_RETRIES="${MAX_RETRIES:-3}" +readonly RETRY_DELAY="${RETRY_DELAY:-2}" + +# Helper functions +log_info() { + echo -e "${GREEN}✓${NC} $1" +} + +log_error() { + echo -e "${RED}✗${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +log_test() { + echo "Test $1: $2" +} + +# Check if OS requires glibc-limited version +is_glibc_limited_os() { + local os="$1" + local glibc_limited_oses="$2" + echo "$glibc_limited_oses" | grep -qE "(^|\|)$os(\||$)" +} + +# Get glibc version message for OS +get_glibc_message() { + local os="$1" + case "$os" in + debian-11) + echo "Note: Debian 11 has glibc 2.31, installing version $GLIBC_LIMITED_VERSION (version 136+ requires glibc >= $MIN_GLIBC_VERSION)" + ;; + almalinux-8) + echo "Note: AlmaLinux 8 has glibc 2.28, installing version $GLIBC_LIMITED_VERSION (version 136+ requires glibc >= $MIN_GLIBC_VERSION)" + ;; + rocky-8) + echo "Note: Rocky 8 has glibc 2.28, installing version $GLIBC_LIMITED_VERSION (version 136+ requires glibc >= $MIN_GLIBC_VERSION)" + ;; + amazonlinux-2) + echo "Note: Amazon Linux 2 has glibc 2.26, installing version $GLIBC_LIMITED_VERSION (version 136+ requires glibc >= $MIN_GLIBC_VERSION)" + ;; + esac +} + +# Install RPM packages (dnf/yum) +install_rpm_packages() { + local pkg_manager="$1" + shift + local packages=("$@") + + "$pkg_manager" makecache -q || true + # Always remove curl-minimal first to avoid conflicts + "$pkg_manager" remove -y -q curl-minimal 2>/dev/null || true + + # Check if curl package is installed (not just curl-minimal) + if rpm -qa curl >/dev/null 2>&1; then + # curl package is already installed, just install other packages + "$pkg_manager" install -y -q "${packages[@]}" || true + else + # curl package not installed, install curl with --allowerasing + "$pkg_manager" install -y -q --allowerasing curl "${packages[@]}" || true + fi +} + +# Install dependencies based on OS +install_dependencies() { + if command -v apt-get >/dev/null 2>&1; then + retry_command "apt-get update" "apt-get update -qq" 3 1 || true + retry_command "apt-get install" "apt-get install -y -qq curl iputils-ping dnsutils" 3 1 || true + elif command -v dnf >/dev/null 2>&1; then + retry_command "dnf install" "install_rpm_packages dnf iputils bind-utils" 3 1 || true + elif command -v yum >/dev/null 2>&1; then + retry_command "yum install" "install_rpm_packages yum iputils bind-utils" 3 1 || true + fi +} + +# Verify curl installation +verify_curl_installation() { + if command -v rpm >/dev/null 2>&1; then + if ! rpm -qa curl >/dev/null 2>&1; then + log_error "curl package is not installed after dependency installation" + exit 1 + fi + # Verify curl-minimal is not present (it should have been removed) + if rpm -qa curl-minimal >/dev/null 2>&1; then + log_warning "curl-minimal is still installed, removing it..." + rpm -e --nodeps curl-minimal 2>/dev/null || true + fi + elif command -v dpkg >/dev/null 2>&1; then + if ! dpkg -s curl >/dev/null 2>&1; then + log_error "curl package is not installed after dependency installation" + exit 1 + fi + fi +} + +# Retry a command with exponential backoff +retry_command() { + local test_name="$1" + local test_command="$2" + local max_attempts="${3:-$MAX_RETRIES}" + local delay="${4:-$RETRY_DELAY}" + local attempt=1 + local last_error=0 + + # Print command before first attempt + if [[ $attempt -eq 1 ]]; then + echo "Executing: $test_command" + fi + + while [[ $attempt -le $max_attempts ]]; do + if [[ $attempt -gt 1 ]]; then + echo "Retry attempt $attempt/$max_attempts: $test_command" + fi + if eval "$test_command"; then + if [[ $attempt -gt 1 ]]; then + log_info "$test_name (succeeded on attempt $attempt)" + fi + return 0 + else + last_error=$? + if [[ $attempt -lt $max_attempts ]]; then + log_warning "$test_name failed (attempt $attempt/$max_attempts), retrying in ${delay}s..." + sleep "$delay" + delay=$((delay * 2)) + fi + attempt=$((attempt + 1)) + fi + done + + log_error "$test_name (failed after $max_attempts attempts)" + return $last_error +} + +# Run a test command and handle errors +run_test() { + local test_name="$1" + local test_command="$2" + local skip_on_error="${3:-false}" + local use_retry="${4:-true}" + + if [[ "$use_retry" == "true" ]]; then + if retry_command "$test_name" "$test_command"; then + log_info "$test_name" + return 0 + else + if [[ "$skip_on_error" == "true" ]]; then + log_warning "$test_name (skipped after retries)" + return 0 + else + log_error "$test_name" + return 1 + fi + fi + else + if eval "$test_command"; then + log_info "$test_name" + return 0 + else + if [[ "$skip_on_error" == "true" ]]; then + log_warning "$test_name (skipped)" + return 0 + else + log_error "$test_name" + return 1 + fi + fi + fi +} + +# Main test execution +main() { + local matrix_os="${MATRIX_OS:-}" + local test_user="${TEST_USER:-default}" + local glibc_limited_oses="${GLIBC_LIMITED_OSES:-}" + + # Determine root flag + local root_flag='' + if [[ "$test_user" == 'root' ]]; then + root_flag='--root' + fi + + # Check if OS requires glibc-limited version + local is_glibc_limited=false + if is_glibc_limited_os "$matrix_os" "$glibc_limited_oses"; then + is_glibc_limited=true + fi + + echo "========================================" + echo "Testing $SCRIPT_NAME" + echo "OS: $matrix_os" + echo "User: $test_user" + echo "Retry settings: max=$MAX_RETRIES, initial_delay=${RETRY_DELAY}s" + echo "========================================" + echo "" + + # Test 1: Check script syntax + echo "::group::Test 1: Check script syntax" + log_test "1" "Checking script syntax..." + echo "Executing: bash -n $SCRIPT_NAME" + if bash -n "$SCRIPT_NAME"; then + log_info "Script syntax is valid" + else + log_error "Script syntax check failed" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 2: Show help + echo "::group::Test 2: Test help command" + log_test "2" "Testing help command..." + echo "Executing: ./$SCRIPT_NAME --help" + if ./"$SCRIPT_NAME" --help > /dev/null; then + log_info "Help command works" + else + log_error "Help command failed" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 3: Install in silent mode + echo "::group::Test 3: Install TorrServer" + log_test "3" "Installing TorrServer (silent mode)..." + if [[ "$is_glibc_limited" == "true" ]]; then + local glibc_msg + glibc_msg=$(get_glibc_message "$matrix_os") + if [[ -n "$glibc_msg" ]]; then + echo "$glibc_msg" + fi + if retry_command "Installation" "./$SCRIPT_NAME --install $GLIBC_LIMITED_VERSION --silent $root_flag"; then + log_info "Installation completed" + else + log_error "Installation failed after retries" + echo "::endgroup::" + exit 1 + fi + else + if retry_command "Installation" "./$SCRIPT_NAME --install --silent $root_flag"; then + log_info "Installation completed" + else + log_error "Installation failed after retries" + echo "::endgroup::" + exit 1 + fi + fi + echo "::endgroup::" + echo "" + + # Test 4: Check installation + echo "::group::Test 4: Verify installation" + log_test "4" "Checking installation..." + echo "Executing: ls $INSTALL_DIR/TorrServer-linux-*" + if ls "$INSTALL_DIR"/TorrServer-linux-* >/dev/null 2>&1; then + log_info "Binary file exists" + else + log_error "Binary file not found" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 5: Check version + echo "::group::Test 5: Check version" + log_test "5" "Checking for updates..." + if [[ "$is_glibc_limited" == "true" ]]; then + echo "Note: Skipping version check (latest version requires glibc >= $MIN_GLIBC_VERSION)" + log_info "Version check skipped (expected)" + else + if retry_command "Version check" "./$SCRIPT_NAME --check --silent $root_flag"; then + log_info "Version check completed" + else + log_error "Version check failed after retries" + echo "::endgroup::" + exit 1 + fi + fi + echo "::endgroup::" + echo "" + + # Test 6: Update (if available) + echo "::group::Test 6: Test update command" + log_test "6" "Testing update command..." + if [[ "$is_glibc_limited" == "true" ]]; then + echo "Note: Skipping update test (latest version requires glibc >= $MIN_GLIBC_VERSION)" + log_info "Update check skipped (expected)" + else + if retry_command "Update check" "./$SCRIPT_NAME --update --silent $root_flag"; then + log_info "Update check completed" + else + log_error "Update check failed after retries" + echo "::endgroup::" + exit 1 + fi + fi + echo "::endgroup::" + echo "" + + # Test 7: Reconfigure + echo "::group::Test 7: Test reconfigure command" + log_test "7" "Testing reconfigure command..." + if retry_command "Reconfigure" "./$SCRIPT_NAME --reconfigure --silent $root_flag"; then + log_info "Reconfigure completed" + else + log_error "Reconfigure failed after retries" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 8: Change user (if not already root) + if [[ "$test_user" == 'default' ]]; then + echo "::group::Test 8: Test change-user to root" + log_test "8" "Testing change-user to root..." + if retry_command "User change to root" "./$SCRIPT_NAME --change-user root --silent"; then + log_info "User change to root completed" + else + log_error "User change to root failed after retries" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 8b: Change user back to default (only for Ubuntu to test full flow) + if [[ "$matrix_os" == 'ubuntu-22.04' ]] || [[ "$matrix_os" == 'ubuntu-24.04' ]]; then + echo "::group::Test 8b: Test change-user back to default" + log_test "8b" "Testing change-user back to default..." + if retry_command "User change back to default" "./$SCRIPT_NAME --change-user torrserver --silent"; then + log_info "User change back to default completed" + else + log_error "User change back to default failed after retries" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + fi + fi + + # Test 9: Cleanup - Uninstall + echo "::group::Test 9: Uninstall TorrServer" + log_test "9" "Uninstalling TorrServer..." + if retry_command "Uninstallation" "./$SCRIPT_NAME --remove --silent"; then + log_info "Uninstallation completed" + else + log_error "Uninstallation failed after retries" + echo "::endgroup::" + exit 1 + fi + echo "::endgroup::" + echo "" + + # Test 10: Verify cleanup + echo "::group::Test 10: Verify cleanup" + log_test "10" "Verifying cleanup..." + echo "Executing: Checking if $INSTALL_DIR is empty or doesn't exist" + if [[ ! -d "$INSTALL_DIR" ]] || [[ -z "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]]; then + log_info "Cleanup verified" + else + log_warning "Installation directory still exists (may be expected)" + fi + echo "::endgroup::" + echo "" + + echo "========================================" + echo "All tests passed! ✓" + echo "========================================" +} + +# Setup and run tests +setup() { + echo "::group::Setup: Install dependencies" + # Install dependencies + install_dependencies + + # Verify curl installation + verify_curl_installation + + # Make script executable + chmod +x "$SCRIPT_NAME" + echo "::endgroup::" + echo "" +} + +# Run setup and main +setup +main diff --git a/.github/workflows/docker_image.yml b/.github/workflows/docker_image.yml new file mode 100644 index 0000000..0e951fb --- /dev/null +++ b/.github/workflows/docker_image.yml @@ -0,0 +1,66 @@ +name: Release Docker multi arch + +on: + push: + branches: + - master + tags: + - "**" + +concurrency: + group: docker-build-${{ github.ref }} + cancel-in-progress: false + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set lower case repository name + run: | + echo "REG_REPO=${REPO,,}" >>${GITHUB_ENV} + env: + REPO: "${{ github.repository }}" + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ env.REG_REPO }} + tags: | + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=ref,event=tag + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,mode=max,scope=${{ github.workflow }} diff --git a/.github/workflows/test-install-script.yml b/.github/workflows/test-install-script.yml new file mode 100644 index 0000000..4ae94b2 --- /dev/null +++ b/.github/workflows/test-install-script.yml @@ -0,0 +1,122 @@ +name: Test Install Script + +on: + push: + branches: + - master + paths: + - "installTorrServerLinux.sh" + - ".github/workflows/test-install-script.yml" + - ".github/scripts/test-install-script.sh" + pull_request: + branches: + - master + paths: + - "installTorrServerLinux.sh" + - ".github/workflows/test-install-script.yml" + - ".github/scripts/test-install-script.sh" + workflow_dispatch: + +# OSes that require version 135 due to glibc < 2.32 +# Version 136+ requires glibc >= 2.32 +env: + GLIBC_LIMITED_OSES: "debian-11|almalinux-8|rocky-8|amazonlinux-2" + MAX_RETRIES: "3" + RETRY_DELAY: "60" + +jobs: + test-script-syntax: + name: Test Script Syntax + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Test script syntax + run: | + chmod +x installTorrServerLinux.sh + bash -n installTorrServerLinux.sh + echo "✓ Script syntax is valid" + + - name: Test help command + run: | + ./installTorrServerLinux.sh --help > /dev/null + ./installTorrServerLinux.sh help > /dev/null + echo "✓ Help command works" + + test-install-script: + name: Test on ${{ matrix.os }} (${{ matrix.test-user }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-24.04 + - debian-11 + - debian-12 + - debian-13 + - fedora-latest + - almalinux-8 + - almalinux-9 + - almalinux-10 + - rocky-8 + - rocky-9 + - rocky-10 + - amazonlinux-2 + test-user: [root, default] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Get Docker image for OS + id: get-image + run: | + # Map OS to Docker image + case "${{ matrix.os }}" in + ubuntu-22.04) IMAGE="ubuntu:22.04" ;; + ubuntu-24.04) IMAGE="ubuntu:24.04" ;; + debian-11) IMAGE="debian:11" ;; + debian-12) IMAGE="debian:12" ;; + debian-13) IMAGE="debian:13" ;; + fedora-latest) IMAGE="fedora:latest" ;; + almalinux-8) IMAGE="almalinux:8" ;; + almalinux-9) IMAGE="almalinux:9" ;; + almalinux-10) IMAGE="almalinux:10" ;; + rocky-8) IMAGE="rockylinux/rockylinux:8" ;; + rocky-9) IMAGE="rockylinux/rockylinux:9" ;; + rocky-10) IMAGE="rockylinux/rockylinux:10" ;; + amazonlinux-2) IMAGE="amazonlinux:2" ;; + *) + echo "Warning: Unknown OS ${{ matrix.os }}, using ubuntu:22.04 as default" + IMAGE="ubuntu:22.04" + ;; + esac + echo "image=$IMAGE" >> $GITHUB_OUTPUT + echo "Using Docker image: $IMAGE for OS: ${{ matrix.os }}" + + - name: Run tests in container + run: | + docker run --rm \ + -v "$PWD:/workspace" \ + -w /workspace \ + --privileged \ + -e MATRIX_OS="${{ matrix.os }}" \ + -e TEST_USER="${{ matrix.test-user }}" \ + -e GLIBC_LIMITED_OSES="${{ env.GLIBC_LIMITED_OSES }}" \ + -e MAX_RETRIES="${{ env.MAX_RETRIES }}" \ + -e RETRY_DELAY="${{ env.RETRY_DELAY }}" \ + "${{ steps.get-image.outputs.image }}" \ + bash -c " + set -e + + # Make test script executable + chmod +x .github/scripts/test-install-script.sh + + # Run the test script + .github/scripts/test-install-script.sh + " diff --git a/.github/workflows/ts_release.yml b/.github/workflows/ts_release.yml new file mode 100644 index 0000000..b82a65b --- /dev/null +++ b/.github/workflows/ts_release.yml @@ -0,0 +1,121 @@ +name: Build and Release + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-22.04 + env: + NODE_OPTIONS: --openssl-legacy-provider + strategy: + matrix: + include: + - go_version: 'stable' + group: 'standard' + - go_version: '1.25' + group: 'android' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go_version }} + cache: true + + - name: Set version from tag + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Install Swag + run: go install github.com/swaggo/swag/cmd/swag@latest + + - name: Build Web and Docs + if: matrix.group == 'standard' + run: | + go run gen_web.go + cd server && ~/go/bin/swag init -g web/server.go + + - name: Build Standard Platforms + if: matrix.group == 'standard' + run: | + LDFLAGS="-s -w -checklinkname=0 -X server/version.Version=${{ env.VERSION }}" + mkdir -p "${{ github.workspace }}/dist" + cd server + + # Linux + GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-amd64" ./cmd + GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-arm64" ./cmd + GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-arm7" ./cmd + GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-arm5" ./cmd + GOOS=linux GOARCH=386 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-386" ./cmd + + # Windows + GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-windows-amd64.exe" ./cmd + GOOS=windows GOARCH=386 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-windows-386.exe" ./cmd + + # Darwin + GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-darwin-amd64" ./cmd + GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-darwin-arm64" ./cmd + + # FreeBSD + GOOS=freebsd GOARCH=amd64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-freebsd-amd64" ./cmd + GOOS=freebsd GOARCH=arm GOARM=7 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-freebsd-arm7" ./cmd + + # MIPS + GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-mips" ./cmd + GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-mipsle" ./cmd + GOOS=linux GOARCH=mips64 GOMIPS64=softfloat go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-mips64" ./cmd + GOOS=linux GOARCH=mips64le GOMIPS64=softfloat go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-mips64le" ./cmd + GOOS=linux GOARCH=riscv64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-linux-riscv64" ./cmd + + - name: Build Android (Go 1.25) + if: matrix.group == 'android' + run: | + go run gen_web.go + + LDFLAGS="-s -w -checklinkname=0 -X server/version.Version=${{ env.VERSION }}" + mkdir -p "${{ github.workspace }}/dist" + export NDK_TOOLCHAIN="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64" + cd server + + CC=$NDK_TOOLCHAIN/bin/armv7a-linux-androideabi21-clang CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-android-arm7" ./cmd + CC=$NDK_TOOLCHAIN/bin/aarch64-linux-android21-clang CGO_ENABLED=1 GOOS=android GOARCH=arm64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-android-arm64" ./cmd + CC=$NDK_TOOLCHAIN/bin/i686-linux-android21-clang CGO_ENABLED=1 GOOS=android GOARCH=386 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-android-386" ./cmd + CC=$NDK_TOOLCHAIN/bin/x86_64-linux-android21-clang CGO_ENABLED=1 GOOS=android GOARCH=amd64 go build -ldflags="$LDFLAGS" -tags=nosqlite -trimpath -o "${{ github.workspace }}/dist/TorrServer-android-amd64" ./cmd + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.group }} + path: ${{ github.workspace }}/dist/* + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: ./all-dist + merge-multiple: true + + - name: Debug - List files + run: | + echo "Listing files to be uploaded:" + ls -R ./all-dist + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ./all-dist/* + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edebc69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Secrets +.env +*.pem +*.key + +# Docker volumes / data +data/ +*-data/ +vw-data/ + +# OS +.DS_Store +Thumbs.db + +# Backups +*.tar.gz +*.bak diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f091f06 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,24 @@ +stages: + - build_go + +build_go: + image: golang:latest + stage: build_go + when: manual + tags: + - amd64 + artifacts: + name: "TorrServer" + paths: + - dist + script: + - apt update + - apt install -y npm zip + - rm -rf /var/lib/apt/lists/* + - npm install -g yarn + - wget -q "https://dl.google.com/android/repository/android-ndk-r25c-linux.zip" + - unzip ./android-ndk-r25c-linux.zip + - rm ./android-ndk-r25c-linux.zip + - pwd + - ls -l + - ./build-all.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..60b7196 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +### FRONT BUILD START ### +FROM --platform=$BUILDPLATFORM node:16-alpine AS front + +WORKDIR /app + +ARG REACT_APP_SERVER_HOST='.' +ARG REACT_APP_TMDB_API_KEY='' +ARG PUBLIC_URL='' + +ENV REACT_APP_SERVER_HOST=$REACT_APP_SERVER_HOST +ENV REACT_APP_TMDB_API_KEY=$REACT_APP_TMDB_API_KEY +ENV PUBLIC_URL=$PUBLIC_URL + +COPY ./web/package.json ./web/yarn.lock ./ +RUN yarn install + +# Build front once upon multiarch build +COPY ./web . +RUN yarn run build +### FRONT BUILD END ### + + +### BUILD TORRSERVER MULTIARCH START ### +FROM --platform=$BUILDPLATFORM golang:1.26.2-alpine AS builder + +COPY . /opt/src +COPY --from=front /app/build /opt/src/web/build + +WORKDIR /opt/src + +ARG TARGETARCH + +# Step for multiarch build with docker buildx +ENV GOARCH=$TARGETARCH + +# Build torrserver +RUN apk add --update g++ \ + && go run gen_web.go \ + && cd server \ + && go mod tidy \ + && go clean -i -r -cache \ + && go build -ldflags '-w -s' --o "torrserver" ./cmd +### BUILD TORRSERVER MULTIARCH END ### + + +### UPX COMPRESSING START ### +FROM ubuntu AS compressed + +COPY --from=builder /opt/src/server/torrserver ./torrserver + +RUN apt update && apt install -y upx-ucl && upx --best --lzma ./torrserver +# Compress torrserver only for amd64 and arm64 no variant platforms +# ARG TARGETARCH +# ARG TARGETVARIANT +# RUN if [ "$TARGETARCH" == 'amd64' ]; then compress=1; elif [ "$TARGETARCH" == 'arm64' ] && [ -z "$TARGETVARIANT" ]; then compress=1; else compress=0; fi \ +# && if [[ "$compress" -eq 1 ]]; then ./upx --best --lzma ./torrserver; fi +### UPX COMPRESSING END ### + + +### BUILD MAIN IMAGE START ### +FROM alpine + +ENV TS_CONF_PATH="/opt/ts/config" +ENV TS_LOG_PATH="/opt/ts/log" +ENV TS_TORR_DIR="/opt/ts/torrents" +ENV TS_PORT=8090 +ENV GODEBUG=madvdontneed=1 + +COPY --from=compressed ./torrserver /usr/bin/torrserver +COPY ./docker-entrypoint.sh /docker-entrypoint.sh + +RUN apk add --no-cache --update ffmpeg + +CMD /docker-entrypoint.sh +### BUILD MAIN IMAGE end ### \ No newline at end of file diff --git a/Dockerfile copy b/Dockerfile copy new file mode 100644 index 0000000..53ec85e --- /dev/null +++ b/Dockerfile copy @@ -0,0 +1,75 @@ +### FRONT BUILD START ### +FROM --platform=$BUILDPLATFORM node:16-alpine AS front + +WORKDIR /app + +ARG REACT_APP_SERVER_HOST='.' +ARG REACT_APP_TMDB_API_KEY='' +ARG PUBLIC_URL='' + +ENV REACT_APP_SERVER_HOST=$REACT_APP_SERVER_HOST +ENV REACT_APP_TMDB_API_KEY=$REACT_APP_TMDB_API_KEY +ENV PUBLIC_URL=$PUBLIC_URL + +COPY ./web/package.json ./web/yarn.lock ./ +RUN yarn install + +# Build front once upon multiarch build +COPY ./web . +RUN yarn run build +### FRONT BUILD END ### + + +### BUILD TORRSERVER MULTIARCH START ### +FROM --platform=$BUILDPLATFORM golang:1.26.2-alpine AS builder + +COPY . /opt/src +COPY --from=front /app/build /opt/src/web/build + +WORKDIR /opt/src + +ARG TARGETARCH + +# Step for multiarch build with docker buildx +ENV GOARCH=$TARGETARCH + +# Build torrserver +RUN apk add --update g++ \ + && go run gen_web.go \ + && cd server \ + && go mod tidy \ + && go clean -i -r -cache \ + && go build -ldflags '-w -s' --o "torrserver" ./cmd +### BUILD TORRSERVER MULTIARCH END ### + + +### UPX COMPRESSING START ### +FROM ubuntu AS compressed + +COPY --from=builder /opt/src/server/torrserver ./torrserver + +RUN apt update && apt install -y upx-ucl && upx --best --lzma ./torrserver +# Compress torrserver only for amd64 and arm64 no variant platforms +# ARG TARGETARCH +# ARG TARGETVARIANT +# RUN if [ "$TARGETARCH" == 'amd64' ]; then compress=1; elif [ "$TARGETARCH" == 'arm64' ] && [ -z "$TARGETVARIANT" ]; then compress=1; else compress=0; fi \ +# && if [[ "$compress" -eq 1 ]]; then ./upx --best --lzma ./torrserver; fi +### UPX COMPRESSING END ### + + +### BUILD MAIN IMAGE START ### +FROM alpine + +ENV TS_CONF_PATH="/opt/ts/config" +ENV TS_LOG_PATH="/opt/ts/log" +ENV TS_TORR_DIR="/opt/ts/torrents" +ENV TS_PORT=8090 +ENV GODEBUG=madvdontneed=1 + +COPY --from=compressed ./torrserver /usr/bin/torrserver +COPY ./docker-entrypoint.sh /docker-entrypoint.sh + +RUN apk add --no-cache --update ffmpeg + +CMD /docker-entrypoint.sh +### BUILD MAIN IMAGE end ### diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1432d90 --- /dev/null +++ b/README.md @@ -0,0 +1,416 @@ +

+
+

+ +

+ Simple and powerful tool for streaming torrents. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + Build + + + GitHub release (latest SemVer) + + + GitHub tag (latest SemVer pre-release) + +

+ +## Introduction + +TorrServer is a program that allows users to view torrents online without the need for preliminary file downloading. +The core functionality of TorrServer includes caching torrents and subsequent data transfer via the HTTP protocol, +allowing the cache size to be adjusted according to the system parameters and the user's internet connection speed. + +## AI Documentation + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/YouROK/TorrServer) + +## Features + +- Caching +- Streaming +- Local and Remote Server +- Viewing torrents on various devices +- Integration with other apps through API +- Torznab search (Jackett, Prowlarr, and similar indexer managers) +- Cross-browser modern web interface +- Optional DLNA server + +## Getting Started + +### Installation + +Download the application for the required platform in the [releases](https://github.com/YouROK/TorrServer/releases) page. After installation, open the link in the browser. + +#### Windows + +Run `TorrServer-windows-amd64.exe`. + +#### Linux + +Run in console + +```bash +curl -s https://raw.githubusercontent.com/YouROK/TorrServer/master/installTorrServerLinux.sh | sudo bash +``` + +The script supports interactive and non-interactive installation, configuration, updates, and removal. When running the script interactively, you can: + +- **Install/Update**: Choose to install or update TorrServer +- **Reconfigure**: If TorrServer is already installed, you'll be prompted to reconfigure settings (port, auth, read-only mode, logging, BBR) +- **Uninstall**: Type `Delete` (or `Удалить` in Russian) to uninstall TorrServer + +**Download first and set execute permissions:** + +```bash +curl -s https://raw.githubusercontent.com/YouROK/TorrServer/master/installTorrServerLinux.sh -o installTorrServerLinux.sh && chmod 755 installTorrServerLinux.sh +``` + +**Command-line examples:** + +- Install a specific version: + + ```bash + sudo bash ./installTorrServerLinux.sh --install 135 --silent + ``` + +- Update to latest version: + + ```bash + sudo bash ./installTorrServerLinux.sh --update --silent + ``` + +- Reconfigure settings interactively: + + ```bash + sudo bash ./installTorrServerLinux.sh --reconfigure + ``` + +- Check for updates: + + ```bash + sudo bash ./installTorrServerLinux.sh --check + ``` + +- Downgrade to a specific version: + + ```bash + sudo bash ./installTorrServerLinux.sh --down 135 + ``` + +- Remove/uninstall: + + ```bash + sudo bash ./installTorrServerLinux.sh --remove --silent + ``` + +- Change the systemd service user: + + ```bash + sudo bash ./installTorrServerLinux.sh --change-user root --silent + ``` + +**All available commands:** + +- `--install [VERSION]` - Install latest or specific version +- `--update` - Update to latest version +- `--reconfigure` - Reconfigure TorrServer settings (port, auth, read-only mode, logging, BBR) +- `--check` - Check for updates (version info only) +- `--down VERSION` - Downgrade to specific version +- `--remove` - Uninstall TorrServer +- `--change-user USER` - Change service user (root|torrserver) +- `--root` - Run service as root user +- `--silent` - Non-interactive mode with defaults +- `--help` - Show help message + +#### macOS + +Run in Terminal.app + +```bash +curl -s https://raw.githubusercontent.com/YouROK/TorrServer/master/installTorrServerMac.sh -o installTorrserverMac.sh && chmod 755 installTorrServerMac.sh && bash ./installTorrServerMac.sh +``` + +Alternative install script for Intel Macs: + +#### IOCage Plugin (Unofficial) + +On FreeBSD (TrueNAS/FreeNAS) you can use this plugin: + +#### NAS Systems (Unofficial) + +- Several releases are available through this link: +- **Synology NAS** packages repo source: + +### Server args + +- `--port PORT`, `-p PORT` - web server port (default 8090) +- `--ip IP`, `-i IP` - web server addr (default empty, binds to all interfaces) +- `--ssl` - enables https for web server +- `--sslport PORT` - web server https port (default 8091). If not set, will be taken from db (if stored previously) or the default will be used. +- `--sslcert PATH` - path to ssl cert file. If not set, will be taken from db (if stored previously) or default self-signed certificate/key will be generated. +- `--sslkey PATH` - path to ssl key file. If not set, will be taken from db (if stored previously) or default self-signed certificate/key will be generated. +- `--force-https` - with `--ssl`, the HTTP listener (`--port`) answers only with **307 Temporary Redirect** to the same path on HTTPS (`--sslport`). The web UI and API are served on HTTPS only; nothing is served on HTTP except redirects. Requires `--ssl` (startup fails if `--force-https` is set without `--ssl`). Default is off so plain HTTP still works when SSL is disabled. +- `--path PATH`, `-d PATH` - database and config dir path +- `--logpath LOGPATH`, `-l LOGPATH` - server log file path +- `--weblogpath WEBLOGPATH`, `-w WEBLOGPATH` - web access log file path +- `--rdb`, `-r` - start in read-only DB mode +- `--httpauth`, `-a` - enable http auth on all requests +- `--dontkill`, `-k` - don't kill server on signal +- `--ui`, `-u` - open torrserver page in browser +- `--torrentsdir TORRENTSDIR`, `-t TORRENTSDIR` - autoload torrents from dir +- `--torrentaddr TORRENTADDR` - Torrent client address (format [IP]:PORT, ex. :32000, 127.0.0.1:32768 etc) +- `--pubipv4 PUBIPV4`, `-4 PUBIPV4` - set public IPv4 addr +- `--pubipv6 PUBIPV6`, `-6 PUBIPV6` - set public IPv6 addr +- `--searchwa`, `-s` - allow search without authentication +- `--maxsize MAXSIZE`, `-m MAXSIZE` - max allowed stream size (in Bytes) +- `--tg TGTOKEN`, `-T TGTOKEN` - [Telegram bot](server/tgbot/README.md) token +- `--fuse FUSEPATH`, `-f FUSEPATH` - fuse mount path +- `--webdav` - enable web dav +- `--proxyurl PROXYURL` - set proxy URL for BitTorrent traffic (http, socks4, socks5, socks5h), example: socks5h://user:password@example.com:2080 +- `--proxymode PROXYMODE` - set proxy mode: "tracker" (only HTTP trackers, default), "peers" (only peer connections), or "full" (all traffic) +- `--help`, `-h` - display this help and exit +- `--version` - display version and exit + +Example: + +```bash +TorrServer-darwin-arm64 [--port PORT] [--ip IP] [--path PATH] [--logpath LOGPATH] [--weblogpath WEBLOGPATH] [--rdb] [--httpauth] [--dontkill] [--ui] [--torrentsdir TORRENTSDIR] [--torrentaddr TORRENTADDR] [--pubipv4 PUBIPV4] [--pubipv6 PUBIPV6] [--searchwa] [--maxsize MAXSIZE] [--tg TGTOKEN] [--fuse FUSEPATH] [--webdav] [--ssl] [--sslport PORT] [--sslcert PATH] [--sslkey PATH] [--force-https] +``` + +### Running in Docker & Docker Compose + +Run in console + +```bash +docker run --rm -d --name torrserver -p 8090:8090 ghcr.io/yourok/torrserver:latest +``` + +For running in persistence mode, just mount volume to container by adding `-v ~/ts:/opt/ts`, where `~/ts` folder path is just example, but you could use it anyway... Result example command: + +```bash +docker run --rm -d --name torrserver -v ~/ts:/opt/ts -p 8090:8090 ghcr.io/yourok/torrserver:latest +``` + +#### Environments + +- `TS_HTTPAUTH` - 1, and place auth file into `~/ts/config` folder for enabling basic auth +- `TS_RDB` - if 1, then the enabling `--rdb` flag +- `TS_DONTKILL` - if 1, then the enabling `--dontkill` flag +- `TS_PORT` - for changind default port to **5555** (example), also u need to change `-p 8090:8090` to `-p 5555:5555` (example) +- `TS_CONF_PATH` - for overriding torrserver config path inside container. Example `/opt/tsss` +- `TS_TORR_DIR` - for overriding torrents directory. Example `/opt/torr_files` +- `TS_LOG_PATH` - for overriding log path. Example `/opt/torrserver.log` +- `TS_PROXYURL` - set proxy URL for BitTorrent traffic (http, socks4, socks5, socks5h), example: socks5h://user:password@example.com:2080 +- `TS_PROXYMODE` - set proxy mode: "tracker" (only HTTP trackers, default), "peers" (only peer connections), or "full" (all traffic) + +Example with full overrided command (on default values): + +```bash +docker run --rm -d -e TS_PORT=5665 -e TS_DONTKILL=1 -e TS_HTTPAUTH=1 -e TS_RDB=1 -e TS_CONF_PATH=/opt/ts/config -e TS_LOG_PATH=/opt/ts/log -e TS_TORR_DIR=/opt/ts/torrents -e TS_PROXYURL=socks5h://user:password@example.com:2080 -e TS_PROXYMODE=tracker --name torrserver -v ~/ts:/opt/ts -p 5665:5665 ghcr.io/yourok/torrserver:latest +``` + +#### Docker Compose + +```yml +# docker-compose.yml + +version: '3.3' +services: + torrserver: + image: ghcr.io/yourok/torrserver + container_name: torrserver + network_mode: host # to allow DLNA feature + environment: + - TS_PORT=5665 + - TS_DONTKILL=1 + - TS_HTTPAUTH=0 + - TS_CONF_PATH=/opt/ts/config + - TS_TORR_DIR=/opt/ts/torrents + volumes: + - './CACHE:/opt/ts/torrents' + - './CONFIG:/opt/ts/config' + ports: + - '5665:5665' + restart: unless-stopped + + +``` + +### Smart TV (using Media Station X) + +1. Install **Media Station X** on your Smart TV (see [platform support](https://msx.benzac.de/info/?tab=PlatformSupport)) + +2. Open it and go to: **Settings -> Start Parameter -> Setup** + +3. Enter current ip and port of the TorrServe(r), e.g. `127.0.0.1:8090` + +## Development + +### Go server + +To run the Go server locally, just run + +```bash +cd server +go run ./cmd +``` + +### Web development + +To run the web server locally, just run + +```bash +yarn start +``` + +More info at + +### Build + +#### Server + +- Install [Golang](https://golang.org/doc/install) 1.20+ +- Go to the TorrServer source directory +- Run build script under linux or macOS `build-all.sh` + +#### Web + +- Install **npm** and **yarn** +- Go to the web directory +- Run `NODE_OPTIONS=--openssl-legacy-provider yarn build` + +#### Android + +To build an Android server you will need the Android Toolchain. + +#### Swagger + +`swag` must be installed on the system to [re]build Swagger documentation. + +```bash +go install github.com/swaggo/swag/cmd/swag@latest +cd server; swag init -g web/server.go + +# Documentation can be linted using +swag fmt +``` + +## API + +### API Docs + +API documentation is hosted as Swagger format available at path `/swagger/index.html`. + +## Authentication + +The users data file should be located near to the settings. Basic auth, read more in wiki . + +`accs.db` in JSON format: + +```json +{ + "User1": "Pass1", + "User2": "Pass2" +} +``` + +Note: You should enable authentication with -a (--httpauth) TorrServer startup option. + +## Whitelist/Blacklist IP + +The lists file should be located in the same directory with config.db. + +- Whitelist file name: `wip.txt` +- Blacklist file name: `bip.txt` + +Whitelist has priority over everything else. + +Example: + +```text +local:127.0.0.0-127.0.0.255 +127.0.0.0-127.0.0.255 +local:127.0.0.1 +127.0.0.1 +# at the beginning of the line, comment +``` + +## Torznab + +TorrServer can talk to **Torznab** indexers so you can search for torrents from tools like **Jackett** and **Prowlarr**, including searching several configured indexers at once. + +Configure it in the web UI: **Settings → Torznab**. + +### Indexer parameters + +Each Torznab indexer needs: + +- **Host URL**: full URL to the Torznab API endpoint. + - Jackett example: + + ```shell + http://192.168.1.10:9117/api/v2.0/indexers/all/results/torznab/ + ``` + + - Prowlarr example: + + ```shell + http://localhost:9696/1 + ``` + + - Make sure to include the correct trailing slash (`/`) in your indexer's URL, + as required by your Torznab provider. TorrServer will try to properly format the path, + but matching your indexer's expected format is best to avoid connection issues. + +- **API Key**: the key from your Torznab indexer manager. + +### Enabling Torznab search + +1. Open **Settings**. +2. Open the **Torznab** tab. +3. Turn on **Enable Torznab Search**. +4. Enter **Host URL** and **API Key**, then **Add Server** for each indexer. +5. **Save** settings. + +## Donate + +- [YooMoney](https://yoomoney.ru/to/410013733697114/200) +- [Boosty](https://boosty.to/yourok) +- [TBank](https://www.tbank.ru/cf/742qEMhKhKn) + +## Thanks to everyone who tested and helped + +- [anacrolix](https://github.com/anacrolix) Matt Joiner +- [tsynik](https://github.com/tsynik) Nikk Gitanes +- [dancheskus](https://github.com/dancheskus) for react web GUI and PWA code +- [kolsys](https://github.com/kolsys) for initial Media Station X support +- [damiva](https://github.com/damiva) for Media Station X code updates +- [vladlenas](https://github.com/vladlenas) for NAS builds +- [pavelpikta](https://github.com/pavelpikta) Pavel Pikta for linux install script and more +- [Nemiroff](https://github.com/Nemiroff) Tw1cker +- [spawnlmg](https://github.com/spawnlmg) SpAwN_LMG for testing +- [TopperBG](https://github.com/TopperBG) Dimitar Maznekov for Bulgarian web translation +- [FaintGhost](https://github.com/FaintGhost) Zhang Yaowei for Simplified Chinese web translation +- [Anton111111](https://github.com/Anton111111) Anton Potekhin for sleep on Windows fixes +- [lieranderl](https://github.com/lieranderl) Evgeni for adding SSL support code +- [cocool97](https://github.com/cocool97) for openapi API documentation and torrent categories +- [shadeov](https://github.com/shadeov) for README improvements +- [butaford](https://github.com/butaford) Pavel for make docker file and scripts +- [filimonic](https://github.com/filimonic) Alexey D. Filimonov +- [leporel](https://github.com/leporel) Viacheslav Evseev +- and others diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 0000000..e5c9365 --- /dev/null +++ b/build-all.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +PLATFORMS=( + 'linux/amd64' + 'linux/arm64' + 'linux/arm7' + 'linux/arm5' + 'linux/386' + 'windows/amd64' + 'windows/386' + 'darwin/amd64' + 'darwin/arm64' + 'freebsd/amd64' + 'freebsd/arm7' + 'linux/mips' + 'linux/mipsle' + 'linux/mips64' + 'linux/mips64le' + 'linux/riscv64' +) + +type setopt >/dev/null 2>&1 + +set_goarm() { + if [[ "$1" =~ arm([5,7]) ]]; then + GOARCH="arm" + GOARM="${BASH_REMATCH[1]}" + GO_ARM="GOARM=${GOARM}" + else + GOARM="" + GO_ARM="" + fi +} +# use softfloat for mips builds +set_gomips() { + if [[ "$1" =~ mips ]]; then + if [[ "$1" =~ mips(64) ]]; then MIPS64="${BASH_REMATCH[1]}"; fi + GO_MIPS="GOMIPS${MIPS64}=softfloat" + else + GO_MIPS="" + fi +} + +GOBIN="go" + +$GOBIN version + +LDFLAGS="'-s -w -checklinkname=0'" +FAILURES="" +ROOT=${PWD} +OUTPUT="${ROOT}/dist/TorrServer" + +#### Build web +echo "Build web" +export NODE_OPTIONS=--openssl-legacy-provider +$GOBIN run gen_web.go + +#### Update api docs +echo "Build docs" +$GOBIN install github.com/swaggo/swag/cmd/swag@latest +cd "${ROOT}/server" || exit 1 +swag init -g web/server.go + +#### Build server +echo "Build server" +cd "${ROOT}/server" || exit 1 +$GOBIN clean -i -r -cache # --modcache +$GOBIN mod tidy + +BUILD_FLAGS="-ldflags=${LDFLAGS} -tags=nosqlite -trimpath" + +##################################### +### X86 build section +##### + +for PLATFORM in "${PLATFORMS[@]}"; do + GOOS=${PLATFORM%/*} + GOARCH=${PLATFORM#*/} + set_goarm "$GOARCH" + set_gomips "$GOARCH" + BIN_FILENAME="${OUTPUT}-${GOOS}-${GOARCH}${GOARM}" + if [[ "${GOOS}" == "windows" ]]; then BIN_FILENAME="${BIN_FILENAME}.exe"; fi + CMD="GOOS=${GOOS} GOARCH=${GOARCH} ${GO_ARM} ${GO_MIPS} ${GOBIN} build ${BUILD_FLAGS} -o ${BIN_FILENAME} ./cmd" + echo "${CMD}" + eval "$CMD" || FAILURES="${FAILURES} ${GOOS}/${GOARCH}${GOARM}" +# CMD="../upx -q ${BIN_FILENAME}"; # upx --brute produce much smaller binaries +# echo "compress with ${CMD}" +# eval "$CMD" +done + +##################################### +### Android build section +##### + +declare -a COMPILERS=( + "arm7:armv7a-linux-androideabi21-clang" + "arm64:aarch64-linux-android21-clang" + "386:i686-linux-android21-clang" + "amd64:x86_64-linux-android21-clang" +) + +export NDK_VERSION="25.2.9519653" # 25.1.8937393 +#export NDK_TOOLCHAIN=${ANDROID_HOME}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/darwin-x86_64 +#export NDK_TOOLCHAIN="${PWD}/../android-ndk-r25c/toolchains/llvm/prebuilt/linux-x86_64" +export NDK_TOOLCHAIN=/Users/yourok/Projects/AndroidNDK +GOOS=android + +for V in "${COMPILERS[@]}"; do + GOARCH=${V%:*} + COMPILER=${V#*:} + export CC="$NDK_TOOLCHAIN/bin/$COMPILER" + export CXX="$NDK_TOOLCHAIN/bin/$COMPILER++" + set_goarm "$GOARCH" + BIN_FILENAME="${OUTPUT}-${GOOS}-${GOARCH}${GOARM}" + CMD="GOOS=${GOOS} GOARCH=${GOARCH} ${GO_ARM} CGO_ENABLED=1 ${GOBIN} build ${BUILD_FLAGS} -o ${BIN_FILENAME} ./cmd" + echo "${CMD}" + eval "${CMD}" || FAILURES="${FAILURES} ${GOOS}/${GOARCH}${GOARM}" +# CMD="../upx -q ${BIN_FILENAME}"; # upx --brute produce much smaller binaries +# echo "compress with ${CMD}" +# eval "$CMD" +done + +# eval errors +if [[ "${FAILURES}" != "" ]]; then + echo "" + echo "failed on: ${FAILURES}" + exit 1 +fi + +cd "${ROOT}/docker/lite" || exit 1 +./makedocker.sh \ No newline at end of file diff --git a/docker-all.sh b/docker-all.sh new file mode 100755 index 0000000..5fc1500 --- /dev/null +++ b/docker-all.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +ROOT=${PWD} + +#### Build web +echo "Build web" +go run gen_web.go + +sudo docker run --rm -v "$PWD":/usr/src/torr -v ~/go/pkg/mod:/go/pkg/mod -w /usr/src/torr golang:1.17.5-stretch ./build-all.sh +sudo chmod 0777 ./dist/* \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..518afe0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + torrserver: + build: . + restart: always + ports: + - "8090:8090" + environment: + TS_CONF_PATH: "/opt/ts/config" + TS_LOG_PATH: "/opt/ts/log" + TS_TORR_DIR: "/opt/ts/torrents" + TS_PORT: "8090" + volumes: + - torrserver_config:/opt/ts/config + - torrserver_log:/opt/ts/log + - torrserver_torrents:/opt/ts/torrents + # Путь к медиа на Synology (настрой под себя) + - /mnt/synology_drive/media:/media:ro + labels: + - traefik.enable=true + - traefik.http.routers.torrserver.rule=Host(`${TORR_SUBDOMAIN}.${DOMAIN_NAME}`) + - traefik.http.routers.torrserver.tls=true + - traefik.http.routers.torrserver.entrypoints=web,websecure + - traefik.http.routers.torrserver.tls.certresolver=myresolver + # Security headers (аналогично n8n) + - traefik.http.middlewares.torrserver-headers.headers.SSLRedirect=true + - traefik.http.middlewares.torrserver-headers.headers.STSSeconds=315360000 + - traefik.http.middlewares.torrserver-headers.headers.browserXSSFilter=true + - traefik.http.middlewares.torrserver-headers.headers.contentTypeNosniff=true + - traefik.http.middlewares.torrserver-headers.headers.forceSTSHeader=true + - traefik.http.middlewares.torrserver-headers.headers.SSLHost=${DOMAIN_NAME} + - traefik.http.middlewares.torrserver-headers.headers.STSIncludeSubdomains=true + - traefik.http.middlewares.torrserver-headers.headers.STSPreload=true + - traefik.http.routers.torrserver.middlewares=torrserver-headers@docker + networks: + - proxy + +volumes: + torrserver_config: + torrserver_log: + torrserver_torrents: + +networks: + proxy: + external: true + name: traefik_proxy diff --git a/docker-entrypoint copy.sh b/docker-entrypoint copy.sh new file mode 100755 index 0000000..8450a9f --- /dev/null +++ b/docker-entrypoint copy.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +FLAGS="--path $TS_CONF_PATH --logpath $TS_LOG_PATH --port $TS_PORT --torrentsdir $TS_TORR_DIR" +if [ -n "$TS_IP" ]; then FLAGS="${FLAGS} -i ${TS_IP}"; fi +if [ "$TS_HTTPAUTH" = "1" ]; then FLAGS="${FLAGS} --httpauth"; fi +if [ "$TS_RDB" = "1" ]; then FLAGS="${FLAGS} --rdb"; fi +if [ "$TS_DONTKILL" = "1" ]; then FLAGS="${FLAGS} --dontkill"; fi +if [ "$TS_EN_SSL" = "1" ]; then FLAGS="${FLAGS} --ssl"; fi +if [ -n "$TS_SSL_PORT" ]; then FLAGS="${FLAGS} --sslport ${TS_SSL_PORT}"; fi +if [ -n "$TS_PROXYURL" ]; then FLAGS="${FLAGS} --proxyurl ${TS_PROXYURL}"; fi +if [ -n "$TS_PROXYMODE" ]; then FLAGS="${FLAGS} --proxymode ${TS_PROXYMODE}"; fi + +if [ ! -d "$TS_CONF_PATH" ]; then + mkdir -p "$TS_CONF_PATH" +fi + +if [ ! -d "$TS_TORR_DIR" ]; then + mkdir -p "$TS_TORR_DIR" +fi + +if [ ! -f "$TS_LOG_PATH" ]; then + touch "$TS_LOG_PATH" +fi + +echo "Running with: ${FLAGS}" + +exec torrserver $FLAGS diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..458a5f1 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Создаём директории для конфига, логов и торрентов +mkdir -p "$TS_CONF_PATH" "$TS_LOG_PATH" "$TS_TORR_DIR" + +# Устанавливаем права доступа (на всякий случай) +chmod 755 "$TS_CONF_PATH" "$TS_LOG_PATH" "$TS_TORR_DIR" 2>/dev/null || true + +# Запускаем TorrServer +exec /usr/bin/torrserver diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..d5edf75 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine +LABEL maintainer "yourok" +RUN apk add --no-cache wget +COPY start.sh /start.sh +ENTRYPOINT /start.sh + diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..7095899 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,86 @@ +## TorrServer + +After starting the container, the latest server is downloaded from GitHub.\ +If you need update server to latest, repull container + +Source code: https://github.com/YouROK/TorrServer + +-------- + +Author of docker file and scripts [butaford (aka Pavel)](https://github.com/butaford) + +-------- + +### Support platforms +* TorrServer-linux-386 +* TorrServer-linux-amd64 +* TorrServer-linux-arm5 +* TorrServer-linux-arm64 +* TorrServer-linux-arm7 + +-------- +### Support env +TS_PORT: TS web port\ +TS_PATH: config path and other\ +TS_LOGPATHDIR: log path\ +TS_LOGFILE: log file name\ +TS_WEBLOGFILE: web log file name\ +TS_RDB: read only config\ +TS_HTTPAUTH: auth for server, accs.db should be in the TS_PATH\ +TS_DONTKILL: don't kill server by signal\ +TS_TORRENTSDIR: torrents listen directory\ +TS_TORRENTADDR: torrents peer listen port\ +TS_PUBIPV4: the IP addresses as our peers should see them. May differ from the local interfaces due to NAT or other network configurations\ +TS_PUBIPV6: the IP addresses as our peers should see them. May differ from the local interfaces due to NAT or other network configurations\ +TS_SEARCHWA: disable auth for search torrents if auth is enable + +-------- +### Docker run example +``` +docker run -p 8090:8090 \ +-e TS_PORT=8090 \ +-e TS_PATH="/opt/torrserver/config" \ +-e TS_LOGPATHDIR="/opt/torrserver/log/" \ +-e TS_LOGFILE="ts.log" \ +-e TS_WEBLOGFILE="tsweb.log" \ +-e TS_RDB=true \ +-e TS_HTTPAUTH=true \ +-e TS_DONTKILL=true \ +-e TS_TORRENTSDIR="/opt/torrserver/torrents" \ +-e TS_TORRENTADDR=32000 \ +-e TS_PUBIPV4=publicIP \ +-e TS_PUBIPV6=publicIP \ +-e TS_SEARCHWA=true \ +yourok/torrserver +``` + +-------- +### Docker compose example +``` +version: '3.6' +services: + torrserver: + container_name: torrserver + image: ghcr.io/yourok/torrserver + restart: unless-stopped + environment: + - TS_PORT=8090 + - TS_PATH=/opt/torrserver/config + - TS_LOGPATHDIR=/opt/torrserver/log + - TS_LOGFILE=ts.log + - TS_WEBLOGFILE=tsweb.log + - TS_RDB=false + - TS_HTTPAUTH=true + - TS_DONTKILL=true + - TS_TORRENTSDIR=/opt/torrserver/torrents + - TS_TORRENTADDR=:32000 + - TS_PUBIPV4=publicIP + - TS_PUBIPV6=publicIP + - TS_SEARCHWA=true + ports: + - 8090:8090 + volumes: + - ./torrserver/config:/opt/torrserver/config + - ./torrserver/log:/opt/torrserver/log + - ./torrserver/torrents:/opt/torrserver/torrents +``` diff --git a/docker/lite/Dockerfile b/docker/lite/Dockerfile new file mode 100644 index 0000000..a384be4 --- /dev/null +++ b/docker/lite/Dockerfile @@ -0,0 +1,9 @@ +FROM debian:bookworm-slim as builder +RUN mkdir /src +COPY ./ /src +RUN /src/cp.sh + +FROM scratch +COPY --from=builder /app/TorrServer / +WORKDIR / +ENTRYPOINT [ "/TorrServer" ] diff --git a/docker/lite/README.md b/docker/lite/README.md new file mode 100644 index 0000000..d5e3aab --- /dev/null +++ b/docker/lite/README.md @@ -0,0 +1,30 @@ +## TorrServer + +A lightweight container that contains a single TorrServer file + +Source code: https://github.com/YouROK/TorrServer + +-------- + +### Support platforms +* TorrServer-linux-386 +* TorrServer-linux-amd64 +* TorrServer-linux-arm5 +* TorrServer-linux-arm64 +* TorrServer-linux-arm7 + +-------- +### Docker run example +``` +docker run -p 8090:8090 yourok/torrlite:TAG [ ARGS ] +``` + +TAG - tag of version in docker hub eg MatriX.134 \ +ARGS - args of torrserver + +You can mount a directory like -v /your/local/path/:/cfg and write logs etc there + +Example of run with args: +``` +docker run -p 8099:8099 yourok/torrlite:MatriX.134 --port=8099 +``` \ No newline at end of file diff --git a/docker/lite/cp.sh b/docker/lite/cp.sh new file mode 100755 index 0000000..4eb3432 --- /dev/null +++ b/docker/lite/cp.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +case $(uname -m) in + i386) architecture="386" ;; + i686) architecture="386" ;; + x86_64) architecture="amd64" ;; + aarch64) architecture="arm64" ;; + armv7|armv7l) architecture="arm7" ;; + armv6|armv6l) architecture="arm5" ;; +# armv5|armv5l) architecture="arm5" ;; + *) echo "Unsupported Arch. Can't continue."; exit 1 ;; +esac + +binName="TorrServer-linux-${architecture}" +mkdir -p /app + +cp /src/dist/$binName /app/TorrServer \ No newline at end of file diff --git a/docker/lite/makedocker.sh b/docker/lite/makedocker.sh new file mode 100755 index 0000000..4567314 --- /dev/null +++ b/docker/lite/makedocker.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cp -r ../../dist ./ +docker buildx build --platform "linux/386,linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6" --tag yourok/torrlite:$* --push . +rm -rf ./dist \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..4a1c7a3 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +case $(uname -m) in + i386) architecture="386" ;; + i686) architecture="386" ;; + x86_64) architecture="amd64" ;; + aarch64) architecture="arm64" ;; + armv7|armv7l) architecture="arm7" ;; + armv6|armv6l) architecture="arm5" ;; +# armv5|armv5l) architecture="arm5" ;; + *) echo "Unsupported Arch. Can't continue."; exit 1 ;; +esac + +binName="TorrServer-linux-${architecture}" + +mkdir -p /opt/torrserver +cd /opt/torrserver + +rm -f ${binName}* + +wget -O $binName "https://github.com/YouROK/TorrServer/releases/latest/download/$binName" +chmod +x $binName + +FLAGS="" + +#sets start flags +[ ! -z "$TS_PORT" ] && echo "TS_PORT: $TS_PORT" && FLAGS="${FLAGS} --port ${TS_PORT}" +[ ! -z "$TS_PATH" ] && echo "TS_PATH: $TS_PATH" && FLAGS="${FLAGS} --path ${TS_PATH}" +[ ! -z "$TS_LOGPATHDIR" ] && echo "TS_LOGPATHDIR: $TS_LOGPATHDIR" && FLAGS="${FLAGS}" +[ ! -z "$TS_LOGFILE" ] && echo "TS_LOGFILE: $TS_LOGPATHDIR/$TS_LOGFILE" && FLAGS="${FLAGS} --logpath $TS_LOGPATHDIR/${TS_LOGFILE}" +[ ! -z "$TS_WEBLOGFILE" ] && echo "TS_WEBLOGFILE: $TS_LOGPATHDIR/$TS_WEBLOGFILE" && FLAGS="${FLAGS} --weblogpath $TS_LOGPATHDIR/${TS_WEBLOGFILE}" +[ ! -z "$TS_RDB" ] | [ "$TS_RDB" = "true" ] && echo "TS_RDB: $TS_RDB" && FLAGS="${FLAGS} --rdb" +[ ! -z "$TS_HTTPAUTH" ] && echo "TS_HTTPAUTH: $TS_HTTPAUTH" && FLAGS="${FLAGS} --httpauth" +[ ! -z "$TS_DONTKILL" ] && echo "TS_DONTKILL: $TS_DONTKILL" && FLAGS="${FLAGS} --dontkill" +[ ! -z "$TS_TORRENTSDIR" ] && echo "TS_TORRENTSDIR: $TS_TORRENTSDIR" && FLAGS="${FLAGS} --torrentsdir ${TS_TORRENTSDIR}" +[ ! -z "$TS_TORRENTADDR" ] && echo "TS_TORRENTADDR: $TS_TORRENTADDR" && FLAGS="${FLAGS} --torrentaddr ${TS_TORRENTADDR}" +[ ! -z "$TS_PUBIPV4" ] && echo "TS_PUBIPV4: $TS_PUBIPV4" && FLAGS="${FLAGS} --pubipv4 ${TS_PUBIPV4}" +[ ! -z "$TS_PUBIPV6" ] && echo "TS_PUBIPV6: $TS_PUBIPV6" && FLAGS="${FLAGS} --pubipv6 ${TS_PUBIPV6}" +[ ! -z "$TS_SEARCHWA" ]&& echo "TS_SEARCHWA: $TS_SEARCHWA" && FLAGS="${FLAGS} --searchwa" + +#make directories +[ ! -z "$TS_PATH" ] && [ ! -d "$TS_PATH" ] && mkdir -p $TS_PATH +[ ! -z "$TS_LOGPATHDIR" ] && [ ! -d "$TS_LOGPATHDIR" ] && mkdir -p $TS_LOGPATHDIR +[ ! -z "$TS_TORRENTSDIR" ] && [ ! -d "$TS_TORRENTSDIR" ] && mkdir $TS_TORRENTSDIR + +echo "Running with: ${FLAGS}" +export GODEBUG=madvdontneed=1 + +/opt/torrserver/${binName} ${FLAGS} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c1e2ade --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + description = "TorrServer - Simple and powerful tool for streaming torrents"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default-linux"; + }; + + outputs = + { + self, + nixpkgs, + systems, + ... + }: + let + inherit (nixpkgs) lib; + eachSystem = f: lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); + in + { + formatter = eachSystem (pkgs: pkgs.alejandra); + + devShells = eachSystem (pkgs: { + default = pkgs.mkShell { + name = "torrserver"; + inputsFrom = [ self.packages.${pkgs.stdenv.system}.torrserver ]; + }; + }); + + packages = eachSystem (pkgs: { + default = self.packages.${pkgs.stdenv.system}.torrserver; + torrserver = pkgs.callPackage ./nix/packages/torrserver.nix { }; + }); + + homeModules = { + default = self.homeModules.torrserver; + torrserver = import ./nix/modules/home-manager.nix { inherit self; }; + }; + + nixosModules = { + default = self.nixosModules.torrserver; + torrserver = import ./nix/modules/nixos.nix { inherit self; }; + }; + }; +} diff --git a/gen_web.go b/gen_web.go new file mode 100644 index 0000000..fdc41f9 --- /dev/null +++ b/gen_web.go @@ -0,0 +1,191 @@ +package main + +import ( + "fmt" + "io/fs" + "log" + "mime" + "os" + "os/exec" + "path/filepath" + "regexp" + "slices" + "sort" + "strings" +) + +func main() { + dir, _ := os.Getwd() + + if len(os.Args) > 0 { + // Here you can be more cunning, but it will work anyway, for a clean build you need to clean the build folder using the --clean command + if slices.ContainsFunc(os.Args, func(s string) bool { + return s == "--clean" || s == "-c" + }) { + // There are problems with running under windows + if err := run("rm", "-rf", "web/build"); err != nil { + if strings.Contains(err.Error(), "executable file not found") { + // Adding the ability to run on Windows with standard Go commands + if err := os.RemoveAll("web/build"); err != nil { + log.Default().Fatalln(err.Error()) + } + } else { + log.Default().Fatalln(err.Error()) + } + } + } else { + // Do not uncomment, be aware - its crash the build + //log.Default().Fatalln("Wrong args?") + } + } + + if _, err := os.Stat("web/build/static"); os.IsNotExist(err) { + os.Chdir("web") + if err = run("yarn"); err != nil { + log.Default().Fatalln(err.Error()) + } + if err = run("yarn", "run", "build"); err != nil { + log.Default().Fatalln(err.Error()) + } + os.Chdir(dir) + } + + compileHtml := "web/build/" + srcGo := "server/web/pages/" + + // There are problems with running under windows + if err := run("rm", "-rf", srcGo+"template/pages"); err != nil { + if strings.Contains(err.Error(), "executable file not found") { + // Adding the ability to run on Windows with standard Go commands + if err = os.RemoveAll(srcGo + "template/pages"); err != nil { + log.Default().Fatalln(err.Error()) + } + } else { + log.Default().Fatalln(err.Error()) + } + } + // There are problems with running under windows + if err := run("cp", "-r", compileHtml, srcGo+"template/pages/"); err != nil { + if strings.Contains(err.Error(), "executable file not found") { + // Adding the ability to run on Windows with standard Go commands + if err = os.CopyFS(srcGo+"template/pages/", os.DirFS(filepath.Dir(compileHtml))); err != nil { + log.Default().Fatalln(err.Error()) + } + } else { + log.Default().Fatalln(err.Error()) + } + } + + files := make([]string, 0) + + filepath.WalkDir(srcGo+"template/pages/", func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() { + name := strings.TrimPrefix(path, srcGo+"template/") + if strings.Contains(name, "\\") { + // Adding the ability to run on Windows with standard Go commands + name = strings.TrimPrefix(strings.ReplaceAll(name, "\\", "/"), "server/web/pages/template/") + } + if !strings.HasPrefix(filepath.Base(name), ".") { + files = append(files, name) + } + } + return nil + }) + sort.Strings(files) + fmap := writeEmbed(srcGo+"template/html.go", files) + writeRoute(srcGo+"template/route.go", fmap) +} + +func writeEmbed(fname string, files []string) map[string]string { + ff, err := os.Create(fname) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer ff.Close() + embedStr := `package template + +import ( + _ "embed" +) +` + ret := make(map[string]string) + + for _, f := range files { + fname := cleanName(strings.TrimPrefix(f, "pages")) + embedStr += "\n//go:embed " + f + "\nvar " + fname + " []byte\n" + ret[strings.TrimPrefix(f, "pages")] = fname + } + + ff.WriteString(embedStr) + return ret +} + +func writeRoute(fname string, fmap map[string]string) { + ff, err := os.Create(fname) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer ff.Close() + embedStr := `package template + +import ( + "crypto/md5" + "fmt" + "github.com/gin-gonic/gin" +) + +func RouteWebPages(route gin.IRouter) { + route.GET("/", func(c *gin.Context) { + etag := fmt.Sprintf("%x", md5.Sum(Indexhtml)) + c.Header("Cache-Control", "public, max-age=31536000") + c.Header("ETag", etag) + c.Data(200, "text/html; charset=utf-8", Indexhtml) + }) +` + mime.AddExtensionType(".map", "application/json") + mime.AddExtensionType(".webmanifest", "application/manifest+json") + // sort fmap + keys := make([]string, 0, len(fmap)) + for key := range fmap { + keys = append(keys, key) + } + sort.Strings(keys) + for _, link := range keys { + fmime := mime.TypeByExtension(filepath.Ext(link)) + if fmime == "application/xml" || fmime == "application/javascript" { + fmime = fmime + "; charset=utf-8" + } + if fmime == "image/x-icon" { + fmime = "image/vnd.microsoft.icon" + } + embedStr += ` + route.GET("` + link + `", func(c *gin.Context) { + etag := fmt.Sprintf("%x", md5.Sum(` + fmap[link] + `)) + c.Header("Cache-Control", "public, max-age=31536000") + c.Header("ETag", etag) + c.Data(200, "` + fmime + `", ` + fmap[link] + `) + }) +` + } + embedStr += "}\n" + + ff.WriteString(embedStr) +} + +func run(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +func cleanName(fn string) string { + reg, err := regexp.Compile("[^a-zA-Z0-9]+") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return strings.Title(reg.ReplaceAllString(fn, "")) +} diff --git a/installTorrServerLinux.sh b/installTorrServerLinux.sh new file mode 100755 index 0000000..1b5f6ba --- /dev/null +++ b/installTorrServerLinux.sh @@ -0,0 +1,2125 @@ +#!/usr/bin/env bash + +set -euo pipefail + +############################################# +# GLOBAL VARIABLES +############################################# + +# Installation settings +DEFAULT_USERNAME="torrserver" +DEFAULT_INSTALL_DIR="/opt/torrserver" +DEFAULT_SERVICE_NAME="torrserver" +DEFAULT_PORT="8090" + +# Runtime variables +username="${DEFAULT_USERNAME}" +dirInstall="${DEFAULT_INSTALL_DIR}" +serviceName="${DEFAULT_SERVICE_NAME}" +scriptname=$(basename "$(test -L "$0" && readlink "$0" || echo "$0")") + +# Flags +SILENT_MODE=0 +USE_ROOT_USER=0 +ROOT_PROMPTED=0 + +# Command-line state +parsedCommand="" +specificVersion="" +downgradeRelease="" +changeUserName="" + +# Service configuration +servicePort="" +isAuth="" +isRdb="" +isLog="" +isBbr="" +isAuthUser="" +isAuthPass="" + +# Constants +readonly REPO_URL="https://github.com/YouROK/TorrServer" +readonly REPO_API_URL="https://api.github.com/repos/YouROK/TorrServer" +readonly VERSION_PREFIX="MatriX" +readonly BINARY_NAME_PREFIX="TorrServer-linux" +readonly MIN_GLIBC_VERSION="2.32" +readonly MIN_VERSION_REQUIRING_GLIBC=136 +readonly SYSCTL_BBR_FILE="/etc/sysctl.d/90-torrserver.conf" + +# Color support +declare -A colors=([black]=0 [red]=1 [green]=2 [yellow]=3 [blue]=4 [magenta]=5 [cyan]=6 [white]=7) +supports_color_output=0 + +if command -v tput >/dev/null 2>&1 && [[ -t 1 ]]; then + if [[ $(tput colors 2>/dev/null) -ge 8 ]]; then + supports_color_output=1 + fi +fi + +# Language +lang="en" + +############################################# +# TRANSLATION SYSTEM +############################################# + +# Message dictionary +declare -A MSG_EN=( + # General + [lang_choice]="Choose Language:" + [lang_english]="English" + [lang_russian]="Русский" + [your_lang]="Your language (Ваш язык): " + [have_fun]="Have Fun!" + + # Script info + [script_title]="TorrServer install and configuration script for Linux" + + # Checks + [need_root]="Script must run as root or user with sudo privileges. Example: sudo $scriptname" + [unsupported_arch]="Unsupported Arch. Can't continue." + [unsupported_os]="It looks like you are running this installer on a system other than Debian, Ubuntu, Fedora, CentOS, Amazon Linux, Oracle Linux or Arch Linux." + + # User management + [user_exists]="User %s exists!" + [user_added]="User %s has been added to system!" + [user_add_failed]="Failed to add %s user!" + [user_removed]="User %s has been removed from system!" + [user_remove_failed]="Failed to remove %s user!" + [user_not_found]="%s - no such user!" + + # Version + [downloading]="Downloading TorrServer" + [target_version]="Target version:" + [installed_version]="installed:" + [target_label]="target:" + [version_not_found]="ERROR: Version %s not found in releases" + [check_versions]="Please check available versions at: $REPO_URL/releases" + [already_installed]="You already have TorrServer %s installed" + [have_latest]="You have latest TorrServer %s" + [update_found]="TorrServer update found!" + [will_install]="Will install TorrServer version %s" + + # Installation + [installing_packages]="Installing missing packages…" + [install_configure]="Install and configure TorrServer…" + [starting_service]="Starting TorrServer…" + [install_complete]="TorrServer %s installed to %s" + [access_web]="You can now open your browser at http://%s:%s to access TorrServer web GUI." + [use_auth]="Use user \"%s\" with password \"%s\" for authentication" + + # Prompts + [want_update]="Want to update TorrServer?" + [want_install]="Want to install or configure TorrServer? Type Delete to uninstall." + [want_reconfigure]="Do you want to reconfigure TorrServer settings?" + [change_port]="Change TorrServer web-port?" + [enter_port]="Enter port number: " + [enable_auth]="Enable server authorization?" + [prompt_user]="User: " + [prompt_password]="Password: " + [change_auth_credentials]="Change authentication username and password?" + [enable_rdb]="Start TorrServer in public read-only mode?" + [enable_log]="Enable TorrServer log output to file?" + [enable_bbr]="Enable BBR (recommended for better download speed)?" + [confirm_delete]="Are you sure you want to delete TorrServer?" + [prompt_run_as_root]="Run service as root user?" + + # Uninstall + [install_dir_label]="TorrServer install dir -" + [uninstall_warning]="This action will delete TorrServer including all it's torrents, settings and files on path above!" + [uninstalled]="TorrServer uninstalled!" + + # Status + [found_in]="TorrServer found in" + [not_found]="TorrServer not found. It's not installed or have zero size." + [no_version_info]="No version information available. Can be server issue." + [config_updated]="Configuration updated successfully" + [store_auth]="Store %s:%s to %s" + [use_existing_auth]="Use existing auth from %s - %s" + [set_readonly]="Set database to read-only mode…" + [readonly_hint]="To change remove --rdb option from %s or rerun install script without parameters" + [log_location]="TorrServer log stored at %s" + [bbr_enabled]="BBR TCP congestion control enabled" + [bbr_already_configured]="BBR is already configured" + [bbr_configured_not_available]="BBR is configured but not available in this kernel" + [bbr_config_failed]="Warning: Failed to configure BBR" + [bbr_not_available]="BBR is not available in this kernel" + [bbr_requires_kernel]="BBR requires Linux kernel 4.9+ with tcp_bbr module" + [bbr_write_failed]="Failed to write to %s" + [bbr_current_values]="Current: qdisc=%s, congestion_control=%s" + [bbr_settings_will_apply]="BBR settings are in %s and will apply after reboot" + [bbr_settings_not_added]="BBR settings were not added to %s" + [bbr_activate_failed]="Warning: Could not activate BBR - module not available" + [bbr_no_optimization]="Service will start without BBR optimization" + [bbr_activate_failed_cc]="Warning: Could not activate BBR (currently: %s)" + [systemctl_missing]="systemctl is not available. Skipping service management commands." + [systemctl_failed]="Warning: systemctl %s failed" + [service_start_failed]="Warning: TorrServer service failed to start. Check systemctl status for details." + [user_change_success]="Service user changed to %s" + [user_change_permissions]="Updated ownership of %s to %s:%s" + [user_change_missing]="TorrServer installation not found in %s. Run install first." + [user_change_invalid]="Only %s or %s are allowed for --change-user" + + # glibc + [glibc_error]="ERROR: TorrServer version %s requires glibc >= $MIN_GLIBC_VERSION" + [glibc_current]="Your system has glibc %s" + [glibc_upgrade]="Please install a version < $MIN_VERSION_REQUIRING_GLIBC or upgrade your system" + [glibc_detected]="Detected glibc version: %s" + [glibc_ok]="OK: glibc version meets requirements for TorrServer %s" + [glibc_no_requirement]="TorrServer version %s: no special glibc requirements" + [glibc_warning]="Warning: Could not detect glibc version" + [glibc_may_fail]="TorrServer version %s requires glibc >= $MIN_GLIBC_VERSION" + [glibc_install_may_fail]="Installation may fail if your system doesn't meet this requirement" + [update_cancelled]="Update cancelled due to glibc incompatibility" + [downgrade_cancelled]="Downgrade cancelled due to glibc incompatibility" + + # OS version errors + [os_not_supported]="Your %s version is not supported." + [os_script_supports]="Script supports only %s %s" + + # User mode + [running_as_root]="Service will run as root user" + [running_as_user]="Service will run as %s user" + + # Error messages + [error_username_required]="Error: Username required for --change-user" + [error_version_required]="Error: Version number required for downgrade" + [error_version_example]="Example: %s -d 101" + [error_unknown_option]="Unknown option: %s" + [installing_specific_version]="Installing specific version: %s" + [service_reconfigured_user]="Service reconfigured for user: %s" + [install_first_required]="Please install TorrServer first using: %s --install" +) + +declare -A MSG_RU=( + # General + [lang_choice]="Choose Language:" + [lang_english]="English" + [lang_russian]="Русский" + [your_lang]="Your language (Ваш язык): " + [have_fun]="Have Fun!" + + # Script info + [script_title]="Скрипт установки, удаления и настройки TorrServer для Linux" + + # Checks + [need_root]="Вам нужно запустить скрипт от root или пользователя с правами sudo. Пример: sudo $scriptname" + [unsupported_arch]="Не поддерживаемая архитектура. Продолжение невозможно." + [unsupported_os]="Похоже, что вы запускаете этот установщик в системе отличной от Debian, Ubuntu, Fedora, CentOS, Amazon Linux, Oracle Linux или Arch Linux." + + # User management + [user_exists]="пользователь %s найден!" + [user_added]="пользователь %s добавлен!" + [user_add_failed]="не удалось добавить пользователя %s!" + [user_removed]="Пользователь %s удален!" + [user_remove_failed]="не удалось удалить пользователя %s!" + [user_not_found]="пользователь %s не найден!" + + # Version + [downloading]="Загружаем TorrServer" + [target_version]="Устанавливаемая версия:" + [installed_version]="установлен:" + [target_label]="устанавливаемая:" + [version_not_found]="ОШИБКА: Версия %s не найдена в релизах" + [check_versions]="Проверьте доступные версии по адресу: $REPO_URL/releases" + [already_installed]="TorrServer %s уже установлен" + [have_latest]="Установлен TorrServer последней версии %s" + [update_found]="Доступно обновление сервера" + [will_install]="Будет установлена версия TorrServer %s" + + # Installation + [installing_packages]="Устанавливаем недостающие пакеты…" + [install_configure]="Устанавливаем и настраиваем TorrServer…" + [starting_service]="Запускаем службу TorrServer…" + [install_complete]="TorrServer %s установлен в директории %s" + [access_web]="Теперь вы можете открыть браузер по адресу http://%s:%s для доступа к вебу TorrServer" + [use_auth]="Для авторизации используйте пользователя «%s» с паролем «%s»" + + # Prompts + [want_update]="Хотите обновить TorrServer?" + [want_install]="Хотите установить, обновить или настроить TorrServer? Для удаления введите «Delete»" + [want_reconfigure]="Хотите перенастроить параметры TorrServer?" + [change_port]="Хотите изменить порт для TorrServer?" + [enter_port]="Введите номер порта: " + [enable_auth]="Включить авторизацию на сервере?" + [prompt_user]="Пользователь: " + [prompt_password]="Пароль: " + [change_auth_credentials]="Изменить имя пользователя и пароль для авторизации?" + [enable_rdb]="Запускать TorrServer в публичном режиме без возможности изменения настроек через веб сервера?" + [enable_log]="Включить запись журнала работы TorrServer в файл?" + [enable_bbr]="Включить BBR (рекомендуется для лучшей скорости загрузки)?" + [confirm_delete]="Вы уверены что хотите удалить программу?" + [prompt_run_as_root]="Запускать службу от пользователя root?" + + # Uninstall + [install_dir_label]="Директория c TorrServer -" + [uninstall_warning]="Это действие удалит все данные TorrServer включая базу данных торрентов и настройки по указанному выше пути!" + [uninstalled]="TorrServer удален из системы!" + + # Status + [found_in]="TorrServer найден в директории" + [not_found]="TorrServer не найден, возможно он не установлен или размер бинарника равен 0." + [no_version_info]="Информация о версии недоступна. Возможно сервер не доступен." + [config_updated]="Конфигурация успешно обновлена" + [store_auth]="Сохраняем %s:%s в %s" + [use_existing_auth]="Используйте реквизиты из %s для авторизации - %s" + [set_readonly]="База данных устанавливается в режим «только для чтения»…" + [readonly_hint]="Для изменения отредактируйте %s, убрав опцию --rdb или запустите интерактивную установку без параметров повторно" + [log_location]="лог TorrServer располагается по пути %s" + [bbr_enabled]="Включено управление перегрузкой TCP BBR" + [bbr_already_configured]="BBR уже настроен" + [bbr_configured_not_available]="BBR настроен, но недоступен в этом ядре" + [bbr_config_failed]="Предупреждение: Не удалось настроить BBR" + [bbr_not_available]="BBR недоступен в этом ядре" + [bbr_requires_kernel]="BBR требует Linux kernel 4.9+ с модулем tcp_bbr" + [bbr_write_failed]="Не удалось записать в %s" + [bbr_current_values]="Текущие значения: qdisc=%s, congestion_control=%s" + [bbr_settings_will_apply]="Настройки BBR находятся в %s и вступят в силу после перезагрузки" + [bbr_settings_not_added]="Настройки BBR не были добавлены в %s" + [bbr_activate_failed]="Предупреждение: Не удалось активировать BBR - модуль недоступен" + [bbr_no_optimization]="Служба запустится без оптимизации BBR" + [bbr_activate_failed_cc]="Предупреждение: Не удалось активировать BBR (текущее: %s)" + [systemctl_missing]="systemctl недоступен. Пропускаем команды управления службой." + [systemctl_failed]="Предупреждение: команда systemctl %s завершилась ошибкой" + [service_start_failed]="Предупреждение: служба TorrServer не запустилась. Проверьте systemctl status для деталей." + [user_change_success]="Сервис TorrServer теперь запускается от пользователя %s" + [user_change_permissions]="Обновлены права на %s: %s:%s" + [user_change_missing]="Установка TorrServer не найдена в %s. Сначала выполните установку." + [user_change_invalid]="Параметр --change-user принимает только %s или %s" + + # glibc + [glibc_error]="ОШИБКА: TorrServer версии %s требует glibc >= $MIN_GLIBC_VERSION" + [glibc_current]="В вашей системе установлена glibc %s" + [glibc_upgrade]="Пожалуйста, установите версию < $MIN_VERSION_REQUIRING_GLIBC или обновите систему" + [glibc_detected]="Обнаружена версия glibc: %s" + [glibc_ok]="OK: версия glibc соответствует требованиям для TorrServer %s" + [glibc_no_requirement]="TorrServer версии %s: нет особых требований к glibc" + [glibc_warning]="Предупреждение: Не удалось определить версию glibc" + [glibc_may_fail]="TorrServer версии %s требует glibc >= $MIN_GLIBC_VERSION" + [glibc_install_may_fail]="Установка может завершиться неудачей, если система не соответствует требованиям" + [update_cancelled]="Обновление отменено из-за несовместимости glibc" + [downgrade_cancelled]="Понижение версии отменено из-за несовместимости glibc" + + # OS version errors + [os_not_supported]="Ваша версия %s не поддерживается." + [os_script_supports]="Скрипт поддерживает только %s %s" + + # User mode + [running_as_root]="Служба будет запущена от пользователя root" + [running_as_user]="Служба будет запущена от пользователя %s" + + # Error messages + [error_username_required]="Ошибка: Требуется имя пользователя для --change-user" + [error_version_required]="Ошибка: Требуется номер версии для понижения версии" + [error_version_example]="Пример: %s -d 101" + [error_unknown_option]="Неизвестная опция: %s" + [installing_specific_version]="Установка конкретной версии: %s" + [service_reconfigured_user]="Служба перенастроена для пользователя: %s" + [install_first_required]="Пожалуйста, сначала установите TorrServer используя: %s --install" +) + +# Translation function +msg() { + local key="$1" + shift + local message="" + + if [[ $lang == "ru" ]]; then + message="${MSG_RU[$key]:-$key}" + else + message="${MSG_EN[$key]:-$key}" + fi + + # Apply printf formatting if additional arguments provided + if [[ $# -gt 0 ]]; then + # shellcheck disable=SC2059 + printf "$message" "$@" + else + printf '%s\n' "$message" + fi +} + +############################################# +# UTILITY FUNCTIONS +############################################# + +colorize() { + if [[ $supports_color_output -eq 1 ]]; then + printf "%s%s%s" "$(tput setaf "${colors[$1]:-7}")" "$2" "$(tput op)" + else + printf "%s" "$2" + fi +} + +# Highlight first letter of a word with specified color +highlightFirstLetter() { + local color="$1" + local word="$2" + local first_char="${word:0:1}" + local rest="${word:1}" + printf "%s%s" "$(colorize "$color" "$first_char")" "$rest" +} + +isRoot() { + [[ $EUID -eq 0 ]] +} + +getBinaryName() { + echo "${BINARY_NAME_PREFIX}-${architecture}" +} + +getVersionTag() { + local version="$1" + echo "${VERSION_PREFIX}.${version}" +} + +buildDownloadUrl() { + local target_version="$1" + local binary_name="$2" + + if [[ "$target_version" == "latest" ]]; then + echo "${REPO_URL}/releases/latest/download/${binary_name}" + else + echo "${REPO_URL}/releases/download/${target_version}/${binary_name}" + fi +} + +getLang() { + lang=$(locale | grep LANG | cut -d= -f2 | tr -d '"' | cut -d_ -f1) + if [[ $lang != "ru" ]]; then + lang="en" + fi +} + +getIP() { + local ip="localhost" + + if command -v dig >/dev/null 2>&1; then + ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null || echo "") + if [[ -z "$ip" ]]; then + ip="localhost" + fi + elif command -v host >/dev/null 2>&1; then + local host_output="" + host_output=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null || true) + ip=$(printf "%s\n" "$host_output" | tail -n1 | awk '{print $NF}') + if [[ -z "$ip" ]]; then + ip="localhost" + fi + fi + + serverIP="$ip" +} + +promptYesNo() { + local prompt="$1" + local default="${2:-n}" + local recommended="${3:-$default}" + + if [[ $SILENT_MODE -eq 1 ]]; then + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + fi + + # Determine colors based on recommendation + local yes_color no_color + if [[ "$recommended" == "y" ]]; then + yes_color="green" + no_color="red" + else + yes_color="red" + no_color="green" + fi + + # Define localized Yes/No words + local yes_word no_word + if [[ $lang == "ru" ]]; then + yes_word="Да" + no_word="Нет" + else + yes_word="Yes" + no_word="No" + fi + + # Highlight first letter of each word + local yes_text + local no_text + yes_text="$(highlightFirstLetter "$yes_color" "$yes_word")" + no_text="$(highlightFirstLetter "$no_color" "$no_word")" + + local answer + IFS= read -r -p " $prompt ($yes_text/$no_text) " answer /dev/null 2>&1; then + if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then + echo " - $(msg systemctl_missing)" + fi + return 1 + fi + + local rc + systemctl "$@" >/dev/null 2>&1 + rc=$? + if [[ $rc -ne 0 ]]; then + if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg systemctl_failed "$*")" + fi + return $rc + fi + + return 0 +} + +############################################# +# VERSION MANAGEMENT +############################################# + +getLatestRelease() { + curl -s "${REPO_API_URL}/releases/latest" | + grep -iE '"tag_name":|"version":' | + sed -E 's/.*"([^"]+)".*/\1/' | + head -n1 +} + +getSpecificRelease() { + local version="$1" + local tag_name + tag_name=$(getVersionTag "$version") + local response + response=$(curl -s "${REPO_API_URL}/releases/tags/$tag_name") + + if echo "$response" | grep -q '"tag_name"'; then + echo "$tag_name" + else + echo "" + fi +} + +getTargetVersion() { + if [[ -n "$specificVersion" ]]; then + local target_release + target_release=$(getSpecificRelease "$specificVersion") + if [[ -z "$target_release" ]]; then + echo " - $(colorize red "$(msg version_not_found "$specificVersion")")" + echo " - $(msg check_versions)" + exit 1 + fi + echo "$target_release" + else + getLatestRelease + fi +} + +downloadBinary() { + local url="$1" + local destination="$2" + local version_info="$3" + + local curl_args=(-L) + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg downloading) $version_info..." + curl_args+=(--progress-bar -#) + else + curl_args+=(-s -S) + fi + + curl "${curl_args[@]}" -o "$destination" "$url" + chmod +x "$destination" +} + +############################################# +# GLIBC COMPATIBILITY +############################################# + +getGlibcVersion() { + local glibc_version + + # Try ldd --version (most reliable) + if command -v ldd >/dev/null 2>&1; then + glibc_version=$(ldd --version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1) + if [[ -n "$glibc_version" ]]; then + echo "$glibc_version" + return 0 + fi + fi + + # Try getconf GNU_LIBC_VERSION + if command -v getconf >/dev/null 2>&1; then + glibc_version=$(getconf GNU_LIBC_VERSION 2>/dev/null | grep -oE '[0-9]+\.[0-9]+') + if [[ -n "$glibc_version" ]]; then + echo "$glibc_version" + return 0 + fi + fi + + # Try rpm package manager + if command -v rpm >/dev/null 2>&1; then + glibc_version=$(rpm -q glibc 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -n1) + if [[ -n "$glibc_version" ]]; then + echo "$glibc_version" + return 0 + fi + fi + + # Try dpkg package manager + if command -v dpkg >/dev/null 2>&1; then + glibc_version=$(dpkg -l libc6 2>/dev/null | awk '/^ii/ {print $3}' | grep -oE '[0-9]+\.[0-9]+' | head -n1) + if [[ -n "$glibc_version" ]]; then + echo "$glibc_version" + return 0 + fi + fi + + return 1 +} + +compareVersions() { + local ver1="$1" + local ver2="$2" + local sorted_first + sorted_first=$(printf '%s\n' "$ver1" "$ver2" | sort -V | head -n1) + [[ "$sorted_first" == "$ver2" ]] +} + +checkGlibcCompatibility() { + local target_version="$1" + local version_number + + # Extract numeric version + if [[ "$target_version" =~ ${VERSION_PREFIX}\.([0-9]+) ]]; then + version_number="${BASH_REMATCH[1]}" + elif [[ "$target_version" =~ ^[0-9]+$ ]]; then + version_number="$target_version" + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg glibc_warning)" + fi + return 0 + fi + + # Check if version requires glibc 2.32+ + if [[ $version_number -ge $MIN_VERSION_REQUIRING_GLIBC ]]; then + local current_glibc + current_glibc=$(getGlibcVersion) + + if [[ -z "$current_glibc" ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg glibc_warning)" + echo " - $(msg glibc_may_fail "$target_version")" + echo " - $(msg glibc_install_may_fail)" + fi + return 0 + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg glibc_detected "$current_glibc")" + fi + + if ! compareVersions "$current_glibc" "$MIN_GLIBC_VERSION"; then + echo " - $(colorize red "$(msg glibc_error "$target_version")")" + echo " - $(msg glibc_current "$current_glibc")" + echo " - $(msg glibc_upgrade)" + return 1 + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(colorize green "$(msg glibc_ok "$target_version")")" + fi + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg glibc_no_requirement "$target_version")" + fi + fi + + return 0 +} + +############################################# +# USER MANAGEMENT +############################################# + +addUser() { + if ! isRoot; then + return 1 + fi + + if [[ $username == "root" ]]; then + return 0 + fi + + if id "$username" >/dev/null 2>&1; then + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_exists "$username")" + fi + return 0 + else + if useradd --home-dir "$dirInstall" --create-home --shell /bin/false -c "TorrServer" "$username" 2>/dev/null; then + chmod 755 "$dirInstall" + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_added "$username")" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_add_failed "$username")" + fi + return 1 + fi + fi +} + +delUser() { + if ! isRoot; then + return 1 + fi + + if [[ $username == "root" ]]; then + return 0 + fi + + if id "$username" >/dev/null 2>&1; then + if userdel --remove "$username" 2>/dev/null; then + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_removed "$username")" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_remove_failed "$username")" + fi + return 1 + fi + else + if [[ $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg user_not_found "$username")" + fi + return 1 + fi +} + +############################################# +# OS DETECTION & PACKAGES +############################################# + +installPackages() { + local pkg_type="$1" + shift + local packages=("$@") + + case "$pkg_type" in + deb) + local missing=() + for pkg in "${packages[@]}"; do + if ! dpkg -s "$pkg" >/dev/null 2>&1; then + missing+=("$pkg") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg installing_packages)" + fi + apt update >/dev/null 2>&1 + apt -y install "${missing[@]}" + fi + ;; + rpm) + local pkg_manager="$1" + shift + packages=("$@") + local needs_update=0 + for pkg in "${packages[@]}"; do + if [[ -z "$(rpm -qa "$pkg" 2>/dev/null)" ]]; then + needs_update=1 + break + fi + done + if [[ $needs_update -eq 1 ]]; then + if [[ "$pkg_manager" == "dnf" ]]; then + dnf makecache -q >/dev/null 2>&1 || true + elif [[ "$pkg_manager" == "yum" ]]; then + yum makecache fast -q >/dev/null 2>&1 || true + fi + for pkg in "${packages[@]}"; do + if [[ -z "$(rpm -qa "$pkg" 2>/dev/null)" ]]; then + $pkg_manager -y install "$pkg" + fi + done + fi + ;; + arch) + local missing=() + for pkg in "${packages[@]}"; do + if ! pacman -Q "$pkg" >/dev/null 2>&1; then + missing+=("$pkg") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + pacman -Sy --noconfirm >/dev/null 2>&1 + pacman -S --noconfirm "${missing[@]}" + fi + ;; + esac +} + +getRpmPackageManager() { + local version_id="$1" + + if [[ "$version_id" =~ ^[0-9]+$ ]] && [[ $version_id -ge 8 ]] && command -v dnf >/dev/null 2>&1; then + echo "dnf" + elif command -v dnf >/dev/null 2>&1; then + echo "dnf" + else + echo "yum" + fi +} + +validateOSVersion() { + local os_name="$1" + local supported_versions="$2" + local version_id="$3" + + local major_version + major_version=$(echo "$version_id" | cut -d '.' -f1) + + if [[ ! $major_version =~ ^($supported_versions)$ ]]; then + echo "" + echo " $(msg os_not_supported "$os_name")" + echo "" + echo " $(msg os_script_supports "$os_name" "$supported_versions")" + echo "" + exit 1 + fi +} + +checkOS() { + if [[ -e /etc/debian_version ]]; then + # shellcheck source=/dev/null + source /etc/os-release + + if [[ $ID == "debian" || $ID == "raspbian" ]]; then + local current_version_id + current_version_id="${VERSION_ID:-}" + if [[ -n "$current_version_id" && $current_version_id -lt 6 ]]; then + validateOSVersion "Debian" ">=6" "$current_version_id" + fi + elif [[ $ID == "ubuntu" ]]; then + local current_version_id + current_version_id="${VERSION_ID:-}" + local major + major=$(echo "$current_version_id" | cut -d '.' -f1) + if [[ -n "$current_version_id" && $major -lt 10 ]]; then + validateOSVersion "Ubuntu" ">=10" "$current_version_id" + fi + fi + + installPackages deb curl iputils-ping dnsutils + + elif [[ -e /etc/system-release ]]; then + # shellcheck source=/dev/null + source /etc/os-release + local pkg_manager + + case "$ID" in + fedora) + pkg_manager=$(getRpmPackageManager "${VERSION_ID%%.*}") + installPackages rpm "$pkg_manager" curl iputils bind-utils + ;; + centos|redhat) + validateOSVersion "CentOS/RedHat" "7|8|9|10" "$VERSION_ID" + pkg_manager=$(getRpmPackageManager "${VERSION_ID%%.*}") + installPackages rpm "$pkg_manager" curl iputils bind-utils + ;; + rocky) + validateOSVersion "RockyLinux" "8|9|10" "$VERSION_ID" + pkg_manager=$(getRpmPackageManager "${VERSION_ID%%.*}") + installPackages rpm "$pkg_manager" curl iputils bind-utils + ;; + almalinux) + validateOSVersion "AlmaLinux" "8|9|10" "$VERSION_ID" + pkg_manager=$(getRpmPackageManager "${VERSION_ID%%.*}") + installPackages rpm "$pkg_manager" curl iputils bind-utils + ;; + ol) + validateOSVersion "Oracle Linux" "8|9|10" "$VERSION_ID" + pkg_manager=$(getRpmPackageManager "${VERSION_ID%%.*}") + installPackages rpm "$pkg_manager" curl iputils bind-utils + ;; + amzn) + if [[ $VERSION_ID != "2" ]]; then + validateOSVersion "Amazon Linux" "2" "$VERSION_ID" + fi + installPackages rpm yum curl iputils bind-utils + ;; + esac + + elif [[ -e /etc/arch-release ]]; then + installPackages arch curl iputils bind-tools + + else + echo " $(msg unsupported_os)" + exit 1 + fi +} + +checkArch() { + case $(uname -m) in + i386|i686) architecture="386" ;; + x86_64) architecture="amd64" ;; + aarch64) architecture="arm64" ;; + armv7|armv7l) architecture="arm7" ;; + armv6|armv6l) architecture="arm5" ;; + *) + echo " $(msg unsupported_arch)" + exit 1 + ;; + esac +} + +initialCheck() { + if ! isRoot; then + echo " $(msg need_root)" + exit 1 + fi + + checkOS + checkArch +} + +############################################# +# INSTALLATION FUNCTIONS +############################################# + +checkInstalled() { + # Set username based on USE_ROOT_USER flag + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + else + username="${DEFAULT_USERNAME}" + if ! addUser; then + username="root" + fi + fi + + local binName + binName=$(getBinaryName) + if [[ -f "$dirInstall/$binName" ]] && [[ $(stat -c%s "$dirInstall/$binName" 2>/dev/null) -ne 0 ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg found_in) $dirInstall" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg not_found)" + fi + return 1 + fi +} + +checkInstalledVersion() { + local binName + binName=$(getBinaryName) + local target_version + target_version=$(getTargetVersion) + local installed_version + installed_version="$("$dirInstall/$binName" --version 2>/dev/null | awk '{print $2}')" + + if [[ -z "$target_version" ]]; then + echo " - $(msg no_version_info)" + exit 1 + fi + + if [[ "$target_version" == "$installed_version" ]]; then + if [[ -n "$specificVersion" ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg already_installed "$target_version")" + fi + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg have_latest "$target_version")" + fi + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + if [[ -n "$specificVersion" ]]; then + echo " - $(msg will_install "$target_version")" + else + echo " - $(msg update_found)" + fi + echo " $(msg installed_version) \"$installed_version\"" + echo " $(msg target_label) \"$target_version\"" + fi + return 1 + fi +} + +createServiceFile() { + cat << EOF > "$dirInstall/$serviceName.service" +[Unit] +Description=TorrServer - stream torrent to http +Wants=network-online.target +After=network.target + +[Service] +User=$username +Group=$username +Type=simple +NonBlocking=true +EnvironmentFile=$dirInstall/$serviceName.config +ExecStart=${dirInstall}/$(getBinaryName) \$DAEMON_OPTIONS +ExecReload=/bin/kill -HUP \$MAINPID +ExecStop=/bin/kill -INT \$MAINPID +TimeoutSec=30 +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +EOF +} + +# Check if BBR is available in the kernel +isBBRAvailable() { + sysctl net.ipv4.tcp_available_congestion_control 2>/dev/null | grep -qw "bbr" +} + +# Load BBR kernel module +loadBBRModule() { + local max_attempts=3 + local attempt=0 + + while [[ $attempt -lt $max_attempts ]]; do + if modprobe tcp_bbr >/dev/null 2>&1; then + sleep 0.3 + if isBBRAvailable; then + return 0 + fi + fi + ((attempt++)) + sleep 0.2 + done + + return 1 +} + +# Ensure BBR module loads at boot +ensureBBRModuleAtBoot() { + local modules_dir="/etc/modules-load.d" + local modules_file="$modules_dir/bbr.conf" + + [[ -d "$modules_dir" ]] || mkdir -p "$modules_dir" 2>/dev/null || return 1 + if [[ ! -f "$modules_file" ]] || ! grep -q "^tcp_bbr$" "$modules_file" 2>/dev/null; then + echo "tcp_bbr" >> "$modules_file" 2>/dev/null || return 1 + fi + return 0 +} + +# Check if BBR is configured in sysctl.d file +isBBRConfiguredInFile() { + [[ -f "$SYSCTL_BBR_FILE" ]] && \ + grep -q "^net.core.default_qdisc=fq" "$SYSCTL_BBR_FILE" 2>/dev/null && \ + grep -q "^net.ipv4.tcp_congestion_control=bbr" "$SYSCTL_BBR_FILE" 2>/dev/null +} + +# Add BBR settings to sysctl.d file +addBBRToSysctl() { + if ! grep -q "^net.core.default_qdisc=fq" "$SYSCTL_BBR_FILE" 2>/dev/null; then + echo "net.core.default_qdisc=fq" >> "$SYSCTL_BBR_FILE" 2>/dev/null || return 1 + fi + if ! grep -q "^net.ipv4.tcp_congestion_control=bbr" "$SYSCTL_BBR_FILE" 2>/dev/null; then + echo "net.ipv4.tcp_congestion_control=bbr" >> "$SYSCTL_BBR_FILE" 2>/dev/null || return 1 + fi + return 0 +} + +# Check if BBR is currently active in the kernel +isBBRActive() { + local current_cc + current_cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || echo "") + [[ "$current_cc" == "bbr" ]] +} + +# Apply BBR settings to kernel (non-critical - returns status for logging only) +applyBBRSettings() { + sysctl -w net.core.default_qdisc=fq >/dev/null 2>&1 || true + if sysctl -w net.ipv4.tcp_congestion_control=bbr >/dev/null 2>&1; then + sleep 0.2 + isBBRActive && return 0 + fi + return 1 +} + +# Ensure BBR is active (non-critical - always returns success) +ensureBBRActive() { + ! isBBRConfiguredInFile && return 0 + isBBRActive && return 0 + + if ! isBBRAvailable && ! loadBBRModule; then + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_activate_failed)")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + } + return 0 + fi + + if applyBBRSettings; then + [[ $SILENT_MODE -eq 0 ]] && echo " - $(msg bbr_enabled)" + else + local current_cc + current_cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || echo "unknown") + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_activate_failed_cc "$current_cc")")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + echo " $(colorize yellow "$(msg bbr_settings_will_apply "$SYSCTL_BBR_FILE")")" + } + fi + return 0 +} + +configureBBR() { + [[ $isBbr -ne 1 ]] && return 0 + + # Check if BBR is available or can be loaded first + if ! isBBRAvailable && ! loadBBRModule; then + # BBR not available - check if it's already in config + if isBBRConfiguredInFile; then + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_configured_not_available)")" + echo " $(colorize yellow "$(msg bbr_requires_kernel)")" + echo " $(colorize yellow "$(msg bbr_settings_will_apply "$SYSCTL_BBR_FILE")")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + } + else + # Not in config and not available - don't add it + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_not_available)")" + echo " $(colorize yellow "$(msg bbr_requires_kernel)")" + echo " $(colorize yellow "$(msg bbr_settings_not_added "$SYSCTL_BBR_FILE")")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + } + fi + return 0 + fi + + # BBR is available - now configure it + if isBBRConfiguredInFile; then + [[ $SILENT_MODE -eq 0 ]] && echo " - $(msg bbr_already_configured)" + else + ! addBBRToSysctl && { + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_config_failed)")" + echo " $(colorize yellow "$(msg bbr_write_failed "$SYSCTL_BBR_FILE")")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + } + return 0 + } + fi + + ensureBBRModuleAtBoot || true + + if applyBBRSettings; then + [[ $SILENT_MODE -eq 0 ]] && echo " - $(msg bbr_enabled)" + return 0 + fi + + local current_cc current_qdisc + current_cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || echo "unknown") + current_qdisc=$(sysctl -n net.core.default_qdisc 2>/dev/null || echo "unknown") + [[ $SILENT_MODE -eq 0 ]] && { + echo " - $(colorize yellow "$(msg bbr_config_failed)")" + echo " $(colorize yellow "$(msg bbr_current_values "$current_qdisc" "$current_cc")")" + echo " $(colorize yellow "$(msg bbr_settings_will_apply "$SYSCTL_BBR_FILE")")" + echo " $(colorize yellow "$(msg bbr_no_optimization)")" + } + return 0 +} + +readExistingConfig() { + local config_file="$dirInstall/$serviceName.config" + + if [[ -f "$config_file" ]]; then + local daemon_options + daemon_options=$(grep "^DAEMON_OPTIONS=" "$config_file" | cut -d'"' -f2) + + # Extract port - use -- to prevent grep from interpreting pattern as option + if echo "$daemon_options" | grep -qE -- "--port[[:space:]]+[0-9]+"; then + servicePort=$(echo "$daemon_options" | grep -oE -- "--port[[:space:]]+[0-9]+" | awk '{print $2}') + fi + + # Check for auth + if echo "$daemon_options" | grep -qE -- "--httpauth"; then + isAuth=1 + else + isAuth=0 + fi + + # Check for rdb + if echo "$daemon_options" | grep -qE -- "--rdb"; then + isRdb=1 + else + isRdb=0 + fi + + # Check for log + if echo "$daemon_options" | grep -qE -- "--logpath"; then + isLog=1 + else + isLog=0 + fi + fi +} + +configureService() { + # Read existing config if available (for reconfiguration) + if [[ -f "$dirInstall/$serviceName.config" ]]; then + readExistingConfig + fi + + # Port configuration + if [[ -z "$servicePort" ]]; then + local inferred_default="$DEFAULT_PORT" + if promptYesNo "$(msg change_port)" "n" "y"; then + servicePort=$(promptInput "$(msg enter_port)" "$inferred_default") + else + servicePort="$inferred_default" + fi + else + # Port exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + if promptYesNo "$(msg change_port)" "n" "y"; then + servicePort=$(promptInput "$(msg enter_port)" "$servicePort") + fi + fi + fi + + # Auth configuration + if [[ -z "$isAuth" ]]; then + if promptYesNo "$(msg enable_auth)" "n" "y"; then + isAuth=1 + else + isAuth=0 + fi + else + # Auth setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_auth_default + current_auth_default="$([[ $isAuth -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_auth)" "$current_auth_default" "y"; then + isAuth=1 + else + isAuth=0 + fi + fi + fi + + # Setup auth if enabled + if [[ $isAuth -eq 1 ]]; then + if [[ ! -f "$dirInstall/accs.db" ]]; then + isAuthUser=$(promptInput "$(msg prompt_user)" "admin") + isAuthPass=$(promptInput "$(msg prompt_password)" "admin") + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" + fi + echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" + else + local auth + auth=$(cat "$dirInstall/accs.db" | head -2 | tail -1 | tr -d '[:space:]' | tr -d '"') + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg use_existing_auth "${dirInstall}/accs.db" "$auth")" + # Ask if user wants to change credentials + if promptYesNo "$(msg change_auth_credentials)" "n" "n"; then + isAuthUser=$(promptInput "$(msg prompt_user)" "admin") + isAuthPass=$(promptInput "$(msg prompt_password)" "admin") + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" + fi + echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" + fi + fi + fi + fi + + # Read-only database configuration + if [[ -z "$isRdb" ]]; then + if promptYesNo "$(msg enable_rdb)" "n" "n"; then + isRdb=1 + else + isRdb=0 + fi + else + # RDB setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_rdb_default + current_rdb_default="$([[ $isRdb -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_rdb)" "$current_rdb_default" "n"; then + isRdb=1 + else + isRdb=0 + fi + fi + fi + + if [[ $isRdb -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg set_readonly)" + printf ' %s\n' "$(msg readonly_hint "$dirInstall/$serviceName.config")" + fi + + # Logging configuration + if [[ -z "$isLog" ]]; then + if promptYesNo "$(msg enable_log)" "n" "y"; then + isLog=1 + else + isLog=0 + fi + else + # Log setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_log_default + current_log_default="$([[ $isLog -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_log)" "$current_log_default" "y"; then + isLog=1 + else + isLog=0 + fi + fi + fi + + if [[ $isLog -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg log_location "$dirInstall/$serviceName.log")" + fi + + # BBR configuration + if [[ -z "$isBbr" ]] && ! isBBRConfiguredInFile; then + if promptYesNo "$(msg enable_bbr)" "n" "y"; then + isBbr=1 + else + isBbr=0 + fi + fi + + # Build the complete config file with all options + local daemon_options="--port $servicePort" + + if [[ $isRdb -eq 1 ]]; then + daemon_options="$daemon_options --rdb" + fi + + if [[ $isLog -eq 1 ]]; then + daemon_options="$daemon_options --logpath $dirInstall/$serviceName.log" + fi + + daemon_options="$daemon_options --path $dirInstall" + + if [[ $isAuth -eq 1 ]]; then + daemon_options="$daemon_options --httpauth" + fi + + cat << EOF > "$dirInstall/$serviceName.config" +DAEMON_OPTIONS="$daemon_options" +EOF +} + +changeServiceUser() { + local target_user="$1" + if [[ -z "$target_user" ]]; then + echo " $(msg error_username_required)" + exit 1 + fi + + if [[ ! -d "$dirInstall" ]] || [[ ! -f "$dirInstall/$serviceName.config" ]]; then + echo " - $(msg user_change_missing "$dirInstall")" + exit 1 + fi + + checkArch + + local normalized_target + normalized_target=$(echo "$target_user" | tr '[:upper:]' '[:lower:]') + local default_lower + default_lower=$(echo "$DEFAULT_USERNAME" | tr '[:upper:]' '[:lower:]') + + if [[ "$normalized_target" == "root" ]]; then + target_user="root" + USE_ROOT_USER=1 + username="root" + elif [[ "$normalized_target" == "$default_lower" ]]; then + target_user="$DEFAULT_USERNAME" + USE_ROOT_USER=0 + username="$DEFAULT_USERNAME" + if ! id "$username" >/dev/null 2>&1; then + if ! addUser; then + printf " - %s\n" "$(msg user_add_failed "$username")" + exit 1 + fi + fi + else + printf " - %s\n" "$(msg user_change_invalid "root" "$DEFAULT_USERNAME")" + exit 1 + fi + + local owner="$username" + local group + if [[ "$username" == "root" ]]; then + group="root" + else + group="$(id -gn "$username" 2>/dev/null || echo "$username")" + fi + + createServiceFile + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.service" + ln -sf "$dirInstall/$serviceName.service" /usr/local/lib/systemd/system/ + + if [[ -d "$dirInstall" ]]; then + chown -R "$owner":"$group" "$dirInstall" + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg user_change_permissions "$dirInstall" "$owner" "$group")" + fi + fi + + local restart_rc=0 + if ! systemctlCmd daemon-reload; then + restart_rc=1 + fi + if ! systemctlCmd restart "$serviceName.service"; then + restart_rc=1 + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg user_change_success "$username")" + if [[ "$username" == "root" ]]; then + echo " - $(msg running_as_root)" + else + printf ' - %s\n' "$(msg running_as_user "$username")" + fi + if [[ $restart_rc -eq 1 ]]; then + echo " - $(colorize yellow "$(msg service_start_failed)")" + fi + else + printf "%s\n" "$(msg user_change_success "$username")" + if [[ $restart_rc -eq 1 ]]; then + printf "%s\n" "$(msg service_start_failed)" + fi + fi +} + +installTorrServer() { + if [[ $SILENT_MODE -eq 0 && $ROOT_PROMPTED -eq 0 ]]; then + if [[ $USE_ROOT_USER -ne 1 ]]; then + if promptYesNo "$(msg prompt_run_as_root)" "n" "n"; then + USE_ROOT_USER=1 + username="root" + fi + fi + ROOT_PROMPTED=1 + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg install_configure)" + fi + + # Get target version and check glibc compatibility + local target_version + target_version=$(getTargetVersion) + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg target_version) $target_version" + fi + + if ! checkGlibcCompatibility "$target_version"; then + exit 1 + fi + + # Check if already installed and up to date + if checkInstalled; then + if ! checkInstalledVersion; then + if promptYesNo "$(msg want_update)" "y" "y"; then + UpdateVersion + return + fi + else + # Already installed and up to date, allow reconfiguration + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg running_as_user "$username")" + echo "" + # Allow user to reconfigure settings + if promptYesNo "$(msg want_reconfigure)" "n" "n"; then + # Read existing config first + if [[ -f "$dirInstall/$serviceName.config" ]]; then + readExistingConfig + fi + # Reconfigure service + configureService + # Configure BBR if enabled + configureBBR + # Update service file + createServiceFile + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.service" + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.config" + # Reload and restart service + if ! systemctlCmd daemon-reload; then + : + fi + if ! systemctlCmd restart "$serviceName.service"; then + : + fi + echo "" + echo " - $(msg config_updated)" + echo "" + fi + fi + return + fi + fi + + # Create directories + if [[ ! -d "$dirInstall" ]]; then + mkdir -p "$dirInstall" + fi + if [[ ! -d "/usr/local/lib/systemd/system" ]]; then + mkdir -p "/usr/local/lib/systemd/system" + fi + + # Download binary if needed + local binName + binName=$(getBinaryName) + if [[ ! -f "$dirInstall/$binName" ]] || [[ ! -x "$dirInstall/$binName" ]] || [[ $(stat -c%s "$dirInstall/$binName" 2>/dev/null) -eq 0 ]]; then + local urlBin + if [[ -n "$specificVersion" ]]; then + urlBin=$(buildDownloadUrl "$target_version" "$binName") + else + urlBin=$(buildDownloadUrl "latest" "$binName") + fi + downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" + fi + + # Create service and config files + createServiceFile + configureService + + # Configure BBR if enabled (non-critical - always succeeds) + configureBBR + + # Set up systemd service + ln -sf "$dirInstall/$serviceName.service" /usr/local/lib/systemd/system/ + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.service" + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.config" + + local service_started=0 + + # Start service + if [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg starting_service)" + fi + if ! systemctlCmd daemon-reload; then + : + fi + if ! systemctlCmd enable "$serviceName.service"; then + : + fi + if systemctlCmd restart "$serviceName.service"; then + service_started=1 + fi + + # Show completion message + getIP + local installed_version="$target_version" + + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + printf ' %s\n' "$(msg install_complete "$installed_version" "$dirInstall")" + echo "" + printf ' %s\n' "$(msg access_web "$serverIP" "$servicePort")" + echo "" + + if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then + printf ' %s\n' "$(msg use_auth "$isAuthUser" "$isAuthPass")" + echo "" + fi + + if [[ $username == "root" ]]; then + echo " $(colorize yellow "$(msg running_as_root)")" + else + printf ' %s\n' "$(msg running_as_user "$username")" + fi + + if [[ $service_started -eq 0 ]]; then + echo " $(colorize yellow "$(msg service_start_failed)")" + fi + echo "" + fi + + if [[ $SILENT_MODE -eq 1 ]]; then + printf "%s\n" "$(msg install_complete "$installed_version" "$dirInstall")" + printf "%s\n" "$(msg access_web "$serverIP" "$servicePort")" + if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then + printf "%s\n" "$(msg use_auth "$isAuthUser" "$isAuthPass")" + fi + fi + + return 0 +} + +# Common function to update/downgrade TorrServer version +updateTorrServerVersion() { + local target_version="$1" + local cancel_message="$2" + local use_latest_url="${3:-0}" + + if ! checkGlibcCompatibility "$target_version"; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg "$cancel_message")" + fi + return 1 + fi + + if ! systemctlCmd stop "$serviceName.service"; then + : + fi + + local binName + binName=$(getBinaryName) + local urlBin + if [[ $use_latest_url -eq 1 && -z "$specificVersion" ]]; then + urlBin=$(buildDownloadUrl "latest" "$binName") + else + urlBin=$(buildDownloadUrl "$target_version" "$binName") + fi + + downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" + + # Update service file to reflect user change + if [[ -f "$dirInstall/$serviceName.service" ]]; then + createServiceFile + if ! systemctlCmd daemon-reload; then + : + fi + fi + + # Ensure BBR is active before starting service (if previously configured) + ensureBBRActive + + if ! systemctlCmd start "$serviceName.service"; then + : + fi + + return 0 +} + +UpdateVersion() { + local target_version + target_version=$(getTargetVersion) + updateTorrServerVersion "$target_version" "update_cancelled" 1 +} + +DowngradeVersion() { + local target_version + target_version=$(getVersionTag "$downgradeRelease") + updateTorrServerVersion "$target_version" "downgrade_cancelled" 0 +} + +############################################# +# CLEANUP FUNCTIONS +############################################# + +cleanup() { + if ! systemctlCmd --quiet stop "$serviceName"; then + : + fi + if ! systemctlCmd --quiet disable "$serviceName"; then + : + fi + rm -rf /usr/local/lib/systemd/system/"$serviceName.service" "$dirInstall" 2>/dev/null + delUser +} + +cleanAll() { + if ! systemctlCmd --quiet stop torr; then + : + fi + if ! systemctlCmd --quiet stop torrserver; then + : + fi + if ! systemctlCmd --quiet disable torr; then + : + fi + if ! systemctlCmd --quiet disable torrserver; then + : + fi + rm -rf /home/torrserver 2>/dev/null + rm -rf /usr/local/torr 2>/dev/null + rm -rf /opt/torr* 2>/dev/null + rm -f /{,etc,usr/local/lib}/systemd/system/tor{,r,rserver}.service 2>/dev/null +} + +uninstall() { + checkArch + checkInstalled + + if [[ $SILENT_MODE -eq 1 ]]; then + cleanup + cleanAll + echo " - $(msg uninstalled)" + return + fi + + echo "" + echo " $(msg install_dir_label) ${dirInstall}" + echo "" + echo " $(msg uninstall_warning)" + echo "" + + if promptYesNo "$(msg confirm_delete)" "n" "n"; then + cleanup + cleanAll + echo " - $(msg uninstalled)" + echo "" + else + echo "" + fi +} + +############################################# +# RECONFIGURATION +############################################# + +reconfigureTorrServer() { + # Check if TorrServer is installed + if ! checkInstalled; then + echo " - $(msg not_found)" + echo " - $(msg install_first_required "$scriptname")" + exit 1 + fi + + # Set username based on USE_ROOT_USER flag + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg running_as_user "$username")" + echo "" + fi + + # Read existing config first + if [[ -f "$dirInstall/$serviceName.config" ]]; then + readExistingConfig + fi + + # Reconfigure service + configureService + + # Configure BBR if enabled + configureBBR + + # Update service file + createServiceFile + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.service" + sed -i 's/^[ \t]*//' "$dirInstall/$serviceName.config" + + # Reload and restart service + if ! systemctlCmd daemon-reload; then + : + fi + if ! systemctlCmd restart "$serviceName.service"; then + : + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + echo " - $(msg config_updated)" + echo "" + else + echo " - $(msg config_updated)" + fi +} + +############################################# +# HELP & MAIN +############################################# + +helpUsage() { + cat << EOF +$scriptname - TorrServer Installation Script + +Usage: $scriptname [COMMAND] [OPTIONS] + +Commands: + -i, --install [VERSION] Install latest or specific version + install [VERSION] + -u, --update Update to latest version + update + -c, --check Check for updates (version info only) + check + -d, --down VERSION Downgrade to specific version + down VERSION + -r, --remove Uninstall TorrServer + remove + --reconfigure Reconfigure TorrServer settings + reconfigure + -C, --change-user USER Change TorrServer service user (root|torrserver) + change-user USER + -h, --help Show this help message + help + +Options: + --root Run service as root user + --silent Non-interactive mode with defaults + +Examples: + # Install latest version interactively + sudo $scriptname --install + + # Install specific version as root user silently + sudo $scriptname --install 135 --root --silent + + # Update with silent mode + sudo $scriptname --update --silent + + # Check for updates + sudo $scriptname --check + + # Uninstall silently + sudo $scriptname --remove --silent + + # Reconfigure TorrServer settings interactively + sudo $scriptname --reconfigure + + # Switch service to run as root + sudo $scriptname -C root + + # Switch service back to torrserver user + sudo $scriptname --change-user torrserver + +Default Settings (silent mode): + - Port: ${portOverride:-$DEFAULT_PORT} + - User: torrserver (or root with --root flag) + - Auth: disabled + - Read-only mode: disabled + - Logging: disabled + - BBR: enabled + +EOF +} + +parseArguments() { + parsedCommand="" + + while [[ $# -gt 0 ]]; do + case $1 in + -i|--install|install) + parsedCommand="install" + shift + # Check for version number + if [[ $# -gt 0 ]]; then + local next_arg="$1" + if [[ "$next_arg" =~ ^[0-9]+$ ]]; then + specificVersion="$next_arg" + shift + fi + fi + ;; + -u|--update|update) + parsedCommand="update" + shift + ;; + -c|--check|check) + parsedCommand="check" + shift + ;; + -d|--down|down) + parsedCommand="downgrade" + shift + if [[ $# -gt 0 ]]; then + local next_arg="$1" + if [[ "$next_arg" =~ ^[0-9]+$ ]]; then + downgradeRelease="$next_arg" + shift + else + echo " $(msg error_version_required)" + echo " $(msg error_version_example "$scriptname")" + exit 1 + fi + else + echo " $(msg error_version_required)" + echo " $(msg error_version_example "$scriptname")" + exit 1 + fi + ;; + -r|--remove|remove) + parsedCommand="remove" + shift + ;; + --reconfigure|reconfigure) + parsedCommand="reconfigure" + shift + ;; + -C|--change-user|change-user) + parsedCommand="change_user" + shift + if [[ $# -gt 0 ]]; then + changeUserName="$1" + shift + else + echo " $(msg error_username_required)" + exit 1 + fi + ;; + -h|--help|help) + getLang # Set language before showing help + helpUsage + exit 0 + ;; + --root) + USE_ROOT_USER=1 + shift + ;; + --silent) + SILENT_MODE=1 + shift + ;; + *) + echo " $(msg error_unknown_option "$1")" + helpUsage + exit 1 + ;; + esac + done +} + +############################################# +# MAIN EXECUTION +############################################# + +main() { + getLang + + parseArguments "$@" + + local command="$parsedCommand" + + case "$command" in + install) + if [[ $SILENT_MODE -eq 0 && -n "$specificVersion" ]]; then + echo " - $(msg installing_specific_version "$specificVersion")" + fi + initialCheck + + if [[ $SILENT_MODE -eq 1 ]]; then + servicePort="$DEFAULT_PORT" + isAuth=0 + isRdb=0 + isLog=0 + isBbr=1 + fi + + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg running_as_root)" + fi + fi + + if ! checkInstalled; then + installTorrServer + else + createServiceFile + if ! systemctlCmd daemon-reload; then + : + fi + if ! systemctlCmd stop "$serviceName.service"; then + : + fi + if ! systemctlCmd start "$serviceName.service"; then + : + fi + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg service_reconfigured_user "$username")" + fi + fi + exit 0 + ;; + update) + initialCheck + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + fi + if checkInstalled; then + if ! checkInstalledVersion; then + UpdateVersion + fi + fi + exit 0 + ;; + check) + initialCheck + if checkInstalled; then + checkInstalledVersion + fi + exit 0 + ;; + downgrade) + initialCheck + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + fi + if checkInstalled; then + DowngradeVersion + fi + exit 0 + ;; + remove) + uninstall + exit 0 + ;; + reconfigure) + initialCheck + if [[ $USE_ROOT_USER -eq 1 ]]; then + username="root" + fi + reconfigureTorrServer + exit 0 + ;; + change_user) + if [[ -z "$changeUserName" ]]; then + echo " $(msg error_username_required)" + exit 1 + fi + if ! isRoot; then + echo " $(msg need_root)" + exit 1 + fi + changeServiceUser "$changeUserName" + exit 0 + ;; + esac + + # Interactive mode if no command provided and not silent + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + echo " $(msg lang_choice)" + echo " [$(colorize green 1)] $(msg lang_english)" + echo " [$(colorize yellow 2)] $(msg lang_russian)" + local answer_lang + answer_lang=$(promptInput "$(msg your_lang)" "1") + if [[ "$answer_lang" == "2" ]]; then + lang="ru" + fi + + echo "" + echo "=============================================================" + echo " $(msg script_title)" + echo "=============================================================" + echo "" + + local user_choice + user_choice=$(promptYesNoDelete "$(msg want_install)" "n" "y") + + if [[ "$user_choice" == "delete" ]]; then + initialCheck + uninstall + elif [[ "$user_choice" == "yes" ]]; then + initialCheck + + if promptYesNo "$(msg prompt_run_as_root)" "n" "n"; then + USE_ROOT_USER=1 + username="root" + fi + ROOT_PROMPTED=1 + + installTorrServer + fi + fi + + echo " $(msg have_fun)" + echo "" +} + +# Run main function +main "$@" diff --git a/installTorrServerMac.sh b/installTorrServerMac.sh new file mode 100755 index 0000000..aac1841 --- /dev/null +++ b/installTorrServerMac.sh @@ -0,0 +1,1338 @@ +#!/usr/bin/env bash + +set -euo pipefail + +############################################# +# GLOBAL VARIABLES +############################################# + +# Installation settings +DEFAULT_INSTALL_DIR="/Users/Shared/TorrServer" +DEFAULT_SERVICE_NAME="torrserver" +DEFAULT_PORT="8090" + +# Runtime variables +dirInstall="${DEFAULT_INSTALL_DIR}" +serviceName="${DEFAULT_SERVICE_NAME}" +scriptname=$(basename "$(test -L "$0" && readlink "$0" || echo "$0")") + +# Flags +SILENT_MODE=0 +USE_USER_LAUNCHAGENT=0 +USER_PROMPTED=0 + +# Command-line state +parsedCommand="" +specificVersion="" +downgradeRelease="" + +# Service configuration +servicePort="" +isAuth="" +isRdb="" +isLog="" +isAuthUser="" +isAuthPass="" +sysPath="" + +# Constants +readonly REPO_URL="https://github.com/YouROK/TorrServer" +readonly REPO_API_URL="https://api.github.com/repos/YouROK/TorrServer" +readonly VERSION_PREFIX="MatriX" +readonly BINARY_NAME_PREFIX="TorrServer-darwin" + +# Color support +getColorCode() { + case "$1" in + black) echo 0 ;; + red) echo 1 ;; + green) echo 2 ;; + yellow) echo 3 ;; + blue) echo 4 ;; + magenta) echo 5 ;; + cyan) echo 6 ;; + white) echo 7 ;; + *) echo 7 ;; # default to white + esac +} +supports_color_output=0 + +if command -v tput >/dev/null 2>&1 && [[ -t 1 ]]; then + if [[ $(tput colors 2>/dev/null) -ge 8 ]]; then + supports_color_output=1 + fi +fi + +# Language +lang="en" + +############################################# +# TRANSLATION SYSTEM +############################################# + +# Message dictionary +# Message dictionary - bash 3.2 compatible (no associative arrays) +# English messages +MSG_EN_lang_choice="Choose Language:" +MSG_EN_lang_english="English" +MSG_EN_lang_russian="Русский" +MSG_EN_your_lang="Your language (Ваш язык): " +MSG_EN_have_fun="Have Fun!" +MSG_EN_script_title="TorrServer install and configuration script for macOS" +MSG_EN_unsupported_arch="Unsupported Arch. Can't continue." +MSG_EN_unsupported_os="It looks like you are running this installer on a system other than macOS." +MSG_EN_downloading="Downloading TorrServer" +MSG_EN_target_version="Target version:" +MSG_EN_installed_version="installed:" +MSG_EN_target_label="target:" +MSG_EN_version_not_found="ERROR: Version %s not found in releases" +MSG_EN_check_versions="Please check available versions at: $REPO_URL/releases" +MSG_EN_already_installed="You already have TorrServer %s installed" +MSG_EN_have_latest="You have latest TorrServer %s" +MSG_EN_update_found="TorrServer update found!" +MSG_EN_will_install="Will install TorrServer version %s" +MSG_EN_installing_packages="Installing missing packages…" +MSG_EN_install_configure="Install and configure TorrServer…" +MSG_EN_starting_service="Starting TorrServer…" +MSG_EN_install_complete="TorrServer %s installed to %s" +MSG_EN_access_web="You can now open your browser at http://%s:%s to access TorrServer web GUI." +MSG_EN_use_auth="Use user \"%s\" with password \"%s\" for authentication" +MSG_EN_want_update="Want to update TorrServer?" +MSG_EN_want_install="Want to install or configure TorrServer? Type Delete to uninstall." +MSG_EN_want_reconfigure="Do you want to reconfigure TorrServer settings?" +MSG_EN_change_port="Change TorrServer web-port?" +MSG_EN_enter_port="Enter port number: " +MSG_EN_enable_auth="Enable server authorization?" +MSG_EN_prompt_user="User: " +MSG_EN_prompt_password="Password: " +MSG_EN_change_auth_credentials="Change authentication username and password?" +MSG_EN_enable_rdb="Start TorrServer in public read-only mode?" +MSG_EN_enable_log="Enable TorrServer log output to file?" +MSG_EN_confirm_delete="Are you sure you want to delete TorrServer?" +MSG_EN_prompt_launchagent="Add autostart for current user (1) or all users (2)?" +MSG_EN_admin_password="System can ask your admin account password" +MSG_EN_install_dir_label="TorrServer install dir -" +MSG_EN_uninstall_warning="This action will delete TorrServer including all it's torrents, settings and files on path above!" +MSG_EN_uninstalled="TorrServer uninstalled!" +MSG_EN_found_in="TorrServer found in" +MSG_EN_not_found="TorrServer not found. It's not installed or have zero size." +MSG_EN_no_version_info="No version information available. Can be server issue." +MSG_EN_config_updated="Configuration updated successfully" +MSG_EN_store_auth="Store %s:%s to %s" +MSG_EN_use_existing_auth="Use existing auth from %s - %s" +MSG_EN_set_readonly="Set database to read-only mode…" +MSG_EN_readonly_hint="To change remove --rdb option from %s or rerun install script without parameters" +MSG_EN_log_location="TorrServer log stored at %s" +MSG_EN_service_added="Autostart service added to %s" +MSG_EN_launchctl_missing="launchctl is not available. Skipping service management commands." +MSG_EN_launchctl_failed="Warning: launchctl %s failed" +MSG_EN_service_start_failed="Warning: TorrServer service failed to start. Check launchctl list for details." +MSG_EN_error_version_required="Error: Version number required for downgrade" +MSG_EN_error_version_example="Example: %s -d 101" +MSG_EN_error_unknown_option="Unknown option: %s" +MSG_EN_installing_specific_version="Installing specific version: %s" +MSG_EN_install_first_required="Please install TorrServer first using: %s --install" + +# Russian messages +MSG_RU_lang_choice="Choose Language:" +MSG_RU_lang_english="English" +MSG_RU_lang_russian="Русский" +MSG_RU_your_lang="Your language (Ваш язык): " +MSG_RU_have_fun="Have Fun!" +MSG_RU_script_title="Скрипт установки, удаления и настройки TorrServer для macOS" +MSG_RU_unsupported_arch="Не поддерживаемая архитектура. Продолжение невозможно." +MSG_RU_unsupported_os="Похоже, что вы запускаете этот установщик в системе отличной от macOS." +MSG_RU_downloading="Загружаем TorrServer" +MSG_RU_target_version="Устанавливаемая версия:" +MSG_RU_installed_version="установлен:" +MSG_RU_target_label="устанавливаемая:" +MSG_RU_version_not_found="ОШИБКА: Версия %s не найдена в релизах" +MSG_RU_check_versions="Проверьте доступные версии по адресу: $REPO_URL/releases" +MSG_RU_already_installed="TorrServer %s уже установлен" +MSG_RU_have_latest="Установлен TorrServer последней версии %s" +MSG_RU_update_found="Доступно обновление сервера" +MSG_RU_will_install="Будет установлена версия TorrServer %s" +MSG_RU_installing_packages="Устанавливаем недостающие пакеты…" +MSG_RU_install_configure="Устанавливаем и настраиваем TorrServer…" +MSG_RU_starting_service="Запускаем службу TorrServer…" +MSG_RU_install_complete="TorrServer %s установлен в директории %s" +MSG_RU_access_web="Теперь вы можете открыть браузер по адресу http://%s:%s для доступа к вебу TorrServer" +MSG_RU_use_auth="Для авторизации используйте пользователя «%s» с паролем «%s»" +MSG_RU_want_update="Хотите обновить TorrServer?" +MSG_RU_want_install="Хотите установить, обновить или настроить TorrServer? Для удаления введите «Delete»" +MSG_RU_want_reconfigure="Хотите перенастроить параметры TorrServer?" +MSG_RU_change_port="Хотите изменить порт для TorrServer?" +MSG_RU_enter_port="Введите номер порта: " +MSG_RU_enable_auth="Включить авторизацию на сервере?" +MSG_RU_prompt_user="Пользователь: " +MSG_RU_prompt_password="Пароль: " +MSG_RU_change_auth_credentials="Изменить имя пользователя и пароль для авторизации?" +MSG_RU_enable_rdb="Запускать TorrServer в публичном режиме без возможности изменения настроек через веб сервера?" +MSG_RU_enable_log="Включить запись журнала работы TorrServer в файл?" +MSG_RU_confirm_delete="Вы уверены что хотите удалить программу?" +MSG_RU_prompt_launchagent="Добавить автозагрузку для текущего пользователя (1) или для всех (2)?" +MSG_RU_admin_password="Система может запросить ваш пароль администратора" +MSG_RU_install_dir_label="Директория c TorrServer -" +MSG_RU_uninstall_warning="Это действие удалит все данные TorrServer включая базу данных торрентов и настройки по указанному выше пути!" +MSG_RU_uninstalled="TorrServer удален из системы!" +MSG_RU_found_in="TorrServer найден в директории" +MSG_RU_not_found="TorrServer не найден, возможно он не установлен или размер бинарника равен 0." +MSG_RU_no_version_info="Информация о версии недоступна. Возможно сервер не доступен." +MSG_RU_config_updated="Конфигурация успешно обновлена" +MSG_RU_store_auth="Сохраняем %s:%s в %s" +MSG_RU_use_existing_auth="Используйте реквизиты из %s для авторизации - %s" +MSG_RU_set_readonly="База данных устанавливается в режим «только для чтения»…" +MSG_RU_readonly_hint="Для изменения отредактируйте %s, убрав опцию --rdb или запустите интерактивную установку без параметров повторно" +MSG_RU_log_location="лог TorrServer располагается по пути %s" +MSG_RU_service_added="Сервис автозагрузки записан в %s" +MSG_RU_launchctl_missing="launchctl недоступен. Пропускаем команды управления службой." +MSG_RU_launchctl_failed="Предупреждение: команда launchctl %s завершилась ошибкой" +MSG_RU_service_start_failed="Предупреждение: служба TorrServer не запустилась. Проверьте launchctl list для деталей." +MSG_RU_error_version_required="Ошибка: Требуется номер версии для понижения версии" +MSG_RU_error_version_example="Пример: %s -d 101" +MSG_RU_error_unknown_option="Неизвестная опция: %s" +MSG_RU_installing_specific_version="Установка конкретной версии: %s" +MSG_RU_install_first_required="Пожалуйста, сначала установите TorrServer используя: %s --install" + +# Translation function - bash 3.2 compatible +msg() { + local key="$1" + shift + local var_name + local message="" + + if [[ $lang == "ru" ]]; then + var_name="MSG_RU_${key}" + else + var_name="MSG_EN_${key}" + fi + + # Use eval to get the variable value (bash 3.2 compatible) + eval "message=\"\${${var_name}:-${key}}\"" + + # Apply printf formatting if additional arguments provided + if [[ $# -gt 0 ]]; then + # shellcheck disable=SC2059 + printf "$message" "$@" + else + printf '%s\n' "$message" + fi +} + +############################################# +# UTILITY FUNCTIONS +############################################# + +colorize() { + if [[ $supports_color_output -eq 1 ]]; then + local color_code + color_code=$(getColorCode "$1") + printf "%s%s%s" "$(tput setaf "$color_code")" "$2" "$(tput op)" + else + printf "%s" "$2" + fi +} + +# Highlight first letter of a word with specified color +highlightFirstLetter() { + local color="$1" + local word="$2" + local first_char="${word:0:1}" + local rest="${word:1}" + printf "%s%s" "$(colorize "$color" "$first_char")" "$rest" +} + +getBinaryName() { + echo "${BINARY_NAME_PREFIX}-${architecture}" +} + +getVersionTag() { + local version="$1" + echo "${VERSION_PREFIX}.${version}" +} + +buildDownloadUrl() { + local target_version="$1" + local binary_name="$2" + + if [[ "$target_version" == "latest" ]]; then + echo "${REPO_URL}/releases/latest/download/${binary_name}" + else + echo "${REPO_URL}/releases/download/${target_version}/${binary_name}" + fi +} + +getLang() { + lang=$(locale | grep LANG | cut -d= -f2 | tr -d '"' | cut -d_ -f1) + if [[ $lang != "ru" ]]; then + lang="en" + fi +} + +getIP() { + local ip="localhost" + + # Try to get local IP address from network interfaces + # On macOS, try ipconfig first (most reliable) + if command -v ipconfig >/dev/null 2>&1; then + # Try common interfaces: en0 (Ethernet/WiFi), en1, etc. + for interface in en0 en1 eth0; do + ip=$(ipconfig getifaddr "$interface" 2>/dev/null || echo "") + if [[ -n "$ip" ]] && [[ "$ip" != "127.0.0.1" ]]; then + break + fi + done + fi + + # Fallback to ifconfig if ipconfig didn't work + if [[ -z "$ip" ]] || [[ "$ip" == "127.0.0.1" ]]; then + if command -v ifconfig >/dev/null 2>&1; then + # Get the first non-loopback inet address + ip=$(ifconfig 2>/dev/null | grep -E "inet " | grep -v "127.0.0.1" | head -n1 | awk '{print $2}' | sed 's/addr://' || echo "") + fi + fi + + # If still no valid IP, use localhost + if [[ -z "$ip" ]] || [[ "$ip" == "127.0.0.1" ]]; then + ip="localhost" + fi + + serverIP="$ip" +} + +promptYesNo() { + local prompt="$1" + local default="${2:-n}" + local recommended="${3:-$default}" + + if [[ $SILENT_MODE -eq 1 ]]; then + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + fi + + # Determine colors based on recommendation + local yes_color no_color + if [[ "$recommended" == "y" ]]; then + yes_color="green" + no_color="red" + else + yes_color="red" + no_color="green" + fi + + # Define localized Yes/No words + local yes_word no_word + if [[ $lang == "ru" ]]; then + yes_word="Да" + no_word="Нет" + else + yes_word="Yes" + no_word="No" + fi + + # Highlight first letter of each word + local yes_text + local no_text + yes_text="$(highlightFirstLetter "$yes_color" "$yes_word")" + no_text="$(highlightFirstLetter "$no_color" "$no_word")" + + local answer + IFS= read -r -p " $prompt ($yes_text/$no_text) " answer /dev/null 2>&1; then + if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then + echo " - $(msg launchctl_missing)" + fi + return 1 + fi + + local rc + launchctl "$@" >/dev/null 2>&1 + rc=$? + if [[ $rc -ne 0 ]]; then + if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then + printf " - %s\n" "$(msg launchctl_failed "$*")" + fi + return $rc + fi + + return 0 +} + +killRunning() { + local self="$(basename "$0")" + local runningPid + runningPid=$(ps -ax | grep -i torrserver | grep -v grep | grep -v "$self" | awk '{print $1}' || echo "") + if [[ -n "$runningPid" ]]; then + sudo kill -9 "$runningPid" 2>/dev/null || true + fi +} + +############################################# +# VERSION MANAGEMENT +############################################# + +getLatestRelease() { + curl -s "${REPO_API_URL}/releases/latest" | + grep -iE '"tag_name":|"version":' | + sed -E 's/.*"([^"]+)".*/\1/' | + head -n1 +} + +getSpecificRelease() { + local version="$1" + local tag_name + tag_name=$(getVersionTag "$version") + local response + response=$(curl -s "${REPO_API_URL}/releases/tags/$tag_name") + + if echo "$response" | grep -q '"tag_name"'; then + echo "$tag_name" + else + echo "" + fi +} + +getTargetVersion() { + if [[ -n "$specificVersion" ]]; then + local target_release + target_release=$(getSpecificRelease "$specificVersion") + if [[ -z "$target_release" ]]; then + echo " - $(colorize red "$(msg version_not_found "$specificVersion")")" + echo " - $(msg check_versions)" + exit 1 + fi + echo "$target_release" + else + getLatestRelease + fi +} + +downloadBinary() { + local url="$1" + local destination="$2" + local version_info="$3" + + local curl_args=(-L) + + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg downloading) $version_info..." + curl_args+=(--progress-bar -#) + else + curl_args+=(-s -S) + fi + + curl "${curl_args[@]}" -o "$destination" "$url" + chmod +x "$destination" + xattr -r -d com.apple.quarantine "$destination" 2>/dev/null || true +} + +############################################# +# OS DETECTION & ARCHITECTURE +############################################# + +checkOS() { + if [[ "$(uname)" != "Darwin" ]]; then + echo " $(msg unsupported_os)" + exit 1 + fi +} + +checkArch() { + case $(uname -m) in + i386|i686) architecture="386" ;; + x86_64) architecture="amd64" ;; + aarch64|arm64) architecture="arm64" ;; + *) + echo " $(msg unsupported_arch)" + exit 1 + ;; + esac +} + +initialCheck() { + checkOS + checkArch +} + +############################################# +# INSTALLATION FUNCTIONS +############################################# + +checkInstalled() { + local binName + binName=$(getBinaryName) + if [[ -f "$dirInstall/$binName" ]] && [[ $(stat -f%z "$dirInstall/$binName" 2>/dev/null) -ne 0 ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg found_in) $dirInstall" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg not_found)" + fi + return 1 + fi +} + +checkInstalledVersion() { + local binName + binName=$(getBinaryName) + local target_version + target_version=$(getTargetVersion) + local installed_version + installed_version="$("$dirInstall/$binName" --version 2>/dev/null | awk '{print $2}')" + + if [[ -z "$target_version" ]]; then + echo " - $(msg no_version_info)" + exit 1 + fi + + if [[ "$target_version" == "$installed_version" ]]; then + if [[ -n "$specificVersion" ]]; then + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg already_installed "$target_version")" + fi + else + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg have_latest "$target_version")" + fi + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + if [[ -n "$specificVersion" ]]; then + echo " - $(msg will_install "$target_version")" + else + echo " - $(msg update_found)" + fi + echo " $(msg installed_version) \"$installed_version\"" + echo " $(msg target_label) \"$target_version\"" + fi + return 1 + fi +} + +createPlistFile() { + local daemon_options="--port $servicePort --path $dirInstall" + + if [[ $isRdb -eq 1 ]]; then + daemon_options="$daemon_options --rdb" + fi + + if [[ $isLog -eq 1 ]]; then + daemon_options="$daemon_options --logpath $dirInstall/$serviceName.log" + fi + + if [[ $isAuth -eq 1 ]]; then + daemon_options="$daemon_options --httpauth" + fi + + # Convert daemon_options to plist array format + local plist_args=() + local arg + for arg in $daemon_options; do + plist_args+=(" $arg") + done + local plist_args_str + plist_args_str=$(printf '%s\n' "${plist_args[@]}") + + cat << EOF > "$dirInstall/$serviceName.plist" + + + + + Label + ${serviceName} + ServiceDescription + TorrServer service for macOS + ProgramArguments + + ${dirInstall}/$(getBinaryName) +${plist_args_str} + + RunAtLoad + + KeepAlive + + SuccessfulExit + + + ProcessType + Background + ThrottleInterval + 10 + AbandonProcessGroup + + StandardOutPath + ${dirInstall}/torrserver.log + StandardErrorPath + ${dirInstall}/torrserver.log + WorkingDirectory + ${dirInstall} + + +EOF +} + +readExistingConfig() { + local plist_file="$dirInstall/$serviceName.plist" + + if [[ -f "$plist_file" ]]; then + # Extract port + if grep -q "--port" "$plist_file"; then + servicePort=$(grep -A1 "--port" "$plist_file" | tail -n1 | sed 's/.*\(.*\)<\/string>.*/\1/') + fi + + # Check for auth + if grep -q "--httpauth" "$plist_file"; then + isAuth=1 + else + isAuth=0 + fi + + # Check for rdb + if grep -q "--rdb" "$plist_file"; then + isRdb=1 + else + isRdb=0 + fi + + # Check for log + if grep -q "--logpath" "$plist_file"; then + isLog=1 + else + isLog=0 + fi + fi +} + +configureService() { + # Read existing config if available (for reconfiguration) + if [[ -f "$dirInstall/$serviceName.plist" ]]; then + readExistingConfig + fi + + # Port configuration + if [[ -z "$servicePort" ]]; then + local inferred_default="$DEFAULT_PORT" + if promptYesNo "$(msg change_port)" "n" "y"; then + servicePort=$(promptInput "$(msg enter_port)" "$inferred_default") + else + servicePort="$inferred_default" + fi + else + # Port exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + if promptYesNo "$(msg change_port)" "n" "y"; then + servicePort=$(promptInput "$(msg enter_port)" "$servicePort") + fi + fi + fi + + # Auth configuration + if [[ -z "$isAuth" ]]; then + if promptYesNo "$(msg enable_auth)" "n" "y"; then + isAuth=1 + else + isAuth=0 + fi + else + # Auth setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_auth_default + current_auth_default="$([[ $isAuth -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_auth)" "$current_auth_default" "y"; then + isAuth=1 + else + isAuth=0 + fi + fi + fi + + # Setup auth if enabled + if [[ $isAuth -eq 1 ]]; then + if [[ ! -f "$dirInstall/accs.db" ]]; then + isAuthUser=$(promptInput "$(msg prompt_user)" "admin") + isAuthPass=$(promptInput "$(msg prompt_password)" "admin") + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" + fi + echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" + else + local auth + auth=$(cat "$dirInstall/accs.db" | head -2 | tail -1 | tr -d '[:space:]' | tr -d '"') + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg use_existing_auth "${dirInstall}/accs.db" "$auth")" + # Ask if user wants to change credentials + if promptYesNo "$(msg change_auth_credentials)" "n" "n"; then + isAuthUser=$(promptInput "$(msg prompt_user)" "admin") + isAuthPass=$(promptInput "$(msg prompt_password)" "admin") + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" + fi + echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" + fi + fi + fi + fi + + # Read-only database configuration + if [[ -z "$isRdb" ]]; then + if promptYesNo "$(msg enable_rdb)" "n" "n"; then + isRdb=1 + else + isRdb=0 + fi + else + # RDB setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_rdb_default + current_rdb_default="$([[ $isRdb -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_rdb)" "$current_rdb_default" "n"; then + isRdb=1 + else + isRdb=0 + fi + fi + fi + + if [[ $isRdb -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg set_readonly)" + printf ' %s\n' "$(msg readonly_hint "$dirInstall/$serviceName.plist")" + fi + + # Logging configuration + if [[ -z "$isLog" ]]; then + if promptYesNo "$(msg enable_log)" "n" "y"; then + isLog=1 + else + isLog=0 + fi + else + # Log setting exists, ask if user wants to change it + if [[ $SILENT_MODE -eq 0 ]]; then + local current_log_default + current_log_default="$([[ $isLog -eq 1 ]] && echo 'y' || echo 'n')" + if promptYesNo "$(msg enable_log)" "$current_log_default" "y"; then + isLog=1 + else + isLog=0 + fi + fi + fi + + if [[ $isLog -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then + printf ' - %s\n' "$(msg log_location "$dirInstall/$serviceName.log")" + fi + + # LaunchAgent/LaunchDaemon selection + if [[ $SILENT_MODE -eq 0 && $USER_PROMPTED -eq 0 ]]; then + local answer_cu + answer_cu=$(promptInput "$(msg prompt_launchagent)" "1") + if [[ "$answer_cu" == "1" ]]; then + USE_USER_LAUNCHAGENT=1 + sysPath="${HOME}/Library/LaunchAgents" + else + USE_USER_LAUNCHAGENT=0 + sysPath="/Library/LaunchDaemons" + fi + USER_PROMPTED=1 + elif [[ $SILENT_MODE -eq 1 ]]; then + # Silent mode defaults to user LaunchAgent + USE_USER_LAUNCHAGENT=1 + sysPath="${HOME}/Library/LaunchAgents" + fi +} + +installTorrServer() { + if [[ $SILENT_MODE -eq 0 ]]; then + echo " $(msg install_configure)" + fi + + # Get target version + local target_version + target_version=$(getTargetVersion) + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg target_version) $target_version" + fi + + # Check if already installed and up to date + if checkInstalled; then + if ! checkInstalledVersion; then + if promptYesNo "$(msg want_update)" "y" "y"; then + UpdateVersion + return + fi + else + # Already installed and up to date, allow reconfiguration + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + # Allow user to reconfigure settings + if promptYesNo "$(msg want_reconfigure)" "n" "n"; then + # Read existing config first + if [[ -f "$dirInstall/$serviceName.plist" ]]; then + readExistingConfig + fi + # Reconfigure service + configureService + # Update plist file + createPlistFile + # Reload and restart service + cleanup + installService + echo "" + echo " - $(msg config_updated)" + echo "" + fi + fi + return + fi + fi + + # Create directories + if [[ ! -d "$dirInstall" ]]; then + mkdir -p "$dirInstall" + chmod a+rw "$dirInstall" + fi + + # Download binary if needed + local binName + binName=$(getBinaryName) + if [[ ! -f "$dirInstall/$binName" ]] || [[ ! -x "$dirInstall/$binName" ]] || [[ $(stat -f%z "$dirInstall/$binName" 2>/dev/null) -eq 0 ]]; then + local urlBin + if [[ -n "$specificVersion" ]]; then + urlBin=$(buildDownloadUrl "$target_version" "$binName") + else + urlBin=$(buildDownloadUrl "latest" "$binName") + fi + downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" + fi + + # Create plist and configure service + configureService + createPlistFile + + # Install service + local service_started=0 + if installService; then + service_started=1 + fi + + # Show completion message + getIP + local installed_version="$target_version" + + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + printf ' %s\n' "$(msg install_complete "$installed_version" "$dirInstall")" + echo "" + printf ' %s\n' "$(msg access_web "$serverIP" "$servicePort")" + echo "" + + if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then + printf ' %s\n' "$(msg use_auth "$isAuthUser" "$isAuthPass")" + echo "" + fi + + if [[ $service_started -eq 0 ]]; then + echo " $(colorize yellow "$(msg service_start_failed)")" + fi + echo "" + fi + + if [[ $SILENT_MODE -eq 1 ]]; then + printf "%s\n" "$(msg install_complete "$installed_version" "$dirInstall")" + printf "%s\n" "$(msg access_web "$serverIP" "$servicePort")" + if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then + printf "%s\n" "$(msg use_auth "$isAuthUser" "$isAuthPass")" + fi + fi + + return 0 +} + +installService() { + # Cleanup existing services first + cleanup + + if [[ $USE_USER_LAUNCHAGENT -eq 1 ]]; then + # User LaunchAgent + sysPath="${HOME}/Library/LaunchAgents" + [[ ! -d "$sysPath" ]] && mkdir -p "$sysPath" + cp "$dirInstall/$serviceName.plist" "$sysPath" + chmod 0644 "$sysPath/$serviceName.plist" + if launchctlCmd load -w "$sysPath/$serviceName.plist"; then + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg service_added "$sysPath")" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg service_added "$sysPath")" + fi + return 1 + fi + else + # System LaunchDaemon + sysPath="/Library/LaunchDaemons" + [[ ! -d "$sysPath" ]] && sudo mkdir -p "$sysPath" + sudo cp "$dirInstall/$serviceName.plist" "$sysPath" + sudo chown root:wheel "$sysPath/$serviceName.plist" + sudo chmod 0644 "$sysPath/$serviceName.plist" + if sudo launchctl load -w "$sysPath/$serviceName.plist" >/dev/null 2>&1; then + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg service_added "$sysPath")" + fi + return 0 + else + if [[ $SILENT_MODE -eq 0 ]]; then + printf ' %s\n' "$(msg service_added "$sysPath")" + fi + return 1 + fi + fi +} + +# Common function to update/downgrade TorrServer version +updateTorrServerVersion() { + local target_version="$1" + local cancel_message="$2" + local use_latest_url="${3:-0}" + + killRunning + + local binName + binName=$(getBinaryName) + local urlBin + if [[ $use_latest_url -eq 1 && -z "$specificVersion" ]]; then + urlBin=$(buildDownloadUrl "latest" "$binName") + else + urlBin=$(buildDownloadUrl "$target_version" "$binName") + fi + + downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" + + # Update plist file + if [[ -f "$dirInstall/$serviceName.plist" ]]; then + createPlistFile + installService + fi + + return 0 +} + +UpdateVersion() { + local target_version + target_version=$(getTargetVersion) + updateTorrServerVersion "$target_version" "update_cancelled" 1 +} + +DowngradeVersion() { + local target_version + target_version=$(getVersionTag "$downgradeRelease") + updateTorrServerVersion "$target_version" "downgrade_cancelled" 0 +} + +############################################# +# CLEANUP FUNCTIONS +############################################# + +cleanup() { + killRunning + launchctl unload "$HOME/Library/LaunchAgents/$serviceName.plist" >/dev/null 2>&1 || true + sudo launchctl unload "/Library/LaunchDaemons/$serviceName.plist" >/dev/null 2>&1 || true + rm -f "$HOME/Library/LaunchAgents/$serviceName.plist" 2>/dev/null || true + sudo rm -f "/Library/LaunchDaemons/$serviceName.plist" 2>/dev/null || true +} + +uninstall() { + checkArch + checkInstalled + + if [[ $SILENT_MODE -eq 1 ]]; then + cleanup + sudo rm -rf "$dirInstall" + echo " - $(msg uninstalled)" + return + fi + + echo "" + echo " $(msg install_dir_label) ${dirInstall}" + echo "" + echo " $(msg uninstall_warning)" + echo "" + + if promptYesNo "$(msg confirm_delete)" "n" "n"; then + cleanup + sudo rm -rf "$dirInstall" + echo " - $(msg uninstalled)" + echo "" + else + echo "" + fi +} + +############################################# +# RECONFIGURATION +############################################# + +reconfigureTorrServer() { + # Check if TorrServer is installed + if ! checkInstalled; then + echo " - $(msg not_found)" + echo " - $(msg install_first_required "$scriptname")" + exit 1 + fi + + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + fi + + # Read existing config first + if [[ -f "$dirInstall/$serviceName.plist" ]]; then + readExistingConfig + fi + + # Reconfigure service + configureService + + # Update plist file + createPlistFile + + # Reload and restart service + cleanup + installService + + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + echo " - $(msg config_updated)" + echo "" + else + echo " - $(msg config_updated)" + fi +} + +############################################# +# HELP & MAIN +############################################# + +helpUsage() { + cat << EOF +$scriptname - TorrServer Installation Script + +Usage: $scriptname [COMMAND] [OPTIONS] + +Commands: + -i, --install [VERSION] Install latest or specific version + install [VERSION] + -u, --update Update to latest version + update + -c, --check Check for updates (version info only) + check + -d, --down VERSION Downgrade to specific version + down VERSION + -r, --remove Uninstall TorrServer + remove + --reconfigure Reconfigure TorrServer settings + reconfigure + -h, --help Show this help message + help + +Options: + --silent Non-interactive mode with defaults + +Examples: + # Install latest version interactively + $scriptname --install + + # Install specific version silently + $scriptname --install 135 --silent + + # Update with silent mode + $scriptname --update --silent + + # Check for updates + $scriptname --check + + # Uninstall silently + $scriptname --remove --silent + + # Reconfigure TorrServer settings interactively + $scriptname --reconfigure + +Default Settings (silent mode): + - Port: ${DEFAULT_PORT} + - LaunchAgent: current user (not system-wide) + - Auth: disabled + - Read-only mode: disabled + - Logging: disabled + +EOF +} + +parseArguments() { + parsedCommand="" + + while [[ $# -gt 0 ]]; do + case $1 in + -i|--install|install) + parsedCommand="install" + shift + # Check for version number + if [[ $# -gt 0 ]]; then + local next_arg="$1" + if [[ "$next_arg" =~ ^[0-9]+$ ]]; then + specificVersion="$next_arg" + shift + fi + fi + ;; + -u|--update|update) + parsedCommand="update" + shift + ;; + -c|--check|check) + parsedCommand="check" + shift + ;; + -d|--down|down) + parsedCommand="downgrade" + shift + if [[ $# -gt 0 ]]; then + local next_arg="$1" + if [[ "$next_arg" =~ ^[0-9]+$ ]]; then + downgradeRelease="$next_arg" + shift + else + echo " $(msg error_version_required)" + echo " $(msg error_version_example "$scriptname")" + exit 1 + fi + else + echo " $(msg error_version_required)" + echo " $(msg error_version_example "$scriptname")" + exit 1 + fi + ;; + -r|--remove|remove) + parsedCommand="remove" + shift + ;; + --reconfigure|reconfigure) + parsedCommand="reconfigure" + shift + ;; + -h|--help|help) + getLang # Set language before showing help + helpUsage + exit 0 + ;; + --silent) + SILENT_MODE=1 + shift + ;; + *) + echo " $(msg error_unknown_option "$1")" + helpUsage + exit 1 + ;; + esac + done +} + +############################################# +# MAIN EXECUTION +############################################# + +main() { + getLang + + parseArguments "$@" + + local command="$parsedCommand" + + case "$command" in + install) + if [[ $SILENT_MODE -eq 0 && -n "$specificVersion" ]]; then + echo " - $(msg installing_specific_version "$specificVersion")" + fi + initialCheck + + if [[ $SILENT_MODE -eq 1 ]]; then + servicePort="$DEFAULT_PORT" + isAuth=0 + isRdb=0 + isLog=0 + USE_USER_LAUNCHAGENT=1 + USER_PROMPTED=1 + fi + + if ! checkInstalled; then + installTorrServer + else + createPlistFile + installService + if [[ $SILENT_MODE -eq 0 ]]; then + echo " - $(msg config_updated)" + fi + fi + exit 0 + ;; + update) + initialCheck + if checkInstalled; then + if ! checkInstalledVersion; then + UpdateVersion + fi + fi + exit 0 + ;; + check) + initialCheck + if checkInstalled; then + checkInstalledVersion + fi + exit 0 + ;; + downgrade) + initialCheck + if checkInstalled; then + DowngradeVersion + fi + exit 0 + ;; + remove) + uninstall + exit 0 + ;; + reconfigure) + initialCheck + reconfigureTorrServer + exit 0 + ;; + esac + + # Interactive mode if no command provided and not silent + if [[ $SILENT_MODE -eq 0 ]]; then + echo "" + echo " $(msg lang_choice)" + echo " [$(colorize green 1)] $(msg lang_english)" + echo " [$(colorize yellow 2)] $(msg lang_russian)" + local answer_lang + answer_lang=$(promptInput "$(msg your_lang)" "1") + if [[ "$answer_lang" == "2" ]]; then + lang="ru" + fi + + echo "" + echo "=============================================================" + echo " $(msg script_title)" + echo "=============================================================" + echo "" + + local user_choice + user_choice=$(promptYesNoDelete "$(msg want_install)" "n" "y") + + if [[ "$user_choice" == "delete" ]]; then + initialCheck + uninstall + elif [[ "$user_choice" == "yes" ]]; then + initialCheck + USER_PROMPTED=0 + installTorrServer + fi + fi + + echo " $(msg have_fun)" + echo "" +} + +# Run main function +main "$@" diff --git a/nix/modules/home-manager.nix b/nix/modules/home-manager.nix new file mode 100644 index 0000000..1afa678 --- /dev/null +++ b/nix/modules/home-manager.nix @@ -0,0 +1,183 @@ +{ + self, +}: +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib.modules) mkIf mkMerge; + inherit (lib.options) mkOption mkEnableOption mkPackageOption; + inherit (lib.types) nullOr bool; + inherit (lib) + optionalString + types + mapAttrs' + mapAttrsToList + nameValuePair + mkDefault + literalExpression + ; + + cfg = config.services.torrserver; + +in +{ + options.services.torrserver = { + enable = mkEnableOption "TorrServer service"; + + package = mkOption { + type = types.package; + default = self.packages.${pkgs.system}.torrserver; + description = "TorrServer package to use"; + }; + + port = mkOption { + type = types.int; + default = 8090; + description = "Web server port"; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Web server address to bind to"; + }; + + dataDir = mkOption { + type = types.path; + default = "${config.home.homeDirectory}/.local/share/torrserver"; + description = "Directory for TorrServer data and cache"; + }; + + logDir = mkOption { + type = types.path; + default = "${config.home.homeDirectory}/.local/log/torrserver"; + description = "Directory for TorrServer logs"; + }; + + enableAuth = mkOption { + type = types.bool; + default = false; + description = "Enable HTTP authentication"; + }; + + enableWebDAV = mkOption { + type = types.bool; + default = false; + description = "Enable WebDAV server"; + }; + + enableDLNA = mkOption { + type = types.bool; + default = false; + description = "Enable DLNA server"; + }; + + maxCacheSize = mkOption { + type = types.nullOr types.str; + default = null; + example = "10GB"; + description = "Maximum cache size (e.g., 10GB, 5000MB)"; + }; + + torrentsDir = mkOption { + type = types.nullOr types.path; + default = null; + description = "Directory for auto-loading torrents"; + }; + + ssl = mkOption { + type = types.bool; + default = false; + description = "Enable HTTPS"; + }; + + sslPort = mkOption { + type = types.int; + default = 8091; + description = "HTTPS port (requires ssl = true)"; + }; + + sslCert = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to SSL certificate file"; + }; + + sslKey = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to SSL key file"; + }; + + telegramToken = mkOption { + type = types.nullOr types.str; + default = null; + description = "Telegram bot token"; + }; + + readOnly = mkOption { + type = types.bool; + default = false; + description = "Run in read-only database mode"; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional command-line arguments"; + }; + }; + + config = mkIf cfg.enable { + home.activation.torrserverDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + mkdir -p ${cfg.dataDir} ${cfg.logDir} + ''; + + systemd.user.services.torrserver = { + Unit = { + Description = "TorrServer - Stream torrents online"; + After = [ "network-online.target" ]; + Wants = [ "network-online.target" ]; + }; + + Service = { + Type = "simple"; + ExecStart = + let + args = [ + "-p ${toString cfg.port}" + "-i ${cfg.address}" + "-d ${cfg.dataDir}" + "-l ${cfg.logDir}/server.log" + "-w ${cfg.logDir}/web.log" + ] + ++ lib.optionals cfg.enableAuth [ "-a" ] + ++ lib.optionals cfg.readOnly [ "-r" ] + ++ lib.optionals cfg.enableWebDAV [ "--webdav" ] + ++ lib.optionals cfg.ssl [ + "--ssl" + "--ssl-port ${toString cfg.sslPort}" + ] + ++ lib.optionals (cfg.sslCert != null) [ "--ssl-cert ${cfg.sslCert}" ] + ++ lib.optionals (cfg.sslKey != null) [ "--ssl-key ${cfg.sslKey}" ] + ++ lib.optionals (cfg.maxCacheSize != null) [ "-m ${cfg.maxCacheSize}" ] + ++ lib.optionals (cfg.torrentsDir != null) [ "-t ${cfg.torrentsDir}" ] + ++ lib.optionals (cfg.telegramToken != null) [ "-T ${cfg.telegramToken}" ] + ++ cfg.extraArgs; + in + "${cfg.package}/bin/torrserver ${lib.strings.concatStringsSep " " args}"; + + Restart = "always"; + RestartSec = "10s"; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix new file mode 100644 index 0000000..7ff891e --- /dev/null +++ b/nix/modules/nixos.nix @@ -0,0 +1,220 @@ +{ + self, +}: +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib.modules) mkIf mkMerge; + inherit (lib.options) mkOption mkEnableOption mkPackageOption; + inherit (lib.types) nullOr bool; + inherit (lib) + optionalString + types + mapAttrs' + mapAttrsToList + nameValuePair + mkDefault + literalExpression + ; + + cfg = config.services.torrserver; + +in +{ + options.services.torrserver = { + enable = mkEnableOption "TorrServer service"; + + package = mkOption { + type = types.package; + default = self.packages.${pkgs.system}.torrserver; + description = "TorrServer package to use"; + }; + + port = mkOption { + type = types.int; + default = 8090; + description = "Web server port"; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Web server address to bind to"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/torrserver"; + description = "Directory for TorrServer data and cache"; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/torrserver"; + description = "Directory for TorrServer logs"; + }; + + user = mkOption { + type = types.str; + default = "torrserver"; + description = "User to run TorrServer as"; + }; + + group = mkOption { + type = types.str; + default = "torrserver"; + description = "Group to run TorrServer as"; + }; + + enableAuth = mkOption { + type = types.bool; + default = false; + description = "Enable HTTP authentication"; + }; + + enableWebDAV = mkOption { + type = types.bool; + default = false; + description = "Enable WebDAV server"; + }; + + enableDLNA = mkOption { + type = types.bool; + default = false; + description = "Enable DLNA server"; + }; + + maxCacheSize = mkOption { + type = types.nullOr types.str; + default = null; + example = "10GB"; + description = "Maximum cache size (e.g., 10GB, 5000MB)"; + }; + + torrentsDir = mkOption { + type = types.nullOr types.path; + default = null; + description = "Directory for auto-loading torrents"; + }; + + ssl = mkOption { + type = types.bool; + default = false; + description = "Enable HTTPS"; + }; + + sslPort = mkOption { + type = types.int; + default = 8091; + description = "HTTPS port (requires ssl = true)"; + }; + + sslCert = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to SSL certificate file"; + }; + + sslKey = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to SSL key file"; + }; + + telegramToken = mkOption { + type = types.nullOr types.str; + default = null; + description = "Telegram bot token"; + }; + + readOnly = mkOption { + type = types.bool; + default = false; + description = "Run in read-only database mode"; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional command-line arguments"; + }; + }; + + config = mkIf cfg.enable { + users.users = mkIf (cfg.user == "torrserver") { + torrserver = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + }; + }; + + users.groups = mkIf (cfg.group == "torrserver") { + torrserver = { }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.logDir} 0755 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.torrserver = { + description = "TorrServer - Stream torrents online"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + + ExecStart = + let + args = [ + "-p ${toString cfg.port}" + "-i ${cfg.address}" + "-d ${cfg.dataDir}" + "-l ${cfg.logDir}/server.log" + "-w ${cfg.logDir}/web.log" + ] + ++ lib.optionals cfg.enableAuth [ "-a" ] + ++ lib.optionals cfg.readOnly [ "-r" ] + ++ lib.optionals cfg.enableWebDAV [ "--webdav" ] + ++ lib.optionals cfg.ssl [ + "--ssl" + "--ssl-port ${toString cfg.sslPort}" + ] + ++ lib.optionals (cfg.sslCert != null) [ "--ssl-cert ${cfg.sslCert}" ] + ++ lib.optionals (cfg.sslKey != null) [ "--ssl-key ${cfg.sslKey}" ] + ++ lib.optionals (cfg.maxCacheSize != null) [ "-m ${cfg.maxCacheSize}" ] + ++ lib.optionals (cfg.torrentsDir != null) [ "-t ${cfg.torrentsDir}" ] + ++ lib.optionals (cfg.telegramToken != null) [ "-T ${cfg.telegramToken}" ] + ++ cfg.extraArgs; + in + "${cfg.package}/bin/torrserver ${lib.strings.concatStringsSep " " args}"; + + Restart = "always"; + RestartSec = "10s"; + + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ + cfg.dataDir + cfg.logDir + ]; + + LimitNOFILE = 65536; + LimitNPROC = 512; + }; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ] ++ lib.optionals cfg.ssl [ cfg.sslPort ]; + }; +} diff --git a/nix/packages/torrserver.nix b/nix/packages/torrserver.nix new file mode 100644 index 0000000..3782198 --- /dev/null +++ b/nix/packages/torrserver.nix @@ -0,0 +1,110 @@ +{ + lib, + buildGoModule, + pkg-config, + fetchFromGitHub, + fetchYarnDeps, + pkgs, + ... +}: +pkgs.stdenv.mkDerivation rec { + pname = "torrserver"; + version = "MatriX.141"; + + src = pkgs.fetchgit { + url = "https://github.com/YouROK/TorrServer.git"; + rev = "${version}"; + hash = "sha256-OeAAYyxfZxcx0ANeRAWJTrZMNWtdrM/pwXyO5QNTwYo="; + }; + yarnOfflineCache = pkgs.fetchYarnDeps { + yarnLock = "${src}/web/yarn.lock"; + hash = "sha256-B2D5HapIbrKLRvfKKF7HhJb6IlWRG2vi/qm4A5gJsNk="; + }; + + goModules = pkgs.buildGoModule.override { go = pkgs.go_1_26; } { + pname = "torrserver-go-deps"; + version = version; + src = "${src}/server"; + vendorHash = "sha256-rjdE9yf6S3ZovEeRO0+5sJsy9PRdFFejFDhkgJLMz58="; + modBuildPhase = '' + go mod download + go mod vendor + ''; + + installPhase = '' + mkdir -p $out + cp -r vendor $out/ + ''; + + doCheck = false; + doInstallCheck = false; + buildPhase = "true"; + }; + + nativeBuildInputs = with pkgs; [ + go_1_26 + pkg-config + git + yarn + fixup-yarn-lock + nodejs + go-swag + ]; + + buildInputs = with pkgs; [ + fuse + ]; + + buildPhase = '' + export GOCACHE=$TMPDIR/go-build + export GOMODCACHE=$TMPDIR/go-mod + export HOME=$(mktemp -d) + export NODE_OPTIONS=--openssl-legacy-provider + export PATH=$PATH:$(go env GOPATH)/bin + + cd web + runHook preConfigure + yarn config --offline set yarn-offline-mirror ${yarnOfflineCache} + fixup-yarn-lock yarn.lock + yarn install --offline --frozen-lockfile --ignore-platform --ignore-scripts --no-progress --non-interactive + patchShebangs node_modules/ + yarn build + cd .. + + cd server + swag init -g web/server.go --parseDependency --parseInternal --parseDepth 5 + cd .. + + mkdir -p server/vendor + cp -r ${goModules}/vendor/* server/vendor/ + chmod -R +w server/vendor + + cd server + mkdir -p ../dist + GOOS=linux GOARCH=amd64 go build \ + -ldflags="-s -w -checklinkname=0" \ + -tags=nosqlite \ + -trimpath \ + -o ../dist/torrserver ./cmd + cd .. + ''; + + installPhase = '' + mkdir -p $out/bin + cp dist/torrserver $out/bin/torrserver + chmod +x $out/bin/torrserver + ''; + + meta = with pkgs.lib; { + description = "Simple and powerful tool for streaming torrents"; + homepage = "https://github.com/YouROK/TorrServer"; + license = licenses.gpl3Only; + mainProgram = "torrserver"; + platforms = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + }; +} diff --git a/patches/00-responsive-reader.patch b/patches/00-responsive-reader.patch new file mode 100644 index 0000000..ab77a83 --- /dev/null +++ b/patches/00-responsive-reader.patch @@ -0,0 +1,15 @@ +--- reader.go.orig 2022-06-01 09:26:35.000000000 +0300 ++++ reader.go 2022-05-17 05:47:36.000000000 +0300 +@@ -102,9 +102,9 @@ + if !ok { + break + } +- if !r.responsive && !r.t.pieceComplete(pieceIndex(req.Index)) { +- break +- } ++// if !r.responsive && !r.t.pieceComplete(pieceIndex(req.Index)) { ++// break ++// } + if !r.t.haveChunk(req) { + break + } diff --git a/patches/01-no-udp-panic.patch b/patches/01-no-udp-panic.patch new file mode 100644 index 0000000..d236171 --- /dev/null +++ b/patches/01-no-udp-panic.patch @@ -0,0 +1,15 @@ +--- tracker/udp/conn-client.go.orig 2022-05-16 05:10:10.000000000 +0300 ++++ tracker/udp/conn-client.go 2022-06-01 09:34:27.000000000 +0300 +@@ -38,9 +38,9 @@ + // TODO: Do bad things to the dispatcher, and incoming calls to the client if we have a + // read error. + cc.readErr = err +- if !cc.closed { +- panic(err) +- } ++// if !cc.closed { ++// panic(err) ++// } + break + } + err = cc.d.Dispatch(b[:n], addr) diff --git a/release.json b/release.json new file mode 100644 index 0000000..2ae865b --- /dev/null +++ b/release.json @@ -0,0 +1,27 @@ +{ + "Name": "TorrServer", + "Version": "1.1.77", + "BuildDate": "05.08.2020", + "Links": { + "android-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-386", + "android-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-amd64", + "android-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm64", + "android-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm7", + "android-10-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm64-10", + "android-10-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-android-arm7-10", + "darwin-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-darwin-amd64", + "freebsd-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-freebsd-amd64", + "linux-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-386", + "linux-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-amd64", + "linux-arm5": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm5", + "linux-arm6": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm6", + "linux-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm64", + "linux-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-arm7", + "linux-mips": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips", + "linux-mips64": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips64", + "linux-mips64le": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mips64le", + "linux-mipsle": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-linux-mipsle", + "windows-386.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-windows-386.exe", + "windows-amd64.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.77/TorrServer-windows-amd64.exe" + } +} diff --git a/server/cmd/TorrServer_windows_386.syso b/server/cmd/TorrServer_windows_386.syso new file mode 100644 index 0000000..d4dd4be Binary files /dev/null and b/server/cmd/TorrServer_windows_386.syso differ diff --git a/server/cmd/TorrServer_windows_amd64.syso b/server/cmd/TorrServer_windows_amd64.syso new file mode 100644 index 0000000..3131763 Binary files /dev/null and b/server/cmd/TorrServer_windows_amd64.syso differ diff --git a/server/cmd/main.go b/server/cmd/main.go new file mode 100644 index 0000000..b02a01f --- /dev/null +++ b/server/cmd/main.go @@ -0,0 +1,493 @@ +package main + +import ( + "context" + "fmt" + "net" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "server/torr/utils" + + "github.com/alexflint/go-arg" + "github.com/pkg/browser" + + "server" + "server/docs" + "server/log" + "server/settings" + "server/torr" + "server/version" +) + +type args struct { + Port string `arg:"-p" help:"web server port (default 8090)"` + IP string `arg:"-i" help:"web server addr (default empty)"` + Ssl bool `help:"enables https"` + SslPort string `help:"web server ssl port, If not set, will be set to default 8091 or taken from db(if stored previously). Accepted if --ssl enabled."` + SslCert string `help:"path to ssl cert file. If not set, will be taken from db(if stored previously) or default self-signed certificate/key will be generated. Accepted if --ssl enabled."` + SslKey string `help:"path to ssl key file. If not set, will be taken from db(if stored previously) or default self-signed certificate/key will be generated. Accepted if --ssl enabled."` + Path string `arg:"-d" help:"database and config dir path"` + LogPath string `arg:"-l" help:"server log file path"` + WebLogPath string `arg:"-w" help:"web access log file path"` + RDB bool `arg:"-r" help:"start in read-only DB mode"` + HttpAuth bool `arg:"-a" help:"enable http auth on all requests"` + DontKill bool `arg:"-k" help:"don't kill server on signal"` + UI bool `arg:"-u" help:"open torrserver page in browser"` + TorrentsDir string `arg:"-t" help:"autoload torrents from dir"` + TorrentAddr string `help:"Torrent client address, like 127.0.0.1:1337 (default :PeersListenPort)"` + PubIPv4 string `arg:"-4" help:"set public IPv4 addr"` + PubIPv6 string `arg:"-6" help:"set public IPv6 addr"` + SearchWA bool `arg:"-s" help:"search without auth"` + MaxSize string `arg:"-m" help:"max allowed stream size (in Bytes)"` + TGToken string `arg:"-T" help:"telegram bot token"` + FusePath string `arg:"-f" help:"fuse mount path"` + WebDAV bool `help:"web dav enable"` + ProxyURL string `help:"proxy URL for BitTorrent traffic (http, socks4, socks5, socks5h), e.g. socks5://user:password@127.0.0.1:8080"` + ProxyMode string `help:"proxy mode: tracker (only HTTP trackers, default), peers (only peer connections), or full (all traffic)"` + ForceHTTPS bool `arg:"--force-https" help:"redirect all HTTP requests to HTTPS (requires --ssl)"` +} + +func (args) Version() string { + return "TorrServer " + version.Version +} + +var params args + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + + arg.MustParse(¶ms) + + if params.Path == "" { + params.Path, _ = os.Getwd() + } + + if params.Port == "" { + params.Port = "8090" + } + + settings.Path = params.Path + settings.HttpAuth = params.HttpAuth + log.Init(params.LogPath, params.WebLogPath) + + fmt.Println("=========== START ===========") + fmt.Println("TorrServer", version.Version+",", runtime.Version()+",", "CPU Num:", runtime.NumCPU()) + if params.HttpAuth { + log.TLogln("Use HTTP Auth file", settings.Path+"/accs.db") + } + if params.RDB { + log.TLogln("Running in Read-only DB mode!") + } + docs.SwaggerInfo.Version = version.Version + + // Simple Usage: + dnsResolve() + + // Advanced Usage: + // config := DNSConfig{ + // PrimaryServers: []string{"1.1.1.1:53", "8.8.8.8:53"}, + // Timeout: 3 * time.Second, + // } + // checker := NewDNSChecker(config) + // // Perform DNS lookup with automatic fallback + // addrs, err := checker.LookupHostWithFallback("themoviedb.org") + // if err != nil { + // log.TLogln("DNS lookup failed:", err) + // } else { + // fmt.Println("DNS resolved:", addrs) + // } + + Preconfig(params.DontKill) + + if params.UI { + go func() { + time.Sleep(time.Second) + if params.Ssl { + browser.OpenURL("https://127.0.0.1:" + params.SslPort) + } else { + browser.OpenURL("http://127.0.0.1:" + params.Port) + } + }() + } + + if params.TorrentAddr != "" { + settings.TorAddr = params.TorrentAddr + } + + if params.PubIPv4 != "" { + settings.PubIPv4 = params.PubIPv4 + } + + if params.PubIPv6 != "" { + settings.PubIPv6 = params.PubIPv6 + } + + if params.TorrentsDir != "" { + go watchTDir(params.TorrentsDir) + } + + if params.MaxSize != "" { + maxSize, err := strconv.ParseInt(params.MaxSize, 10, 64) + if err == nil { + settings.MaxSize = maxSize + } + } + + if params.ProxyURL != "" && params.ProxyMode == "" { + params.ProxyMode = "tracker" // default + } + if params.ProxyMode != "" && params.ProxyMode != "tracker" && params.ProxyMode != "peers" && params.ProxyMode != "full" { + log.TLogln("Invalid proxy mode, using default 'tracker'") + params.ProxyMode = "tracker" + } + + settings.Args = &settings.ExecArgs{ + Port: params.Port, + IP: params.IP, + Ssl: params.Ssl, + SslPort: params.SslPort, + SslCert: params.SslCert, + SslKey: params.SslKey, + Path: params.Path, + LogPath: params.LogPath, + WebLogPath: params.WebLogPath, + RDB: params.RDB, + HttpAuth: params.HttpAuth, + DontKill: params.DontKill, + UI: params.UI, + TorrentsDir: params.TorrentsDir, + TorrentAddr: params.TorrentAddr, + PubIPv4: params.PubIPv4, + PubIPv6: params.PubIPv6, + SearchWA: params.SearchWA, + MaxSize: params.MaxSize, + TGToken: params.TGToken, + FusePath: params.FusePath, + WebDAV: params.WebDAV, + ProxyURL: params.ProxyURL, + ProxyMode: params.ProxyMode, + ForceHTTPS: params.ForceHTTPS, + } + + if params.ProxyURL != "" { + log.TLogln("Proxy configured from CLI:", params.ProxyURL, "mode:", settings.Args.ProxyMode) + } + + if params.ForceHTTPS && !params.Ssl { + log.TLogln("Error: --force-https requires --ssl") + os.Exit(1) + } + + server.Start() + log.TLogln(server.WaitServer()) + log.Close() + time.Sleep(time.Second * 3) + os.Exit(0) +} + +func watchTDir(dir string) { + time.Sleep(5 * time.Second) + path, err := filepath.Abs(dir) + if err != nil { + path = dir + } + for { + files, err := os.ReadDir(path) + if err == nil { + for _, file := range files { + filename := filepath.Join(path, file.Name()) + if strings.ToLower(filepath.Ext(file.Name())) == ".torrent" { + sp, err := utils.OpenTorrentFile(filename) + if err == nil { + tor, err := torr.AddTorrent(sp, "", "", "", "") + if err == nil { + if tor.GotInfo() { + if tor.Title == "" { + tor.Title = tor.Name() + } + torr.SaveTorrentToDB(tor) + tor.Drop() + os.Remove(filename) + time.Sleep(time.Second) + } else { + log.TLogln("Error get info from torrent") + } + } else { + log.TLogln("Error parse torrent file:", err) + } + } else { + log.TLogln("Error parse file name:", err) + } + } + } + } else { + log.TLogln("Error read dir:", err) + } + time.Sleep(time.Second * 5) + } +} + +/////////// +/// DNS +/// + +// DNSConfig holds DNS resolver configuration +type DNSConfig struct { + PrimaryServers []string + FallbackServers []string + Timeout time.Duration + CacheDuration time.Duration +} + +// DefaultDNSConfig returns a sensible default configuration +func DefaultDNSConfig() DNSConfig { + return DNSConfig{ + PrimaryServers: []string{ + "8.8.8.8:53", // Google DNS + "1.1.1.1:53", // CloudFlare DNS + "9.9.9.9:53", // Quad9 DNS + }, + FallbackServers: []string{ + "208.67.222.222:53", // OpenDNS + "64.6.64.6:53", // Verisign + }, + Timeout: 5 * time.Second, + CacheDuration: 5 * time.Minute, + } +} + +// DNSChecker manages DNS resolution with fallback support +type DNSChecker struct { + config DNSConfig + customResolver *net.Resolver + cache map[string][]string + cacheTime map[string]time.Time + mu sync.RWMutex + useFallback bool +} + +// NewDNSChecker creates a new DNS checker instance +func NewDNSChecker(config DNSConfig) *DNSChecker { + if len(config.PrimaryServers) == 0 { + config = DefaultDNSConfig() + } + + return &DNSChecker{ + config: config, + cache: make(map[string][]string), + cacheTime: make(map[string]time.Time), + } +} + +// CheckAndResolve performs DNS check and returns a resolver +func (d *DNSChecker) CheckAndResolve() *net.Resolver { + // Test system DNS first + if d.testSystemDNS() { + log.TLogln("System DNS check passed") + return net.DefaultResolver + } + + log.TLogln("System DNS check failed, using custom resolver") + d.initCustomResolver() + return d.customResolver +} + +// testSystemDNS checks if system DNS is working properly +func (d *DNSChecker) testSystemDNS() bool { + _, cancel := context.WithTimeout(context.Background(), d.config.Timeout) + defer cancel() + + addrs, err := net.LookupHost("themoviedb.org") + if err != nil { + log.TLogln("DNS lookup error:", err) + return false + } + + if len(addrs) == 0 { + log.TLogln("DNS lookup returned no addresses") + return false + } + + // Check for suspicious addresses (DNS hijacking/pollution) + for _, addr := range addrs { + if isSuspiciousAddress(addr) { + log.TLogln("Suspicious DNS address detected:", addr) + return false + } + } + + return true +} + +// isSuspiciousAddress checks if an address indicates DNS issues +func isSuspiciousAddress(addr string) bool { + suspiciousPrefixes := []string{ + "127.0.0.1", // Localhost + "0.0.0.0", // Invalid address + "::1", // IPv6 localhost + // "10.", // Private network + "192.168.", // Private network + "169.254.", // Link-local + // "172.16.", "172.17.", "172.18.", "172.19.", + // "172.20.", "172.21.", "172.22.", "172.23.", + // "172.24.", "172.25.", "172.26.", "172.27.", + // "172.28.", "172.29.", "172.30.", "172.31.", // Private network range + } + + for _, prefix := range suspiciousPrefixes { + if strings.HasPrefix(addr, prefix) { + return true + } + } + + return false +} + +// initCustomResolver creates a custom resolver with fallback support +func (d *DNSChecker) initCustomResolver() { + d.customResolver = &net.Resolver{ + PreferGo: true, // Use Go's DNS implementation + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + dialer := &net.Dialer{ + Timeout: d.config.Timeout, + KeepAlive: 30 * time.Second, + } + + // Try primary servers first + for _, dns := range d.config.PrimaryServers { + conn, err := dialer.DialContext(ctx, network, dns) + if err == nil { + return conn, nil + } + log.TLogln("Failed to connect to DNS server", dns, ":", err) + } + + // Try fallback servers if primary fails + for _, dns := range d.config.FallbackServers { + conn, err := dialer.DialContext(ctx, network, dns) + if err == nil { + log.TLogln("Using fallback DNS server:", dns) + return conn, nil + } + log.TLogln("Failed to connect to fallback DNS", dns, ":", err) + } + + return nil, fmt.Errorf("all DNS servers failed") + }, + } + + d.useFallback = true +} + +// LookupHostWithFallback performs DNS lookup with automatic fallback +func (d *DNSChecker) LookupHostWithFallback(host string) ([]string, error) { + // Check cache first + if addrs, ok := d.getFromCache(host); ok { + return addrs, nil + } + + // Use appropriate resolver + var resolver *net.Resolver + if d.useFallback && d.customResolver != nil { + resolver = d.customResolver + } else { + resolver = net.DefaultResolver + } + + // Perform lookup with timeout + ctx, cancel := context.WithTimeout(context.Background(), d.config.Timeout) + defer cancel() + + addrs, err := resolver.LookupHost(ctx, host) + if err != nil { + // If using system DNS fails, try custom resolver + if !d.useFallback && d.customResolver != nil { + log.TLogln("System DNS failed, trying custom resolver") + addrs, err = d.customResolver.LookupHost(ctx, host) + } + } + + // Cache successful results + if err == nil && len(addrs) > 0 { + d.addToCache(host, addrs) + } + + return addrs, err +} + +// getFromCache retrieves DNS results from cache +func (d *DNSChecker) getFromCache(host string) ([]string, bool) { + d.mu.RLock() + defer d.mu.RUnlock() + + if addrs, ok := d.cache[host]; ok { + if time.Since(d.cacheTime[host]) < d.config.CacheDuration { + return addrs, true + } + // Expired, remove from cache + delete(d.cache, host) + delete(d.cacheTime, host) + } + + return nil, false +} + +// addToCache adds DNS results to cache +func (d *DNSChecker) addToCache(host string, addrs []string) { + d.mu.Lock() + defer d.mu.Unlock() + + d.cache[host] = addrs + d.cacheTime[host] = time.Now() +} + +// Simple usage function (backward compatible) +func dnsResolve() { + checker := NewDNSChecker(DefaultDNSConfig()) + resolver := checker.CheckAndResolve() + + // Store the resolver for later use if needed + net.DefaultResolver = resolver // Optional: replace global resolver + + // Test the resolver + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + addrs, err := resolver.LookupHost(ctx, "themoviedb.org") + if err != nil { + log.TLogln("DNS resolution test failed:", err) + } else { + log.TLogln("DNS resolution successful, addresses:", addrs) + } +} + +// func dnsResolve() { +// addrs, err := net.LookupHost("themoviedb.org") +// if len(addrs) == 0 { +// log.TLogln("System DNS check failed", err) + +// fn := func(ctx context.Context, network, address string) (net.Conn, error) { +// d := net.Dialer{} +// return d.DialContext(ctx, "udp", "1.1.1.1:53") +// } + +// net.DefaultResolver = &net.Resolver{ +// Dial: fn, +// } + +// addrs, err = net.LookupHost("themoviedb.org") +// if err != nil { +// log.TLogln("Check CloudFlare DNS error:", err) +// } else { +// log.TLogln("Use CloudFlare DNS") +// } +// } else { +// log.TLogln("System DNS check passed") +// } +// } diff --git a/server/cmd/preconfig_and.go b/server/cmd/preconfig_and.go new file mode 100644 index 0000000..0a2169e --- /dev/null +++ b/server/cmd/preconfig_and.go @@ -0,0 +1,55 @@ +//go:build android +// +build android + +package main + +// #cgo LDFLAGS: -static-libstdc++ +import "C" + +import ( + "os" + "os/signal" + "syscall" + "time" + + "server" + "server/log" + "server/settings" +) + +func Preconfig(dkill bool) { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + go func() { + for s := range sigc { + if dkill { + if settings.BTsets.EnableDebug || s != syscall.SIGPIPE { + log.TLogln("Signal catched:", s) + log.TLogln("To stop server, close it from web / api") + } + continue + } + + log.TLogln("Signal catched:", s, "stopping server...") + + done := make(chan struct{}) + + go func() { + server.Stop() + close(done) + }() + + select { + case <-done: + log.TLogln("Server stopped gracefully") + case <-time.After(5 * time.Second): + log.TLogln("Server stop timeout, exiting forcefully") + os.Exit(1) + } + } + }() +} diff --git a/server/cmd/preconfig_pos.go b/server/cmd/preconfig_pos.go new file mode 100644 index 0000000..3ec1f10 --- /dev/null +++ b/server/cmd/preconfig_pos.go @@ -0,0 +1,53 @@ +//go:build !windows && !android +// +build !windows,!android + +package main + +import ( + "os" + "os/signal" + "syscall" + "time" + + "server" + + "server/log" + "server/settings" +) + +func Preconfig(dkill bool) { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + go func() { + for s := range sigc { + if dkill { + if settings.BTsets.EnableDebug || s != syscall.SIGPIPE { + log.TLogln("Signal catched:", s) + log.TLogln("To stop server, close it from web / api") + } + continue + } + + log.TLogln("Signal catched:", s, "stopping server...") + + done := make(chan struct{}) + + go func() { + server.Stop() + close(done) + }() + + select { + case <-done: + log.TLogln("Server stopped gracefully") + case <-time.After(5 * time.Second): + log.TLogln("Server stop timeout, exiting forcefully") + os.Exit(1) + } + } + }() +} diff --git a/server/cmd/preconfig_win.go b/server/cmd/preconfig_win.go new file mode 100644 index 0000000..21e6b3d --- /dev/null +++ b/server/cmd/preconfig_win.go @@ -0,0 +1,74 @@ +//go:build windows +// +build windows + +package main + +import ( + "runtime" + "syscall" + "time" + + "server/torr" + "server/torr/state" +) + +const ( + EsSystemRequired = 0x00000001 + EsAwaymodeRequired = 0x00000040 // Added for future improvements + EsContinuous = 0x80000000 +) + +var ( + pulseTime = 60 * time.Second + clearFlagTimeout = 3 * 60 * time.Second +) + +func Preconfig(kill bool) { + go func() { + // need work on one thread because SetThreadExecutionState sets flag to thread. We need set and clear flag for same thread. + runtime.LockOSThread() + // don't sleep/hibernate windows + kernel32 := syscall.NewLazyDLL("kernel32.dll") + setThreadExecStateProc := kernel32.NewProc("SetThreadExecutionState") + currentExecState := uintptr(EsContinuous) + normalExecutionState := uintptr(EsContinuous) + systemRequireState := uintptr(EsSystemRequired | EsContinuous) + pulse := time.NewTicker(pulseTime) + var clearFlagTime int64 = -1 + for { + select { + case <-pulse.C: + { + systemRequired := false + for _, torrent := range torr.ListTorrent() { + if torrent.Stat != state.TorrentInDB { + systemRequired = true + break + } + } + if systemRequired && currentExecState != systemRequireState { + // Looks like sending just EsSystemRequired to clear timer is broken in Win11. + // Enable system required to avoid the system to idle to sleep. + currentExecState = systemRequireState + setThreadExecStateProc.Call(systemRequireState) + } + + if !systemRequired && currentExecState != normalExecutionState { + // Clear EXECUTION_STATE flags to disable away mode and allow the system to idle to sleep normally. + + // Avoid clear flag immediately to add time to start next episode + if clearFlagTime == -1 { + clearFlagTime = time.Now().Unix() + int64(clearFlagTimeout.Seconds()) + } + + if clearFlagTime >= time.Now().Unix() { + clearFlagTime = -1 + currentExecState = normalExecutionState + setThreadExecStateProc.Call(normalExecutionState) + } + } + } + } + } + }() +} diff --git a/server/dlna/dlna.go b/server/dlna/dlna.go new file mode 100644 index 0000000..6a0599d --- /dev/null +++ b/server/dlna/dlna.go @@ -0,0 +1,205 @@ +package dlna + +import ( + "fmt" + "net" + "os" + "os/user" + "path/filepath" + "runtime" + "sort" + "strconv" + "time" + + "github.com/anacrolix/dms/dlna/dms" + "github.com/anacrolix/log" + "github.com/wlynxg/anet" + + "server/settings" + "server/web/pages/template" +) + +var dmsServer *dms.Server + +func Start() { + logger := log.Default.WithNames("dlna") + dmsServer = &dms.Server{ + Logger: logger.WithNames("dms", "server"), + Interfaces: func() (ifs []net.Interface) { + var err error + ifaces, err := anet.Interfaces() + if err != nil { + logger.Levelf(log.Error, "%v", err) + return + // os.Exit(1) // avoid start on Android 13+ + } + for _, i := range ifaces { + // interface flags seem to always be 0 on Windows + if runtime.GOOS != "windows" && (i.Flags&net.FlagLoopback != 0 || i.Flags&net.FlagUp == 0 || i.Flags&net.FlagMulticast == 0) { + continue + } + ifs = append(ifs, i) + } + return + }(), + HTTPConn: func() net.Listener { + port := 9080 + for { + logger.Levelf(log.Info, "Check dlna port %d", port) + m, err := net.Listen("tcp", settings.IP+":"+strconv.Itoa(port)) + if m != nil { + m.Close() + } + if err == nil { + break + } + port++ + } + logger.Levelf(log.Info, "Set dlna port %d", port) + conn, err := net.Listen("tcp", settings.IP+":"+strconv.Itoa(port)) + if err != nil { + logger.Levelf(log.Error, "%v", err) + os.Exit(1) + } + return conn + }(), + FriendlyName: getDefaultFriendlyName(), + NoTranscode: true, + NoProbe: true, + StallEventSubscribe: false, + Icons: []dms.Icon{ + { + Width: 48, + Height: 48, + Depth: 24, + Mimetype: "image/png", + Bytes: template.Dlnaicon48png, + }, + { + Width: 120, + Height: 120, + Depth: 24, + Mimetype: "image/png", + Bytes: template.Dlnaicon120png, + }, + }, + LogHeaders: settings.BTsets.EnableDebug, + NotifyInterval: 30 * time.Second, + AllowedIpNets: func() []*net.IPNet { + var nets []*net.IPNet + _, ipnet, _ := net.ParseCIDR("0.0.0.0/0") + nets = append(nets, ipnet) + _, ipnet, _ = net.ParseCIDR("::/0") + nets = append(nets, ipnet) + return nets + }(), + OnBrowseDirectChildren: onBrowse, + OnBrowseMetadata: onBrowseMeta, + } + + if err := dmsServer.Init(); err != nil { + logger.Levelf(log.Error, "error initing dms server: %v", err) + os.Exit(1) + } + go func() { + if err := dmsServer.Run(); err != nil { + logger.Levelf(log.Error, "%v", err) + os.Exit(1) + } + }() +} + +func Stop() { + if dmsServer != nil { + dmsServer.Close() + dmsServer = nil + } +} + +func onBrowse(path, rootObjectPath, host, userAgent string) (ret []interface{}, err error) { + if path == "/" { + ret = getRoot() + return + } else if path == "/TR" { + ret = getTorrents() + return + } else if isHashPath(path) { + ret = getTorrent(path, host) + return + } else if filepath.Base(path) == "LD" { + ret = loadTorrent(path, host) + } + return +} + +func onBrowseMeta(path string, rootObjectPath string, host, userAgent string) (ret interface{}, err error) { + ret = getTorrentMeta(path, host) + if ret == nil { + err = fmt.Errorf("meta not found") + } + return +} + +func getDefaultFriendlyName() string { + logger := log.Default.WithNames("dlna") + + if settings.BTsets.FriendlyName != "" { + return settings.BTsets.FriendlyName + } + + ret := "TorrServer" + userName := "" + user, err := user.Current() + if err != nil { + logger.Printf("getDefaultFriendlyName could not get username: %s", err) + } else { + userName = user.Name + } + host, err := os.Hostname() + if err != nil { + logger.Printf("getDefaultFriendlyName could not get hostname: %s", err) + } + + if userName == "" && host == "" { + return ret + } + + if userName != "" && host != "" { + if userName == host { + return ret + ": " + userName + } + return ret + ": " + userName + " on " + host + } + + if host == "localhost" { // useless host, use 1st IP + ifaces, err := anet.Interfaces() + if err != nil { + return ret + ": " + userName + "@" + host + } + var list []string + for _, i := range ifaces { + // interface flags seem to always be 0 on Windows + if runtime.GOOS != "windows" && (i.Flags&net.FlagLoopback != 0 || i.Flags&net.FlagUp == 0 || i.Flags&net.FlagMulticast == 0) { + continue + } + addrs, _ := anet.InterfaceAddrsByInterface(&i) + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !ip.IsLoopback() && ip.To4() != nil { + list = append(list, ip.String()) + } + } + } + if len(list) > 0 { + sort.Strings(list) + return ret + " " + list[0] + } + } + return ret + ": " + userName + "@" + host +} diff --git a/server/dlna/list.go b/server/dlna/list.go new file mode 100644 index 0000000..466b8df --- /dev/null +++ b/server/dlna/list.go @@ -0,0 +1,288 @@ +package dlna + +import ( + "fmt" + "net/url" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/anacrolix/dms/dlna" + "github.com/anacrolix/dms/upnpav" + + "server/log" + mt "server/mimetype" + "server/settings" + "server/torr" + "server/torr/state" +) + +func getRoot() (ret []interface{}) { + // Torrents Object (ROOT) + tObj := upnpav.Object{ + ID: "%2FTR", + ParentID: "0", + Restricted: 1, + Title: "Torrents", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + + // add Torrents Object + vol := len(torr.ListTorrent()) + cnt := upnpav.Container{Object: tObj, ChildCount: vol} + ret = append(ret, cnt) + + return +} + +func getTorrents() (ret []interface{}) { + torrs := torr.ListTorrent() + // sort by title as in cds SortCaps + sort.Slice(torrs, func(i, j int) bool { + return torrs[i].Title < torrs[j].Title + }) + + vol := 0 + for _, t := range torrs { + vol++ + obj := upnpav.Object{ + ID: "%2F" + t.TorrentSpec.InfoHash.HexString(), + ParentID: "%2FTR", + Restricted: 1, + Title: strings.ReplaceAll(t.Title, "/", "|"), + Class: "object.container.storageFolder", + Icon: t.Poster, + AlbumArtURI: t.Poster, + Date: upnpav.Timestamp{Time: time.Unix(t.Timestamp, 0)}, // time.Now() + } + cnt := upnpav.Container{Object: obj, ChildCount: 1} + ret = append(ret, cnt) + } + if vol == 0 { + obj := upnpav.Object{ + ID: "%2FNT", + ParentID: "%2FTR", + Restricted: 1, + Title: "No Torrents", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 0} + ret = append(ret, cnt) + } + return +} + +func getTorrent(path, host string) (ret []interface{}) { + // find torrent without load + torrs := torr.ListTorrent() + var torr *torr.Torrent + for _, t := range torrs { + if strings.Contains(path, t.TorrentSpec.InfoHash.HexString()) { + torr = t + break + } + } + if torr == nil { + return nil + } + + // get content from torrent + parent := "%2F" + torr.TorrentSpec.InfoHash.HexString() + // if torrent not loaded, get button for load + if torr.Files() == nil { + obj := upnpav.Object{ + ID: parent + "%2FLD", + ParentID: parent, + Restricted: 1, + Title: "Load Torrent", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 1} + ret = append(ret, cnt) + return + } + + ret = loadTorrent(path, host) + return +} + +func getTorrentMeta(path, host string) (ret interface{}) { + // Meta object + if path == "/" { + // root object meta + rootObj := upnpav.Object{ + ID: "0", + ParentID: "-1", + Restricted: 1, + Searchable: 1, + Title: "TorrServer", + Date: upnpav.Timestamp{Time: time.Now()}, + Class: "object.container.storageFolder", + } + meta := upnpav.Container{Object: rootObj, ChildCount: 1} + return meta + } else if filepath.Base(path) == "TR" { + // TR Object Meta + trObj := upnpav.Object{ + ID: "%2FTR", + ParentID: "0", + Restricted: 1, + Searchable: 1, + Title: "Torrents", + Date: upnpav.Timestamp{Time: time.Now()}, + Class: "object.container.storageFolder", + } + torrs := torr.ListTorrent() + vol := len(torrs) + meta := upnpav.Container{Object: trObj, ChildCount: vol} + return meta + } else if isHashPath(path) { + // find torrent without load + torrs := torr.ListTorrent() + var torr *torr.Torrent + for _, t := range torrs { + if strings.Contains(path, t.TorrentSpec.InfoHash.HexString()) { + torr = t + break + } + } + if torr == nil { + return nil + } + // hash object meta + obj := upnpav.Object{ + ID: "%2F" + torr.TorrentSpec.InfoHash.HexString(), + ParentID: "%2FTR", + Restricted: 1, + Title: torr.Title, + Date: upnpav.Timestamp{Time: time.Unix(torr.Timestamp, 0)}, // time.Now() + } + meta := upnpav.Container{Object: obj, ChildCount: 1} + return meta + } else if filepath.Base(path) == "LD" { + parent := url.PathEscape(filepath.Dir(path)) + // LD object meta + obj := upnpav.Object{ + ID: parent + "%2FLD", + ParentID: parent, + Restricted: 1, + Searchable: 1, + Title: "Load Torrents", + Date: upnpav.Timestamp{Time: time.Now()}, + } + meta := upnpav.Container{Object: obj, ChildCount: 1} + return meta + } else { + file := filepath.Base(path) + id := url.PathEscape(path) + parent := url.PathEscape(filepath.Dir(path)) + // file object meta + obj := upnpav.Object{ + ID: id, + ParentID: parent, + Restricted: 1, + Searchable: 1, + Title: file, + Date: upnpav.Timestamp{Time: time.Now()}, + } + meta := upnpav.Container{Object: obj, ChildCount: 1} + return meta + } +} + +func loadTorrent(path, host string) (ret []interface{}) { + hash := filepath.Base(filepath.Dir(path)) + if hash == "/" || hash == "\\" { + hash = filepath.Base(path) + } + if len(hash) != 40 { + return + } + + tor := torr.GetTorrent(hash) + if tor == nil { + log.TLogln("Dlna error get info from torrent", hash) + return + } + if len(tor.Files()) == 0 { + time.Sleep(time.Millisecond * 200) + timeout := time.Now().Add(time.Second * 60) + for { + tor = torr.GetTorrent(hash) + if len(tor.Files()) > 0 { + break + } + time.Sleep(time.Millisecond * 200) + if time.Now().After(timeout) { + return + } + } + } + parent := "%2F" + tor.TorrentSpec.InfoHash.HexString() + files := tor.Status().FileStats + for _, f := range files { + obj := getObjFromTorrent(path, parent, host, tor, f) + if obj != nil { + ret = append(ret, obj) + } + } + return +} + +func getLink(host, path string) string { + if !strings.HasPrefix(host, "http") { + host = "http://" + host + } + pos := strings.LastIndex(host, ":") + if pos > 7 { + host = host[:pos] + } + return host + ":" + settings.Port + "/" + path +} + +func getObjFromTorrent(path, parent, host string, torr *torr.Torrent, file *state.TorrentFileStat) (ret interface{}) { + mime, err := mt.MimeTypeByPath(file.Path) + if err != nil { + if settings.BTsets.EnableDebug { + log.TLogln("Can't detect mime type", err) + } + return + } + // TODO: handle subtitles for media + if !mime.IsMedia() { + return + } + if settings.BTsets.EnableDebug { + log.TLogln("mime type", mime.String(), file.Path) + } + + obj := upnpav.Object{ + ID: parent + "%2F" + url.PathEscape(file.Path), + ParentID: parent, + Restricted: 1, + Title: file.Path, + Class: "object.item." + mime.Type() + "Item", + Date: upnpav.Timestamp{Time: time.Now()}, + } + + item := upnpav.Item{ + Object: obj, + Res: make([]upnpav.Resource, 0, 1), + } + // pathPlay := "stream/" + url.PathEscape(file.Path) + "?link=" + torr.TorrentSpec.InfoHash.HexString() + "&play&index=" + strconv.Itoa(file.Id) + pathPlay := "play/" + torr.TorrentSpec.InfoHash.HexString() + "/" + strconv.Itoa(file.Id) + item.Res = append(item.Res, upnpav.Resource{ + URL: getLink(host, pathPlay), + ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mime, dlna.ContentFeatures{ + SupportRange: true, + SupportTimeSeek: true, + }.String()), + Size: uint64(file.Length), + }) + return item +} diff --git a/server/dlna/utils.go b/server/dlna/utils.go new file mode 100644 index 0000000..decf282 --- /dev/null +++ b/server/dlna/utils.go @@ -0,0 +1,19 @@ +package dlna + +import ( + "path/filepath" +) + +func isHashPath(path string) bool { + base := filepath.Base(path) + if len(base) == 40 { + data := []byte(base) + for _, v := range data { + if !(v >= 48 && v <= 57 || v >= 65 && v <= 70 || v >= 97 && v <= 102) { + return false + } + } + return true + } + return false +} diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 0000000..b864807 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,1316 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "GPL 3.0" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/cache": { + "post": { + "description": "Return cache stats.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Return cache stats", + "parameters": [ + { + "description": "Cache stats request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.cacheReqJS" + } + } + ], + "responses": { + "200": { + "description": "Cache stats", + "schema": { + "$ref": "#/definitions/state.CacheState" + } + } + } + } + }, + "/download/{size}": { + "get": { + "description": "Download the test file of given size (for speed testing purpose).", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Generates test file of given size", + "parameters": [ + { + "type": "string", + "description": "Test file size (in MB)", + "name": "size", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/echo": { + "get": { + "description": "Tests whether server is alive or not", + "produces": [ + "text/plain" + ], + "tags": [ + "API" + ], + "summary": "Tests server status", + "responses": { + "200": { + "description": "Server version", + "schema": { + "type": "string" + } + } + } + } + }, + "/ffp/{hash}/{id}": { + "get": { + "description": "Gather informations using ffprobe.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Gather informations using ffprobe", + "parameters": [ + { + "type": "string", + "description": "Torrent hash", + "name": "hash", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Data returned from ffprobe" + } + } + } + }, + "/magnets": { + "get": { + "description": "Get HTML of magnet links.", + "produces": [ + "text/html" + ], + "tags": [ + "Pages" + ], + "summary": "Get HTML of magnet links", + "responses": { + "200": { + "description": "HTML with Magnet links" + } + } + } + }, + "/play/{hash}/{id}": { + "get": { + "description": "Play given torrent referenced by infohash and file id.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Play given torrent by infohash", + "parameters": [ + { + "type": "string", + "description": "Torrent infohash", + "name": "hash", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Torrent data" + } + } + } + }, + "/playlist": { + "get": { + "description": "Get HTTP link of torrent in M3U list.", + "produces": [ + "audio/x-mpegurl" + ], + "tags": [ + "API" + ], + "summary": "Get HTTP link of torrent in M3U list", + "parameters": [ + { + "type": "string", + "description": "Torrent hash", + "name": "hash", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "From last play file", + "name": "fromlast", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/playlistall/all.m3u": { + "get": { + "description": "Retrieve all torrents and generates a bundled M3U playlist.", + "produces": [ + "audio/x-mpegurl" + ], + "tags": [ + "API" + ], + "summary": "Get a M3U playlist with all torrents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/search": { + "get": { + "description": "Makes a rutor search.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Makes a rutor search", + "parameters": [ + { + "type": "string", + "description": "Rutor query", + "name": "query", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Rutor torrent search result(s)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TorrentDetails" + } + } + } + } + } + }, + "/settings": { + "post": { + "description": "Allow to get or set server settings.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get / Set server settings", + "parameters": [ + { + "description": "Settings request. Available params for action: get, set, def", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.setsReqJS" + } + } + ], + "responses": { + "200": { + "description": "Settings JSON or nothing. Depends on what action has been asked.", + "schema": { + "$ref": "#/definitions/settings.BTSets" + } + } + } + } + }, + "/shutdown": { + "get": { + "description": "Gracefully shuts down server after 1 second.", + "tags": [ + "API" + ], + "summary": "Shuts down server", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/stat": { + "get": { + "description": "Show server and torrents statistics.", + "produces": [ + "text/plain" + ], + "tags": [ + "Pages" + ], + "summary": "TorrServer Statistics", + "responses": { + "200": { + "description": "TorrServer statistics" + } + } + } + }, + "/storage/settings": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieves the current storage preferences for settings and viewed history", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get storage configuration settings", + "responses": { + "200": { + "description": "Storage preferences", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the storage preferences for settings and viewed history. Requires application restart for changes to take effect.", + "consumes": [ + "application/json", + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Update storage configuration settings", + "parameters": [ + { + "description": "Storage preferences to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + }, + { + "enum": [ + "json", + "bbolt" + ], + "type": "string", + "description": "Settings storage type", + "name": "settings", + "in": "formData" + }, + { + "enum": [ + "json", + "bbolt" + ], + "type": "string", + "description": "Viewed history storage type", + "name": "viewed", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "Update successful", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid input data", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Read-only mode", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/stream": { + "get": { + "description": "Multi usage endpoint.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Multi usage endpoint", + "parameters": [ + { + "type": "string", + "description": "Magnet/hash/link to torrent", + "name": "link", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "index", + "in": "query" + }, + { + "type": "string", + "description": "Should preload torrent", + "name": "preload", + "in": "query" + }, + { + "type": "string", + "description": "Get statistics from torrent", + "name": "stat", + "in": "query" + }, + { + "type": "string", + "description": "Should save torrent", + "name": "save", + "in": "query" + }, + { + "type": "string", + "description": "Get torrent as M3U playlist", + "name": "m3u", + "in": "query" + }, + { + "type": "string", + "description": "Get M3U from last played file", + "name": "fromlast", + "in": "query" + }, + { + "type": "string", + "description": "Start stream torrent", + "name": "play", + "in": "query" + }, + { + "type": "string", + "description": "Set title of torrent", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "Set poster link of torrent", + "name": "poster", + "in": "query" + }, + { + "type": "string", + "description": "Set category of torrent, used in web: movie, tv, music, other", + "name": "category", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Data returned according to query" + } + } + } + }, + "/tmdb/settings": { + "get": { + "description": "Get TMDB API configuration", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get TMDB settings", + "responses": { + "200": { + "description": "TMDB settings", + "schema": { + "$ref": "#/definitions/settings.TMDBConfig" + } + } + } + } + }, + "/torrent/upload": { + "post": { + "description": "Supports multiple files. Returns array of statuses.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Add .torrent files", + "parameters": [ + { + "type": "file", + "description": "Torrent file(s) to insert", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Save to DB", + "name": "save", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent title (single file only)", + "name": "title", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent category", + "name": "category", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent poster (single file only)", + "name": "poster", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent data", + "name": "data", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "Torrent statuses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/state.TorrentStatus" + } + } + } + } + } + }, + "/torrents": { + "post": { + "description": "Allow to list, add, remove, get, set, drop, wipe torrents on server. The action depends of what has been asked.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Handle torrents informations", + "parameters": [ + { + "description": "Torrent request. Available params for action: add, get, set, rem, list, drop, wipe. link required for add, hash required for get, set, rem, drop.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.torrReqJS" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/torznab/search": { + "get": { + "description": "Makes a torznab search.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Makes a torznab search", + "parameters": [ + { + "type": "string", + "description": "Torznab query", + "name": "query", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Torznab torrent search result(s)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TorrentDetails" + } + } + } + } + } + }, + "/viewed": { + "post": { + "description": "Allow to set, list or remove viewed torrents from server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Set / List / Remove viewed torrents", + "parameters": [ + { + "description": "Viewed torrent request. Available params for action: set, rem, list", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.viewedReqJS" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/settings.Viewed" + } + } + } + } + } + } + }, + "definitions": { + "api.cacheReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "hash": { + "type": "string" + } + } + }, + "api.setsReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "sets": { + "$ref": "#/definitions/settings.BTSets" + } + } + }, + "api.torrReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "category": { + "type": "string" + }, + "data": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "link": { + "type": "string" + }, + "poster": { + "type": "string" + }, + "save_to_db": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "api.viewedReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "file_index": { + "type": "integer" + }, + "hash": { + "type": "string" + } + } + }, + "models.TorrentDetails": { + "type": "object", + "properties": { + "audioQuality": { + "type": "integer" + }, + "categories": { + "type": "string" + }, + "createDate": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "imdbid": { + "type": "string" + }, + "link": { + "type": "string" + }, + "magnet": { + "type": "string" + }, + "name": { + "type": "string" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + }, + "peer": { + "type": "integer" + }, + "seed": { + "type": "integer" + }, + "size": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tracker": { + "type": "string" + }, + "videoQuality": { + "type": "integer" + }, + "year": { + "type": "integer" + } + } + }, + "settings.BTSets": { + "type": "object", + "properties": { + "cacheSize": { + "description": "Cache", + "type": "integer", + "format": "int64" + }, + "connectionsLimit": { + "type": "integer" + }, + "disableDHT": { + "type": "boolean" + }, + "disablePEX": { + "type": "boolean" + }, + "disableTCP": { + "type": "boolean" + }, + "disableUPNP": { + "type": "boolean" + }, + "disableUTP": { + "type": "boolean" + }, + "disableUpload": { + "type": "boolean" + }, + "downloadRateLimit": { + "description": "in kb, 0 - inf", + "type": "integer" + }, + "enableDLNA": { + "description": "DLNA", + "type": "boolean" + }, + "enableDebug": { + "description": "debug logs", + "type": "boolean" + }, + "enableIPv6": { + "description": "BT Config", + "type": "boolean" + }, + "enableProxy": { + "description": "P2P Proxy", + "type": "boolean" + }, + "enableRutorSearch": { + "description": "Rutor", + "type": "boolean" + }, + "enableTorznabSearch": { + "description": "Torznab", + "type": "boolean" + }, + "forceEncrypt": { + "description": "Torrent", + "type": "boolean" + }, + "friendlyName": { + "type": "string" + }, + "peersListenPort": { + "type": "integer" + }, + "preloadCache": { + "description": "in percent", + "type": "integer" + }, + "proxyHosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "readerReadAHead": { + "description": "in percent, 5%-100%, [...S__X__E...] [S-E] not clean", + "type": "integer" + }, + "removeCacheOnDrop": { + "type": "boolean" + }, + "responsiveMode": { + "description": "Reader", + "type": "boolean" + }, + "retrackersMode": { + "description": "0 - don` + "`" + `t add, 1 - add retrackers (def), 2 - remove retrackers 3 - replace retrackers", + "type": "integer" + }, + "showFSActiveTorr": { + "description": "FS", + "type": "boolean" + }, + "sslCert": { + "type": "string" + }, + "sslKey": { + "type": "string" + }, + "sslPort": { + "description": "HTTPS", + "type": "integer" + }, + "storeSettingsInJson": { + "description": "Storage preferences", + "type": "boolean" + }, + "storeViewedInJson": { + "type": "boolean" + }, + "tmdbsettings": { + "description": "TMDB", + "allOf": [ + { + "$ref": "#/definitions/settings.TMDBConfig" + } + ] + }, + "torrentDisconnectTimeout": { + "description": "in seconds", + "type": "integer" + }, + "torrentsSavePath": { + "type": "string" + }, + "torznabUrls": { + "type": "array", + "items": { + "$ref": "#/definitions/settings.TorznabConfig" + } + }, + "uploadRateLimit": { + "description": "in kb, 0 - inf", + "type": "integer" + }, + "useDisk": { + "description": "Disk", + "type": "boolean" + } + } + }, + "settings.TMDBConfig": { + "type": "object", + "properties": { + "apikey": { + "description": "TMDB API Key", + "type": "string" + }, + "apiurl": { + "description": "Base API URL (default: https://api.themoviedb.org)", + "type": "string" + }, + "imageURL": { + "description": "Image URL (default: https://image.tmdb.org)", + "type": "string" + }, + "imageURLRu": { + "description": "Image URL for Russian users (default: https://imagetmdb.com)", + "type": "string" + } + } + }, + "settings.TorznabConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "settings.Viewed": { + "type": "object", + "properties": { + "file_index": { + "type": "integer" + }, + "hash": { + "type": "string" + } + } + }, + "state.CacheState": { + "type": "object", + "properties": { + "capacity": { + "type": "integer", + "format": "int64" + }, + "filled": { + "type": "integer", + "format": "int64" + }, + "hash": { + "type": "string" + }, + "pieces": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/state.ItemState" + } + }, + "piecesCount": { + "type": "integer" + }, + "piecesLength": { + "type": "integer", + "format": "int64" + }, + "readers": { + "type": "array", + "items": { + "$ref": "#/definitions/state.ReaderState" + } + }, + "torrent": { + "$ref": "#/definitions/state.TorrentStatus" + } + } + }, + "state.ItemState": { + "type": "object", + "properties": { + "completed": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "length": { + "type": "integer", + "format": "int64" + }, + "priority": { + "type": "integer" + }, + "size": { + "type": "integer", + "format": "int64" + } + } + }, + "state.ReaderState": { + "type": "object", + "properties": { + "end": { + "type": "integer" + }, + "reader": { + "type": "integer" + }, + "start": { + "type": "integer" + } + } + }, + "state.TorrentFileStat": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "path": { + "type": "string" + } + } + }, + "state.TorrentStat": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "TorrentAdded", + "TorrentGettingInfo", + "TorrentPreload", + "TorrentWorking", + "TorrentClosed", + "TorrentInDB" + ] + }, + "state.TorrentStatus": { + "type": "object", + "properties": { + "active_peers": { + "type": "integer" + }, + "bit_rate": { + "type": "string" + }, + "bytes_read": { + "type": "integer" + }, + "bytes_read_data": { + "type": "integer" + }, + "bytes_read_useful_data": { + "type": "integer" + }, + "bytes_written": { + "type": "integer" + }, + "bytes_written_data": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "chunks_read": { + "type": "integer" + }, + "chunks_read_useful": { + "type": "integer" + }, + "chunks_read_wasted": { + "type": "integer" + }, + "chunks_written": { + "type": "integer" + }, + "connected_seeders": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "download_speed": { + "type": "number" + }, + "duration_seconds": { + "type": "number" + }, + "file_stats": { + "type": "array", + "items": { + "$ref": "#/definitions/state.TorrentFileStat" + } + }, + "half_open_peers": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "loaded_size": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "pending_peers": { + "type": "integer" + }, + "pieces_dirtied_bad": { + "type": "integer" + }, + "pieces_dirtied_good": { + "type": "integer" + }, + "poster": { + "type": "string" + }, + "preload_size": { + "type": "integer" + }, + "preloaded_bytes": { + "type": "integer" + }, + "stat": { + "$ref": "#/definitions/state.TorrentStat" + }, + "stat_string": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "torrent_size": { + "type": "integer" + }, + "torrs_hash": { + "type": "string" + }, + "total_peers": { + "type": "integer" + }, + "upload_speed": { + "type": "number" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "{version.Version}", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "Swagger Torrserver API", + Description: "Torrent streaming server.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 0000000..77930a7 --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,1291 @@ +{ + "swagger": "2.0", + "info": { + "description": "Torrent streaming server.", + "title": "Swagger Torrserver API", + "contact": {}, + "license": { + "name": "GPL 3.0" + }, + "version": "{version.Version}" + }, + "basePath": "/", + "paths": { + "/cache": { + "post": { + "description": "Return cache stats.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Return cache stats", + "parameters": [ + { + "description": "Cache stats request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.cacheReqJS" + } + } + ], + "responses": { + "200": { + "description": "Cache stats", + "schema": { + "$ref": "#/definitions/state.CacheState" + } + } + } + } + }, + "/download/{size}": { + "get": { + "description": "Download the test file of given size (for speed testing purpose).", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Generates test file of given size", + "parameters": [ + { + "type": "string", + "description": "Test file size (in MB)", + "name": "size", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/echo": { + "get": { + "description": "Tests whether server is alive or not", + "produces": [ + "text/plain" + ], + "tags": [ + "API" + ], + "summary": "Tests server status", + "responses": { + "200": { + "description": "Server version", + "schema": { + "type": "string" + } + } + } + } + }, + "/ffp/{hash}/{id}": { + "get": { + "description": "Gather informations using ffprobe.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Gather informations using ffprobe", + "parameters": [ + { + "type": "string", + "description": "Torrent hash", + "name": "hash", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Data returned from ffprobe" + } + } + } + }, + "/magnets": { + "get": { + "description": "Get HTML of magnet links.", + "produces": [ + "text/html" + ], + "tags": [ + "Pages" + ], + "summary": "Get HTML of magnet links", + "responses": { + "200": { + "description": "HTML with Magnet links" + } + } + } + }, + "/play/{hash}/{id}": { + "get": { + "description": "Play given torrent referenced by infohash and file id.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Play given torrent by infohash", + "parameters": [ + { + "type": "string", + "description": "Torrent infohash", + "name": "hash", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Torrent data" + } + } + } + }, + "/playlist": { + "get": { + "description": "Get HTTP link of torrent in M3U list.", + "produces": [ + "audio/x-mpegurl" + ], + "tags": [ + "API" + ], + "summary": "Get HTTP link of torrent in M3U list", + "parameters": [ + { + "type": "string", + "description": "Torrent hash", + "name": "hash", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "From last play file", + "name": "fromlast", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/playlistall/all.m3u": { + "get": { + "description": "Retrieve all torrents and generates a bundled M3U playlist.", + "produces": [ + "audio/x-mpegurl" + ], + "tags": [ + "API" + ], + "summary": "Get a M3U playlist with all torrents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + } + } + }, + "/search": { + "get": { + "description": "Makes a rutor search.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Makes a rutor search", + "parameters": [ + { + "type": "string", + "description": "Rutor query", + "name": "query", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Rutor torrent search result(s)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TorrentDetails" + } + } + } + } + } + }, + "/settings": { + "post": { + "description": "Allow to get or set server settings.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get / Set server settings", + "parameters": [ + { + "description": "Settings request. Available params for action: get, set, def", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.setsReqJS" + } + } + ], + "responses": { + "200": { + "description": "Settings JSON or nothing. Depends on what action has been asked.", + "schema": { + "$ref": "#/definitions/settings.BTSets" + } + } + } + } + }, + "/shutdown": { + "get": { + "description": "Gracefully shuts down server after 1 second.", + "tags": [ + "API" + ], + "summary": "Shuts down server", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/stat": { + "get": { + "description": "Show server and torrents statistics.", + "produces": [ + "text/plain" + ], + "tags": [ + "Pages" + ], + "summary": "TorrServer Statistics", + "responses": { + "200": { + "description": "TorrServer statistics" + } + } + } + }, + "/storage/settings": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieves the current storage preferences for settings and viewed history", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get storage configuration settings", + "responses": { + "200": { + "description": "Storage preferences", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the storage preferences for settings and viewed history. Requires application restart for changes to take effect.", + "consumes": [ + "application/json", + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Update storage configuration settings", + "parameters": [ + { + "description": "Storage preferences to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + }, + { + "enum": [ + "json", + "bbolt" + ], + "type": "string", + "description": "Settings storage type", + "name": "settings", + "in": "formData" + }, + { + "enum": [ + "json", + "bbolt" + ], + "type": "string", + "description": "Viewed history storage type", + "name": "viewed", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "Update successful", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid input data", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Read-only mode", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/stream": { + "get": { + "description": "Multi usage endpoint.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "API" + ], + "summary": "Multi usage endpoint", + "parameters": [ + { + "type": "string", + "description": "Magnet/hash/link to torrent", + "name": "link", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "File index in torrent", + "name": "index", + "in": "query" + }, + { + "type": "string", + "description": "Should preload torrent", + "name": "preload", + "in": "query" + }, + { + "type": "string", + "description": "Get statistics from torrent", + "name": "stat", + "in": "query" + }, + { + "type": "string", + "description": "Should save torrent", + "name": "save", + "in": "query" + }, + { + "type": "string", + "description": "Get torrent as M3U playlist", + "name": "m3u", + "in": "query" + }, + { + "type": "string", + "description": "Get M3U from last played file", + "name": "fromlast", + "in": "query" + }, + { + "type": "string", + "description": "Start stream torrent", + "name": "play", + "in": "query" + }, + { + "type": "string", + "description": "Set title of torrent", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "Set poster link of torrent", + "name": "poster", + "in": "query" + }, + { + "type": "string", + "description": "Set category of torrent, used in web: movie, tv, music, other", + "name": "category", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Data returned according to query" + } + } + } + }, + "/tmdb/settings": { + "get": { + "description": "Get TMDB API configuration", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Get TMDB settings", + "responses": { + "200": { + "description": "TMDB settings", + "schema": { + "$ref": "#/definitions/settings.TMDBConfig" + } + } + } + } + }, + "/torrent/upload": { + "post": { + "description": "Supports multiple files. Returns array of statuses.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Add .torrent files", + "parameters": [ + { + "type": "file", + "description": "Torrent file(s) to insert", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Save to DB", + "name": "save", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent title (single file only)", + "name": "title", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent category", + "name": "category", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent poster (single file only)", + "name": "poster", + "in": "formData" + }, + { + "type": "string", + "description": "Torrent data", + "name": "data", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "Torrent statuses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/state.TorrentStatus" + } + } + } + } + } + }, + "/torrents": { + "post": { + "description": "Allow to list, add, remove, get, set, drop, wipe torrents on server. The action depends of what has been asked.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Handle torrents informations", + "parameters": [ + { + "description": "Torrent request. Available params for action: add, get, set, rem, list, drop, wipe. link required for add, hash required for get, set, rem, drop.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.torrReqJS" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/torznab/search": { + "get": { + "description": "Makes a torznab search.", + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Makes a torznab search", + "parameters": [ + { + "type": "string", + "description": "Torznab query", + "name": "query", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Torznab torrent search result(s)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TorrentDetails" + } + } + } + } + } + }, + "/viewed": { + "post": { + "description": "Allow to set, list or remove viewed torrents from server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API" + ], + "summary": "Set / List / Remove viewed torrents", + "parameters": [ + { + "description": "Viewed torrent request. Available params for action: set, rem, list", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.viewedReqJS" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/settings.Viewed" + } + } + } + } + } + } + }, + "definitions": { + "api.cacheReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "hash": { + "type": "string" + } + } + }, + "api.setsReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "sets": { + "$ref": "#/definitions/settings.BTSets" + } + } + }, + "api.torrReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "category": { + "type": "string" + }, + "data": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "link": { + "type": "string" + }, + "poster": { + "type": "string" + }, + "save_to_db": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "api.viewedReqJS": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "file_index": { + "type": "integer" + }, + "hash": { + "type": "string" + } + } + }, + "models.TorrentDetails": { + "type": "object", + "properties": { + "audioQuality": { + "type": "integer" + }, + "categories": { + "type": "string" + }, + "createDate": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "imdbid": { + "type": "string" + }, + "link": { + "type": "string" + }, + "magnet": { + "type": "string" + }, + "name": { + "type": "string" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + }, + "peer": { + "type": "integer" + }, + "seed": { + "type": "integer" + }, + "size": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tracker": { + "type": "string" + }, + "videoQuality": { + "type": "integer" + }, + "year": { + "type": "integer" + } + } + }, + "settings.BTSets": { + "type": "object", + "properties": { + "cacheSize": { + "description": "Cache", + "type": "integer", + "format": "int64" + }, + "connectionsLimit": { + "type": "integer" + }, + "disableDHT": { + "type": "boolean" + }, + "disablePEX": { + "type": "boolean" + }, + "disableTCP": { + "type": "boolean" + }, + "disableUPNP": { + "type": "boolean" + }, + "disableUTP": { + "type": "boolean" + }, + "disableUpload": { + "type": "boolean" + }, + "downloadRateLimit": { + "description": "in kb, 0 - inf", + "type": "integer" + }, + "enableDLNA": { + "description": "DLNA", + "type": "boolean" + }, + "enableDebug": { + "description": "debug logs", + "type": "boolean" + }, + "enableIPv6": { + "description": "BT Config", + "type": "boolean" + }, + "enableProxy": { + "description": "P2P Proxy", + "type": "boolean" + }, + "enableRutorSearch": { + "description": "Rutor", + "type": "boolean" + }, + "enableTorznabSearch": { + "description": "Torznab", + "type": "boolean" + }, + "forceEncrypt": { + "description": "Torrent", + "type": "boolean" + }, + "friendlyName": { + "type": "string" + }, + "peersListenPort": { + "type": "integer" + }, + "preloadCache": { + "description": "in percent", + "type": "integer" + }, + "proxyHosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "readerReadAHead": { + "description": "in percent, 5%-100%, [...S__X__E...] [S-E] not clean", + "type": "integer" + }, + "removeCacheOnDrop": { + "type": "boolean" + }, + "responsiveMode": { + "description": "Reader", + "type": "boolean" + }, + "retrackersMode": { + "description": "0 - don`t add, 1 - add retrackers (def), 2 - remove retrackers 3 - replace retrackers", + "type": "integer" + }, + "showFSActiveTorr": { + "description": "FS", + "type": "boolean" + }, + "sslCert": { + "type": "string" + }, + "sslKey": { + "type": "string" + }, + "sslPort": { + "description": "HTTPS", + "type": "integer" + }, + "storeSettingsInJson": { + "description": "Storage preferences", + "type": "boolean" + }, + "storeViewedInJson": { + "type": "boolean" + }, + "tmdbsettings": { + "description": "TMDB", + "allOf": [ + { + "$ref": "#/definitions/settings.TMDBConfig" + } + ] + }, + "torrentDisconnectTimeout": { + "description": "in seconds", + "type": "integer" + }, + "torrentsSavePath": { + "type": "string" + }, + "torznabUrls": { + "type": "array", + "items": { + "$ref": "#/definitions/settings.TorznabConfig" + } + }, + "uploadRateLimit": { + "description": "in kb, 0 - inf", + "type": "integer" + }, + "useDisk": { + "description": "Disk", + "type": "boolean" + } + } + }, + "settings.TMDBConfig": { + "type": "object", + "properties": { + "apikey": { + "description": "TMDB API Key", + "type": "string" + }, + "apiurl": { + "description": "Base API URL (default: https://api.themoviedb.org)", + "type": "string" + }, + "imageURL": { + "description": "Image URL (default: https://image.tmdb.org)", + "type": "string" + }, + "imageURLRu": { + "description": "Image URL for Russian users (default: https://imagetmdb.com)", + "type": "string" + } + } + }, + "settings.TorznabConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "settings.Viewed": { + "type": "object", + "properties": { + "file_index": { + "type": "integer" + }, + "hash": { + "type": "string" + } + } + }, + "state.CacheState": { + "type": "object", + "properties": { + "capacity": { + "type": "integer", + "format": "int64" + }, + "filled": { + "type": "integer", + "format": "int64" + }, + "hash": { + "type": "string" + }, + "pieces": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/state.ItemState" + } + }, + "piecesCount": { + "type": "integer" + }, + "piecesLength": { + "type": "integer", + "format": "int64" + }, + "readers": { + "type": "array", + "items": { + "$ref": "#/definitions/state.ReaderState" + } + }, + "torrent": { + "$ref": "#/definitions/state.TorrentStatus" + } + } + }, + "state.ItemState": { + "type": "object", + "properties": { + "completed": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "length": { + "type": "integer", + "format": "int64" + }, + "priority": { + "type": "integer" + }, + "size": { + "type": "integer", + "format": "int64" + } + } + }, + "state.ReaderState": { + "type": "object", + "properties": { + "end": { + "type": "integer" + }, + "reader": { + "type": "integer" + }, + "start": { + "type": "integer" + } + } + }, + "state.TorrentFileStat": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "path": { + "type": "string" + } + } + }, + "state.TorrentStat": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "TorrentAdded", + "TorrentGettingInfo", + "TorrentPreload", + "TorrentWorking", + "TorrentClosed", + "TorrentInDB" + ] + }, + "state.TorrentStatus": { + "type": "object", + "properties": { + "active_peers": { + "type": "integer" + }, + "bit_rate": { + "type": "string" + }, + "bytes_read": { + "type": "integer" + }, + "bytes_read_data": { + "type": "integer" + }, + "bytes_read_useful_data": { + "type": "integer" + }, + "bytes_written": { + "type": "integer" + }, + "bytes_written_data": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "chunks_read": { + "type": "integer" + }, + "chunks_read_useful": { + "type": "integer" + }, + "chunks_read_wasted": { + "type": "integer" + }, + "chunks_written": { + "type": "integer" + }, + "connected_seeders": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "download_speed": { + "type": "number" + }, + "duration_seconds": { + "type": "number" + }, + "file_stats": { + "type": "array", + "items": { + "$ref": "#/definitions/state.TorrentFileStat" + } + }, + "half_open_peers": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "loaded_size": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "pending_peers": { + "type": "integer" + }, + "pieces_dirtied_bad": { + "type": "integer" + }, + "pieces_dirtied_good": { + "type": "integer" + }, + "poster": { + "type": "string" + }, + "preload_size": { + "type": "integer" + }, + "preloaded_bytes": { + "type": "integer" + }, + "stat": { + "$ref": "#/definitions/state.TorrentStat" + }, + "stat_string": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "torrent_size": { + "type": "integer" + }, + "torrs_hash": { + "type": "string" + }, + "total_peers": { + "type": "integer" + }, + "upload_speed": { + "type": "number" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 0000000..2a78d6a --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,866 @@ +basePath: / +definitions: + api.cacheReqJS: + properties: + action: + type: string + hash: + type: string + type: object + api.setsReqJS: + properties: + action: + type: string + sets: + $ref: '#/definitions/settings.BTSets' + type: object + api.torrReqJS: + properties: + action: + type: string + category: + type: string + data: + type: string + filter: + type: string + hash: + type: string + link: + type: string + poster: + type: string + save_to_db: + type: boolean + title: + type: string + type: object + api.viewedReqJS: + properties: + action: + type: string + file_index: + type: integer + hash: + type: string + type: object + models.TorrentDetails: + properties: + audioQuality: + type: integer + categories: + type: string + createDate: + type: string + hash: + type: string + imdbid: + type: string + link: + type: string + magnet: + type: string + name: + type: string + names: + items: + type: string + type: array + peer: + type: integer + seed: + type: integer + size: + type: string + title: + type: string + tracker: + type: string + videoQuality: + type: integer + year: + type: integer + type: object + settings.BTSets: + properties: + cacheSize: + description: Cache + format: int64 + type: integer + connectionsLimit: + type: integer + disableDHT: + type: boolean + disablePEX: + type: boolean + disableTCP: + type: boolean + disableUPNP: + type: boolean + disableUTP: + type: boolean + disableUpload: + type: boolean + downloadRateLimit: + description: in kb, 0 - inf + type: integer + enableDLNA: + description: DLNA + type: boolean + enableDebug: + description: debug logs + type: boolean + enableIPv6: + description: BT Config + type: boolean + enableProxy: + description: P2P Proxy + type: boolean + enableRutorSearch: + description: Rutor + type: boolean + enableTorznabSearch: + description: Torznab + type: boolean + forceEncrypt: + description: Torrent + type: boolean + friendlyName: + type: string + peersListenPort: + type: integer + preloadCache: + description: in percent + type: integer + proxyHosts: + items: + type: string + type: array + readerReadAHead: + description: in percent, 5%-100%, [...S__X__E...] [S-E] not clean + type: integer + removeCacheOnDrop: + type: boolean + responsiveMode: + description: Reader + type: boolean + retrackersMode: + description: 0 - don`t add, 1 - add retrackers (def), 2 - remove retrackers + 3 - replace retrackers + type: integer + showFSActiveTorr: + description: FS + type: boolean + sslCert: + type: string + sslKey: + type: string + sslPort: + description: HTTPS + type: integer + storeSettingsInJson: + description: Storage preferences + type: boolean + storeViewedInJson: + type: boolean + tmdbsettings: + allOf: + - $ref: '#/definitions/settings.TMDBConfig' + description: TMDB + torrentDisconnectTimeout: + description: in seconds + type: integer + torrentsSavePath: + type: string + torznabUrls: + items: + $ref: '#/definitions/settings.TorznabConfig' + type: array + uploadRateLimit: + description: in kb, 0 - inf + type: integer + useDisk: + description: Disk + type: boolean + type: object + settings.TMDBConfig: + properties: + apikey: + description: TMDB API Key + type: string + apiurl: + description: 'Base API URL (default: https://api.themoviedb.org)' + type: string + imageURL: + description: 'Image URL (default: https://image.tmdb.org)' + type: string + imageURLRu: + description: 'Image URL for Russian users (default: https://imagetmdb.com)' + type: string + type: object + settings.TorznabConfig: + properties: + host: + type: string + key: + type: string + name: + type: string + type: object + settings.Viewed: + properties: + file_index: + type: integer + hash: + type: string + type: object + state.CacheState: + properties: + capacity: + format: int64 + type: integer + filled: + format: int64 + type: integer + hash: + type: string + pieces: + additionalProperties: + $ref: '#/definitions/state.ItemState' + type: object + piecesCount: + type: integer + piecesLength: + format: int64 + type: integer + readers: + items: + $ref: '#/definitions/state.ReaderState' + type: array + torrent: + $ref: '#/definitions/state.TorrentStatus' + type: object + state.ItemState: + properties: + completed: + type: boolean + id: + type: integer + length: + format: int64 + type: integer + priority: + type: integer + size: + format: int64 + type: integer + type: object + state.ReaderState: + properties: + end: + type: integer + reader: + type: integer + start: + type: integer + type: object + state.TorrentFileStat: + properties: + id: + type: integer + length: + type: integer + path: + type: string + type: object + state.TorrentStat: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + type: integer + x-enum-varnames: + - TorrentAdded + - TorrentGettingInfo + - TorrentPreload + - TorrentWorking + - TorrentClosed + - TorrentInDB + state.TorrentStatus: + properties: + active_peers: + type: integer + bit_rate: + type: string + bytes_read: + type: integer + bytes_read_data: + type: integer + bytes_read_useful_data: + type: integer + bytes_written: + type: integer + bytes_written_data: + type: integer + category: + type: string + chunks_read: + type: integer + chunks_read_useful: + type: integer + chunks_read_wasted: + type: integer + chunks_written: + type: integer + connected_seeders: + type: integer + data: + type: string + download_speed: + type: number + duration_seconds: + type: number + file_stats: + items: + $ref: '#/definitions/state.TorrentFileStat' + type: array + half_open_peers: + type: integer + hash: + type: string + loaded_size: + type: integer + name: + type: string + pending_peers: + type: integer + pieces_dirtied_bad: + type: integer + pieces_dirtied_good: + type: integer + poster: + type: string + preload_size: + type: integer + preloaded_bytes: + type: integer + stat: + $ref: '#/definitions/state.TorrentStat' + stat_string: + type: string + timestamp: + type: integer + title: + type: string + torrent_size: + type: integer + torrs_hash: + type: string + total_peers: + type: integer + upload_speed: + type: number + type: object +externalDocs: + description: OpenAPI + url: https://swagger.io/resources/open-api/ +info: + contact: {} + description: Torrent streaming server. + license: + name: GPL 3.0 + title: Swagger Torrserver API + version: '{version.Version}' +paths: + /cache: + post: + description: Return cache stats. + parameters: + - description: Cache stats request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.cacheReqJS' + produces: + - application/json + responses: + "200": + description: Cache stats + schema: + $ref: '#/definitions/state.CacheState' + summary: Return cache stats + tags: + - API + /download/{size}: + get: + description: Download the test file of given size (for speed testing purpose). + parameters: + - description: Test file size (in MB) + in: path + name: size + required: true + type: string + produces: + - application/octet-stream + responses: + "200": + description: OK + schema: + type: file + summary: Generates test file of given size + tags: + - API + /echo: + get: + description: Tests whether server is alive or not + produces: + - text/plain + responses: + "200": + description: Server version + schema: + type: string + summary: Tests server status + tags: + - API + /ffp/{hash}/{id}: + get: + description: Gather informations using ffprobe. + parameters: + - description: Torrent hash + in: path + name: hash + required: true + type: string + - description: File index in torrent + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Data returned from ffprobe + summary: Gather informations using ffprobe + tags: + - API + /magnets: + get: + description: Get HTML of magnet links. + produces: + - text/html + responses: + "200": + description: HTML with Magnet links + summary: Get HTML of magnet links + tags: + - Pages + /play/{hash}/{id}: + get: + description: Play given torrent referenced by infohash and file id. + parameters: + - description: Torrent infohash + in: path + name: hash + required: true + type: string + - description: File index in torrent + in: path + name: id + required: true + type: string + produces: + - application/octet-stream + responses: + "200": + description: Torrent data + summary: Play given torrent by infohash + tags: + - API + /playlist: + get: + description: Get HTTP link of torrent in M3U list. + parameters: + - description: Torrent hash + in: query + name: hash + required: true + type: string + - description: From last play file + in: query + name: fromlast + type: boolean + produces: + - audio/x-mpegurl + responses: + "200": + description: OK + schema: + type: file + summary: Get HTTP link of torrent in M3U list + tags: + - API + /playlistall/all.m3u: + get: + description: Retrieve all torrents and generates a bundled M3U playlist. + produces: + - audio/x-mpegurl + responses: + "200": + description: OK + schema: + type: file + summary: Get a M3U playlist with all torrents + tags: + - API + /search: + get: + description: Makes a rutor search. + parameters: + - description: Rutor query + in: query + name: query + required: true + type: string + produces: + - application/json + responses: + "200": + description: Rutor torrent search result(s) + schema: + items: + $ref: '#/definitions/models.TorrentDetails' + type: array + summary: Makes a rutor search + tags: + - API + /settings: + post: + consumes: + - application/json + description: Allow to get or set server settings. + parameters: + - description: 'Settings request. Available params for action: get, set, def' + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.setsReqJS' + produces: + - application/json + responses: + "200": + description: Settings JSON or nothing. Depends on what action has been asked. + schema: + $ref: '#/definitions/settings.BTSets' + summary: Get / Set server settings + tags: + - API + /shutdown: + get: + description: Gracefully shuts down server after 1 second. + responses: + "200": + description: OK + summary: Shuts down server + tags: + - API + /stat: + get: + description: Show server and torrents statistics. + produces: + - text/plain + responses: + "200": + description: TorrServer statistics + summary: TorrServer Statistics + tags: + - Pages + /storage/settings: + get: + consumes: + - application/json + description: Retrieves the current storage preferences for settings and viewed + history + produces: + - application/json + responses: + "200": + description: Storage preferences + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Get storage configuration settings + tags: + - API + post: + consumes: + - application/json + - application/x-www-form-urlencoded + description: Updates the storage preferences for settings and viewed history. + Requires application restart for changes to take effect. + parameters: + - description: Storage preferences to update + in: body + name: request + required: true + schema: + additionalProperties: true + type: object + - description: Settings storage type + enum: + - json + - bbolt + in: formData + name: settings + type: string + - description: Viewed history storage type + enum: + - json + - bbolt + in: formData + name: viewed + type: string + produces: + - application/json + responses: + "200": + description: Update successful + schema: + additionalProperties: + type: string + type: object + "400": + description: Invalid input data + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "403": + description: Read-only mode + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Update storage configuration settings + tags: + - API + /stream: + get: + description: Multi usage endpoint. + parameters: + - description: Magnet/hash/link to torrent + in: query + name: link + required: true + type: string + - description: File index in torrent + in: query + name: index + type: string + - description: Should preload torrent + in: query + name: preload + type: string + - description: Get statistics from torrent + in: query + name: stat + type: string + - description: Should save torrent + in: query + name: save + type: string + - description: Get torrent as M3U playlist + in: query + name: m3u + type: string + - description: Get M3U from last played file + in: query + name: fromlast + type: string + - description: Start stream torrent + in: query + name: play + type: string + - description: Set title of torrent + in: query + name: title + type: string + - description: Set poster link of torrent + in: query + name: poster + type: string + - description: 'Set category of torrent, used in web: movie, tv, music, other' + in: query + name: category + type: string + produces: + - application/octet-stream + responses: + "200": + description: Data returned according to query + summary: Multi usage endpoint + tags: + - API + /tmdb/settings: + get: + description: Get TMDB API configuration + produces: + - application/json + responses: + "200": + description: TMDB settings + schema: + $ref: '#/definitions/settings.TMDBConfig' + summary: Get TMDB settings + tags: + - API + /torrent/upload: + post: + consumes: + - multipart/form-data + description: Supports multiple files. Returns array of statuses. + parameters: + - description: Torrent file(s) to insert + in: formData + name: file + required: true + type: file + - description: Save to DB + in: formData + name: save + type: string + - description: Torrent title (single file only) + in: formData + name: title + type: string + - description: Torrent category + in: formData + name: category + type: string + - description: Torrent poster (single file only) + in: formData + name: poster + type: string + - description: Torrent data + in: formData + name: data + type: string + produces: + - application/json + responses: + "200": + description: Torrent statuses + schema: + items: + $ref: '#/definitions/state.TorrentStatus' + type: array + summary: Add .torrent files + tags: + - API + /torrents: + post: + consumes: + - application/json + description: Allow to list, add, remove, get, set, drop, wipe torrents on server. + The action depends of what has been asked. + parameters: + - description: 'Torrent request. Available params for action: add, get, set, + rem, list, drop, wipe. link required for add, hash required for get, set, + rem, drop.' + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.torrReqJS' + produces: + - application/json + responses: + "200": + description: OK + summary: Handle torrents informations + tags: + - API + /torznab/search: + get: + description: Makes a torznab search. + parameters: + - description: Torznab query + in: query + name: query + required: true + type: string + produces: + - application/json + responses: + "200": + description: Torznab torrent search result(s) + schema: + items: + $ref: '#/definitions/models.TorrentDetails' + type: array + summary: Makes a torznab search + tags: + - API + /viewed: + post: + consumes: + - application/json + description: Allow to set, list or remove viewed torrents from server. + parameters: + - description: 'Viewed torrent request. Available params for action: set, rem, + list' + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.viewedReqJS' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/settings.Viewed' + type: array + summary: Set / List / Remove viewed torrents + tags: + - API +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/server/ffprobe/ffprobe.go b/server/ffprobe/ffprobe.go new file mode 100644 index 0000000..72ecdf7 --- /dev/null +++ b/server/ffprobe/ffprobe.go @@ -0,0 +1,52 @@ +package ffprobe + +import ( + "context" + "io" + "os" + "os/exec" + "path/filepath" + "time" + + "gopkg.in/vansante/go-ffprobe.v2" +) + +var binFile = "ffprobe" + +func init() { + path, err := exec.LookPath("ffprobe") + if err == nil { + ffprobe.SetFFProbeBinPath(path) + binFile = path + } else { + // working dir + if _, err := os.Stat("ffprobe"); os.IsNotExist(err) { + ffprobe.SetFFProbeBinPath(filepath.Dir(os.Args[0]) + "/ffprobe") + binFile = filepath.Dir(os.Args[0]) + "/ffprobe" + } + } +} + +func Exists() bool { + _, err := os.Stat(binFile) + return !os.IsNotExist(err) +} + +func ProbeUrl(link string) (*ffprobe.ProbeData, error) { + data, err := ffprobe.ProbeURL(getCtx(), link) + return data, err +} + +func ProbeReader(reader io.Reader) (*ffprobe.ProbeData, error) { + data, err := ffprobe.ProbeReader(getCtx(), reader) + return data, err +} + +func getCtx() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(5 * time.Minute) + cancel() + }() + return ctx +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..e68ecd9 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,206 @@ +module server + +go 1.25 + +replace ( + github.com/anacrolix/torrent v1.59.1 => github.com/tsynik/torrent v1.2.22 + github.com/anacrolix/upnp v0.1.4 => github.com/tsynik/upnp v0.1.5 +) + +require ( + github.com/YouROK/tunsgo v0.0.8 + github.com/agnivade/levenshtein v1.2.1 + github.com/alexflint/go-arg v1.6.0 + github.com/anacrolix/dms v1.7.2 + github.com/anacrolix/log v0.17.0 + github.com/anacrolix/missinggo/v2 v2.10.0 + github.com/anacrolix/publicip v0.3.1 + github.com/anacrolix/torrent v1.59.1 + github.com/dustin/go-humanize v1.0.1 + github.com/gin-contrib/cors v1.7.6 + github.com/gin-contrib/location/v2 v2.0.0 + github.com/gin-gonic/gin v1.11.0 + github.com/hanwen/go-fuse/v2 v2.9.0 + github.com/kljensen/snowball v0.10.0 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/pkg/errors v0.9.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + github.com/wlynxg/anet v0.0.5 + go.etcd.io/bbolt v1.4.3 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 + golang.org/x/image v0.33.0 + golang.org/x/net v0.49.0 + golang.org/x/time v0.14.0 + gopkg.in/telebot.v4 v4.0.0-beta.7 + gopkg.in/vansante/go-ffprobe.v2 v2.2.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/RoaringBitmap/roaring v1.9.4 // indirect + github.com/alecthomas/atomic v0.1.0-alpha2 // indirect + github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/anacrolix/chansync v0.7.0 // indirect + github.com/anacrolix/dht/v2 v2.23.0 // indirect + github.com/anacrolix/envpprof v1.4.0 // indirect + github.com/anacrolix/ffprobe v1.1.0 // indirect + github.com/anacrolix/generics v0.1.0 // indirect + github.com/anacrolix/missinggo v1.3.0 // indirect + github.com/anacrolix/missinggo/perf v1.0.0 // indirect + github.com/anacrolix/multiless v0.4.0 // indirect + github.com/anacrolix/stm v0.5.0 // indirect + github.com/anacrolix/sync v0.5.4 // indirect + github.com/anacrolix/upnp v0.1.4 // indirect + github.com/anacrolix/utp v0.2.0 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/benbjohnson/immutable v0.4.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/edsrzf/mmap-go v1.2.0 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.3 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/spec v0.22.1 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/boxo v0.36.0 // indirect + github.com/ipfs/go-cid v0.6.0 // indirect + github.com/ipfs/go-datastore v0.9.1 // indirect + github.com/ipfs/go-log/v2 v2.9.1 // indirect + github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.3.0 // indirect + github.com/libp2p/go-libp2p v0.47.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.0 // indirect + github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect + github.com/libp2p/go-libp2p-record v0.3.1 // indirect + github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr v0.16.1 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/dtls/v3 v3.1.1 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/polydawn/refmt v0.89.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect + github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect + github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.41.0 // indirect + gonum.org/v1/gonum v0.17.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..8f98647 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,1458 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= +crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= +github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= +github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ= +github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/YouROK/tunsgo v0.0.8 h1:Fo4k2nYzfhRpAHQJRezDaQqp9/EYtIdtYKMwQrvINyo= +github.com/YouROK/tunsgo v0.0.8/go.mod h1:aaOLeXmDgb85h41oq2gwegQMpOfkPzErUDP0lqODYfE= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= +github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo= +github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/anacrolix/chansync v0.7.0 h1:wgwxbsJRmOqNjil4INpxHrDp4rlqQhECxR8/WBP4Et0= +github.com/anacrolix/chansync v0.7.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= +github.com/anacrolix/dht/v2 v2.23.0 h1:EuD17ykTTEkAMPLjBsS5QjGOwuBgLTdQhds6zPAjeVY= +github.com/anacrolix/dht/v2 v2.23.0/go.mod h1:seXRz6HLw8zEnxlysf9ye2eQbrKUmch6PyOHpe/Nb/U= +github.com/anacrolix/dms v1.7.2 h1:JAAJJIlXp+jT2yEah1EbR1AFpGALHL238uSKFXec2qw= +github.com/anacrolix/dms v1.7.2/go.mod h1:excFJW5MKBhn5yt5ZMyeE9iFVqnO6tEGQl7YG/2tUoQ= +github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/envpprof v1.4.0 h1:QHeIcrgHcRChhnxR8l6rlaLlRQx9zd7Q2NII6Zbt83w= +github.com/anacrolix/envpprof v1.4.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0= +github.com/anacrolix/ffprobe v1.1.0 h1:eKBudnERW9zRJ0+ge6FzkQ0pWLyq142+FJrwRwSRMT4= +github.com/anacrolix/ffprobe v1.1.0/go.mod h1:MXe+zG/RRa5OdIf5+VYYfS/CfsSqOH7RrvGIqJBzqhI= +github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/generics v0.1.0 h1:r6OgogjCdml3K5A8ixUG0X9DM4jrQiMfIkZiBOGvIfg= +github.com/anacrolix/generics v0.1.0/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8= +github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= +github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= +github.com/anacrolix/log v0.17.0 h1:cZvEGRPCbIg+WK+qAxWj/ap2Gj8cx1haOCSVxNZQpK4= +github.com/anacrolix/log v0.17.0/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= +github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= +github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= +github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= +github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= +github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= +github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= +github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= +github.com/anacrolix/missinggo/v2 v2.10.0 h1:pg0iO4Z/UhP2MAnmGcaMtp5ZP9kyWsusENWN9aolrkY= +github.com/anacrolix/missinggo/v2 v2.10.0/go.mod h1:nCRMW6bRCMOVcw5z9BnSYKF+kDbtenx+hQuphf4bK8Y= +github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM= +github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM= +github.com/anacrolix/publicip v0.3.1 h1:a8tXUS4L6dG3mBFp4ZhvzmEztEEPThJDLTaF2KOAz0g= +github.com/anacrolix/publicip v0.3.1/go.mod h1:tF1kAG96Ao3t9Q8zyfA7Lso1wOEfHHEcZQTRI+PMm4k= +github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= +github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8= +github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M= +github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/sync v0.5.4 h1:yXZLIjXh/G+Rh2mYGCAPmszmF/fvEPadDy7/pPChpKM= +github.com/anacrolix/sync v0.5.4/go.mod h1:21cUWerw9eiu/3T3kyoChu37AVO+YFue1/H15qqubS0= +github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI= +github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA= +github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= +github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/location/v2 v2.0.0 h1:iLx5RatHQHSxgC0tm2AG0sIuQKecI7FhREessVd6RWY= +github.com/gin-contrib/location/v2 v2.0.0/go.mod h1:276TDNr25NENBA/NQZUuEIlwxy/I5CYVFIr/d2TgOdU= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= +github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= +github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= +github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ipfs/boxo v0.36.0 h1:DarrMBM46xCs6GU6Vz+AL8VUyXykqHAqZYx8mR0Oics= +github.com/ipfs/boxo v0.36.0/go.mod h1:92hnRXfP5ScKEIqlq9Ns7LR1dFXEVADKWVGH0fjk83k= +github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= +github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= +github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= +github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk= +github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= +github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= +github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= +github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= +github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kljensen/snowball v0.10.0 h1:8qgaBLraSuUVHtGH5tJ+VdGpqgfcaE2WkswL/C3nVhY= +github.com/kljensen/snowball v0.10.0/go.mod h1:bJcxtur1W5Qw4fVj9tk5W88zyRcGQQjqahFErdcDTHk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= +github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.38.0 h1:NToFzwvICo6ghDfSwuTmROCtl9LDXSZT1VawEbm4NUs= +github.com/libp2p/go-libp2p-kad-dht v0.38.0/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= +github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= +github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= +github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= +github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= +github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tsynik/torrent v1.2.22 h1:J6Se1Tk4W6l7yew3Ykw1my3EfG75uUNTRUdA5XPtHVk= +github.com/tsynik/torrent v1.2.22/go.mod h1:C5byvvOO9wEdqWHHkjSp2+AH9kinJ2r1Z4GhT1xkRdI= +github.com/tsynik/upnp v0.1.5 h1:2KXGBtY6MZHxq17p/N372Kr/0MJX3ADAimfvPcTpap8= +github.com/tsynik/upnp v0.1.5/go.mod h1:NiwP/o2sXNehx/4DbRT78Itii9F13d7NUrdHcVlpPPs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= +github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/telebot.v4 v4.0.0-beta.7 h1:j4DcNfkPe5dnMQqsjY7bYoEnU3LxmlPvZRQmCB13Fe4= +gopkg.in/telebot.v4 v4.0.0-beta.7/go.mod h1:jhcQjM/176jZm/s9Up/MzV5VFGPjyI8oiJhWvCMxayI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/vansante/go-ffprobe.v2 v2.2.1 h1:sFV08OT1eZ1yroLCZVClIVd9YySgCh9eGjBWO0oRayI= +gopkg.in/vansante/go-ffprobe.v2 v2.2.1/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/log/log.go b/server/log/log.go new file mode 100644 index 0000000..64d796e --- /dev/null +++ b/server/log/log.go @@ -0,0 +1,121 @@ +package log + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/gin-gonic/gin" +) + +var ( + logPath = "" + webLogPath = "" +) + +var webLog *log.Logger + +var ( + logFile *os.File + webLogFile *os.File +) + +func Init(path, webpath string) { + webLogPath = webpath + logPath = path + + if webpath != "" { + ff, err := os.OpenFile(webLogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + TLogln("Error create web log file:", err) + } else { + webLogFile = ff + webLog = log.New(ff, " ", log.LstdFlags) + } + } + + if path != "" { + if fi, err := os.Lstat(path); err == nil { + if fi.Size() >= 100*1024*1024 { // 100MB + os.Remove(path) + } + } + ff, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + TLogln("Error create log file:", err) + return + } + logFile = ff + os.Stdout = ff + os.Stderr = ff + // var timeFmt string + // var ok bool + // timeFmt, ok = os.LookupEnv("GO_LOG_TIME_FMT") + // if !ok { + // timeFmt = "2006-01-02T15:04:05-0700" + // } + // log.SetFlags(log.Lmsgprefix) + // log.SetPrefix(time.Now().Format(timeFmt) + " TSM ") + log.SetFlags(log.LstdFlags | log.LUTC | log.Lmsgprefix) + log.SetPrefix("UTC0 ") + log.SetOutput(ff) + } +} + +func Close() { + if logFile != nil { + logFile.Close() + } + if webLogFile != nil { + webLogFile.Close() + } +} + +func TLogln(v ...interface{}) { + log.Println(v...) +} + +func WebLogln(v ...interface{}) { + if webLog != nil { + webLog.Println(v...) + } +} + +func WebLogger() gin.HandlerFunc { + return func(c *gin.Context) { + if webLog == nil { + c.Next() + return + } + body := "" + // save body if not form or file + if !strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data") { + body, _ := io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } else { + body = "body hidden, too large" + } + c.Next() + + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + method := c.Request.Method + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + if raw != "" { + path = path + "?" + raw + } + + logStr := fmt.Sprintf("%3d | %12s | %-7s %#v %v", + statusCode, + clientIP, + method, + path, + string(body), + ) + WebLogln(logStr) + } +} diff --git a/server/mimetype/mimetype.go b/server/mimetype/mimetype.go new file mode 100644 index 0000000..94126a5 --- /dev/null +++ b/server/mimetype/mimetype.go @@ -0,0 +1,144 @@ +package mimetype + +import ( + "log" + "mime" + "net/http" + "os" + "path" + "strings" +) + +func init() { + // Add a minimal number of mime types to augment go's built in types + // for environments which don't have access to a mime.types file (e.g. + // Termux on android) + for _, t := range []struct { + mimeType string + extensions string + }{ + {"image/bmp", ".bmp"}, + {"image/gif", ".gif"}, + {"image/jpeg", ".jpg,.jpeg"}, + {"image/png", ".png"}, + {"image/tiff", ".tiff,.tif"}, + {"audio/x-aac", ".aac"}, + {"audio/dsd", ".dsd,.dsf,.dff"}, + {"audio/flac", ".flac"}, + {"audio/mpeg", ".mpga,.mpega,.mp2,.mp3,.m4a"}, + {"audio/ogg", ".oga,.ogg,.opus,.spx"}, + {"audio/opus", ".opus"}, + {"audio/weba", ".weba"}, + {"audio/x-ape", ".ape"}, + // {"audio/x-dsd", ".dsd"}, + // {"audio/x-dff", ".dff"}, + // {"audio/x-dsf", ".dsf"}, + {"audio/x-wav", ".wav"}, + {"video/dv", ".dif,.dv"}, + {"video/fli", ".fli"}, + {"video/mp4", ".mp4"}, + {"video/mpeg", ".mpeg,.mpg,.mpe"}, + {"video/x-matroska", ".mpv,.mkv"}, + {"video/mp2t", ".ts,.m2ts,.mts"}, + {"video/ogg", ".ogv"}, + {"video/webm", ".webm"}, + {"video/x-ms-vob", ".vob"}, + {"video/x-msvideo", ".avi"}, + {"video/x-quicktime", ".qt,.mov"}, + {"text/srt", ".srt"}, + {"text/smi", ".smi"}, + {"text/ssa", ".ssa"}, + } { + for _, ext := range strings.Split(t.extensions, ",") { + err := mime.AddExtensionType(ext, t.mimeType) + if err != nil { + panic(err) + } + } + } + if err := mime.AddExtensionType(".rmvb", "application/vnd.rn-realmedia-vbr"); err != nil { + log.Printf("Could not register application/vnd.rn-realmedia-vbr MIME type: %s", err) + } +} + +// Example: "video/mpeg" +type mimeType string + +// IsMedia returns true for media MIME-types +func (mt mimeType) IsMedia() bool { + return mt.IsVideo() || mt.IsAudio() || mt.IsImage() +} + +// IsVideo returns true for video MIME-types +func (mt mimeType) IsVideo() bool { + return strings.HasPrefix(string(mt), "video/") || mt == "application/vnd.rn-realmedia-vbr" +} + +// IsAudio returns true for audio MIME-types +func (mt mimeType) IsAudio() bool { + return strings.HasPrefix(string(mt), "audio/") +} + +// IsImage returns true for image MIME-types +func (mt mimeType) IsImage() bool { + return strings.HasPrefix(string(mt), "image/") +} + +// IsSub returns true for subtitles MIME-types +func (mt mimeType) IsSub() bool { + return strings.HasPrefix(string(mt), "text/srt") || strings.HasPrefix(string(mt), "text/smi") || strings.HasPrefix(string(mt), "text/ssa") +} + +// Returns the group "type", the part before the '/'. +func (mt mimeType) Type() string { + return strings.SplitN(string(mt), "/", 2)[0] +} + +// Returns the string representation of this MIME-type +func (mt mimeType) String() string { + return string(mt) +} + +// MimeTypeByPath determines the MIME-type of file at the given path +func MimeTypeByPath(filePath string) (ret mimeType, err error) { + ret = mimeTypeByBaseName(path.Base(filePath)) + if ret == "" { + ret, err = mimeTypeByContent(filePath) + } + // Custom DLNA-compat mime mappings + // TODO: make this depend on client headers / profile map + if ret == "video/mp2t" { + ret = "video/mpeg" + // } else if ret == "video/x-matroska" { + // ret = "video/mpeg" + } else if ret == "video/x-msvideo" { + ret = "video/avi" + } else if ret == "" { + ret = "application/octet-stream" + } + return +} + +// Guess MIME-type from the extension, ignoring ".part". +func mimeTypeByBaseName(name string) mimeType { + name = strings.TrimSuffix(name, ".part") + ext := path.Ext(name) + if ext != "" { + return mimeType(mime.TypeByExtension(ext)) + } + return mimeType("") +} + +// Guess the MIME-type by analysing the first 512 bytes of the file. +func mimeTypeByContent(path string) (ret mimeType, err error) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + var data [512]byte + if n, err := file.Read(data[:]); err == nil { + ret = mimeType(http.DetectContentType(data[:n])) + } + return +} diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go new file mode 100644 index 0000000..bff6840 --- /dev/null +++ b/server/proxy/proxy.go @@ -0,0 +1,36 @@ +package proxy + +import ( + "server/log" + "server/settings" + + "github.com/YouROK/tunsgo/opts" + "github.com/YouROK/tunsgo/p2p" +) + +var ( + P2Proxy *p2p.P2PServer +) + +func Start() { + if settings.BTsets.EnableProxy { + cfg := opts.DefOptions() + var err error + + cfg.Server.Port = settings.Args.Port + cfg.Hosts = settings.BTsets.ProxyHosts + + P2Proxy, err = p2p.NewP2PServer(cfg) + if err != nil { + log.TLogln("Error starting P2PServer:", err) + return + } + } +} + +func Stop() { + if P2Proxy != nil { + P2Proxy.Stop() + P2Proxy = nil + } +} diff --git a/server/rutor/mem_test.go b/server/rutor/mem_test.go new file mode 100644 index 0000000..70e1400 --- /dev/null +++ b/server/rutor/mem_test.go @@ -0,0 +1,77 @@ +package rutor + +import ( + "compress/flate" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "server/rutor/models" +) + +func TestParseChannel(t *testing.T) { + channel := make(chan *models.TorrentDetails, 0) + var ftors []*models.TorrentDetails + go func() { + for torr := range channel { + ftors = append(ftors, torr) + } + }() + + path, _ := os.Getwd() + ff, err := os.Open(filepath.Join(path, "rutor.ls")) + if err == nil { + defer ff.Close() + r := flate.NewReader(ff) + defer r.Close() + dec := json.NewDecoder(r) + + _, err := dec.Token() + if err != nil { + t.Error(err) + } + + for dec.More() { + var torr *models.TorrentDetails + err = dec.Decode(&torr) + if err != nil { + t.Error(err) + } + channel <- torr + } + close(channel) + } else { + t.Error(err) + } +} + +func TestParseArr(t *testing.T) { + var ftors []*models.TorrentDetails + path, _ := os.Getwd() + ff, err := os.Open(filepath.Join(path, "rutor.ls")) + if err == nil { + defer ff.Close() + r := flate.NewReader(ff) + defer r.Close() + dec := json.NewDecoder(r) + + _, err := dec.Token() + if err != nil { + t.Error(err) + } + + for dec.More() { + var torr *models.TorrentDetails + err = dec.Decode(&torr) + if err != nil { + t.Error(err) + } + ftors = append(ftors, torr) + fmt.Println(len(ftors)) + } + } else { + t.Error(err) + } +} diff --git a/server/rutor/models/torrentDetails.go b/server/rutor/models/torrentDetails.go new file mode 100644 index 0000000..0c38a3c --- /dev/null +++ b/server/rutor/models/torrentDetails.go @@ -0,0 +1,76 @@ +package models + +import ( + "strings" + "time" +) + +const ( + CatMovie = "Movie" + CatSeries = "Series" + CatDocMovie = "DocMovie" + CatDocSeries = "DocSeries" + CatCartoonMovie = "CartoonMovie" + CatCartoonSeries = "CartoonSeries" + CatTVShow = "TVShow" + CatAnime = "Anime" + + Q_LOWER = 0 + Q_WEBDL_720 = 100 + Q_BDRIP_720 = 101 + Q_BDRIP_HEVC_720 = 102 + Q_WEBDL_1080 = 200 + Q_BDRIP_1080 = 201 + Q_BDRIP_HEVC_1080 = 202 + Q_BDREMUX_1080 = 203 + Q_WEBDL_SDR_2160 = 300 + Q_WEBDL_HDR_2160 = 301 + Q_WEBDL_DV_2160 = 302 + Q_BDRIP_SDR_2160 = 303 + Q_BDRIP_HDR_2160 = 304 + Q_BDRIP_DV_2160 = 305 + Q_UHD_BDREMUX_SDR = 306 + Q_UHD_BDREMUX_HDR = 307 + Q_UHD_BDREMUX_DV = 308 + + Q_UNKNOWN = 0 + Q_A = 1 // Авторский, по типу Гоблина или старых переводчиков + Q_L1 = 100 // Любительский одноголосый закадровый + Q_L2 = 101 // Любительский двухголосый закадровый + Q_L = 102 // Любительский 3-5 человек закадровый + Q_LS = 103 // Любительский студия + Q_P1 = 200 // Професиональный одноголосый закадровый + Q_P2 = 201 // Профессиональный двухголосый закадровый + Q_P = 202 // Профессиональный 3-5 человек закадровый + Q_PS = 203 // Профессиональный студия + Q_D = 300 // Официальное профессиональное многоголосое озвучивание + Q_LICENSE = 301 // Лицензия +) + +type TorrentDetails struct { + Title string + Name string + Names []string + Categories string + Size string + CreateDate time.Time + Tracker string + Link string + Year int + Peer int + Seed int + Magnet string + Hash string + IMDBID string + VideoQuality int + AudioQuality int +} + +type TorrentFile struct { + Name string + Size int64 +} + +func (d TorrentDetails) GetNames() string { + return strings.Join(d.Names, " ") +} diff --git a/server/rutor/race_test.go b/server/rutor/race_test.go new file mode 100644 index 0000000..7d892e7 --- /dev/null +++ b/server/rutor/race_test.go @@ -0,0 +1,97 @@ +package rutor + +import ( + "bytes" + "compress/flate" + "encoding/json" + "os" + "path/filepath" + "strconv" + "sync" + "testing" + "time" + + "server/rutor/models" + "server/settings" +) + +// TestConcurrentSearchAndLoadDB проверяет отсутствие гонки при одновременном +// обновлении индекса (loadDB) и поиске (Search). +// !Запускать с -count=3 +func TestConcurrentSearchAndLoadDB(t *testing.T) { + if settings.BTsets == nil { + settings.BTsets = &settings.BTSets{EnableRutorSearch: true} + defer func() { settings.BTsets = nil }() + } else { + old := settings.BTsets.EnableRutorSearch + settings.BTsets.EnableRutorSearch = true + defer func() { settings.BTsets.EnableRutorSearch = old }() + } + + dir := t.TempDir() + oldPath := settings.Path + settings.Path = dir + defer func() { settings.Path = oldPath }() + + const numTorrents = 800 + seed := make([]*models.TorrentDetails, numTorrents) + for i := 0; i < numTorrents; i++ { + s := strconv.Itoa(i) + seed[i] = &models.TorrentDetails{ + Title: "Test Film Number " + s + " Part One Two Three Year", + Name: "Film " + s, + Year: 2015 + i%10, + } + } + data, err := json.Marshal(seed) + if err != nil { + t.Fatal(err) + } + var compressed bytes.Buffer + w, _ := flate.NewWriter(&compressed, flate.DefaultCompression) + _, _ = w.Write(data) + _ = w.Close() + if err := os.WriteFile(filepath.Join(dir, "rutor.ls"), compressed.Bytes(), 0o600); err != nil { + t.Fatal(err) + } + + done := make(chan struct{}) + var wg sync.WaitGroup + + // Горутина: многократно перезагружает БД (долгая перезапись индекса) + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 20; i++ { + select { + case <-done: + return + default: + loadDB() + time.Sleep(5 * time.Millisecond) + } + } + }() + + // Несколько горутин: постоянный поиск, пока идёт переиндексация + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + queries := []string{"Test", "Film", "Number", "Part", "Year", "xxx"} + for j := 0; j < 200; j++ { + select { + case <-done: + return + default: + _ = Search(queries[j%len(queries)]) + } + } + }() + } + + // Даём время на пересечение loadDB и Search + time.Sleep(800 * time.Millisecond) + close(done) + wg.Wait() +} diff --git a/server/rutor/rutor.go b/server/rutor/rutor.go new file mode 100644 index 0000000..ac2b3fb --- /dev/null +++ b/server/rutor/rutor.go @@ -0,0 +1,183 @@ +package rutor + +import ( + "compress/flate" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/agnivade/levenshtein" + + "server/log" + "server/rutor/models" + "server/rutor/torrsearch" + "server/rutor/utils" + "server/settings" + utils2 "server/torr/utils" +) + +var ( + mu sync.RWMutex + torrs []*models.TorrentDetails + isStop bool +) + +func Start() { + go func() { + if settings.BTsets.EnableRutorSearch { + if !updateDB() { + loadDB() + } + isStop = false + for !isStop { + for i := 0; i < 3*60*60; i++ { + time.Sleep(time.Second) + if isStop { + return + } + } + updateDB() + } + } + }() +} + +func Stop() { + mu.Lock() + isStop = true + torrs = nil + torrsearch.NewIndex(nil) + mu.Unlock() + utils2.FreeOSMemGC() + time.Sleep(time.Millisecond * 1500) +} + +// http://releases.yourok.ru/torr/rutor.ls +func updateDB() bool { + log.TLogln("Update rutor db") + + fnOrig := filepath.Join(settings.Path, "rutor.ls") + + if fi, err := os.Stat(fnOrig); err == nil { + if time.Since(fi.ModTime()) < time.Minute*175 /*2:55*/ { + log.TLogln("Less 3 hours rutor db old") + return false + } + } + + fnTmp := filepath.Join(settings.Path, "rutor.tmp") + out, err := os.Create(fnTmp) + if err != nil { + log.TLogln("Error create file rutor.tmp:", err) + return false + } + + resp, err := http.Get("http://releases.yourok.ru/torr/rutor.ls") + if err != nil { + log.TLogln("Error connect to rutor db:", err) + out.Close() + return false + } + defer resp.Body.Close() + _, err = io.Copy(out, resp.Body) + out.Close() + if err != nil { + log.TLogln("Error download rutor db:", err) + return false + } + + md5Tmp := utils.MD5File(fnTmp) + md5Orig := utils.MD5File(fnOrig) + if md5Tmp != md5Orig { + err = os.Remove(fnOrig) + if err != nil && !os.IsNotExist(err) { + log.TLogln("Error remove old rutor db:", err) + return false + } + err = os.Rename(fnTmp, fnOrig) + if err != nil { + log.TLogln("Error rename rutor db:", err) + return false + } + loadDB() + return true + } else { + os.Remove(fnTmp) + } + return false +} + +func loadDB() { + log.TLogln("Load rutor db") + ff, err := os.Open(filepath.Join(settings.Path, "rutor.ls")) + if err == nil { + defer ff.Close() + r := flate.NewReader(ff) + defer r.Close() + var ftorrs []*models.TorrentDetails + dec := json.NewDecoder(r) + + _, err := dec.Token() + if err != nil { + log.TLogln("Error read token rutor db:", err) + return + } + + for dec.More() { + var torr *models.TorrentDetails + err = dec.Decode(&torr) + if err == nil { + ftorrs = append(ftorrs, torr) + } + } + mu.Lock() + defer mu.Unlock() + torrs = ftorrs + log.TLogln("Index rutor db") + torrsearch.NewIndex(torrs) + log.TLogln("Torrents count:", len(torrs)) + log.TLogln("Indexed words:", len(torrsearch.GetIDX())) + + } else { + log.TLogln("Error load rutor db:", err) + } + utils2.FreeOSMemGC() +} + +func Search(query string) []*models.TorrentDetails { + if !settings.BTsets.EnableRutorSearch { + return nil + } + mu.RLock() + matchedIDs := torrsearch.Search(query) + if len(matchedIDs) == 0 { + mu.RUnlock() + return nil + } + var list []*models.TorrentDetails + for _, id := range matchedIDs { + list = append(list, torrs[id]) + } + mu.RUnlock() + + hash := utils.ClearStr(query) + + sort.Slice(list, func(i, j int) bool { + lhash := utils.ClearStr(strings.ToLower(list[i].Name+list[i].GetNames())) + strconv.Itoa(list[i].Year) + lev1 := levenshtein.ComputeDistance(hash, lhash) + lhash = utils.ClearStr(strings.ToLower(list[j].Name+list[j].GetNames())) + strconv.Itoa(list[j].Year) + lev2 := levenshtein.ComputeDistance(hash, lhash) + if lev1 == lev2 { + return list[j].CreateDate.Before(list[i].CreateDate) + } + return lev1 < lev2 + }) + return list +} diff --git a/server/rutor/torrsearch/filter.go b/server/rutor/torrsearch/filter.go new file mode 100644 index 0000000..4d66417 --- /dev/null +++ b/server/rutor/torrsearch/filter.go @@ -0,0 +1,68 @@ +package torrsearch + +import ( + "strings" + + snowballeng "github.com/kljensen/snowball/english" + snowballru "github.com/kljensen/snowball/russian" +) + +// lowercaseFilter returns a slice of tokens normalized to lower case. +func lowercaseFilter(tokens []string) []string { + r := make([]string, len(tokens)) + for i, token := range tokens { + r[i] = replaceChars(strings.ToLower(token)) + } + return r +} + +// stopwordFilter returns a slice of tokens with stop words removed. +func stopwordFilter(tokens []string) []string { + r := make([]string, 0, len(tokens)) + for _, token := range tokens { + if !isStopWord(token) { + r = append(r, token) + } + } + return r +} + +// stemmerFilter returns a slice of stemmed tokens. +func stemmerFilter(tokens []string) []string { + r := make([]string, len(tokens)) + for i, token := range tokens { + worden := snowballeng.Stem(token, false) + wordru := snowballru.Stem(token, false) + if wordru == "" || worden == "" { + continue + } + if wordru != token { + r[i] = wordru + } else { + r[i] = worden + } + } + return r +} + +func replaceChars(word string) string { + out := []rune(word) + for i, r := range out { + if r == 'ё' { + out[i] = 'е' + } + } + return string(out) +} + +func isStopWord(word string) bool { + switch word { + case "a", "am", "an", "and", "are", "as", "at", "be", + "by", "did", "do", "is", "of", "or", "s", "so", "t", + "и", "в", "с", "со", "а", "но", "к", "у", + "же", "бы", "по", "от", "о", "из", "ну", + "ли", "ни", "нибудь", "уж", "ведь", "ж", "об": + return true + } + return false +} diff --git a/server/rutor/torrsearch/index.go b/server/rutor/torrsearch/index.go new file mode 100644 index 0000000..dd0197d --- /dev/null +++ b/server/rutor/torrsearch/index.go @@ -0,0 +1,77 @@ +package torrsearch + +import ( + "server/rutor/models" +) + +// Index is an inverted Index. It maps tokens to document IDs. +type Index map[string][]int + +var idx Index + +func NewIndex(torrs []*models.TorrentDetails) { + idx = make(Index) + idx.add(torrs) +} + +func Search(text string) []int { + return idx.search(text) +} + +func GetIDX() Index { + return idx +} + +func (idx Index) add(torrs []*models.TorrentDetails) { + for ID, torr := range torrs { + for _, token := range analyze(torr.Title) { + ids := idx[token] + if ids != nil && ids[len(ids)-1] == ID { + // Don't add same ID twice. + continue + } + idx[token] = append(ids, ID) + } + } +} + +// intersection returns the set intersection between a and b. +// a and b have to be sorted in ascending order and contain no duplicates. +func intersection(a []int, b []int) []int { + maxLen := len(a) + if len(b) > maxLen { + maxLen = len(b) + } + r := make([]int, 0, maxLen) + var i, j int + for i < len(a) && j < len(b) { + if a[i] < b[j] { + i++ + } else if a[i] > b[j] { + j++ + } else { + r = append(r, a[i]) + i++ + j++ + } + } + return r +} + +// Search queries the Index for the given text. +func (idx Index) search(text string) []int { + var r []int + for _, token := range analyze(text) { + if ids, ok := idx[token]; ok { + if r == nil { + r = ids + } else { + r = intersection(r, ids) + } + } else { + // Token doesn't exist. + return nil + } + } + return r +} diff --git a/server/rutor/torrsearch/tokenizer.go b/server/rutor/torrsearch/tokenizer.go new file mode 100644 index 0000000..be75275 --- /dev/null +++ b/server/rutor/torrsearch/tokenizer.go @@ -0,0 +1,23 @@ +package torrsearch + +import ( + "strings" + "unicode" +) + +// tokenize returns a slice of tokens for the given text. +func tokenize(text string) []string { + return strings.FieldsFunc(text, func(r rune) bool { + // Split on any character that is not a letter or a number. + return !unicode.IsLetter(r) && !unicode.IsNumber(r) + }) +} + +// analyze analyzes the text and returns a slice of tokens. +func analyze(text string) []string { + tokens := tokenize(text) + tokens = lowercaseFilter(tokens) + tokens = stopwordFilter(tokens) + // tokens = stemmerFilter(tokens) + return tokens +} diff --git a/server/rutor/utils/utils.go b/server/rutor/utils/utils.go new file mode 100644 index 0000000..991719d --- /dev/null +++ b/server/rutor/utils/utils.go @@ -0,0 +1,42 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "os" + "strings" +) + +func ClearStr(str string) string { + ret := "" + str = strings.ToLower(str) + for _, r := range str { + if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'а' && r <= 'я') || r == 'ё' { + ret = ret + string(r) + } + } + return ret +} + +func MD5File(fname string) string { + f, err := os.Open(fname) + if err != nil { + return "" + } + + defer f.Close() + + buf := make([]byte, 1024*1024) + h := sha256.New() + + for { + bytesRead, err := f.Read(buf) + if err != nil { + break + } + + h.Write(buf[:bytesRead]) + } + + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..d8b58b3 --- /dev/null +++ b/server/server.go @@ -0,0 +1,146 @@ +package server + +import ( + "net" + "os" + "path/filepath" + "strconv" + + "server/tgbot" + + "server/log" + "server/settings" + "server/web" +) + +func Start() { + settings.InitSets(settings.Args.RDB, settings.Args.SearchWA) + // https checks + if settings.Args.Ssl { + // set settings ssl enabled + settings.Ssl = settings.Args.Ssl + if settings.Args.SslPort == "" { + dbSSlPort := strconv.Itoa(settings.BTsets.SslPort) + if dbSSlPort != "0" { + settings.Args.SslPort = dbSSlPort + } else { + settings.Args.SslPort = "8091" + } + } else { // store ssl port from params to DB + dbSSlPort, err := strconv.Atoi(settings.Args.SslPort) + if err == nil { + settings.BTsets.SslPort = dbSSlPort + } + } + // check if ssl cert and key files exist + if settings.Args.SslCert != "" && settings.Args.SslKey != "" { + // set settings ssl cert and key files + settings.BTsets.SslCert = settings.Args.SslCert + settings.BTsets.SslKey = settings.Args.SslKey + } + log.TLogln("Check web ssl port", settings.Args.SslPort) + l, err := net.Listen("tcp", settings.Args.IP+":"+settings.Args.SslPort) + if l != nil { + l.Close() + } + if err != nil { + log.TLogln("Port", settings.Args.SslPort, "already in use! Please set different ssl port for HTTPS. Abort") + os.Exit(1) + } + } + // http checks + if settings.Args.Port == "" { + settings.Args.Port = "8090" + } + + log.TLogln("Check web port", settings.Args.Port) + l, err := net.Listen("tcp", settings.Args.IP+":"+settings.Args.Port) + if l != nil { + l.Close() + } + if err != nil { + log.TLogln("Port", settings.Args.Port, "already in use! Please set different port for HTTP. Abort") + os.Exit(1) + } + // remove old disk caches + go cleanCache() + // set settings http and https ports. Start web server. + settings.Port = settings.Args.Port + settings.SslPort = settings.Args.SslPort + settings.IP = settings.Args.IP + + if settings.Args.TGToken != "" { + if err := tgbot.Start(settings.Args.TGToken); err != nil { + log.TLogln("tg bot start failed", err) + } + } + web.Start() +} + +func cleanCache() { + if !settings.BTsets.UseDisk || settings.BTsets.TorrentsSavePath == "/" || settings.BTsets.TorrentsSavePath == "" { + return + } + + dirs, err := os.ReadDir(settings.BTsets.TorrentsSavePath) + if err != nil { + return + } + + torrs := settings.ListTorrent() + + log.TLogln("Remove unused cache in dir:", settings.BTsets.TorrentsSavePath) + keep := map[string]bool{} + for _, d := range dirs { + if len(d.Name()) != 40 { + // Not a hash + continue + } + + if !settings.BTsets.RemoveCacheOnDrop { + keep[d.Name()] = true + for _, t := range torrs { + if d.IsDir() && d.Name() == t.InfoHash.HexString() { + keep[d.Name()] = false + break + } + } + for hash, del := range keep { + if del && hash == d.Name() { + log.TLogln("Remove unused cache:", d.Name()) + removeAllFiles(filepath.Join(settings.BTsets.TorrentsSavePath, d.Name())) + } + } + } else { + if d.IsDir() { + log.TLogln("Remove unused cache:", d.Name()) + removeAllFiles(filepath.Join(settings.BTsets.TorrentsSavePath, d.Name())) + } + } + } +} + +func removeAllFiles(path string) { + files, err := os.ReadDir(path) + if err != nil { + return + } + for _, f := range files { + name := filepath.Join(path, f.Name()) + os.Remove(name) + } + os.Remove(path) +} + +func WaitServer() string { + err := web.Wait() + if err != nil { + return err.Error() + } + return "" +} + +func Stop() { + web.Stop() + settings.CloseDB() +} diff --git a/server/settings/args.go b/server/settings/args.go new file mode 100644 index 0000000..eea8277 --- /dev/null +++ b/server/settings/args.go @@ -0,0 +1,31 @@ +package settings + +type ExecArgs struct { + Port string + IP string + Ssl bool + SslPort string + SslCert string + SslKey string + Path string + LogPath string + WebLogPath string + RDB bool + HttpAuth bool + DontKill bool + UI bool + TorrentsDir string + TorrentAddr string + PubIPv4 string + PubIPv6 string + SearchWA bool + MaxSize string + TGToken string + FusePath string + WebDAV bool + ProxyURL string + ProxyMode string + ForceHTTPS bool +} + +var Args *ExecArgs diff --git a/server/settings/btsets.go b/server/settings/btsets.go new file mode 100644 index 0000000..ac27bd1 --- /dev/null +++ b/server/settings/btsets.go @@ -0,0 +1,212 @@ +package settings + +import ( + "encoding/json" + "io" + "io/fs" + + "path/filepath" + "strings" + + "server/log" +) + +type TorznabConfig struct { + Host string + Key string + Name string +} + +type TMDBConfig struct { + APIKey string // TMDB API Key + APIURL string // Base API URL (default: https://api.themoviedb.org) + ImageURL string // Image URL (default: https://image.tmdb.org) + ImageURLRu string // Image URL for Russian users (default: https://imagetmdb.com) +} + +type BTSets struct { + // Cache + CacheSize int64 // in byte, def 64 MB + ReaderReadAHead int // in percent, 5%-100%, [...S__X__E...] [S-E] not clean + PreloadCache int // in percent + + // Disk + UseDisk bool + TorrentsSavePath string + RemoveCacheOnDrop bool + + // Torrent + ForceEncrypt bool + RetrackersMode int // 0 - don`t add, 1 - add retrackers (def), 2 - remove retrackers 3 - replace retrackers + TorrentDisconnectTimeout int // in seconds + EnableDebug bool // debug logs + + // DLNA + EnableDLNA bool + FriendlyName string + + // Rutor + EnableRutorSearch bool + + // Torznab + EnableTorznabSearch bool + TorznabUrls []TorznabConfig + + // TMDB + TMDBSettings TMDBConfig + + // BT Config + EnableIPv6 bool + DisableTCP bool + DisableUTP bool + DisableUPNP bool + DisableDHT bool + DisablePEX bool + DisableUpload bool + DownloadRateLimit int // in kb, 0 - inf + UploadRateLimit int // in kb, 0 - inf + ConnectionsLimit int + PeersListenPort int + + // HTTPS + SslPort int + SslCert string + SslKey string + + // Reader + ResponsiveMode bool // enable Responsive reader (don't wait pieceComplete) + + // FS + ShowFSActiveTorr bool + + // Storage preferences + StoreSettingsInJson bool + StoreViewedInJson bool + + // P2P Proxy + EnableProxy bool + ProxyHosts []string +} + +func (v *BTSets) String() string { + buf, _ := json.Marshal(v) + return string(buf) +} + +var BTsets *BTSets + +func SetBTSets(sets *BTSets) { + if ReadOnly { + return + } + // failsafe checks (use defaults) + if sets.CacheSize == 0 { + sets.CacheSize = 64 * 1024 * 1024 + } + if sets.ConnectionsLimit == 0 { + sets.ConnectionsLimit = 25 + } + if sets.TorrentDisconnectTimeout == 0 { + sets.TorrentDisconnectTimeout = 30 + } + + if sets.ReaderReadAHead < 5 { + sets.ReaderReadAHead = 5 + } + if sets.ReaderReadAHead > 100 { + sets.ReaderReadAHead = 100 + } + + if sets.PreloadCache < 0 { + sets.PreloadCache = 0 + } + if sets.PreloadCache > 100 { + sets.PreloadCache = 100 + } + + if sets.TorrentsSavePath == "" { + sets.UseDisk = false + } else if sets.UseDisk { + BTsets = sets + + go filepath.WalkDir(sets.TorrentsSavePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && strings.ToLower(d.Name()) == ".tsc" { + BTsets.TorrentsSavePath = path + log.TLogln("Find directory \"" + BTsets.TorrentsSavePath + "\", use as cache dir") + return io.EOF + } + if d.IsDir() && strings.HasPrefix(d.Name(), ".") { + return filepath.SkipDir + } + return nil + }) + } + + BTsets = sets + buf, err := json.Marshal(BTsets) + if err != nil { + log.TLogln("Error marshal btsets", err) + return + } + tdb.Set("Settings", "BitTorr", buf) +} + +func SetDefaultConfig() { + sets := new(BTSets) + sets.CacheSize = 64 * 1024 * 1024 // 64 MB + sets.PreloadCache = 50 + sets.ConnectionsLimit = 25 + sets.RetrackersMode = 1 + sets.TorrentDisconnectTimeout = 30 + sets.ReaderReadAHead = 95 // 95% + sets.ResponsiveMode = true + sets.ShowFSActiveTorr = true + sets.StoreSettingsInJson = true + // Set default TMDB settings + sets.TMDBSettings = TMDBConfig{ + APIKey: "", + APIURL: "https://api.themoviedb.org", + ImageURL: "https://image.tmdb.org", + ImageURLRu: "https://imagetmdb.com", + } + BTsets = sets + if !ReadOnly { + buf, err := json.Marshal(BTsets) + if err != nil { + log.TLogln("Error marshal btsets", err) + return + } + tdb.Set("Settings", "BitTorr", buf) + } + //Proxy + sets.EnableProxy = false + sets.ProxyHosts = []string{"*themoviedb.org", "*tmdb.org", "rutor.info"} +} + +func loadBTSets() { + buf := tdb.Get("Settings", "BitTorr") + if len(buf) > 0 { + err := json.Unmarshal(buf, &BTsets) + if err == nil { + if BTsets.ReaderReadAHead < 5 { + BTsets.ReaderReadAHead = 5 + } + // Set default TMDB settings if missing (for existing configs) + if BTsets.TMDBSettings.APIURL == "" { + BTsets.TMDBSettings = TMDBConfig{ + APIKey: "", + APIURL: "https://api.themoviedb.org", + ImageURL: "https://image.tmdb.org", + ImageURLRu: "https://imagetmdb.com", + } + } + return + } + log.TLogln("Error unmarshal btsets", err) + } + // initialize defaults on error + SetDefaultConfig() +} diff --git a/server/settings/db.go b/server/settings/db.go new file mode 100644 index 0000000..adc1907 --- /dev/null +++ b/server/settings/db.go @@ -0,0 +1,207 @@ +package settings + +import ( + "path/filepath" + "strings" + "time" + + "server/log" + + bolt "go.etcd.io/bbolt" +) + +type TDB struct { + Path string + db *bolt.DB +} + +var globalBboltDB TorrServerDB + +func NewTDB() TorrServerDB { + if globalBboltDB != nil { + return globalBboltDB // Return existing instance + } + db, err := bolt.Open(filepath.Join(Path, "config.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second}) + if err != nil { + log.TLogln(err) + return nil + } + + tdb := new(TDB) + tdb.db = db + tdb.Path = Path + globalBboltDB = tdb + return globalBboltDB +} + +func (v *TDB) CloseDB() { + if v.db != nil { + v.db.Close() + v.db = nil + } +} + +func (v *TDB) Get(xpath, name string) []byte { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return nil + } + var ret []byte + err := v.db.View(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + data := buckt.Get([]byte(name)) + if data != nil { + // CRITICAL: Copy the data before returning + ret = make([]byte, len(data)) + copy(ret, data) + } + return nil + }) + if err != nil { + log.TLogln("Error get sets", xpath+"/"+name, ", error:", err) + } + + return ret +} + +func (v *TDB) Set(xpath, name string, value []byte) { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return + } + err := v.db.Update(func(tx *bolt.Tx) error { + buckt, err := tx.CreateBucketIfNotExists([]byte(spath[0])) + if err != nil { + return err + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt, err = buckt.CreateBucketIfNotExists([]byte(p)) + if err != nil { + return err + } + } + + return buckt.Put([]byte(name), value) + }) + if err != nil { + log.TLogln("Error put sets", xpath+"/"+name, ", error:", err) + log.TLogln("value:", value) + } +} + +func (v *TDB) List(xpath string) []string { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return nil + } + var ret []string + err := v.db.View(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + buckt.ForEach(func(k, _ []byte) error { + if len(k) > 0 { + ret = append(ret, string(k)) + } + return nil + }) + + return nil + }) + if err != nil { + log.TLogln("Error list sets", xpath, ", error:", err) + } + + return ret +} + +func (v *TDB) Rem(xpath, name string) { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return + } + err := v.db.Update(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + return buckt.Delete([]byte(name)) + }) + if err != nil { + log.TLogln("Error rem sets", xpath+"/"+name, ", error:", err) + } +} + +func (v *TDB) Clear(xPath string) { + spath := strings.Split(xPath, "/") + if len(spath) == 0 { + return + } + + err := v.db.Update(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + // Delete all entries in this bucket + return buckt.ForEach(func(k, _ []byte) error { + return buckt.Delete(k) + }) + }) + + if err != nil { + log.TLogln("Error clear xPath", xPath, ", error:", err) + } +} diff --git a/server/settings/dbreadcache.go b/server/settings/dbreadcache.go new file mode 100644 index 0000000..d9667f3 --- /dev/null +++ b/server/settings/dbreadcache.go @@ -0,0 +1,175 @@ +package settings + +import ( + "sync" + + "server/log" +) + +type DBReadCache struct { + db TorrServerDB + listCache map[string][]string + listCacheMutex sync.RWMutex + dataCache map[[2]string][]byte + dataCacheMutex sync.RWMutex +} + +func NewDBReadCache(db TorrServerDB) TorrServerDB { + cdb := &DBReadCache{ + db: db, + listCache: map[string][]string{}, + dataCache: map[[2]string][]byte{}, + } + return cdb +} + +func (v *DBReadCache) CloseDB() { + v.db.CloseDB() + v.db = nil + v.listCache = nil + v.dataCache = nil +} + +func (v *DBReadCache) Get(xPath, name string) []byte { + if v.dataCache == nil { + return nil // или panic, или возвращаем ошибку + } + cacheKey := v.makeDataCacheKey(xPath, name) + + v.dataCacheMutex.RLock() + if data, ok := v.dataCache[cacheKey]; ok { + defer v.dataCacheMutex.RUnlock() + return data + } + v.dataCacheMutex.RUnlock() + + // Если база данных закрыта, не пытаемся к ней обращаться + if v.db == nil { + return nil + } + data := v.db.Get(xPath, name) + + v.dataCacheMutex.Lock() + if v.dataCache != nil { // Двойная проверка + v.dataCache[cacheKey] = data + } + v.dataCacheMutex.Unlock() + + return data +} + +func (v *DBReadCache) Set(xPath, name string, value []byte) { + if ReadOnly { + if IsDebug() { + log.TLogln("DBReadCache.Set: Read-only DB mode!", name) + } + return + } + // Проверяем, не закрыта ли база + if v.dataCache == nil || v.db == nil { + log.TLogln("DBReadCache.Set: no dataCache or DB is closed, cannot set", name) + return + } + + cacheKey := v.makeDataCacheKey(xPath, name) + + v.dataCacheMutex.Lock() + if v.dataCache != nil { // Двойная проверка + v.dataCache[cacheKey] = value + } + v.dataCacheMutex.Unlock() + + if v.listCache != nil { + delete(v.listCache, xPath) + } + + v.db.Set(xPath, name, value) +} + +func (v *DBReadCache) List(xPath string) []string { + if v.listCache == nil { + return nil + } + + v.listCacheMutex.RLock() + if names, ok := v.listCache[xPath]; ok { + defer v.listCacheMutex.RUnlock() + return names + } + v.listCacheMutex.RUnlock() + + // Проверяем, не закрыта ли база + if v.db == nil { + return nil + } + + names := v.db.List(xPath) + + v.listCacheMutex.Lock() + if v.listCache != nil { // Двойная проверка + v.listCache[xPath] = names + } + v.listCacheMutex.Unlock() + + return names +} + +func (v *DBReadCache) Rem(xPath, name string) { + if ReadOnly { + if IsDebug() { + log.TLogln("DBReadCache.Rem: Read-only DB mode!", name) + } + return + } + // Проверяем, не закрыта ли база + if v.dataCache == nil || v.db == nil { + log.TLogln("DBReadCache.Rem: no dataCache or DB is closed, cannot remove", name) + return + } + + cacheKey := v.makeDataCacheKey(xPath, name) + + v.dataCacheMutex.Lock() + if v.dataCache != nil { + delete(v.dataCache, cacheKey) + } + v.dataCacheMutex.Unlock() + + if v.listCache != nil { + delete(v.listCache, xPath) + } + + v.db.Rem(xPath, name) +} + +func (v *DBReadCache) Clear(xPath string) { + if ReadOnly { + if IsDebug() { + log.TLogln("DBReadCache.Clear: Read-only DB mode!", xPath) + } + return + } + + // Clear from underlying DB first + if v.db != nil { + v.db.Clear(xPath) + } + + // Clear cache + v.listCacheMutex.Lock() + delete(v.listCache, xPath) + v.listCacheMutex.Unlock() + + // Clear data cache entries for this xPath + v.dataCacheMutex.Lock() + for key := range v.dataCache { + if key[0] == xPath { + delete(v.dataCache, key) + } + } + v.dataCacheMutex.Unlock() +} + +func (v *DBReadCache) makeDataCacheKey(xPath, name string) [2]string { + return [2]string{xPath, name} +} diff --git a/server/settings/jsondb.go b/server/settings/jsondb.go new file mode 100644 index 0000000..19ad11f --- /dev/null +++ b/server/settings/jsondb.go @@ -0,0 +1,192 @@ +package settings + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "sync" + + "server/log" +) + +type JsonDB struct { + Path string + filenameDelimiter string + filenameExtension string + fileMode fs.FileMode + xPathDelimeter string +} + +var globalJsonDB TorrServerDB +var jsonDbLocks = make(map[string]*sync.Mutex) +var jsonDbLocksMutex sync.Mutex + +func NewJsonDB() TorrServerDB { + if globalJsonDB != nil { + return globalJsonDB + } + globalJsonDB := &JsonDB{ + Path: Path, + filenameDelimiter: ".", + filenameExtension: ".json", + fileMode: fs.FileMode(0o666), + xPathDelimeter: "/", + } + return globalJsonDB +} + +func (v *JsonDB) CloseDB() { + // Not necessary +} + +func (v *JsonDB) Set(xPath, name string, value []byte) { + var err error = nil + jsonObj := map[string]interface{}{} + if err := json.Unmarshal(value, &jsonObj); err == nil { + if filename, err := v.xPathToFilename(xPath); err == nil { + v.lock(filename) + defer v.unlock(filename) + if root, err := v.readJsonFileAsMap(filename); err == nil { + root[name] = jsonObj + if err = v.writeMapAsJsonFile(filename, root); err == nil { + return + } + } + } + } + v.log(fmt.Sprintf("Set: error writing entry %s->%s", xPath, name), err) +} + +func (v *JsonDB) Get(xPath, name string) []byte { + var err error = nil + if filename, err := v.xPathToFilename(xPath); err == nil { + v.lock(filename) + defer v.unlock(filename) + if root, err := v.readJsonFileAsMap(filename); err == nil { + if jsonData, ok := root[name]; ok { + if byteData, err := json.Marshal(jsonData); err == nil { + // Return a copy to be safe + data := make([]byte, len(byteData)) + copy(data, byteData) + return data + } + } else { + // We assume this is not 'error' but 'no entry' which is normal + return nil + } + } + } + v.log(fmt.Sprintf("Get: error reading entry %s->%s", xPath, name), err) + return nil +} + +func (v *JsonDB) List(xPath string) []string { + var err error = nil + if filename, err := v.xPathToFilename(xPath); err == nil { + v.lock(filename) + defer v.unlock(filename) + if root, err := v.readJsonFileAsMap(filename); err == nil { + nameList := make([]string, 0, len(root)) + for k := range root { + nameList = append(nameList, k) + } + return nameList + } + } + v.log(fmt.Sprintf("List: error reading entries in xPath %s", xPath), err) + return nil +} + +func (v *JsonDB) Rem(xPath, name string) { + var err error = nil + if filename, err := v.xPathToFilename(xPath); err == nil { + v.lock(filename) + defer v.unlock(filename) + if root, err := v.readJsonFileAsMap(filename); err == nil { + delete(root, name) + if err = v.writeMapAsJsonFile(filename, root); err == nil { + return + } + } + } + v.log(fmt.Sprintf("Rem: error removing entry %s->%s", xPath, name), err) +} + +func (v *JsonDB) Clear(xPath string) { + filename, err := v.xPathToFilename(xPath) + if err != nil { + v.log(fmt.Sprintf("Clear: error converting xPath %s to filename: %v", xPath, err)) + return + } + + v.lock(filename) + defer v.unlock(filename) + + path := filepath.Join(v.Path, filename) + emptyData := []byte("{}") + + if err := os.WriteFile(path, emptyData, v.fileMode); err != nil { + v.log(fmt.Sprintf("Clear: error writing empty file for xPath %s: %v", xPath, err)) + } +} + +func (v *JsonDB) lock(filename string) { + jsonDbLocksMutex.Lock() + mtx, ok := jsonDbLocks[filename] + if !ok { + mtx = &sync.Mutex{} + jsonDbLocks[filename] = mtx + } + jsonDbLocksMutex.Unlock() + mtx.Lock() +} + +func (v *JsonDB) unlock(filename string) { + jsonDbLocksMutex.Lock() + if mtx, ok := jsonDbLocks[filename]; ok { + mtx.Unlock() + } + jsonDbLocksMutex.Unlock() +} + +func (v *JsonDB) xPathToFilename(xPath string) (string, error) { + if pathComponents := strings.Split(xPath, v.xPathDelimeter); len(pathComponents) > 0 { + return strings.ToLower(strings.Join(pathComponents, v.filenameDelimiter) + v.filenameExtension), nil + } + return "", errors.New("xPath has no components") +} + +func (v *JsonDB) readJsonFileAsMap(filename string) (map[string]interface{}, error) { + var err error = nil + jsonData := map[string]interface{}{} + path := filepath.Join(v.Path, filename) + if fileData, err := os.ReadFile(path); err == nil { + if err = json.Unmarshal(fileData, &jsonData); err != nil { + v.log(fmt.Sprintf("readJsonFileAsMap(%s) fileData: %s error", filename, fileData), err) + } + } + return jsonData, err +} + +func (v *JsonDB) writeMapAsJsonFile(filename string, o map[string]interface{}) error { + var err error = nil + path := filepath.Join(v.Path, filename) + if fileData, err := json.MarshalIndent(o, "", " "); err == nil { + if err = os.WriteFile(path, fileData, v.fileMode); err != nil { + v.log(fmt.Sprintf("writeMapAsJsonFile path: %s, fileMode: %s, fileData: %s error", path, v.fileMode, fileData), err) + } + } + return err +} + +func (v *JsonDB) log(s string, params ...interface{}) { + if len(params) > 0 { + log.TLogln(fmt.Sprintf("JsonDB: %s: %s", s, fmt.Sprint(params...))) + } else { + log.TLogln(fmt.Sprintf("JsonDB: %s", s)) + } +} diff --git a/server/settings/migrate.go b/server/settings/migrate.go new file mode 100644 index 0000000..7d80153 --- /dev/null +++ b/server/settings/migrate.go @@ -0,0 +1,431 @@ +package settings + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "time" + + "server/log" + "server/web/api/utils" + + bolt "go.etcd.io/bbolt" +) + +var dbTorrentsName = []byte("Torrents") + +type torrentBackupDB struct { + Name string + Magnet string + InfoBytes []byte + Hash string + Size int64 + Timestamp int64 +} + +// Migrate from torrserver.db to config.db +// TODO: migrate categories and data too +func MigrateTorrents() { + if _, err := os.Lstat(filepath.Join(Path, "torrserver.db")); os.IsNotExist(err) { + return + } + + db, err := bolt.Open(filepath.Join(Path, "torrserver.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second}) + if err != nil { + log.TLogln("MigrateTorrents", err) + return + } + + torrs := make([]*torrentBackupDB, 0) + err = db.View(func(tx *bolt.Tx) error { + tdb := tx.Bucket(dbTorrentsName) + if tdb == nil { + return nil + } + c := tdb.Cursor() + for h, _ := c.First(); h != nil; h, _ = c.Next() { + hdb := tdb.Bucket(h) + if hdb != nil { + torr := new(torrentBackupDB) + torr.Hash = string(h) + tmp := hdb.Get([]byte("Name")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Name = string(tmp) + + tmp = hdb.Get([]byte("Link")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Magnet = string(tmp) + + tmp = hdb.Get([]byte("Size")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Size = b2i(tmp) + + tmp = hdb.Get([]byte("Timestamp")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Timestamp = b2i(tmp) + + torrs = append(torrs, torr) + } + } + return nil + }) + db.Close() + if err == nil && len(torrs) > 0 { + for _, torr := range torrs { + spec, err := utils.ParseLink(torr.Magnet) + if err != nil { + continue + } + + title := torr.Name + if len(spec.DisplayName) > len(title) { + title = spec.DisplayName + } + log.TLogln("Migrate torrent", torr.Name, torr.Hash, torr.Size) + AddTorrent(&TorrentDB{ + TorrentSpec: spec, + Title: title, + Timestamp: torr.Timestamp, + Size: torr.Size, + }) + } + } + os.Remove(filepath.Join(Path, "torrserver.db")) +} + +// MigrateSettingsToJson migrates Settings from BBolt to JSON +func MigrateSettingsToJson(bboltDB, jsonDB TorrServerDB) error { + // if BTsets != nil { + // return errors.New("migration must be called before initializing BTSets") + // } + migrated, err := MigrateSingle(bboltDB, jsonDB, "Settings", "BitTorr") + if migrated { + log.TLogln("Settings migrated from BBolt to JSON") + } + return err +} + +// MigrateSettingsFromJson migrates Settings from JSON to BBolt +func MigrateSettingsFromJson(jsonDB, bboltDB TorrServerDB) error { + // if BTsets != nil { + // return errors.New("migration must be called before initializing BTSets") + // } + migrated, err := MigrateSingle(jsonDB, bboltDB, "Settings", "BitTorr") + if migrated { + log.TLogln("Settings migrated from JSON to BBolt") + } + return err +} + +// MigrateViewedToJson migrates Viewed data from BBolt to JSON +func MigrateViewedToJson(bboltDB, jsonDB TorrServerDB) error { + migrated, skipped, err := MigrateAll(bboltDB, jsonDB, "Viewed") + log.TLogln(fmt.Sprintf("Viewed->JSON: %d migrated, %d skipped", migrated, skipped)) + return err +} + +// MigrateViewedFromJson migrates Viewed data from JSON to BBolt +func MigrateViewedFromJson(jsonDB, bboltDB TorrServerDB) error { + migrated, skipped, err := MigrateAll(jsonDB, bboltDB, "Viewed") + log.TLogln(fmt.Sprintf("Viewed->BBolt: %d migrated, %d skipped", migrated, skipped)) + return err +} + +// MigrateSingle migrates a single entry with validation +// Returns: (migrated bool, error) +func MigrateSingle(source, target TorrServerDB, xpath, name string) (bool, error) { + sourceData := source.Get(xpath, name) + if sourceData == nil { + if IsDebug() { + log.TLogln(fmt.Sprintf("No data to migrate for %s/%s", xpath, name)) + } + return false, nil + } + + targetData := target.Get(xpath, name) + if targetData != nil { + // Check if already identical + if equal, err := isByteArraysEqualJson(sourceData, targetData); err == nil && equal { + if IsDebug() { + log.TLogln(fmt.Sprintf("Skipping %s/%s (already identical)", xpath, name)) + } + return false, nil + } + } + + // Perform migration + target.Set(xpath, name, sourceData) + if IsDebug() { + log.TLogln(fmt.Sprintf("Migrating %s/%s", xpath, name)) + } + + // Verify migration + if err := verifyMigration(source, target, xpath, name, sourceData); err != nil { + return false, fmt.Errorf("migration verification failed for %s/%s: %w", xpath, name, err) + } + if IsDebug() { + log.TLogln(fmt.Sprintf("Successfully migrated %s/%s", xpath, name)) + } + return true, nil +} + +// MigrateAll migrates all entries in an xpath with validation +// Returns: (migratedCount, skippedCount, error) +func MigrateAll(source, target TorrServerDB, xpath string) (int, int, error) { + names := source.List(xpath) + if len(names) == 0 { + if IsDebug() { + log.TLogln(fmt.Sprintf("No entries to migrate for %s", xpath)) + } + return 0, 0, nil + } + + migratedCount := 0 + skippedCount := 0 + var firstError error + if IsDebug() { + log.TLogln(fmt.Sprintf("Starting migration of %d %s entries", len(names), xpath)) + } + for i, name := range names { + sourceData := source.Get(xpath, name) + if sourceData == nil { + skippedCount++ + if IsDebug() { + log.TLogln(fmt.Sprintf("[%d/%d] Skipping %s/%s (no data in source)", + i+1, len(names), xpath, name)) + } + continue + } + + targetData := target.Get(xpath, name) + if targetData != nil { + // Check if already identical + if equal, err := isByteArraysEqualJson(sourceData, targetData); err == nil && equal { + skippedCount++ + if IsDebug() { + log.TLogln(fmt.Sprintf("[%d/%d] Skipping %s/%s (already identical)", + i+1, len(names), xpath, name)) + } + continue + } + } + + // Perform migration + target.Set(xpath, name, sourceData) + + // Verify migration + if err := verifyMigration(source, target, xpath, name, sourceData); err != nil { + log.TLogln(fmt.Sprintf("[%d/%d] Migration failed for %s/%s: %v", + i+1, len(names), xpath, name, err)) + if firstError == nil { + firstError = err + } + } else { + migratedCount++ + if IsDebug() { + log.TLogln(fmt.Sprintf("[%d/%d] Successfully migrated %s/%s", + i+1, len(names), xpath, name)) + } + } + } + + summary := fmt.Sprintf("%s migration complete: %d migrated, %d skipped", + xpath, migratedCount, skippedCount) + if firstError != nil { + summary += fmt.Sprintf(", 1+ errors (first: %v)", firstError) + } + if IsDebug() { + log.TLogln(summary) + } + + return migratedCount, skippedCount, firstError +} + +// SmartMigrate - keep for manual/advanced use +func SmartMigrate(bboltDB, jsonDB TorrServerDB, forceDirection string) error { + // if BTsets != nil { + // return errors.New("migration must be called before initializing BTSets") + // } + switch forceDirection { + case "viewed_to_json": + return MigrateViewedToJson(bboltDB, jsonDB) + case "viewed_to_bbolt": + return MigrateViewedFromJson(jsonDB, bboltDB) + case "settings_to_json": + return MigrateSettingsToJson(bboltDB, jsonDB) + case "settings_to_bbolt": + return MigrateSettingsFromJson(jsonDB, bboltDB) + case "sync_both": + // Simple sync: copy missing data both ways + if err := migrateMissing(bboltDB, jsonDB, "Settings", "BitTorr"); err != nil { + return err + } + return syncViewedSimple(bboltDB, jsonDB) + default: + return fmt.Errorf("unknown migration direction: %s", forceDirection) + } +} + +func isByteArraysEqualJson(a, b []byte) (bool, error) { + if len(a) == 0 && len(b) == 0 { + return true, nil + } + if len(a) == 0 || len(b) == 0 { + return false, nil + } + // Quick check: same length and byte equality + if len(a) == len(b) { + // Fast path: byte-by-byte comparison + for i := range a { + if a[i] != b[i] { + break // Need to parse as JSON + } + } + // If we get here, bytes are identical + return true, nil + } + // Parse as JSON for structural comparison + var objectA, objectB interface{} + + if err := json.Unmarshal(a, &objectA); err != nil { + return false, fmt.Errorf("error unmarshalling A: %w", err) + } + + if err := json.Unmarshal(b, &objectB); err != nil { + return false, fmt.Errorf("error unmarshalling B: %w", err) + } + + return reflect.DeepEqual(objectA, objectB), nil +} + +// Optimized version for performance +func isByteArraysEqualJsonOptimized(a, b []byte) (bool, error) { + // Fast paths + if a == nil && b == nil { + return true, nil + } + if len(a) != len(b) { + return false, nil + } + if len(a) == 0 { + return true, nil + } + // Byte equality (fastest check) + equal := true + for i := range a { + if a[i] != b[i] { + equal = false + break + } + } + if equal { + return true, nil + } + // Parse as JSON (slower but accurate) + return isByteArraysEqualJson(a, b) +} + +func verifyMigration(source, target TorrServerDB, xpath, name string, originalData []byte) error { + // Get migrated data + migratedData := target.Get(xpath, name) + if migratedData == nil { + return fmt.Errorf("migration failed: no data after migration for %s/%s", xpath, name) + } + // Compare with original + if equal, err := isByteArraysEqualJsonOptimized(originalData, migratedData); err != nil { + return fmt.Errorf("verification failed for %s/%s: %w", xpath, name, err) + } else if !equal { + return fmt.Errorf("data mismatch after migration for %s/%s", xpath, name) + } + if IsDebug() { + log.TLogln(fmt.Sprintf("Verified migration of %s/%s", xpath, name)) + } + return nil +} + +func b2i(v []byte) int64 { + return int64(binary.BigEndian.Uint64(v)) +} + +func migrateMissing(db1, db2 TorrServerDB, xpath, name string) error { + // Copy from db1 to db2 if missing + if db2.Get(xpath, name) == nil { + if data := db1.Get(xpath, name); data != nil { + db2.Set(xpath, name, data) + } + } + // Copy from db2 to db1 if missing + if db1.Get(xpath, name) == nil { + if data := db2.Get(xpath, name); data != nil { + db1.Set(xpath, name, data) + } + } + return nil +} + +func syncViewedSimple(bboltDB, jsonDB TorrServerDB) error { + // Get all hashes from both + bboltHashes := bboltDB.List("Viewed") + jsonHashes := jsonDB.List("Viewed") + + allHashes := make(map[string]bool) + for _, h := range bboltHashes { + allHashes[h] = true + } + for _, h := range jsonHashes { + allHashes[h] = true + } + + // For each hash, ensure it exists in both with merged data + for hash := range allHashes { + bboltData := bboltDB.Get("Viewed", hash) + jsonData := jsonDB.Get("Viewed", hash) + + merged := mergeViewedDataSimple(bboltData, jsonData) + if merged != nil { + bboltDB.Set("Viewed", hash, merged) + jsonDB.Set("Viewed", hash, merged) + } + } + + return nil +} + +func mergeViewedDataSimple(data1, data2 []byte) []byte { + if data1 == nil && data2 == nil { + return nil + } + if data1 == nil { + return data2 + } + if data2 == nil { + return data1 + } + + // Try to merge + var indices1, indices2 map[int]struct{} + json.Unmarshal(data1, &indices1) + json.Unmarshal(data2, &indices2) + + merged := make(map[int]struct{}) + for idx := range indices1 { + merged[idx] = struct{}{} + } + for idx := range indices2 { + merged[idx] = struct{}{} + } + + result, _ := json.Marshal(merged) + return result +} diff --git a/server/settings/settings.go b/server/settings/settings.go new file mode 100644 index 0000000..982c9ea --- /dev/null +++ b/server/settings/settings.go @@ -0,0 +1,452 @@ +package settings + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "server/log" +) + +// Add a global lock for database operations during migration +var dbMigrationLock sync.RWMutex + +func IsDebug() bool { + if BTsets != nil { + return BTsets.EnableDebug + } + return false +} + +var ( + tdb TorrServerDB + Path string + IP string + Port string + Ssl bool + SslPort string + ReadOnly bool + HttpAuth bool + SearchWA bool + PubIPv4 string + PubIPv6 string + TorAddr string + MaxSize int64 +) + +func InitSets(readOnly, searchWA bool) { + ReadOnly = readOnly + SearchWA = searchWA + + bboltDB := NewTDB() + if bboltDB == nil { + log.TLogln("Error open bboltDB:", filepath.Join(Path, "config.db")) + os.Exit(1) + } + + jsonDB := NewJsonDB() + if jsonDB == nil { + log.TLogln("Error open jsonDB") + os.Exit(1) + } + + // Optional forced migration (for manual control) + if migrationMode := os.Getenv("TS_MIGRATION_MODE"); migrationMode != "" { + log.TLogln(fmt.Sprintf("Executing forced migration: %s", migrationMode)) + if err := SmartMigrate(bboltDB, jsonDB, migrationMode); err != nil { + log.TLogln("Migration warning:", err) + } + } + + // Determine storage preferences + settingsStoragePref, viewedStoragePref := determineStoragePreferences(bboltDB, jsonDB) + + // Apply migrations (clean, one-way) + applyCleanMigrations(bboltDB, jsonDB, settingsStoragePref, viewedStoragePref) + + // Setup routing + setupDatabaseRouting(bboltDB, jsonDB, settingsStoragePref, viewedStoragePref) + + // Load settings + loadBTSets() + + // Update preferences if they changed + if BTsets != nil && (BTsets.StoreSettingsInJson != settingsStoragePref || BTsets.StoreViewedInJson != viewedStoragePref) { + BTsets.StoreSettingsInJson = settingsStoragePref + BTsets.StoreViewedInJson = viewedStoragePref + SetBTSets(BTsets) + } + + // Migrate old torrents + MigrateTorrents() + + logConfiguration(settingsStoragePref, viewedStoragePref) +} + +func determineStoragePreferences(bboltDB, jsonDB TorrServerDB) (settingsInJson, viewedInJson bool) { + // Try to load existing settings first + if existing := loadExistingSettings(bboltDB, jsonDB); existing != nil { + if IsDebug() { + log.TLogln(fmt.Sprintf("Found settings: StoreSettingsInJson=%v, StoreViewedInJson=%v", + existing.StoreSettingsInJson, existing.StoreViewedInJson)) + } + // Check if these are actually set or just default zero values + // For now, trust the stored values + return existing.StoreSettingsInJson, existing.StoreViewedInJson + } + + // Defaults (if not set by user) + settingsInJson = true // JSON for settings (easy editable) + viewedInJson = false // BBolt for viewed (performance) + + // Environment overrides + if env := os.Getenv("TS_SETTINGS_STORAGE"); env != "" { + settingsInJson = (env == "json") + } + if env := os.Getenv("TS_VIEWED_STORAGE"); env != "" { + viewedInJson = (env == "json") + } + + if IsDebug() { + log.TLogln(fmt.Sprintf("Using flags: settingsInJson=%v, viewedInJson=%v", + settingsInJson, viewedInJson)) + } + return settingsInJson, viewedInJson +} + +func loadExistingSettings(bboltDB, jsonDB TorrServerDB) *BTSets { + // Try JSON first + if buf := jsonDB.Get("Settings", "BitTorr"); buf != nil { + var sets BTSets + if err := json.Unmarshal(buf, &sets); err == nil { + return &sets + } + } + // Try BBolt + if buf := bboltDB.Get("Settings", "BitTorr"); buf != nil { + var sets BTSets + if err := json.Unmarshal(buf, &sets); err == nil { + return &sets + } + } + return nil +} + +// func loadExistingSettingsDebug(bboltDB, jsonDB TorrServerDB) *BTSets { +// // Try JSON first +// if buf := jsonDB.Get("Settings", "BitTorr"); buf != nil { +// log.TLogln(fmt.Sprintf("Found settings in JSON, size: %d bytes", len(buf))) +// var sets BTSets +// if err := json.Unmarshal(buf, &sets); err == nil { +// log.TLogln(fmt.Sprintf("Parsed from JSON: StoreSettingsInJson=%v, StoreViewedInJson=%v", +// sets.StoreSettingsInJson, sets.StoreViewedInJson)) +// return &sets +// } else { +// log.TLogln(fmt.Sprintf("Failed to parse JSON settings: %v", err)) +// } +// } else { +// log.TLogln("No settings found in JSON") +// } + +// // Try BBolt +// if buf := bboltDB.Get("Settings", "BitTorr"); buf != nil { +// log.TLogln(fmt.Sprintf("Found settings in BBolt, size: %d bytes", len(buf))) +// var sets BTSets +// if err := json.Unmarshal(buf, &sets); err == nil { +// log.TLogln(fmt.Sprintf("Parsed from BBolt: StoreSettingsInJson=%v, StoreViewedInJson=%v", +// sets.StoreSettingsInJson, sets.StoreViewedInJson)) +// return &sets +// } else { +// log.TLogln(fmt.Sprintf("Failed to parse BBolt settings: %v", err)) +// } +// } else { +// log.TLogln("No settings found in BBolt") +// } + +// log.TLogln("No existing storage settings found") +// return nil +// } + +func applyCleanMigrations(bboltDB, jsonDB TorrServerDB, settingsInJson, viewedInJson bool) { + // Settings migration + if settingsInJson { + safeMigrate(bboltDB, jsonDB, "Settings", "BitTorr", "JSON", true) + } else { + safeMigrate(jsonDB, bboltDB, "Settings", "BitTorr", "BBolt", true) + } + + // Viewed migration + if viewedInJson { + safeMigrateAll(bboltDB, jsonDB, "Viewed", "JSON", true) + } else { + safeMigrateAll(jsonDB, bboltDB, "Viewed", "BBolt", true) + } +} + +func safeMigrate(source, target TorrServerDB, xpath, name, targetName string, clearSource bool) { + if IsDebug() { + log.TLogln(fmt.Sprintf("Checking migration of %s/%s to %s", xpath, name, targetName)) + } + + migrated, err := MigrateSingle(source, target, xpath, name) + if err != nil { + log.TLogln(fmt.Sprintf("Migration error for %s/%s: %v", xpath, name, err)) + return + } + + if migrated { + log.TLogln(fmt.Sprintf("Successfully migrated %s/%s to %s", xpath, name, targetName)) + // Clear source if requested + if clearSource { + source.Rem(xpath, name) + if IsDebug() { + log.TLogln(fmt.Sprintf("Cleared %s/%s from source", xpath, name)) + } + } + } else { + log.TLogln(fmt.Sprintf("No migration needed for %s/%s (already exists or no data)", + xpath, name)) + } +} + +func safeMigrateAll(source, target TorrServerDB, xpath, targetName string, clearSource bool) { + if IsDebug() { + log.TLogln(fmt.Sprintf("Starting migration of all %s entries to %s", xpath, targetName)) + } + + migrated, skipped, err := MigrateAll(source, target, xpath) + log.TLogln(fmt.Sprintf("%s migration result: %d migrated, %d skipped", xpath, migrated, skipped)) + if err != nil { + log.TLogln(fmt.Sprintf("Migration had errors: %v", err)) + } + // Clear source if requested and we successfully migrated entries + if clearSource && migrated > 0 { + sourceCount := len(source.List(xpath)) + // Only clear if we migrated at least as many as were in source + // (accounting for possible duplicates) + if migrated >= sourceCount { + source.Clear(xpath) + if IsDebug() { + log.TLogln(fmt.Sprintf("Cleared all %s entries from source", xpath)) + } + } else { + log.TLogln(fmt.Sprintf("Not clearing %s: only migrated %d of %d entries", + xpath, migrated, sourceCount)) + } + } +} + +func setupDatabaseRouting(bboltDB, jsonDB TorrServerDB, settingsInJson, viewedInJson bool) { + dbRouter := NewXPathDBRouter() + + if settingsInJson { + dbRouter.RegisterRoute(jsonDB, "Settings") + } else { + dbRouter.RegisterRoute(bboltDB, "Settings") + } + + if viewedInJson { + dbRouter.RegisterRoute(jsonDB, "Viewed") + } else { + dbRouter.RegisterRoute(bboltDB, "Viewed") + } + + dbRouter.RegisterRoute(bboltDB, "Torrents") + tdb = NewDBReadCache(dbRouter) +} + +func logConfiguration(settingsInJson, viewedInJson bool) { + settingsLoc := "JSON" + if !settingsInJson { + settingsLoc = "BBolt" + } + viewedLoc := "JSON" + if !viewedInJson { + viewedLoc = "BBolt" + } + + log.TLogln(fmt.Sprintf("Storage: Settings->%s, Viewed->%s, Torrents->BBolt", + settingsLoc, viewedLoc)) +} + +// SwitchSettingsStorage - simplified version +func SwitchSettingsStorage(useJson bool) error { + if ReadOnly { + return errors.New("read-only mode") + } + // Acquire exclusive lock for migration + dbMigrationLock.Lock() + defer dbMigrationLock.Unlock() + + bboltDB := NewTDB() + if bboltDB == nil { + return errors.New("failed to open BBolt DB") + } + // DON'T CLOSE! They're still in use by tdb + // defer bboltDB.CloseDB() + + jsonDB := NewJsonDB() + if jsonDB == nil { + return errors.New("failed to open JSON DB") + } + // DON'T CLOSE! They're still in use by tdb + // defer jsonDB.CloseDB() + + log.TLogln(fmt.Sprintf("Switching Settings storage to %s", + map[bool]string{true: "JSON", false: "BBolt"}[useJson])) + + // Update storage preference (must be called before migrate as this setting migrate too) + if BTsets != nil { + BTsets.StoreSettingsInJson = useJson + SetBTSets(BTsets) + } + + var err error + if useJson { + err = MigrateSettingsToJson(bboltDB, jsonDB) + } else { + err = MigrateSettingsFromJson(jsonDB, bboltDB) + } + + if err != nil { + return err + } + + log.TLogln("Settings storage switched. Restart required for routing changes.") + return nil +} + +// SwitchViewedStorage - simplified version +func SwitchViewedStorage(useJson bool) error { + if ReadOnly { + return errors.New("read-only mode") + } + // Acquire exclusive lock for migration + dbMigrationLock.Lock() + defer dbMigrationLock.Unlock() + + bboltDB := NewTDB() + if bboltDB == nil { + return errors.New("failed to open BBolt DB") + } + // DON'T CLOSE! They're still in use by tdb + // defer bboltDB.CloseDB() + + jsonDB := NewJsonDB() + if jsonDB == nil { + return errors.New("failed to open JSON DB") + } + // DON'T CLOSE! They're still in use by tdb + // defer jsonDB.CloseDB() + + log.TLogln(fmt.Sprintf("Switching Viewed storage to %s", + map[bool]string{true: "JSON", false: "BBolt"}[useJson])) + + var err error + if useJson { + err = MigrateViewedToJson(bboltDB, jsonDB) + if err == nil { + bboltDB.Clear("Viewed") + } + } else { + err = MigrateViewedFromJson(jsonDB, bboltDB) + if err == nil { + jsonDB.Clear("Viewed") + } + } + + if err != nil { + return err + } + + // Update preference + if BTsets != nil { + BTsets.StoreViewedInJson = useJson + SetBTSets(BTsets) + } + + log.TLogln("Viewed storage switched. Restart required for routing changes.") + return nil +} + +// Used in /storage/settings web API +func GetStoragePreferences() map[string]interface{} { + prefs := map[string]interface{}{ + "settings": "json", // Default fallback + "viewed": "bbolt", // Default fallback + } + + if BTsets != nil { + // Convert boolean preferences to string values + if BTsets.StoreSettingsInJson { + prefs["settings"] = "json" + } else { + prefs["settings"] = "bbolt" + } + + if BTsets.StoreViewedInJson { + prefs["viewed"] = "json" + } else { + prefs["viewed"] = "bbolt" + } + } + + if IsDebug() { + log.TLogln(fmt.Sprintf("GetStoragePreferences: settings=%s, viewed=%s", + prefs["settings"], prefs["viewed"])) + } + if tdb != nil { + prefs["viewedCount"] = len(tdb.List("Viewed")) + } + + return prefs +} + +// Used in /storage/settings web API +func SetStoragePreferences(prefs map[string]interface{}) error { + if ReadOnly || BTsets == nil { + return errors.New("cannot change storage preferences. Read-only mode") + } + + if IsDebug() { + log.TLogln(fmt.Sprintf("SetStoragePreferences received: %v", prefs)) + } + + // Apply changes + if settingsPref, ok := prefs["settings"].(string); ok && settingsPref != "" { + useJson := (settingsPref == "json") + if IsDebug() { + log.TLogln(fmt.Sprintf("Changing settings storage to useJson=%v (was %v)", + useJson, BTsets.StoreSettingsInJson)) + } + if BTsets.StoreSettingsInJson != useJson { + if err := SwitchSettingsStorage(useJson); err != nil { + return fmt.Errorf("failed to switch settings storage: %w", err) + } + } + } + + if viewedPref, ok := prefs["viewed"].(string); ok && viewedPref != "" { + useJson := (viewedPref == "json") + if IsDebug() { + log.TLogln(fmt.Sprintf("Changing viewed storage to useJson=%v (was %v)", + useJson, BTsets.StoreViewedInJson)) + } + if BTsets.StoreViewedInJson != useJson { + if err := SwitchViewedStorage(useJson); err != nil { + return fmt.Errorf("failed to switch viewed storage: %w", err) + } + } + } + + return nil +} + +func CloseDB() { + if tdb != nil { + tdb.CloseDB() + } +} diff --git a/server/settings/torrent.go b/server/settings/torrent.go new file mode 100644 index 0000000..53a9f96 --- /dev/null +++ b/server/settings/torrent.go @@ -0,0 +1,86 @@ +package settings + +import ( + "encoding/json" + "sort" + "sync" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +type TorrentDB struct { + *torrent.TorrentSpec + + Title string `json:"title,omitempty"` + Category string `json:"category,omitempty"` + Poster string `json:"poster,omitempty"` + Data string `json:"data,omitempty"` + + Timestamp int64 `json:"timestamp,omitempty"` + Size int64 `json:"size,omitempty"` +} + +type File struct { + Name string `json:"name,omitempty"` + Id int `json:"id,omitempty"` + Size int64 `json:"size,omitempty"` +} + +var mu sync.Mutex + +func AddTorrent(torr *TorrentDB) { + list := ListTorrent() + mu.Lock() + find := -1 + for i, db := range list { + if db.InfoHash.HexString() == torr.InfoHash.HexString() { + find = i + break + } + } + if find != -1 { + list[find] = torr + } else { + list = append(list, torr) + } + for _, db := range list { + buf, err := json.Marshal(db) + if err == nil { + tdb.Set("Torrents", db.InfoHash.HexString(), buf) + } + } + mu.Unlock() +} + +func ListTorrent() []*TorrentDB { + // Use read lock to prevent migration during read + dbMigrationLock.RLock() + defer dbMigrationLock.RUnlock() + + mu.Lock() + defer mu.Unlock() + + var list []*TorrentDB + keys := tdb.List("Torrents") + for _, key := range keys { + buf := tdb.Get("Torrents", key) + if len(buf) > 0 { + var torr *TorrentDB + err := json.Unmarshal(buf, &torr) + if err == nil { + list = append(list, torr) + } + } + } + sort.Slice(list, func(i, j int) bool { + return list[i].Timestamp > list[j].Timestamp + }) + return list +} + +func RemTorrent(hash metainfo.Hash) { + mu.Lock() + tdb.Rem("Torrents", hash.HexString()) + mu.Unlock() +} diff --git a/server/settings/torrserverdb.go b/server/settings/torrserverdb.go new file mode 100644 index 0000000..dcf1d1e --- /dev/null +++ b/server/settings/torrserverdb.go @@ -0,0 +1,10 @@ +package settings + +type TorrServerDB interface { + CloseDB() + Get(xPath, name string) []byte + Set(xPath, name string, value []byte) + List(xPath string) []string + Rem(xPath, name string) + Clear(xPath string) +} diff --git a/server/settings/viewed.go b/server/settings/viewed.go new file mode 100644 index 0000000..4cc1643 --- /dev/null +++ b/server/settings/viewed.go @@ -0,0 +1,98 @@ +package settings + +import ( + "encoding/json" + + "server/log" +) + +type Viewed struct { + Hash string `json:"hash"` + FileIndex int `json:"file_index"` +} + +func SetViewed(vv *Viewed) { + var indexes map[int]struct{} + var err error + + buf := tdb.Get("Viewed", vv.Hash) + if len(buf) == 0 { + indexes = make(map[int]struct{}) + indexes[vv.FileIndex] = struct{}{} + buf, err = json.Marshal(indexes) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } else { + err = json.Unmarshal(buf, &indexes) + if err == nil { + indexes[vv.FileIndex] = struct{}{} + buf, err = json.Marshal(indexes) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } + } + if err != nil { + log.TLogln("Error set viewed:", err) + } +} + +func RemViewed(vv *Viewed) { + buf := tdb.Get("Viewed", vv.Hash) + var indeces map[int]struct{} + err := json.Unmarshal(buf, &indeces) + if err == nil { + if vv.FileIndex != -1 { + delete(indeces, vv.FileIndex) + buf, err = json.Marshal(indeces) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } else { + tdb.Rem("Viewed", vv.Hash) + } + } + if err != nil { + log.TLogln("Error rem viewed:", err) + } +} + +func ListViewed(hash string) []*Viewed { + var err error + if hash != "" { + buf := tdb.Get("Viewed", hash) + if len(buf) == 0 { + return []*Viewed{} + } + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + var ret []*Viewed + for i := range indeces { + ret = append(ret, &Viewed{hash, i}) + } + return ret + } + } else { + var ret []*Viewed + keys := tdb.List("Viewed") + for _, key := range keys { + buf := tdb.Get("Viewed", key) + if len(buf) == 0 { + return []*Viewed{} + } + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + for i := range indeces { + ret = append(ret, &Viewed{key, i}) + } + } + } + return ret + } + + log.TLogln("Error list viewed:", err) + return []*Viewed{} +} diff --git a/server/settings/xpathdbrouter.go b/server/settings/xpathdbrouter.go new file mode 100644 index 0000000..745d2e3 --- /dev/null +++ b/server/settings/xpathdbrouter.go @@ -0,0 +1,129 @@ +package settings + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "server/log" + + "golang.org/x/exp/slices" +) + +type XPathDBRouter struct { + dbs []TorrServerDB + routes []string + route2db map[string]TorrServerDB + dbNames map[TorrServerDB]string +} + +func NewXPathDBRouter() *XPathDBRouter { + router := &XPathDBRouter{ + dbs: []TorrServerDB{}, + dbNames: map[TorrServerDB]string{}, + routes: []string{}, + route2db: map[string]TorrServerDB{}, + } + return router +} + +func (v *XPathDBRouter) RegisterRoute(db TorrServerDB, xPath string) error { + newRoute := v.xPathToRoute(xPath) + + if slices.Contains(v.routes, newRoute) { + return fmt.Errorf("route \"%s\" already in routing table", newRoute) + } + + // First DB becomes Default DB with default route + if len(v.dbs) == 0 && len(newRoute) != 0 { + v.RegisterRoute(db, "") + } + + if !slices.Contains(v.dbs, db) { + v.dbs = append(v.dbs, db) + v.dbNames[db] = reflect.TypeOf(db).Elem().Name() + v.log(fmt.Sprintf("Registered new DB \"%s\", total %d DBs registered", v.getDBName(db), len(v.dbs))) + } + + v.route2db[newRoute] = db + v.routes = append(v.routes, newRoute) + + // Sort routes by length descending. + // It is important later to help selecting + // most suitable route in getDBForXPath(xPath) + sort.Slice(v.routes, func(iLeft, iRight int) bool { + return len(v.routes[iLeft]) > len(v.routes[iRight]) + }) + v.log(fmt.Sprintf("Registered new route \"%s\" for DB \"%s\", total %d routes", getDefaultRoureName(newRoute), v.getDBName(db), len(v.routes))) + return nil +} + +func getDefaultRoureName(route string) string { + if len(route) > 0 { + return route + } + return "default" +} + +func (v *XPathDBRouter) xPathToRoute(xPath string) string { + return strings.ToLower(strings.TrimSpace(xPath)) +} + +func (v *XPathDBRouter) getDBForXPath(xPath string) TorrServerDB { + if len(v.dbs) == 0 { + return nil + } + lookup_route := v.xPathToRoute(xPath) + var db TorrServerDB = nil + // Expected v.routes sorted by length descending + for _, route_prefix := range v.routes { + if strings.HasPrefix(lookup_route, route_prefix) { + db = v.route2db[route_prefix] + break + } + } + return db +} + +func (v *XPathDBRouter) Get(xPath, name string) []byte { + return v.getDBForXPath(xPath).Get(xPath, name) +} + +func (v *XPathDBRouter) Set(xPath, name string, value []byte) { + v.getDBForXPath(xPath).Set(xPath, name, value) +} + +func (v *XPathDBRouter) List(xPath string) []string { + return v.getDBForXPath(xPath).List(xPath) +} + +func (v *XPathDBRouter) Rem(xPath, name string) { + v.getDBForXPath(xPath).Rem(xPath, name) +} + +func (v *XPathDBRouter) Clear(xPath string) { + v.getDBForXPath(xPath).Clear(xPath) +} + +func (v *XPathDBRouter) CloseDB() { + for _, db := range v.dbs { + db.CloseDB() + } + v.dbs = nil + v.routes = nil + v.route2db = nil + v.dbNames = nil +} + +func (v *XPathDBRouter) getDBName(db TorrServerDB) string { + return v.dbNames[db] +} + +func (v *XPathDBRouter) log(s string, params ...interface{}) { + if len(params) > 0 { + log.TLogln(fmt.Sprintf("XPathDBRouter: %s: %s", s, fmt.Sprint(params...))) + } else { + log.TLogln(fmt.Sprintf("XPathDBRouter: %s", s)) + } +} diff --git a/server/tgbot/README.md b/server/tgbot/README.md new file mode 100644 index 0000000..da06aac --- /dev/null +++ b/server/tgbot/README.md @@ -0,0 +1,162 @@ +# TorrServer Telegram Bot + +[![GitHub License](https://img.shields.io/github/license/YouROK/TorrServer)](https://github.com/YouROK/TorrServer/blob/master/LICENSE) +[![TorrServer Integrated](https://img.shields.io/badge/TorrServer-integrated-blue)](https://github.com/YouROK/TorrServer) + +## Introduction + +Telegram bot for managing [TorrServer](https://github.com/YouROK/TorrServer) — add torrents, stream, search, and control the server directly from Telegram. + +## Features + +- Torrent management — add, remove, drop, list via magnet, hash, or `torrs://` +- Export & import — magnets list; import multiple from text +- Streaming — playback links, M3U playlists, preload +- Search — RuTor and Torznab with one-click add +- Inline mode — `@botname` in any chat: list torrents or search +- Status & snake — real-time status, cache visualization +- File operations — browse files, download to Telegram +- FFprobe — media metadata via `/ffp` +- Localization — Russian and English +- Admin — shutdown, settings, presets (whitelist users only) + +## Getting Started + +### Enable the Bot + +Start TorrServer with a Telegram bot token: + +```bash +TorrServer --tg YOUR_BOT_TOKEN +``` + +Or use `-T`: + +```bash +TorrServer -T YOUR_BOT_TOKEN +``` + +Create a bot via [@BotFather](https://t.me/BotFather) to get the token. + +### Configuration + +Config file `tg.cfg` (JSON) in the TorrServer data directory: + +| Field | Description | +|------------|-------------| +| `HostTG` | Telegram API URL (default: `https://api.telegram.org`) | +| `HostWeb` | Base URL for stream links (auto-detected if empty) | +| `Socks5` | Optional SOCKS5 for reaching Telegram (e.g. `127.0.0.1:1080`, `socks5://user:pass@host:port`) if direct access to `api.telegram.org` is blocked or times out | +| `WhiteIds` | Allowed user IDs (empty = allow all) | +| `BlackIds` | Blocked user IDs | + +Example: + +```json +{ + "HostTG": "https://api.telegram.org", + "HostWeb": "http://192.168.1.100:8090", + "Socks5": "127.0.0.1:1080", + "WhiteIds": [123456789], + "BlackIds": [] +} +``` + +If your network cannot connect to Telegram’s API directly, run a local SOCKS5 proxy (for example [sing-box](https://github.com/SagerNet/sing-box), v2ray, or `ssh -D`) and set `Socks5` to its address. + +## Commands + +### Core + +| Command | Description | +|---------|-------------| +| `/help`, `/start`, `/id` | Help and user ID | +| `/list [compact]` | List torrents with buttons | +| `/add ` | Add torrent (magnet, hash, torrs://) | +| `/clear` | Remove all (with confirmation) | +| `/hash [N]` | Show info hashes | + +### Management + +| Command | Description | +|---------|-------------| +| `/remove ` | Remove torrent | +| `/drop ` | Disconnect (keep in DB) | +| `/set ` | Set title | +| `/status [hash\|N]` | Status with refresh/stop | +| `/cache <hash\|N>` | Cache stats | +| `/preload <hash\|N> <index>` | Preload file | + +### Links & Playback + +| Command | Description | +|---------|-------------| +| `/link`, `/play` | Stream URL | +| `/m3u`, `/m3uall` | M3U playlist | + +### Search + +| Command | Description | +|---------|-------------| +| `/search <query>` | RuTor + Torznab (all sources) | +| `/rutor <query>` | RuTor only | +| `/torznab <query> [index]` | Torznab indexers | + +### Other + +| Command | Description | +|---------|-------------| +| `/export`, `/import` | Export/import magnets | +| `/categories` | List categories | +| `/server`, `/stats`, `/stat` | Server info | +| `/viewed` | Viewed files | +| `/ffp <hash\|N> <id> [json]` | FFprobe metadata | +| `/speedtest [size]` | Download test (1–100 MB) | +| `/snake [hash\|N] [cols] [rows]` | Cache visualization | +| `/lang [RU\|EN]` | Language | + +### Admin Only + +| Command | Description | +|---------|-------------| +| `/shutdown` | Shut down server | +| `/settings` | Interactive settings menu (sub-pages: Search, Network, Other, Cache, Paths, Storage) | +| `/preset <name>` | Apply named preset: `performance`, `storage`, `streaming`, `low`, `default` | +| `/preset <key> <value> ...` | Apply key-value pairs: `cache 256`, `preload 50`, `conn 100`, etc. | + +**Preset examples:** +- `/preset performance` — max cache, high preload, no limits +- `/preset cache 256 preload 50` — set cache 256 MB and preload 50% +- `/preset cache 512 conn 100 down 0 up 0` — multiple values + +**Preset keys:** `cache`, `preload`, `readahead`, `conn`, `timeout`, `port`, `down`, `up`, `retr`, `responsive`, `cachedrop` + +## Inline Mode + +Type `@YourBotName` in any chat: + +- **Empty, "list", or "play"** — torrents with play links +- **2+ characters** — search RuTor + Torznab + +## Text Input + +Paste as plain message to add torrent: + +- `magnet:?xt=urn:btih:...` +- `torrs://...` +- 40-char info hash + +Reply to file list with `2-12` to download files 2–12 to Telegram. + +## Security + +- **Whitelist** — restrict to specific user IDs +- **Blacklist** — block user IDs +- **Admin** — when whitelist is used, admin = whitelisted users +- **Settings** — sensitive values masked in `/settings` + +## Dependencies + +- [telebot v4](https://gopkg.in/telebot.v4) — Telegram Bot API +- [go-humanize](https://github.com/dustin/go-humanize) +- [go-ffprobe](https://gopkg.in/vansante/go-ffprobe.v2) diff --git a/server/tgbot/add.go b/server/tgbot/add.go new file mode 100644 index 0000000..cc42a8a --- /dev/null +++ b/server/tgbot/add.go @@ -0,0 +1,129 @@ +package tgbot + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/anacrolix/torrent" + tele "gopkg.in/telebot.v4" + "server/log" + set "server/settings" + "server/torr" + "server/web/api/utils" +) + +func addTorrentFromSpec(c tele.Context, torrSpec *torrent.TorrentSpec, displayLabel string) error { + msg, err := c.Bot().Send(c.Sender(), tr(c.Sender().ID, "connecting")) + if err != nil { + return err + } + + tor, err := torr.AddTorrent(torrSpec, "", "", "", "") + if err != nil { + log.TLogln("tg add err", err) + _, _ = c.Bot().Edit(msg, fmt.Sprintf(tr(c.Sender().ID, "add_error"), err.Error())) + return err + } + if tor == nil { + _, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_not_created")) + return errors.New("torrent not created") + } + + if set.BTsets != nil && set.BTsets.EnableDebug { + if tor.Data != "" { + log.TLogln("tg add data", logSafeStr(tor.Data, 60)) + } + if tor.Category != "" { + log.TLogln("tg add category", logSafeStr(tor.Category, 40)) + } + } + + _, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_getting_meta")) + if !tor.GotInfo() { + log.TLogln("tg add err", "timeout get torrent info") + _, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_timeout")) + return errors.New("timeout connection get torrent info") + } + + if tor.Title == "" { + tor.Title = torrSpec.DisplayName + tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "") + tor.Title = strings.ReplaceAll(tor.Title, "_", " ") + tor.Title = strings.Trim(tor.Title, " ") + if tor.Title == "" { + tor.Title = tor.Name() + } + } + + torr.SaveTorrentToDB(tor) + + if len(displayLabel) > 80 { + displayLabel = displayLabel[:77] + "..." + } + _, _ = c.Bot().Edit(msg, fmt.Sprintf(tr(c.Sender().ID, "add_success"), displayLabel)) + + return nil +} + +func addTorrent(c tele.Context, link string) error { + log.TLogln("tg add torrent", logHashOrTruncate(link)) + link = strings.ReplaceAll(link, "&", "&") + var torrSpec *torrent.TorrentSpec + var err error + if strings.HasPrefix(strings.ToLower(link), "torrs://") { + torrSpec, _, err = utils.ParseTorrsHash(link) + } else { + torrSpec, err = utils.ParseLink(link) + } + if err != nil { + log.TLogln("tg add parse err", err) + return err + } + return addTorrentFromSpec(c, torrSpec, link) +} + +func addTorrentFromDocument(c tele.Context, doc *tele.Document) error { + if doc == nil || doc.FileID == "" { + return errors.New("no document") + } + reader, err := c.Bot().File(&doc.File) + if err != nil { + log.TLogln("tg add document getfile err", err) + return err + } + defer func() { _ = reader.Close() }() + data, err := io.ReadAll(reader) + if err != nil { + log.TLogln("tg add document read err", err) + return err + } + torrSpec, err := utils.ParseFromBytes(data) + if err != nil { + log.TLogln("tg add document parse err", err) + return err + } + displayLabel := doc.FileName + if displayLabel == "" { + displayLabel = ".torrent" + } + return addTorrentFromSpec(c, torrSpec, displayLabel) +} + +func cmdAdd(c tele.Context) error { + uid := c.Sender().ID + args := c.Args() + if len(args) == 0 { + return c.Send(tr(uid, "add_usage")) + } + link := strings.TrimSpace(strings.Join(args, " ")) + if link == "" { + return c.Send(tr(uid, "add_no_link")) + } + err := addTorrent(c, link) + if err != nil { + return err + } + return list(c) +} diff --git a/server/tgbot/admin_common.go b/server/tgbot/admin_common.go new file mode 100644 index 0000000..9200fea --- /dev/null +++ b/server/tgbot/admin_common.go @@ -0,0 +1,17 @@ +package tgbot + +import ( + "server/tgbot/config" +) + +func isAdmin(userID int64) bool { + if len(config.Cfg.WhiteIds) == 0 { + return false + } + for _, id := range config.Cfg.WhiteIds { + if id == userID { + return true + } + } + return false +} diff --git a/server/tgbot/admin_preset.go b/server/tgbot/admin_preset.go new file mode 100644 index 0000000..8361d56 --- /dev/null +++ b/server/tgbot/admin_preset.go @@ -0,0 +1,275 @@ +package tgbot + +import ( + "strings" + "sync" + "time" + + tele "gopkg.in/telebot.v4" + "server/dlna" + "server/rutor" + "server/settings" + "server/torr" +) + +type pendingPreset struct { + Sets *settings.BTSets + Preset string // name for display + UserID int64 + IsDef bool + CreatedAt time.Time +} + +var ( + pendingPresetMu sync.Mutex + pendingPresets = make(map[string]pendingPreset) +) + +func init() { + go func() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + pendingPresetMu.Lock() + now := time.Now() + for key, p := range pendingPresets { + if now.Sub(p.CreatedAt) > 30*time.Minute { + delete(pendingPresets, key) + } + } + pendingPresetMu.Unlock() + } + }() +} + +func cmdPreset(c tele.Context) error { + uid := c.Sender().ID + if !isAdmin(uid) { + return c.Send(tr(uid, "admin_only")) + } + if settings.BTsets == nil { + return c.Send(tr(uid, "settings_not_loaded")) + } + if settings.ReadOnly { + return c.Send(tr(uid, "settings_readonly")) + } + + args := strings.Fields(c.Text()) + if len(args) < 2 { + return c.Send(tr(uid, "preset_usage")) + } + + sets := new(settings.BTSets) + *sets = *settings.BTsets + + first := strings.ToLower(args[1]) + presetName := first + + if len(args) == 2 { + if ok, _ := applyNamedPreset(sets, first, uid); ok { + return sendPresetConfirm(c, uid, sets, presetName, false) + } + if first == "default" || first == "def" || first == "сброс" { + return sendPresetConfirm(c, uid, nil, "default", true) + } + } + + // Parse key-value pairs: cache 256 preload 50 conn 100 + applied, errMsg := applyPresetKV(sets, args[1:], uid) + if !applied { + return c.Send(errMsg) + } + presetName = strings.Join(args[1:], " ") + return sendPresetConfirm(c, uid, sets, presetName, false) +} + +func sendPresetConfirm(c tele.Context, uid int64, sets *settings.BTSets, presetName string, isDef bool) error { + btnYes := tele.InlineButton{Text: tr(uid, "btn_yes"), Unique: "fpreset", Data: "1"} + btnNo := tele.InlineButton{Text: tr(uid, "btn_no"), Unique: "fpreset", Data: "0"} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnYes, btnNo}}} + msg := tr(uid, "preset_confirm") + "\n\n<code>" + presetName + "</code>" + sent, err := c.Bot().Send(c.Chat(), msg, kbd, tele.ModeHTML) + if err != nil { + return err + } + pendingPresetMu.Lock() + pendingPresets[chatMsgKey(sent.Chat.ID, sent.ID)] = pendingPreset{ + Sets: sets, Preset: presetName, UserID: uid, IsDef: isDef, CreatedAt: time.Now(), + } + pendingPresetMu.Unlock() + return nil +} + +func presetConfirm(c tele.Context, confirm string) error { + uid := c.Sender().ID + if !isAdmin(uid) { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "admin_only")}) + } + key := chatMsgKey(c.Callback().Message.Chat.ID, c.Callback().Message.ID) + pendingPresetMu.Lock() + p, ok := pendingPresets[key] + delete(pendingPresets, key) + pendingPresetMu.Unlock() + if !ok || p.UserID != uid { + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "canceled")}) + } + if confirm != "1" { + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "canceled")}) + } + _ = c.Bot().Delete(c.Callback().Message) + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + if p.IsDef { + torr.SetDefSettings() + dlna.Stop() + rutor.Stop() + rutor.Start() + return c.Send(tr(uid, "settings_reset_done")) + } + if p.Sets == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "callback_unknown")}) + } + torr.SetSettings(p.Sets) + dlna.Stop() + if p.Sets.EnableDLNA { + dlna.Start() + } + rutor.Stop() + rutor.Start() + return c.Send(tr(uid, "preset_applied") + p.Preset) +} + +func applyNamedPreset(s *settings.BTSets, name string, uid int64) (bool, string) { + switch name { + case "performance", "perf", "производительность": + s.CacheSize = 512 * 1024 * 1024 + s.PreloadCache = 95 + s.ReaderReadAHead = 100 + s.ConnectionsLimit = 100 + s.TorrentDisconnectTimeout = 60 + s.PeersListenPort = 0 + s.DownloadRateLimit = 0 + s.UploadRateLimit = 0 + s.RetrackersMode = 1 + s.ResponsiveMode = true + return true, tr(uid, "preset_applied") + " performance" + case "storage", "store", "хранение": + s.CacheSize = 64 * 1024 * 1024 + s.PreloadCache = 25 + s.ReaderReadAHead = 50 + s.RemoveCacheOnDrop = true + return true, tr(uid, "preset_applied") + " storage" + case "streaming", "stream", "стриминг": + s.CacheSize = 256 * 1024 * 1024 + s.PreloadCache = 75 + s.ReaderReadAHead = 95 + s.ConnectionsLimit = 50 + s.ResponsiveMode = true + return true, tr(uid, "preset_applied") + " streaming" + case "low", "minimal", "минимум": + s.CacheSize = 64 * 1024 * 1024 + s.PreloadCache = 25 + s.ReaderReadAHead = 50 + s.ConnectionsLimit = 25 + s.TorrentDisconnectTimeout = 30 + return true, tr(uid, "preset_applied") + " low" + case "default", "def", "сброс": + return false, "" // handled in cmdPreset + } + return false, "" +} + +func applyPresetKV(s *settings.BTSets, args []string, uid int64) (bool, string) { + if len(args) < 2 { + return false, tr(uid, "preset_usage") + } + applied := false + for i := 0; i < len(args)-1; i += 2 { + key := strings.ToLower(args[i]) + val := strings.ToLower(strings.TrimSpace(args[i+1])) + ok := false + switch key { + case "cache": + if v := parseInt(val); v > 0 { + s.CacheSize = int64(v) * 1024 * 1024 + ok = true + } + case "preload": + if v := parseInt(val); v >= 0 && v <= 100 { + s.PreloadCache = v + ok = true + } + case "readahead": + if v := parseInt(val); v >= 5 && v <= 100 { + s.ReaderReadAHead = v + ok = true + } + case "conn", "connections": + if v := parseInt(val); v > 0 { + s.ConnectionsLimit = v + ok = true + } + case "timeout": + if v := parseInt(val); v > 0 { + s.TorrentDisconnectTimeout = v + ok = true + } + case "port": + v := parseInt(val) + if val == "auto" || val == "0" { + v = 0 + } + if v >= 0 && (v == 0 || (v >= 1024 && v <= 65535)) { + s.PeersListenPort = v + ok = true + } + case "down", "download": + v := 0 + if val != "inf" && val != "∞" && val != "0" { + v = parseInt(val) + } + s.DownloadRateLimit = v + ok = true + case "up", "upload": + v := 0 + if val != "inf" && val != "∞" && val != "0" { + v = parseInt(val) + } + s.UploadRateLimit = v + ok = true + case "retr", "retrackers": + var v int + switch val { + case "off": + v = 0 + case "add": + v = 1 + case "rem", "remove": + v = 2 + case "repl", "replace": + v = 3 + default: + v = parseInt(val) + } + if v >= 0 && v <= 3 { + s.RetrackersMode = v + ok = true + } + case "responsive": + s.ResponsiveMode = val == "1" || val == "on" || val == "true" || val == "да" || val == "yes" + ok = true + case "cachedrop": + s.RemoveCacheOnDrop = val == "1" || val == "on" || val == "true" || val == "да" || val == "yes" + ok = true + } + if ok { + applied = true + } + } + if !applied { + return false, tr(uid, "preset_usage") + } + return true, "" +} diff --git a/server/tgbot/admin_settings.go b/server/tgbot/admin_settings.go new file mode 100644 index 0000000..b56a3e0 --- /dev/null +++ b/server/tgbot/admin_settings.go @@ -0,0 +1,654 @@ +package tgbot + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + tele "gopkg.in/telebot.v4" + "server/dlna" + "server/rutor" + "server/settings" + "server/torr" +) + +func cmdSettings(c tele.Context) error { + uid := c.Sender().ID + if settings.BTsets == nil { + return c.Send(tr(uid, "settings_not_loaded")) + } + return sendSettingsMenu(c, uid) +} + +func sendSettingsMenu(c tele.Context, uid int64) error { + return sendSettingsMenuPage(c, uid, "1") +} + +func sendSettingsMenuPage(c tele.Context, uid int64, page string) error { + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + return c.Send(msg, kbd) +} + +func sendSettingsMenuText(c tele.Context, uid int64, page string) string { + s := settings.BTsets + msg := "⚙️ <b>" + tr(uid, "settings_title") + "</b>" + switch page { + case "1": + msg += "\n\n" + msg += fmt.Sprintf("🔍 %s: RuTor %s · Torznab %s\n", tr(uid, "settings_section_search"), boolIcon(s.EnableRutorSearch), boolIcon(s.EnableTorznabSearch)) + msg += fmt.Sprintf("📺 %s: DLNA %s · IPv6 %s · DHT %s · PEX %s · TCP %s · UTP %s\n", tr(uid, "settings_section_network"), boolIcon(s.EnableDLNA), boolIcon(s.EnableIPv6), boolIcon(!s.DisableDHT), boolIcon(!s.DisablePEX), boolIcon(!s.DisableTCP), boolIcon(!s.DisableUTP)) + msg += fmt.Sprintf("📦 %s: CacheDrop %s · Proxy %s · UseDisk %s\n", tr(uid, "settings_section_other"), boolIcon(s.RemoveCacheOnDrop), boolIcon(s.EnableProxy), boolIcon(s.UseDisk)) + case "1a": + msg += " — " + tr(uid, "settings_section_search") + msg += "\n\n" + msg += fmt.Sprintf("RuTor %s · Torznab %s", boolIcon(s.EnableRutorSearch), boolIcon(s.EnableTorznabSearch)) + case "1b": + msg += " — " + tr(uid, "settings_section_network") + msg += "\n\n" + msg += fmt.Sprintf("DLNA %s · IPv6 %s · Upload %s · DHT %s · PEX %s\n", boolIcon(s.EnableDLNA), boolIcon(s.EnableIPv6), boolIcon(!s.DisableUpload), boolIcon(!s.DisableDHT), boolIcon(!s.DisablePEX)) + msg += fmt.Sprintf("TCP %s · UTP %s · UPNP %s · Encrypt %s · Debug %s", boolIcon(!s.DisableTCP), boolIcon(!s.DisableUTP), boolIcon(!s.DisableUPNP), boolIcon(s.ForceEncrypt), boolIcon(s.EnableDebug)) + case "1c": + msg += " — " + tr(uid, "settings_section_other") + msg += "\n\n" + msg += fmt.Sprintf("CacheDrop %s · Responsive %s · Proxy %s · UseDisk %s · FSActive %s", boolIcon(s.RemoveCacheOnDrop), boolIcon(s.ResponsiveMode), boolIcon(s.EnableProxy), boolIcon(s.UseDisk), boolIcon(s.ShowFSActiveTorr)) + case "2": + msg += " — " + tr(uid, "settings_page2") + msg += "\n\n" + msg += fmt.Sprintf("💾 %s: %d MB · Preload %d%% · ReadAhead %d%%\n", tr(uid, "settings_limits_cache"), s.CacheSize/(1024*1024), s.PreloadCache, s.ReaderReadAHead) + msg += fmt.Sprintf("🔌 %s: %d · Port %s · Timeout %ds\n", tr(uid, "settings_limits_connections"), s.ConnectionsLimit, portStr(s.PeersListenPort), s.TorrentDisconnectTimeout) + msg += fmt.Sprintf("⬇️ %s: Down %s · Up %s · Retr %s\n", tr(uid, "settings_limits_speed"), rateStr(s.DownloadRateLimit), rateStr(s.UploadRateLimit), retrackersStr(s.RetrackersMode)) + case "2a": + msg += " — " + tr(uid, "settings_page2") + " · " + tr(uid, "settings_limits_cache") + msg += "\n\n" + msg += fmt.Sprintf("Cache %d MB · Preload %d%% · ReadAhead %d%%", s.CacheSize/(1024*1024), s.PreloadCache, s.ReaderReadAHead) + case "2b": + msg += " — " + tr(uid, "settings_page2") + " · " + tr(uid, "settings_limits_connections") + msg += "\n\n" + msg += fmt.Sprintf("Connections %d · Port %s · Timeout %ds", s.ConnectionsLimit, portStr(s.PeersListenPort), s.TorrentDisconnectTimeout) + case "2c": + msg += " — " + tr(uid, "settings_page2") + " · " + tr(uid, "settings_limits_speed") + msg += "\n\n" + msg += fmt.Sprintf("Down %s · Up %s · Retrackers %s", rateStr(s.DownloadRateLimit), rateStr(s.UploadRateLimit), retrackersStr(s.RetrackersMode)) + case "3": + msg += " — " + tr(uid, "settings_page3") + msg += "\n\n" + msg += fmt.Sprintf("📺 DLNA: %s · 💾 Path: %s\n", maskStr(s.FriendlyName, 25), maskVal(s.TorrentsSavePath)) + msg += fmt.Sprintf("🔐 SSL: %s · 🔑 TMDB: %s · Torznab: %d\n", maskVal(s.SslCert), maskVal(s.TMDBSettings.APIKey), len(s.TorznabUrls)) + msg += fmt.Sprintf("🌐 Proxy: %s", maskStr(strings.Join(s.ProxyHosts, ", "), 35)) + case "4": + msg += " — " + tr(uid, "settings_page4") + msg += "\n\n" + msg += fmt.Sprintf("📄 %s: %s · 📺 %s: %s\n", tr(uid, "settings_storage_settings"), storageType(s.StoreSettingsInJson), tr(uid, "settings_storage_viewed"), storageType(s.StoreViewedInJson)) + msg += fmt.Sprintf("🔑 TMDB: %s · 🖼 URL: %s", maskVal(s.TMDBSettings.APIKey), maskStr(s.TMDBSettings.ImageURL, 20)) + } + return msg +} + +func storageType(useJSON bool) string { + if useJSON { + return "json" + } + return "bbolt" +} + +func rateStr(kb int) string { + if kb == 0 { + return "∞" + } + return fmt.Sprintf("%d", kb) +} + +func portStr(port int) string { + if port == 0 { + return "auto" + } + return fmt.Sprintf("%d", port) +} + +func maskStr(s string, maxLen int) string { + if s == "" { + return "—" + } + if len(s) > maxLen { + return s[:maxLen-3] + "..." + } + return s +} + +func maskVal(s string) string { + if s == "" { + return "—" + } + return "***" +} + +func retrackersStr(mode int) string { + switch mode { + case 0: + return "off" + case 1: + return "add" + case 2: + return "remove" + case 3: + return "replace" + default: + return "?" + } +} + +func sendSettingsMenuKbd(uid int64, page string) *tele.ReplyMarkup { + s := settings.BTsets + var btns [][]tele.InlineButton + + switch page { + case "1": + btns = [][]tele.InlineButton{ + { + {Text: "🔍 " + tr(uid, "settings_section_search"), Unique: "fset", Data: "page|1a"}, + {Text: "📺 " + tr(uid, "settings_section_network"), Unique: "fset", Data: "page|1b"}, + {Text: "📦 " + tr(uid, "settings_section_other"), Unique: "fset", Data: "page|1c"}, + }, + { + {Text: "📥 " + tr(uid, "settings_export"), Unique: "fset", Data: "export"}, + {Text: "📊 " + tr(uid, "settings_nav_cache"), Unique: "fset", Data: "page|2"}, + {Text: "✏️ " + tr(uid, "settings_nav_paths"), Unique: "fset", Data: "page|3"}, + {Text: "💾 " + tr(uid, "settings_nav_storage"), Unique: "fset", Data: "page|4"}, + }, + } + case "1a": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + }, + { + {Text: toggleBtn("RuTor", s.EnableRutorSearch), Unique: "fset", Data: "rutor|1a"}, + {Text: toggleBtn("Torznab", s.EnableTorznabSearch), Unique: "fset", Data: "torznab|1a"}, + }, + } + case "1b": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + }, + { + {Text: toggleBtn("DLNA", s.EnableDLNA), Unique: "fset", Data: "dlna|1b"}, + {Text: toggleBtn("IPv6", s.EnableIPv6), Unique: "fset", Data: "ipv6|1b"}, + {Text: toggleBtn("Upload", !s.DisableUpload), Unique: "fset", Data: "upload|1b"}, + }, + { + {Text: toggleBtn("DHT", !s.DisableDHT), Unique: "fset", Data: "dht|1b"}, + {Text: toggleBtn("PEX", !s.DisablePEX), Unique: "fset", Data: "pex|1b"}, + {Text: toggleBtn("TCP", !s.DisableTCP), Unique: "fset", Data: "tcp|1b"}, + {Text: toggleBtn("UTP", !s.DisableUTP), Unique: "fset", Data: "utp|1b"}, + }, + { + {Text: toggleBtn("UPNP", !s.DisableUPNP), Unique: "fset", Data: "upnp|1b"}, + {Text: toggleBtn("Encrypt", s.ForceEncrypt), Unique: "fset", Data: "encrypt|1b"}, + {Text: toggleBtn("Debug", s.EnableDebug), Unique: "fset", Data: "debug|1b"}, + }, + } + case "1c": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + }, + { + {Text: toggleBtn("CacheDrop", s.RemoveCacheOnDrop), Unique: "fset", Data: "cachedrop|1c"}, + {Text: toggleBtn("Responsive", s.ResponsiveMode), Unique: "fset", Data: "responsive|1c"}, + {Text: toggleBtn("Proxy", s.EnableProxy), Unique: "fset", Data: "proxy|1c"}, + }, + { + {Text: toggleBtn("UseDisk", s.UseDisk), Unique: "fset", Data: "usedisk|1c"}, + {Text: toggleBtn("FSActive", s.ShowFSActiveTorr), Unique: "fset", Data: "fsactive|1c"}, + }, + } + case "2": + btns = [][]tele.InlineButton{ + { + {Text: "💾 " + tr(uid, "settings_limits_cache"), Unique: "fset", Data: "page|2a"}, + {Text: "🔌 " + tr(uid, "settings_limits_connections"), Unique: "fset", Data: "page|2b"}, + {Text: "⬇️ " + tr(uid, "settings_limits_speed"), Unique: "fset", Data: "page|2c"}, + }, + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + {Text: "✏️ " + tr(uid, "settings_nav_paths"), Unique: "fset", Data: "page|3"}, + {Text: "💾 " + tr(uid, "settings_nav_storage"), Unique: "fset", Data: "page|4"}, + }, + } + case "2a": + cacheMB := int(s.CacheSize / (1024 * 1024)) + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|2"}, + }, + { + {Text: "💾 " + optBtn("64", cacheMB == 64), Unique: "fset", Data: "cache|64|2a"}, + {Text: optBtn("128", cacheMB == 128), Unique: "fset", Data: "cache|128|2a"}, + {Text: optBtn("256", cacheMB == 256), Unique: "fset", Data: "cache|256|2a"}, + {Text: optBtn("512", cacheMB == 512), Unique: "fset", Data: "cache|512|2a"}, + }, + { + {Text: "📥 " + optBtn("25%", s.PreloadCache == 25), Unique: "fset", Data: "preload|25|2a"}, + {Text: optBtn("50%", s.PreloadCache == 50), Unique: "fset", Data: "preload|50|2a"}, + {Text: optBtn("75%", s.PreloadCache == 75), Unique: "fset", Data: "preload|75|2a"}, + {Text: optBtn("95%", s.PreloadCache == 95), Unique: "fset", Data: "preload|95|2a"}, + }, + { + {Text: "📖 " + optBtn("50%", s.ReaderReadAHead == 50), Unique: "fset", Data: "readahead|50|2a"}, + {Text: optBtn("75%", s.ReaderReadAHead == 75), Unique: "fset", Data: "readahead|75|2a"}, + {Text: optBtn("95%", s.ReaderReadAHead == 95), Unique: "fset", Data: "readahead|95|2a"}, + {Text: optBtn("100%", s.ReaderReadAHead == 100), Unique: "fset", Data: "readahead|100|2a"}, + }, + } + case "2b": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|2"}, + }, + { + {Text: "🔌 " + optBtn("25", s.ConnectionsLimit == 25), Unique: "fset", Data: "conn|25|2b"}, + {Text: optBtn("50", s.ConnectionsLimit == 50), Unique: "fset", Data: "conn|50|2b"}, + {Text: optBtn("100", s.ConnectionsLimit == 100), Unique: "fset", Data: "conn|100|2b"}, + }, + { + {Text: "⏱ " + optBtn("15s", s.TorrentDisconnectTimeout == 15), Unique: "fset", Data: "timeout|15|2b"}, + {Text: optBtn("30s", s.TorrentDisconnectTimeout == 30), Unique: "fset", Data: "timeout|30|2b"}, + {Text: optBtn("60s", s.TorrentDisconnectTimeout == 60), Unique: "fset", Data: "timeout|60|2b"}, + {Text: optBtn("120s", s.TorrentDisconnectTimeout == 120), Unique: "fset", Data: "timeout|120|2b"}, + }, + { + {Text: "🔌 " + optBtn("auto", s.PeersListenPort == 0), Unique: "fset", Data: "port|0|2b"}, + {Text: optBtn("6881", s.PeersListenPort == 6881), Unique: "fset", Data: "port|6881|2b"}, + {Text: optBtn("51413", s.PeersListenPort == 51413), Unique: "fset", Data: "port|51413|2b"}, + }, + } + case "2c": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|2"}, + }, + { + {Text: "⬇️ " + optBtn("∞", s.DownloadRateLimit == 0), Unique: "fset", Data: "down|0|2c"}, + {Text: optBtn("1M", s.DownloadRateLimit == 1024), Unique: "fset", Data: "down|1024|2c"}, + {Text: optBtn("5M", s.DownloadRateLimit == 5120), Unique: "fset", Data: "down|5120|2c"}, + {Text: optBtn("10M", s.DownloadRateLimit == 10240), Unique: "fset", Data: "down|10240|2c"}, + }, + { + {Text: "⬆️ " + optBtn("∞", s.UploadRateLimit == 0), Unique: "fset", Data: "up|0|2c"}, + {Text: optBtn("1M", s.UploadRateLimit == 1024), Unique: "fset", Data: "up|1024|2c"}, + {Text: optBtn("5M", s.UploadRateLimit == 5120), Unique: "fset", Data: "up|5120|2c"}, + {Text: optBtn("10M", s.UploadRateLimit == 10240), Unique: "fset", Data: "up|10240|2c"}, + }, + { + {Text: "🔄 " + optBtn("off", s.RetrackersMode == 0), Unique: "fset", Data: "retr|0|2c"}, + {Text: optBtn("add", s.RetrackersMode == 1), Unique: "fset", Data: "retr|1|2c"}, + {Text: optBtn("rem", s.RetrackersMode == 2), Unique: "fset", Data: "retr|2|2c"}, + {Text: optBtn("repl", s.RetrackersMode == 3), Unique: "fset", Data: "retr|3|2c"}, + }, + } + case "3": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + {Text: "📊 " + tr(uid, "settings_nav_cache"), Unique: "fset", Data: "page|2"}, + {Text: "💾 " + tr(uid, "settings_nav_storage"), Unique: "fset", Data: "page|4"}, + }, + { + {Text: "✏️ " + tr(uid, "settings_set_friendlyname"), Unique: "fset", Data: "ask|friendlyname"}, + }, + { + {Text: "✏️ " + tr(uid, "settings_set_path"), Unique: "fset", Data: "ask|torrentssavepath"}, + }, + { + {Text: "🔐 " + tr(uid, "settings_set_sslcert"), Unique: "fset", Data: "ask|sslcert"}, + {Text: "🔑 " + tr(uid, "settings_set_sslkey"), Unique: "fset", Data: "ask|sslkey"}, + }, + { + {Text: "🎬 " + tr(uid, "settings_set_tmdbkey"), Unique: "fset", Data: "ask|tmdbkey"}, + }, + { + {Text: "🔍 " + tr(uid, "settings_torznab_test"), Unique: "fset", Data: "ask|torznab_test"}, + {Text: "➕ " + tr(uid, "settings_add_torznab"), Unique: "fset", Data: "ask|torznab_add"}, + {Text: "🗑 " + tr(uid, "settings_clear_torznab"), Unique: "fset", Data: "torznab_clear"}, + }, + { + {Text: "✏️ " + tr(uid, "settings_set_proxyhosts"), Unique: "fset", Data: "ask|proxyhosts"}, + }, + } + case "4": + btns = [][]tele.InlineButton{ + { + {Text: "◀️ " + tr(uid, "settings_back"), Unique: "fset", Data: "page|1"}, + {Text: "📊 " + tr(uid, "settings_nav_cache"), Unique: "fset", Data: "page|2"}, + {Text: "✏️ " + tr(uid, "settings_nav_paths"), Unique: "fset", Data: "page|3"}, + }, + { + {Text: "📄 " + optBtn("json", s.StoreSettingsInJson), Unique: "fset", Data: "storage_set|json"}, + {Text: optBtn("bbolt", !s.StoreSettingsInJson), Unique: "fset", Data: "storage_set|bbolt"}, + }, + { + {Text: "📺 " + optBtn("json", s.StoreViewedInJson), Unique: "fset", Data: "storage_view|json"}, + {Text: optBtn("bbolt", !s.StoreViewedInJson), Unique: "fset", Data: "storage_view|bbolt"}, + }, + { + {Text: "🔄 " + tr(uid, "settings_reset"), Unique: "fset", Data: "reset_confirm"}, + }, + } + } + return &tele.ReplyMarkup{InlineKeyboard: btns} +} + +func boolIcon(v bool) string { + if v { + return "✅" + } + return "❌" +} + +func toggleBtn(label string, on bool) string { + if on { + return label + " ✅" + } + return label + " ❌" +} + +func optBtn(label string, isCurrent bool) string { + if isCurrent { + return label + " ✓" + } + return label +} + +func settingsCallback(c tele.Context, action string) error { + uid := c.Sender().ID + if !isAdmin(uid) { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "admin_only")}) + } + if settings.BTsets == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_not_loaded")}) + } + + if action == "export" { + buf, err := json.MarshalIndent(settings.BTsets, "", " ") + if err != nil { + return c.Respond(&tele.CallbackResponse{Text: fmt.Sprintf(tr(uid, "settings_error"), err.Error())}) + } + doc := &tele.Document{} + doc.FileName = "torrserver_settings.json" + doc.FileReader = bytes.NewReader(buf) + doc.Caption = "⚙️ " + tr(uid, "settings_export_caption") + _ = c.Send(doc) + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_exported")}) + } + + if action == "input_cancel" { + return cancelSettingsInput(c) + } + + if action == "reset_confirm" { + btnYes := tele.InlineButton{Text: tr(uid, "btn_yes"), Unique: "fset", Data: "reset_def|1"} + btnNo := tele.InlineButton{Text: tr(uid, "btn_no"), Unique: "fset", Data: "reset_def|0"} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnYes, btnNo}}} + msg := sendSettingsMenuText(c, uid, "4") + "\n\n⚠️ " + tr(uid, "settings_reset_confirm") + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = c.Send(tr(uid, "settings_reset_confirm"), kbd) + } + return c.Respond(&tele.CallbackResponse{}) + } + + if len(action) > 9 && action[:9] == "reset_def|" { + if action[9:] != "1" { + msg := sendSettingsMenuText(c, uid, "4") + kbd := sendSettingsMenuKbd(uid, "4") + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, "4") + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "canceled")}) + } + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + torr.SetDefSettings() + dlna.Stop() + rutor.Stop() + rutor.Start() + msg := sendSettingsMenuText(c, uid, "4") + kbd := sendSettingsMenuKbd(uid, "4") + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, "4") + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_reset_done")}) + } + + if len(action) > 12 && action[:12] == "storage_set|" { + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + val := action[12:] + prefs := map[string]interface{}{"settings": val} + if err := settings.SetStoragePreferences(prefs); err != nil { + return c.Respond(&tele.CallbackResponse{Text: fmt.Sprintf(tr(uid, "settings_error"), err.Error())}) + } + page := "4" + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, page) + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_saved")}) + } + + if len(action) > 12 && action[:12] == "storage_view|" { + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + val := action[12:] + prefs := map[string]interface{}{"viewed": val} + if err := settings.SetStoragePreferences(prefs); err != nil { + return c.Respond(&tele.CallbackResponse{Text: fmt.Sprintf(tr(uid, "settings_error"), err.Error())}) + } + page := "4" + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, page) + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_saved")}) + } + + if len(action) > 4 && action[:4] == "ask|" { + setting := action[4:] + var hint string + switch setting { + case "friendlyname": + hint = tr(uid, "settings_hint_friendlyname") + case "torrentssavepath": + hint = tr(uid, "settings_hint_path") + case "sslcert": + hint = tr(uid, "settings_hint_sslcert") + case "sslkey": + hint = tr(uid, "settings_hint_sslkey") + case "tmdbkey": + hint = tr(uid, "settings_hint_tmdbkey") + case "proxyhosts": + hint = tr(uid, "settings_hint_proxyhosts") + case "torznab_add": + hint = tr(uid, "settings_hint_torznab") + case "torznab_test": + hint = tr(uid, "settings_hint_torznab_test") + default: + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "callback_unknown")}) + } + return sendSettingsInputPrompt(c, uid, setting, hint) + } + + if len(action) > 5 && action[:5] == "page|" { + page := action[5:] + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, page) + } + return c.Respond(&tele.CallbackResponse{}) + } + + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + + sets := new(settings.BTSets) + *sets = *settings.BTsets + page := "1" + + // Extract return page from action (e.g. "rutor|1a" -> action "rutor", page "1a") + if idx := strings.Index(action, "|"); idx >= 0 { + suffix := action[idx+1:] + if suffix == "1a" || suffix == "1b" || suffix == "1c" { + page = suffix + action = action[:idx] + } + } + + switch action { + case "rutor": + sets.EnableRutorSearch = !sets.EnableRutorSearch + case "torznab": + sets.EnableTorznabSearch = !sets.EnableTorznabSearch + case "dlna": + sets.EnableDLNA = !sets.EnableDLNA + case "ipv6": + sets.EnableIPv6 = !sets.EnableIPv6 + case "upload": + sets.DisableUpload = !sets.DisableUpload + case "dht": + sets.DisableDHT = !sets.DisableDHT + case "pex": + sets.DisablePEX = !sets.DisablePEX + case "tcp": + sets.DisableTCP = !sets.DisableTCP + case "utp": + sets.DisableUTP = !sets.DisableUTP + case "upnp": + sets.DisableUPNP = !sets.DisableUPNP + case "encrypt": + sets.ForceEncrypt = !sets.ForceEncrypt + case "debug": + sets.EnableDebug = !sets.EnableDebug + case "cachedrop": + sets.RemoveCacheOnDrop = !sets.RemoveCacheOnDrop + case "responsive": + sets.ResponsiveMode = !sets.ResponsiveMode + case "proxy": + sets.EnableProxy = !sets.EnableProxy + case "usedisk": + sets.UseDisk = !sets.UseDisk + case "fsactive": + sets.ShowFSActiveTorr = !sets.ShowFSActiveTorr + case "storejson": + sets.StoreSettingsInJson = !sets.StoreSettingsInJson + case "viewedjson": + sets.StoreViewedInJson = !sets.StoreViewedInJson + case "torznab_clear": + if settings.ReadOnly { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_readonly")}) + } + sets.TorznabUrls = nil + page = "3" + torr.SetSettings(sets) + rutor.Stop() + rutor.Start() + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, page) + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_saved")}) + default: + if parts := splitAction(action); len(parts) == 2 { + key, value := parts[0], parts[1] + page = "2" + if idx := strings.Index(value, "|"); idx >= 0 { + if ret := value[idx+1:]; ret == "2a" || ret == "2b" || ret == "2c" { + page = ret + } + value = value[:idx] + } + switch key { + case "cache": + if v := parseInt(value); v > 0 { + sets.CacheSize = int64(v) * 1024 * 1024 + } + case "preload": + if v := parseInt(value); v >= 0 && v <= 100 { + sets.PreloadCache = v + } + case "readahead": + if v := parseInt(value); v >= 5 && v <= 100 { + sets.ReaderReadAHead = v + } + case "conn": + if v := parseInt(value); v > 0 { + sets.ConnectionsLimit = v + } + case "timeout": + if v := parseInt(value); v > 0 { + sets.TorrentDisconnectTimeout = v + } + case "port": + v := parseInt(value) + if v >= 0 && (v == 0 || (v >= 1024 && v <= 65535)) { + sets.PeersListenPort = v + } + case "down": + sets.DownloadRateLimit = parseInt(value) + case "up": + sets.UploadRateLimit = parseInt(value) + case "retr": + if v := parseInt(value); v >= 0 && v <= 3 { + sets.RetrackersMode = v + } + default: + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "callback_unknown")}) + } + } else { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "callback_unknown")}) + } + } + + torr.SetSettings(sets) + dlna.Stop() + if sets.EnableDLNA { + dlna.Start() + } + rutor.Stop() + rutor.Start() + + msg := sendSettingsMenuText(c, uid, page) + kbd := sendSettingsMenuKbd(uid, page) + if _, err := c.Bot().Edit(c.Callback().Message, msg, kbd, tele.ModeHTML); err != nil { + _ = sendSettingsMenuPage(c, uid, page) + } + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "settings_saved")}) +} + +func splitAction(action string) []string { + for i := 0; i < len(action); i++ { + if action[i] == '|' { + return []string{action[:i], action[i+1:]} + } + } + return nil +} + +func parseInt(s string) int { + var n int + for _, c := range s { + if c >= '0' && c <= '9' { + n = n*10 + int(c-'0') + } + } + return n +} diff --git a/server/tgbot/admin_shutdown.go b/server/tgbot/admin_shutdown.go new file mode 100644 index 0000000..bfa5836 --- /dev/null +++ b/server/tgbot/admin_shutdown.go @@ -0,0 +1,32 @@ +package tgbot + +import ( + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdShutdown(c tele.Context) error { + uid := c.Sender().ID + btnYes := tele.InlineButton{Text: tr(uid, "btn_yes"), Unique: "fshutdown", Data: "1"} + btnNo := tele.InlineButton{Text: tr(uid, "btn_no"), Unique: "fshutdown", Data: "0"} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnYes, btnNo}}} + return c.Send(tr(uid, "shutdown_confirm"), kbd) +} + +func shutdownConfirm(c tele.Context, confirm string) error { + uid := c.Sender().ID + if !isAdmin(uid) { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "admin_only")}) + } + if confirm != "1" { + _ = c.Respond(&tele.CallbackResponse{Text: tr(uid, "canceled")}) + return c.Bot().Delete(c.Callback().Message) + } + _ = c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "server_stopped")}) + _ = c.Bot().Delete(c.Callback().Message) + _ = c.Send(tr(c.Sender().ID, "server_stopped")) + go func() { + torr.Shutdown() + }() + return nil +} diff --git a/server/tgbot/bot.go b/server/tgbot/bot.go new file mode 100644 index 0000000..0b2c4d2 --- /dev/null +++ b/server/tgbot/bot.go @@ -0,0 +1,363 @@ +package tgbot + +import ( + "context" + "errors" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/net/proxy" + tele "gopkg.in/telebot.v4" + "gopkg.in/telebot.v4/middleware" + + "server/log" + "server/tgbot/config" + up "server/tgbot/upload" +) + +func newTelegramHTTPClient() *http.Client { + const timeout = 5 * time.Minute + trimmed := strings.TrimSpace(config.Cfg.Socks5) + if trimmed == "" { + return &http.Client{Timeout: timeout} + } + raw := trimmed + if !strings.Contains(raw, "://") { + raw = "socks5://" + raw + } + u, err := url.Parse(raw) + if err != nil { + log.TLogln("tg cfg Socks5 parse err, using direct", err) + return &http.Client{Timeout: timeout} + } + if u.Scheme != "socks5" { + log.TLogln("tg cfg Socks5: only socks5 is supported, got", u.Scheme) + return &http.Client{Timeout: timeout} + } + proxyHost := u.Host + if proxyHost == "" { + log.TLogln("tg cfg Socks5: empty host, using direct") + return &http.Client{Timeout: timeout} + } + var auth *proxy.Auth + if u.User != nil { + pw, _ := u.User.Password() + auth = &proxy.Auth{User: u.User.Username(), Password: pw} + } + socksDial, err := proxy.SOCKS5("tcp", proxyHost, auth, proxy.Direct) + if err != nil { + log.TLogln("tg socks5 dialer err, using direct", err) + return &http.Client{Timeout: timeout} + } + log.TLogln("tg using SOCKS5 proxy", proxyHost) + transport := &http.Transport{ + Proxy: nil, // respect explicit socks only, not HTTP_PROXY, for this client + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + _ = ctx + return socksDial.Dial(network, address) + }, + } + return &http.Client{Transport: transport, Timeout: timeout} +} + +func Start(token string) error { + config.LoadConfig() + loadUserLangs() + + pref := tele.Settings{ + URL: config.Cfg.HostTG, + Token: token, + Poller: &tele.LongPoller{Timeout: 5 * time.Minute}, + ParseMode: tele.ModeHTML, + Client: newTelegramHTTPClient(), + } + + log.TLogln("tg bot starting") + + b, err := tele.NewBot(pref) + if err != nil { + log.TLogln("tg bot start err", err) + return err + } + + up.TrFunc = tr + up.EscapeFunc = escapeHtml + + if err := b.SetCommands([]tele.Command{ + {Text: "help", Description: "Help and user ID"}, + {Text: "start", Description: "Start bot"}, + {Text: "list", Description: "List torrents"}, + {Text: "add", Description: "Add torrent"}, + {Text: "search", Description: "Search all (RuTor+Torznab)"}, + {Text: "rutor", Description: "Search RuTor"}, + {Text: "torznab", Description: "Search Torznab"}, + {Text: "remove", Description: "Remove torrent"}, + {Text: "status", Description: "Torrent status"}, + {Text: "link", Description: "Stream link"}, + {Text: "m3u", Description: "M3U playlist"}, + {Text: "preload", Description: "Preload file"}, + {Text: "queue", Description: "Upload queue status"}, + {Text: "server", Description: "Server info"}, + {Text: "stats", Description: "Summary statistics"}, + {Text: "stat", Description: "Detailed status"}, + {Text: "snake", Description: "Cache visualization"}, + {Text: "clear", Description: "Remove all torrents"}, + {Text: "hash", Description: "Show hashes"}, + {Text: "export", Description: "Export torrents"}, + {Text: "import", Description: "Import torrents"}, + {Text: "categories", Description: "List categories"}, + {Text: "lang", Description: "Set language RU|EN"}, + }); err != nil { + log.TLogln("tg setcmd err", err) + } + + if len(config.Cfg.WhiteIds) > 0 { + b.Use(middleware.Whitelist(config.Cfg.WhiteIds...)) + } + if len(config.Cfg.BlackIds) > 0 { + b.Use(middleware.Blacklist(config.Cfg.BlackIds...)) + } + + b.Use(func(next tele.HandlerFunc) tele.HandlerFunc { + return func(c tele.Context) error { + if c.Sender() == nil { + return nil + } + if c.Message() != nil && c.Message().Text != "" { + cmd := logSafeStr(c.Message().Text, 60) + log.TLogln("tg cmd", logUser(c.Sender()), cmd) + } + err := next(c) + if err != nil { + log.TLogln("tg cmd err", logUser(c.Sender()), err) + } + return err + } + }) + + b.Handle("help", help) + b.Handle("Help", help) + b.Handle("/help", help) + b.Handle("/Help", help) + b.Handle("/start", help) + b.Handle("/id", help) + + b.Handle("/list", list) + b.Handle("/clear", clear) + b.Handle("/add", cmdAdd) + b.Handle("/remove", cmdRemove) + b.Handle("/drop", cmdDrop) + b.Handle("/status", cmdStatus) + b.Handle("/server", cmdServer) + b.Handle("/link", cmdLink) + b.Handle("/play", cmdLink) + b.Handle("/cache", cmdCache) + b.Handle("/m3u", cmdM3u) + b.Handle("/m3uall", cmdM3uAll) + b.Handle("/search", cmdSearch) + b.Handle("/rutor", cmdSearchRutor) + b.Handle("/torznab", cmdTorznab) + b.Handle("/preload", cmdPreload) + b.Handle("/queue", up.ShowQueue) + b.Handle("/set", cmdSet) + b.Handle("/hash", cmdHash) + b.Handle("/export", cmdExport) + b.Handle("/import", cmdImport) + b.Handle("/categories", cmdCategories) + b.Handle("/echo", cmdEcho) + b.Handle("/db", cmdDb) + b.Handle("/viewed", cmdViewed) + b.Handle("/ffp", cmdFfp) + b.Handle("/speedtest", cmdSpeedtest) + b.Handle("/shutdown", adminOnly(cmdShutdown)) + b.Handle("/settings", adminOnly(cmdSettings)) + b.Handle("/preset", adminOnly(cmdPreset)) + b.Handle("/lang", cmdLang) + b.Handle("/stats", cmdStats) + b.Handle("/stat", cmdStat) + b.Handle("/snake", cmdSnake) + + b.Handle(tele.OnDocument, func(c tele.Context) error { + if c.Message() == nil { + return nil + } + doc := c.Message().Document + if doc == nil { + return nil + } + lowerName := strings.ToLower(doc.FileName) + isTorrent := strings.HasSuffix(lowerName, ".torrent") || + strings.Contains(strings.ToLower(doc.MIME), "bittorrent") + if isTorrent { + err := addTorrentFromDocument(c, doc) + if err != nil { + return err + } + return list(c) + } + return nil + }) + + b.Handle(tele.OnText, func(c tele.Context) error { + txt := c.Text() + if handleSettingsInputReply(c) { + return nil + } + lower := strings.ToLower(txt) + if strings.HasPrefix(lower, "magnet:") || strings.HasPrefix(lower, "torrs://") || + strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || + isHash(txt) { + err := addTorrent(c, txt) + if err != nil { + return err + } + return list(c) + } else if c.Message().ReplyTo != nil && c.Message().ReplyTo.ReplyMarkup != nil && len(c.Message().ReplyTo.ReplyMarkup.InlineKeyboard) > 0 { + var hash string + for _, row := range c.Message().ReplyTo.ReplyMarkup.InlineKeyboard { + for _, btn := range row { + if btn.Data == "" { + continue + } + if idx := strings.Index(btn.Data, "all|"); idx >= 0 { + h := btn.Data[idx+4:] + if len(h) >= 40 && isHash(h[:40]) { + hash = h[:40] + } else if isHash(h) { + hash = h + } + } else if isHash(btn.Data) { + hash = btn.Data + } + if hash != "" { + break + } + } + if hash != "" { + break + } + } + if hash != "" { + from, to, err := ParseRange(c.Sender().ID, c.Message().Text) + if err != nil { + _ = c.Send(tr(c.Sender().ID, "range_error")) + return err + } + up.AddRange(c, hash, from, to) + } + return nil + } else { + return c.Send(tr(c.Sender().ID, "add_magnet")) + } + }) + + b.Handle(tele.OnQuery, handleInlineQuery) + + b.Handle(tele.OnCallback, func(c tele.Context) error { + args := c.Args() + if len(args) > 0 { + cbInfo := strings.TrimPrefix(args[0], "\f") + if len(args) >= 2 { + cbInfo += " " + args[1] + } + cbInfo = logSafeStr(cbInfo, 80) + log.TLogln("tg cb", logUser(c.Sender()), cbInfo) + } + err := handleCallback(c) + if err != nil && len(args) > 0 { + log.TLogln("tg cb err", logUser(c.Sender()), logSafeStr(args[0], 40), err) + } + return err + }) + + up.Start() + + go b.Start() + return nil +} + +func help(c tele.Context) error { + uid := c.Sender().ID + id := strconv.FormatInt(uid, 10) + var arr []string + if c.Sender().Username != "" { + arr = append(arr, c.Sender().Username) + } + if c.Sender().FirstName != "" { + arr = append(arr, c.Sender().FirstName) + } + if c.Sender().LastName != "" { + arr = append(arr, c.Sender().LastName) + } + msg := "🤖 <b>" + tr(uid, "help") + "</b>\n\n" + msg += "📋 <b>" + tr(uid, "help_main") + "</b>\n" + msg += " • /help — " + tr(uid, "help_help") + "\n" + msg += " • " + tr(uid, "help_list") + "\n" + msg += " • " + tr(uid, "help_clear") + "\n" + msg += " • " + tr(uid, "help_add") + "\n" + msg += " • " + tr(uid, "help_hash") + "\n" + msg += " • /stats, /stat — " + tr(uid, "help_stats") + ", " + tr(uid, "help_stat") + "\n\n" + msg += "🎛 <b>" + tr(uid, "help_manage") + "</b> " + tr(uid, "help_manage_desc") + "\n" + msg += " • " + tr(uid, "help_remove") + "\n" + msg += " • " + tr(uid, "help_links") + "\n\n" + msg += "🔍 <b>" + tr(uid, "help_search") + "</b> " + tr(uid, "help_search_desc") + "\n" + msg += " • " + tr(uid, "help_search_cmd") + "\n\n" + msg += "📦 <b>" + tr(uid, "help_export_import") + "</b>\n" + msg += " • " + tr(uid, "help_export") + "\n" + msg += " • " + tr(uid, "help_import") + "\n\n" + msg += "📁 <b>" + tr(uid, "help_categories_section") + "</b>\n" + msg += " • " + tr(uid, "help_categories") + "\n\n" + msg += "🖥 <b>" + tr(uid, "help_server") + "</b>\n" + msg += " • " + tr(uid, "help_server_cmd") + "\n" + msg += " • " + tr(uid, "help_echo") + "\n" + msg += " • " + tr(uid, "help_db") + "\n\n" + msg += "⚙️ <b>" + tr(uid, "help_other") + "</b>\n" + msg += " • " + tr(uid, "help_other_cmd") + "\n" + msg += " • " + tr(uid, "help_lang") + "\n" + msg += " • " + tr(uid, "help_admin") + "\n\n" + msg += "👤 " + tr(uid, "help_id") + ": <code>" + id + "</code>" + if len(arr) > 0 { + msg += " • " + strings.Join(arr, ", ") + } + return c.Send(msg) +} + +func isHash(txt string) bool { + if len(txt) == 40 { + for _, c := range strings.ToLower(txt) { + switch c { + case 'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + default: + return false + } + } + return true + } + return false +} + +func ParseRange(userID int64, rng string) (int, int, error) { + parts := strings.Split(rng, "-") + + if len(parts) != 2 { + return -1, -1, errors.New(tr(userID, "parse_range_err")) + } + + num1, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) + if err1 != nil { + return -1, -1, err1 + } + + num2, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) + if err2 != nil { + return -1, -1, err2 + } + if num1 < 1 || num2 < 1 || num1 > num2 { + return -1, -1, errors.New(tr(userID, "parse_range_err")) + } + return num1, num2, nil +} diff --git a/server/tgbot/cache.go b/server/tgbot/cache.go new file mode 100644 index 0000000..f33106e --- /dev/null +++ b/server/tgbot/cache.go @@ -0,0 +1,39 @@ +package tgbot + +import ( + "fmt" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdCache(c tele.Context) error { + arg := "" + if args := c.Args(); len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + if hash == "" { + return c.Send(tr(c.Sender().ID, "cache_usage")) + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + st := t.CacheState() + if st == nil { + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "cache_unavailable"), hash)) + } + + uid := c.Sender().ID + txt := "💾 <b>" + escapeHtml(st.Torrent.Title) + "</b>\n\n" + txt += fmt.Sprintf("%s: %s\n", tr(uid, "cache_capacity"), humanize.IBytes(uint64(st.Capacity))) + txt += fmt.Sprintf("%s: %s\n", tr(uid, "cache_filled"), humanize.IBytes(uint64(st.Filled))) + txt += fmt.Sprintf("%s: %d\n", tr(uid, "cache_pieces"), st.PiecesCount) + txt += fmt.Sprintf("%s: %d\n", tr(uid, "cache_readers"), len(st.Readers)) + txt += fmt.Sprintf("<code>%s</code>", hash) + return c.Send(txt) +} diff --git a/server/tgbot/callbacks.go b/server/tgbot/callbacks.go new file mode 100644 index 0000000..c095f10 --- /dev/null +++ b/server/tgbot/callbacks.go @@ -0,0 +1,31 @@ +package tgbot + +import tele "gopkg.in/telebot.v4" + +// handleCallback routes callback queries to appropriate handlers +func handleCallback(c tele.Context) error { + if c.Sender() == nil { + return nil + } + args := c.Args() + if len(args) == 0 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + + switch args[0] { + case "\ffiles", "\fdelete", "\fupload", "\fuploadall", "\ffall", "\fcancel", + "\ffstatus", "\ffm3u", "\fflink", "\ffdrop", "\ffstatusrefresh", "\ffstatusstop", + "\fflist", "\ffrefresh", "\ffnop", "\ffpreload", "\ffitems", "\ffifresh", + "\ffsnakerefresh", "\ffsnakestop": + return handleCallbackTorrent(c, args) + case "\ffadd", "\ffmore": + return handleCallbackSearch(c, args) + case "\ffexport", "\ffexportrefresh", "\ffhash", "\ffhashrefresh", + "\ffstatusall", "\ffstatusallrefresh", "\ffdb", "\ffdbrefresh": + return handleCallbackExport(c, args) + case "\ffclear", "\ffshutdown", "\ffpreset", "\ffset": + return handleCallbackAdmin(c, args) + default: + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } +} diff --git a/server/tgbot/callbacks_admin.go b/server/tgbot/callbacks_admin.go new file mode 100644 index 0000000..5f8ecbd --- /dev/null +++ b/server/tgbot/callbacks_admin.go @@ -0,0 +1,29 @@ +package tgbot + +import tele "gopkg.in/telebot.v4" + +func handleCallbackAdmin(c tele.Context, args []string) error { + switch args[0] { + case "\ffclear": + if len(args) > 1 { + return clearConfirm(c, args[1]) + } + case "\ffshutdown": + if len(args) > 1 { + return shutdownConfirm(c, args[1]) + } + case "\ffpreset": + if len(args) > 1 { + return presetConfirm(c, args[1]) + } + case "\ffset": + if len(args) > 1 { + action := args[1] + for i := 2; i < len(args); i++ { + action += "|" + args[i] + } + return settingsCallback(c, action) + } + } + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) +} diff --git a/server/tgbot/callbacks_export.go b/server/tgbot/callbacks_export.go new file mode 100644 index 0000000..d984ca0 --- /dev/null +++ b/server/tgbot/callbacks_export.go @@ -0,0 +1,57 @@ +package tgbot + +import tele "gopkg.in/telebot.v4" + +func handleCallbackExport(c tele.Context, args []string) error { + switch args[0] { + case "\ffexport": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackExportPage(c, data) + case "\ffexportrefresh": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackExportRefresh(c, data) + case "\ffhash": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackHashPage(c, data) + case "\ffhashrefresh": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackHashRefresh(c, data) + case "\ffstatusall": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackStatusAllPage(c, data) + case "\ffstatusallrefresh": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackStatusAllRefresh(c, data) + case "\ffdb": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackDbPage(c, data) + case "\ffdbrefresh": + data := "" + if len(args) > 1 { + data = args[1] + } + return callbackDbRefresh(c, data) + } + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) +} diff --git a/server/tgbot/callbacks_search.go b/server/tgbot/callbacks_search.go new file mode 100644 index 0000000..2e5638e --- /dev/null +++ b/server/tgbot/callbacks_search.go @@ -0,0 +1,17 @@ +package tgbot + +import tele "gopkg.in/telebot.v4" + +func handleCallbackSearch(c tele.Context, args []string) error { + switch args[0] { + case "\ffadd": + if len(args) > 1 { + return callbackSearchAdd(c, args[1]) + } + case "\ffmore": + if len(args) > 1 { + return callbackSearchMore(c, args[1]) + } + } + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) +} diff --git a/server/tgbot/callbacks_torrent.go b/server/tgbot/callbacks_torrent.go new file mode 100644 index 0000000..cd5ddca --- /dev/null +++ b/server/tgbot/callbacks_torrent.go @@ -0,0 +1,100 @@ +package tgbot + +import ( + "strconv" + + tele "gopkg.in/telebot.v4" + up "server/tgbot/upload" +) + +func handleCallbackTorrent(c tele.Context, args []string) error { + switch args[0] { + case "\ffiles": + return files(c) + case "\fdelete": + if len(args) < 2 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + deleteTorrent(c) + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "deleted")}) + case "\fupload": + return upload(c) + case "\fuploadall", "\ffall": + return uploadall(c) + case "\fcancel": + if len(args) > 1 { + if num, err := strconv.Atoi(args[1]); err == nil { + up.Cancel(num) + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{}) + } + } + return c.Respond(&tele.CallbackResponse{}) + case "\ffstatus", "\ffm3u", "\fflink", "\ffdrop", "\ffstatusrefresh", "\ffstatusstop": + hash := "" + if len(args) >= 2 { + hash = args[1] + } + switch args[0] { + case "\ffstatus": + if hash == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + return callbackStatus(c, hash) + case "\ffstatusrefresh": + return callbackStatusRefresh(c, hash) + case "\ffstatusstop": + return callbackStatusStop(c, hash) + case "\ffm3u": + if hash == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + return callbackM3u(c, hash) + case "\fflink": + if len(args) < 2 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + return callbackLink(c, args[1]) + case "\ffdrop": + if hash == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + return callbackDrop(c, hash) + } + case "\fflist": + if len(args) > 1 { + return callbackListPage(c, args[1]) + } + case "\ffrefresh": + if len(args) > 1 { + return callbackListRefresh(c, args[1]) + } + case "\ffitems": + if len(args) >= 3 { + return callbackFileListPage(c, args[1], args[2]) + } + case "\ffifresh": + if len(args) >= 3 { + return callbackFileListRefresh(c, args[1], args[2]) + } + case "\ffnop": + return c.Respond(&tele.CallbackResponse{}) + case "\ffpreload": + if len(args) >= 3 { + return callbackPreload(c, args[1], args[2]) + } + case "\ffsnakerefresh", "\ffsnakestop": + data := "" + if len(args) >= 2 { + data = args[1] + } + switch args[0] { + case "\ffsnakerefresh": + return callbackSnakeRefresh(c, data) + case "\ffsnakestop": + return callbackSnakeStop(c, data) + } + } + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) +} diff --git a/server/tgbot/categories.go b/server/tgbot/categories.go new file mode 100644 index 0000000..fe418c3 --- /dev/null +++ b/server/tgbot/categories.go @@ -0,0 +1,37 @@ +package tgbot + +import ( + "fmt" + "sort" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdCategories(c tele.Context) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + uid := c.Sender().ID + catCount := make(map[string]int) + for _, t := range torrents { + cat := t.Category + if cat == "" { + cat = tr(uid, "categories_uncategorized") + } + catCount[cat]++ + } + var cats []string + for c := range catCount { + cats = append(cats, c) + } + sort.Strings(cats) + var sb strings.Builder + fmt.Fprintf(&sb, "📁 <b>%s</b>\n\n", tr(uid, "categories_title")) + for _, cat := range cats { + fmt.Fprintf(&sb, "• %s: %d\n", escapeHtml(cat), catCount[cat]) + } + return c.Send(strings.TrimSuffix(sb.String(), "\n")) +} diff --git a/server/tgbot/config/config.go b/server/tgbot/config/config.go new file mode 100644 index 0000000..169e05d --- /dev/null +++ b/server/tgbot/config/config.go @@ -0,0 +1,52 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "server/log" + "server/settings" +) + +type Config struct { + HostTG string + HostWeb string + Socks5 string + WhiteIds []int64 + BlackIds []int64 +} + +var Cfg *Config + +func LoadConfig() { + Cfg = &Config{} + fn := filepath.Join(settings.Path, "tg.cfg") + buf, err := os.ReadFile(fn) + if err != nil { + Cfg.WhiteIds = []int64{} + Cfg.BlackIds = []int64{} + Cfg.HostTG = "https://api.telegram.org" + buf, _ = json.MarshalIndent(Cfg, "", " ") + if buf != nil { + os.WriteFile(fn, buf, 0o600) + } + return + } + err = json.Unmarshal(buf, &Cfg) + if err != nil { + log.TLogln("tg config read err", err) + Cfg.WhiteIds = []int64{} + Cfg.BlackIds = []int64{} + } + if Cfg.HostTG == "" || (!strings.HasPrefix(Cfg.HostTG, "http://") && !strings.HasPrefix(Cfg.HostTG, "https://")) { + Cfg.HostTG = "https://api.telegram.org" + } + if Cfg.WhiteIds == nil { + Cfg.WhiteIds = []int64{} + } + if Cfg.BlackIds == nil { + Cfg.BlackIds = []int64{} + } +} diff --git a/server/tgbot/db.go b/server/tgbot/db.go new file mode 100644 index 0000000..1684775 --- /dev/null +++ b/server/tgbot/db.go @@ -0,0 +1,98 @@ +package tgbot + +import ( + "strconv" + "strings" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/log" + sets "server/settings" +) + +const dbPageSize = 10 + +func cmdDb(c tele.Context) error { + return sendDbPage(c, 0) +} + +func sendDbPage(c tele.Context, page int) error { + uid := c.Sender().ID + dbList := sets.ListTorrent() + if len(dbList) == 0 { + return c.Send(tr(uid, "db_empty")) + } + + totalPages := (len(dbList) + dbPageSize - 1) / dbPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * dbPageSize + end := start + dbPageSize + if end > len(dbList) { + end = len(dbList) + } + pageList := dbList[start:end] + + var sb strings.Builder + sb.WriteString("📁 <b>" + tr(uid, "db_title") + "</b> (" + strconv.Itoa(len(dbList)) + ")\n\n") + for i, t := range pageList { + hash := t.InfoHash.HexString() + sb.WriteString(strconv.Itoa(start+i+1) + ". <b>" + escapeHtml(t.Title) + "</b>") + if t.Size > 0 { + sb.WriteString(" <i>" + humanize.IBytes(uint64(t.Size)) + "</i>") + } + sb.WriteString("\n<code>" + hash + "</code>\n\n") + } + msg := strings.TrimSuffix(sb.String(), "\n\n") + + navRow := []tele.InlineButton{} + if totalPages > 1 { + if page > 0 { + navRow = append(navRow, tele.InlineButton{Text: "◀️", Unique: "fdb", Data: strconv.Itoa(page - 1)}) + } + navRow = append(navRow, tele.InlineButton{Text: strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages), Unique: "fnop", Data: ""}) + if page < totalPages-1 { + navRow = append(navRow, tele.InlineButton{Text: "▶️", Unique: "fdb", Data: strconv.Itoa(page + 1)}) + } + } + navRow = append(navRow, tele.InlineButton{Text: "🔄", Unique: "fdbrefresh", Data: strconv.Itoa(page)}) + + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{navRow}} + if err := c.Send(msg, kbd); err != nil { + log.TLogln("tg db send err", err) + return err + } + return nil +} + +func callbackDbPage(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendDbPage(c, page) +} + +func callbackDbRefresh(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendDbPage(c, page) +} diff --git a/server/tgbot/delete.go b/server/tgbot/delete.go new file mode 100644 index 0000000..d84e941 --- /dev/null +++ b/server/tgbot/delete.go @@ -0,0 +1,49 @@ +package tgbot + +import ( + "fmt" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func deleteTorrent(c tele.Context) { + args := c.Args() + if len(args) < 2 { + return + } + hash := args[1] + if !isHash(hash) { + return + } + torr.RemTorrent(hash) +} + +func clear(c tele.Context) error { + torrents := torr.ListTorrent() + count := len(torrents) + if count == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + uid := c.Sender().ID + btnYes := tele.InlineButton{Text: tr(uid, "btn_yes"), Unique: "fclear", Data: "1"} + btnNo := tele.InlineButton{Text: tr(uid, "btn_no"), Unique: "fclear", Data: "0"} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnYes, btnNo}}} + return c.Send(fmt.Sprintf(tr(uid, "clear_confirm"), count), kbd) +} + +func clearConfirm(c tele.Context, confirm string) error { + uid := c.Sender().ID + if confirm != "1" { + _ = c.Respond(&tele.CallbackResponse{Text: tr(uid, "canceled")}) + return c.Bot().Delete(c.Callback().Message) + } + torrents := torr.ListTorrent() + count := len(torrents) + for _, t := range torrents { + torr.RemTorrent(t.Hash().HexString()) + } + _ = c.Respond(&tele.CallbackResponse{Text: tr(uid, "deleted")}) + _ = c.Bot().Delete(c.Callback().Message) + return c.Send(fmt.Sprintf(tr(uid, "clear_done"), count)) +} diff --git a/server/tgbot/drop.go b/server/tgbot/drop.go new file mode 100644 index 0000000..2e5d0a3 --- /dev/null +++ b/server/tgbot/drop.go @@ -0,0 +1,28 @@ +package tgbot + +import ( + "fmt" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func callbackDrop(c tele.Context, hash string) error { + torr.DropTorrent(hash) + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "drop_done")}) +} + +func cmdDrop(c tele.Context) error { + arg := "" + if args := c.Args(); len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + if hash == "" { + return c.Send(tr(c.Sender().ID, "remove_usage")) + } + + torr.DropTorrent(hash) + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "drop_done_hash"), hash)) +} diff --git a/server/tgbot/echo.go b/server/tgbot/echo.go new file mode 100644 index 0000000..6d33e43 --- /dev/null +++ b/server/tgbot/echo.go @@ -0,0 +1,16 @@ +package tgbot + +import ( + "fmt" + + tele "gopkg.in/telebot.v4" + "server/version" +) + +func cmdEcho(c tele.Context) error { + v := version.Version + if v == "" { + v = "unknown" + } + return c.Send(fmt.Sprintf("🔄 TorrServer %s", v)) +} diff --git a/server/tgbot/export.go b/server/tgbot/export.go new file mode 100644 index 0000000..fe4bb82 --- /dev/null +++ b/server/tgbot/export.go @@ -0,0 +1,128 @@ +package tgbot + +import ( + "bytes" + "fmt" + "net/url" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/log" + "server/torr" +) + +const exportPageSize = 10 + +func cmdExport(c tele.Context) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + uid := c.Sender().ID + + var magnets strings.Builder + for _, t := range torrents { + hash := t.Hash().HexString() + title := t.Title + if title == "" { + title = t.Name() + } + magnet := fmt.Sprintf("magnet:?xt=urn:btih:%s", hash) + if title != "" { + magnet += "&dn=" + url.QueryEscape(title) + } + magnets.WriteString(magnet + "\n") + } + + doc := &tele.Document{} + doc.FileName = "torrents.txt" + doc.FileReader = bytes.NewReader([]byte(strings.TrimSuffix(magnets.String(), "\n"))) + doc.Caption = "📁 " + tr(uid, "export_file_caption") + if err := c.Send(doc); err != nil { + return err + } + + return sendExportPage(c, 0) +} + +func sendExportPage(c tele.Context, page int) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + totalPages := (len(torrents) + exportPageSize - 1) / exportPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * exportPageSize + end := start + exportPageSize + if end > len(torrents) { + end = len(torrents) + } + pageTorrents := torrents[start:end] + + uid := c.Sender().ID + var hashes strings.Builder + fmt.Fprintf(&hashes, "📁 <b>%s</b> (%d)\n\n", tr(uid, "export_title"), len(torrents)) + for i, t := range pageTorrents { + hash := t.Hash().HexString() + title := t.Title + if title == "" { + title = t.Name() + } + fmt.Fprintf(&hashes, "%d. %s\n<code>%s</code>\n\n", start+i+1, escapeHtml(title), hash) + } + msg := strings.TrimSuffix(hashes.String(), "\n\n") + + navRow := []tele.InlineButton{} + if totalPages > 1 { + if page > 0 { + navRow = append(navRow, tele.InlineButton{Text: "◀️", Unique: "fexport", Data: strconv.Itoa(page - 1)}) + } + navRow = append(navRow, tele.InlineButton{Text: strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages), Unique: "fnop", Data: ""}) + if page < totalPages-1 { + navRow = append(navRow, tele.InlineButton{Text: "▶️", Unique: "fexport", Data: strconv.Itoa(page + 1)}) + } + } + navRow = append(navRow, tele.InlineButton{Text: "🔄", Unique: "fexportrefresh", Data: strconv.Itoa(page)}) + + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{navRow}} + if err := c.Send(msg, kbd); err != nil { + log.TLogln("tg export send err", err) + return err + } + return nil +} + +func callbackExportPage(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendExportPage(c, page) +} + +func callbackExportRefresh(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendExportPage(c, page) +} diff --git a/server/tgbot/ffp.go b/server/tgbot/ffp.go new file mode 100644 index 0000000..0b212dc --- /dev/null +++ b/server/tgbot/ffp.go @@ -0,0 +1,209 @@ +package tgbot + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "server/ffprobe" + "server/settings" + "server/torr" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + ffp "gopkg.in/vansante/go-ffprobe.v2" +) + +// TODO: Use internal API for ffp + +func cmdFfp(c tele.Context) error { + uid := c.Sender().ID + args := c.Args() + if len(args) < 2 { + return c.Send(tr(uid, "ffp_usage")) + } + hash := resolveHash(c, args[0]) + if hash == "" { + return c.Send(tr(uid, "invalid_hash")) + } + id, err := strconv.Atoi(args[1]) + if err != nil || id < 1 { + return c.Send(tr(uid, "ffp_file_index")) + } + + asJSON := false + if len(args) >= 3 { + last := strings.ToLower(strings.TrimSpace(args[len(args)-1])) + if last == "json" || last == "--json" || last == "-j" { + asJSON = true + } + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(uid, "torrent_not_found")) + } + + proto := "http" + port := settings.Port + if settings.Ssl { + proto = "https" + port = settings.SslPort + } + link := fmt.Sprintf("%s://127.0.0.1:%s/play/%s/%d", proto, port, hash, id) + + data, err := ffprobe.ProbeUrl(link) + if err != nil { + return c.Send(fmt.Sprintf(tr(uid, "ffp_error"), err.Error())) + } + + var msg string + if asJSON { + buf, _ := json.MarshalIndent(data, "", " ") + msg = "<pre>" + strings.ReplaceAll(string(buf), "<", "<") + "</pre>" + if len(msg) > 4000 { + msg = msg[:4000] + "\n...</pre>" + } + } else { + msg = formatFfpHuman(data, uid) + if len(msg) > 4000 { + msg = msg[:4000] + "\n..." + } + } + return c.Send(msg) +} + +func formatFfpHuman(data *ffp.ProbeData, uid int64) string { + var sb strings.Builder + + if data.Format != nil { + f := data.Format + sb.WriteString("<b>📁 " + tr(uid, "ffp_format") + "</b>\n") + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_container"), f.FormatLongName) + if f.DurationSeconds > 0 { + d := int(f.DurationSeconds) + h, m, s := d/3600, (d%3600)/60, d%60 + fmt.Fprintf(&sb, " %s: %02d:%02d:%02d\n", tr(uid, "ffp_duration"), h, m, s) + } + if f.Size != "" { + if size, err := strconv.ParseInt(f.Size, 10, 64); err == nil { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_size"), humanize.IBytes(uint64(size))) + } else { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_size"), f.Size) + } + } + if f.BitRate != "" { + if br, err := strconv.ParseInt(f.BitRate, 10, 64); err == nil { + fmt.Fprintf(&sb, " %s: %s/s\n", tr(uid, "ffp_bitrate"), humanize.IBytes(uint64(br))) + } else { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_bitrate"), f.BitRate) + } + } + sb.WriteString("\n") + } + + sb.WriteString("<b>🎬 " + tr(uid, "ffp_streams") + "</b>\n\n") + for i, s := range data.Streams { + title := getTag(s.TagList, "title") + lang := getTag(s.TagList, "language") + if lang != "" { + lang = " [" + lang + "]" + } + + switch s.CodecType { + case "video": + fmt.Fprintf(&sb, "<b>#%d %s</b>%s\n", i, tr(uid, "ffp_video"), lang) + fmt.Fprintf(&sb, " %s: %s", tr(uid, "ffp_codec"), s.CodecLongName) + if s.Profile != "" { + fmt.Fprintf(&sb, " (%s)", s.Profile) + } + sb.WriteString("\n") + if s.Width > 0 && s.Height > 0 { + fmt.Fprintf(&sb, " %s: %d×%d", tr(uid, "ffp_resolution"), s.Width, s.Height) + if s.DisplayAspectRatio != "" && s.DisplayAspectRatio != "0:0" { + fmt.Fprintf(&sb, " (%s)", s.DisplayAspectRatio) + } + sb.WriteString("\n") + } + if s.PixFmt != "" { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_pixel"), s.PixFmt) + } + if s.RFrameRate != "" && s.RFrameRate != "0/0" { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_fps"), s.RFrameRate) + } + if s.BitRate != "" { + if br, err := strconv.ParseInt(s.BitRate, 10, 64); err == nil { + fmt.Fprintf(&sb, " %s: %s/s\n", tr(uid, "ffp_bitrate"), humanize.IBytes(uint64(br))) + } + } + if s.ColorSpace != "" || s.ColorTransfer != "" { + fmt.Fprintf(&sb, " %s: %s / %s / %s\n", tr(uid, "ffp_color"), s.ColorSpace, s.ColorTransfer, s.ColorPrimaries) + } + if title != "" { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_title"), escapeHtml(title)) + } + + case "audio": + fmt.Fprintf(&sb, "<b>#%d %s</b>%s\n", i, tr(uid, "ffp_audio"), lang) + fmt.Fprintf(&sb, " %s: %s", tr(uid, "ffp_codec"), s.CodecLongName) + if s.Profile != "" { + fmt.Fprintf(&sb, " (%s)", s.Profile) + } + sb.WriteString("\n") + if s.SampleRate != "" { + fmt.Fprintf(&sb, " %s: %s Hz\n", tr(uid, "ffp_samplerate"), s.SampleRate) + } + if s.Channels > 0 { + ch := s.ChannelLayout + if ch == "" { + ch = fmt.Sprintf("%d ch", s.Channels) + } + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_channels"), ch) + } + if s.BitRate != "" { + if br, err := strconv.ParseInt(s.BitRate, 10, 64); err == nil { + fmt.Fprintf(&sb, " %s: %s/s\n", tr(uid, "ffp_bitrate"), humanize.IBytes(uint64(br))) + } + } + if title != "" { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_title"), escapeHtml(title)) + } + + case "subtitle": + fmt.Fprintf(&sb, "<b>#%d %s</b>%s\n", i, tr(uid, "ffp_subtitle"), lang) + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_codec"), s.CodecLongName) + if title != "" { + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_title"), escapeHtml(title)) + } + + default: + fmt.Fprintf(&sb, "<b>#%d %s</b>\n", i, s.CodecType) + fmt.Fprintf(&sb, " %s: %s\n", tr(uid, "ffp_codec"), s.CodecLongName) + } + sb.WriteString("\n") + } + + return strings.TrimSuffix(sb.String(), "\n\n") +} + +func getTag(tags ffp.Tags, key string) string { + if tags == nil { + return "" + } + if v, ok := tags[key]; ok && v != nil { + if s, ok := v.(string); ok { + return s + } + return fmt.Sprint(v) + } + for k, v := range tags { + if strings.HasPrefix(k, key+"-") && v != nil { + if s, ok := v.(string); ok { + return s + } + return fmt.Sprint(v) + } + } + return "" +} diff --git a/server/tgbot/files.go b/server/tgbot/files.go new file mode 100644 index 0000000..1d4286d --- /dev/null +++ b/server/tgbot/files.go @@ -0,0 +1,229 @@ +package tgbot + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + + "server/log" + sets "server/settings" + "server/torr" +) + +// Telegram limits the serialized reply_markup size; many file rows with long +// labels/URLs would exceed it (e.g. "reply markup is too long"). +const filesPageSize = 5 + +// Inline button text is limited to 64 characters in the Bot API. +func truncateBtnText(s string) string { + const max = 64 + r := []rune(s) + if len(r) <= max { + return s + } + if max <= 1 { + return string(r[:max]) + } + return string(r[:max-1]) + "…" +} + +func files(c tele.Context) error { + args := c.Args() + if len(args) < 2 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + hash := args[1] + if !isHash(hash) { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + msg, err := c.Bot().Send(c.Sender(), tr(c.Sender().ID, "connecting")) + t := torr.GetTorrent(hash) + if t == nil { + if err == nil { + _, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "torrent_not_found")+":\n<code>"+hash+"</code>") + } + return nil + } + if err == nil { + api := c.Bot() + recipient := c.Sender() + uid := c.Sender().ID + go sendFilesList(api, recipient, msg, hash, uid, 0) + } + return err +} + +// sendFilesList shows one page of per-file actions; fitems / fifresh change the page in-place. +func sendFilesList(api tele.API, recipient tele.Recipient, statusMsg *tele.Message, hash string, uid int64, page int) { + t := torr.GetTorrent(hash) + for t != nil && !t.WaitInfo() { + time.Sleep(time.Second) + t = torr.GetTorrent(hash) + } + _ = api.Delete(statusMsg) + t = torr.GetTorrent(hash) + if t == nil { + return + } + ti := t.Status() + if ti == nil { + return + } + + host := getHost() + txt, kbd := buildFilesListView(t, host, uid, page) + if kbd == nil { + return + } + if _, err := api.Send(recipient, txt, kbd, tele.ModeHTML); err != nil { + log.TLogln("tg files send err", err) + } +} + +func buildFilesListView(t *torr.Torrent, host string, uid int64, page int) (string, *tele.ReplyMarkup) { + ti := t.Status() + if ti == nil { + return "", nil + } + hex := t.Hash().HexString() + n := len(ti.FileStats) + if n == 0 { + return "", nil + } + + totalPages := (n + filesPageSize - 1) / filesPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * filesPageSize + end := start + filesPageSize + if end > n { + end = n + } + pageFiles := ti.FileStats[start:end] + + viewedSet := make(map[int]struct{}) + for _, v := range sets.ListViewed(ti.Hash) { + viewedSet[v.FileIndex] = struct{}{} + } + + txt := "📁 <b>" + escapeHtml(ti.Title) + "</b> " + + "<i>" + humanize.IBytes(uint64(ti.TorrentSize)) + "</i>\n\n" + + "<code>" + ti.Hash + "</code>" + if totalPages > 1 { + txt += "\n\n" + tr(uid, "page") + " " + strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages) + } + if n > 1 { + txt += "\n\n" + fmt.Sprintf(tr(uid, "files_range_hint"), n) + } + + m := &tele.ReplyMarkup{} + var rows []tele.Row + + for _, f := range pageFiles { + viewedMark := "" + if _, ok := viewedSet[f.Id]; ok { + viewedMark = "✓ " + } + baseName := filepath.Base(f.Path) + mline := viewedMark + "#" + strconv.Itoa(f.Id) + ": " + humanize.IBytes(uint64(f.Length)) + " — " + baseName + fileLabel := truncateBtnText(mline) + idStr := strconv.Itoa(f.Id) + streamURL := host + "/stream/" + filepath.Base(f.Path) + "?link=" + hex + "&index=" + idStr + "&play" + rows = append(rows, m.Row( + m.Data(fileLabel, "upload", ti.Hash, idStr), + m.URL(tr(uid, "files_link"), streamURL), + m.Data("⏳", "fpreload", ti.Hash, idStr), + )) + } + + if totalPages > 1 { + var nav []tele.Btn + if page > 0 { + nav = append(nav, m.Data("◀️", "fitems", strconv.Itoa(page-1), ti.Hash)) + } + nav = append(nav, m.Data(strconv.Itoa(page+1)+"/"+strconv.Itoa(totalPages), "fnop")) + if page < totalPages-1 { + nav = append(nav, m.Data("▶️", "fitems", strconv.Itoa(page+1), ti.Hash)) + } + nav = append(nav, m.Data("🔄", "fifresh", strconv.Itoa(page), ti.Hash)) + rows = append(rows, m.Row(nav...)) + } else { + rows = append(rows, m.Row(m.Data("🔄", "fifresh", strconv.Itoa(page), ti.Hash))) + } + if n > 1 { + rows = append(rows, m.Row(m.Data(tr(uid, "files_download_all"), "fall", "all", ti.Hash))) + } + m.Inline(rows...) + return txt, m +} + +func callbackFileListPage(c tele.Context, pageStr, hash string) error { + page, err := strconv.Atoi(pageStr) + if err != nil { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + if !isHash(hash) { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + _ = c.Respond(&tele.CallbackResponse{}) + + return editFilesListMessage(c, hash, c.Sender().ID, page) +} + +func callbackFileListRefresh(c tele.Context, pageStr, hash string) error { + if !isHash(hash) { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + page, err := strconv.Atoi(pageStr) + if err != nil { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + return editFilesListMessage(c, hash, c.Sender().ID, page) +} + +func editFilesListMessage(c tele.Context, hash string, uid int64, page int) error { + t := torr.GetTorrent(hash) + if t == nil { + _ = c.Send(tr(uid, "torrent_not_found") + ":\n<code>" + hash + "</code>") + return nil + } + for t != nil && !t.WaitInfo() { + time.Sleep(time.Second) + t = torr.GetTorrent(hash) + } + t = torr.GetTorrent(hash) + if t == nil { + _ = c.Send(tr(uid, "torrent_not_found") + ":\n<code>" + hash + "</code>") + return nil + } + host := getHost() + txt, kbd := buildFilesListView(t, host, uid, page) + if kbd == nil { + log.TLogln("tg files: empty kbd for hash", logSafeStr(hash, 20)) + return nil + } + if c.Callback() == nil || c.Callback().Message == nil { + _, err := c.Bot().Send(c.Sender(), txt, kbd, tele.ModeHTML) + return err + } + _, err := c.Bot().Edit(c.Callback().Message, txt, kbd, tele.ModeHTML) + if err != nil { + if strings.Contains(err.Error(), "message is not modified") { + return nil + } + log.TLogln("tg files edit err", err) + _, _ = c.Bot().Send(c.Sender(), tr(uid, "error")+":\n"+escapeHtml(err.Error()), tele.ModeHTML) + return err + } + return nil +} diff --git a/server/tgbot/hash.go b/server/tgbot/hash.go new file mode 100644 index 0000000..c2d7fdd --- /dev/null +++ b/server/tgbot/hash.go @@ -0,0 +1,156 @@ +package tgbot + +import ( + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/log" + "server/torr" +) + +// resolveHash returns hash from: 1) full hash string, 2) numeric index from list, 3) reply-to message +func resolveHash(c tele.Context, arg string) string { + arg = strings.TrimSpace(arg) + if arg == "" { + return extractHashFromReply(c) + } + if isHash(arg) { + return arg + } + if idx, err := strconv.Atoi(arg); err == nil && idx > 0 { + torrents := torr.ListTorrent() + if idx <= len(torrents) { + return torrents[idx-1].Hash().HexString() + } + } + return "" +} + +func extractHashFromReply(c tele.Context) string { + if c.Message() == nil || c.Message().ReplyTo == nil { + return "" + } + reply := c.Message().ReplyTo + if reply.ReplyMarkup == nil || len(reply.ReplyMarkup.InlineKeyboard) == 0 { + return "" + } + for _, row := range reply.ReplyMarkup.InlineKeyboard { + for _, btn := range row { + if btn.Data == "" { + continue + } + if isHash(btn.Data) { + return btn.Data + } + if idx := strings.Index(btn.Data, "all|"); idx >= 0 { + h := btn.Data[idx+4:] + if len(h) >= 40 && isHash(h[:40]) { + return h[:40] + } + if isHash(h) { + return h + } + } + } + } + return "" +} + +const hashPageSize = 10 + +func cmdHash(c tele.Context) error { + args := c.Args() + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + if len(args) > 0 { + idx, err := strconv.Atoi(strings.TrimSpace(args[0])) + if err != nil || idx < 1 || idx > len(torrents) { + return c.Send(tr(c.Sender().ID, "invalid_index")) + } + hash := torrents[idx-1].Hash().HexString() + return c.Send("🔑 <code>" + hash + "</code>") + } + + return sendHashPage(c, 0) +} + +func sendHashPage(c tele.Context, page int) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + totalPages := (len(torrents) + hashPageSize - 1) / hashPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * hashPageSize + end := start + hashPageSize + if end > len(torrents) { + end = len(torrents) + } + pageTorrents := torrents[start:end] + + uid := c.Sender().ID + var sb strings.Builder + sb.WriteString("🔑 <b>" + tr(uid, "hash_title") + "</b> (" + strconv.Itoa(len(torrents)) + ")\n\n") + for i, t := range pageTorrents { + sb.WriteString(strconv.Itoa(start+i+1) + ". <code>" + t.Hash().HexString() + "</code>\n") + sb.WriteString(" " + escapeHtml(t.Title) + "\n\n") + } + msg := strings.TrimSuffix(sb.String(), "\n\n") + + navRow := []tele.InlineButton{} + if totalPages > 1 { + if page > 0 { + navRow = append(navRow, tele.InlineButton{Text: "◀️", Unique: "fhash", Data: strconv.Itoa(page - 1)}) + } + navRow = append(navRow, tele.InlineButton{Text: strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages), Unique: "fnop", Data: ""}) + if page < totalPages-1 { + navRow = append(navRow, tele.InlineButton{Text: "▶️", Unique: "fhash", Data: strconv.Itoa(page + 1)}) + } + } + navRow = append(navRow, tele.InlineButton{Text: "🔄", Unique: "fhashrefresh", Data: strconv.Itoa(page)}) + + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{navRow}} + if err := c.Send(msg, kbd); err != nil { + log.TLogln("tg hash send err", err) + return err + } + return nil +} + +func callbackHashPage(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendHashPage(c, page) +} + +func callbackHashRefresh(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendHashPage(c, page) +} diff --git a/server/tgbot/import_cmd.go b/server/tgbot/import_cmd.go new file mode 100644 index 0000000..6f50878 --- /dev/null +++ b/server/tgbot/import_cmd.go @@ -0,0 +1,60 @@ +package tgbot + +import ( + "fmt" + "regexp" + "strings" + + tele "gopkg.in/telebot.v4" +) + +var magnetRegex = regexp.MustCompile(`magnet:\?[^\s]+`) +var torrsRegex = regexp.MustCompile(`torrs://[^\s]+`) +var hashRegex = regexp.MustCompile(`\b([a-fA-F0-9]{40})\b`) + +func cmdImport(c tele.Context) error { + text := "" + if c.Message() != nil && c.Message().Text != "" { + text = strings.TrimPrefix(strings.TrimSpace(c.Message().Text), "/import") + text = strings.TrimSpace(text) + } + if text == "" { + return c.Send(tr(c.Sender().ID, "import_usage")) + } + var links []string + seen := make(map[string]bool) + for _, m := range magnetRegex.FindAllString(text, -1) { + m = strings.TrimSpace(m) + if m != "" && !seen[m] { + seen[m] = true + links = append(links, m) + } + } + for _, m := range torrsRegex.FindAllString(text, -1) { + m = strings.TrimSpace(m) + if m != "" && !seen[m] { + seen[m] = true + links = append(links, m) + } + } + for _, m := range hashRegex.FindAllString(text, -1) { + h := strings.ToLower(strings.TrimSpace(m)) + if h != "" && !seen[h] { + seen[h] = true + links = append(links, h) + } + } + if len(links) == 0 { + return c.Send(tr(c.Sender().ID, "import_no_links")) + } + uid := c.Sender().ID + added := 0 + for _, link := range links { + if err := addTorrent(c, link); err != nil { + _ = c.Send(fmt.Sprintf(tr(uid, "add_error"), err.Error())) + continue + } + added++ + } + return c.Send(fmt.Sprintf(tr(uid, "import_done"), added, len(links))) +} diff --git a/server/tgbot/inline.go b/server/tgbot/inline.go new file mode 100644 index 0000000..6914795 --- /dev/null +++ b/server/tgbot/inline.go @@ -0,0 +1,103 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/rutor" + "server/rutor/models" + sets "server/settings" + "server/torr" + "server/torznab" +) + +const inlineMaxResults = 20 + +func handleInlineQuery(c tele.Context) error { + query := strings.TrimSpace(c.Query().Text) + uid := int64(0) + if c.Query().Sender != nil { + uid = c.Query().Sender.ID + } + + var results tele.Results + id := 0 + + if query == "" || strings.ToLower(query) == "list" || strings.ToLower(query) == "play" { + torrents := torr.ListTorrent() + host := getHost() + for _, t := range torrents { + if id >= inlineMaxResults { + break + } + hash := t.Hash().HexString() + url := fmt.Sprintf("%s/play/%s/1", host, hash) + title := t.Title + if len(title) > 60 { + title = title[:57] + "..." + } + results = append(results, &tele.ArticleResult{ + ResultBase: tele.ResultBase{ID: strconv.Itoa(id)}, + Title: "▶ " + title, + Description: hash[:8] + "...", + URL: url, + Text: url, + }) + id++ + } + } + + if len(query) >= 2 && sets.BTsets != nil && (sets.BTsets.EnableRutorSearch || sets.BTsets.EnableTorznabSearch) { + var list []*models.TorrentDetails + if sets.BTsets.EnableRutorSearch { + list = append(list, rutor.Search(query)...) + } + if sets.BTsets.EnableTorznabSearch { + list = append(list, torznab.Search(query, -1)...) + } + for _, item := range list { + if id >= inlineMaxResults { + break + } + link := item.Magnet + if link == "" { + link = item.Link + } + if link == "" { + continue + } + title := item.Title + if len(title) > 60 { + title = title[:57] + "..." + } + size := item.Size + if size == "" { + size = "?" + } + results = append(results, &tele.ArticleResult{ + ResultBase: tele.ResultBase{ID: strconv.Itoa(id)}, + Title: "➕ " + title, + Description: fmt.Sprintf("%s S:%d P:%d", size, item.Seed, item.Peer), + Text: link, + }) + id++ + } + } + + if len(results) == 0 { + results = append(results, &tele.ArticleResult{ + ResultBase: tele.ResultBase{ID: "0"}, + Title: tr(uid, "no_torrents"), + Description: tr(uid, "add_magnet"), + Text: "", + }) + } + + return c.Answer(&tele.QueryResponse{ + Results: results, + CacheTime: 60, + IsPersonal: true, + }) +} diff --git a/server/tgbot/lang.go b/server/tgbot/lang.go new file mode 100644 index 0000000..0c5b1ba --- /dev/null +++ b/server/tgbot/lang.go @@ -0,0 +1,118 @@ +package tgbot + +import ( + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + tele "gopkg.in/telebot.v4" + "server/settings" +) + +const ( + LangRU = "ru" + LangEN = "en" + saveUserLangsWait = 2 * time.Second +) + +var ( + userLang = make(map[int64]string) + userLangMu sync.RWMutex + saveUserLangsMu sync.Mutex + saveUserLangsTimer *time.Timer +) + +func getUserLang(userID int64) string { + userLangMu.RLock() + defer userLangMu.RUnlock() + if lang, ok := userLang[userID]; ok { + return lang + } + return LangRU +} + +func setUserLang(userID int64, lang string) { + if lang != LangRU && lang != LangEN { + return + } + userLangMu.Lock() + userLang[userID] = lang + userLangMu.Unlock() + scheduleSaveUserLangs() +} + +func scheduleSaveUserLangs() { + saveUserLangsMu.Lock() + defer saveUserLangsMu.Unlock() + if saveUserLangsTimer != nil { + saveUserLangsTimer.Stop() + } + saveUserLangsTimer = time.AfterFunc(saveUserLangsWait, func() { + saveUserLangsMu.Lock() + saveUserLangsTimer = nil + saveUserLangsMu.Unlock() + saveUserLangs() + }) +} + +func loadUserLangs() { + fn := filepath.Join(settings.Path, "tg_langs.json") + buf, err := os.ReadFile(fn) + if err != nil { + return + } + var m map[string]string + if err := json.Unmarshal(buf, &m); err != nil { + return + } + userLangMu.Lock() + for k, v := range m { + if v == LangRU || v == LangEN { + if id, parseErr := strconv.ParseInt(k, 10, 64); parseErr == nil { + userLang[id] = v + } + } + } + userLangMu.Unlock() +} + +func saveUserLangs() { + userLangMu.RLock() + m := make(map[string]string) + for k, v := range userLang { + m[strconv.FormatInt(k, 10)] = v + } + userLangMu.RUnlock() + buf, err := json.Marshal(m) + if err != nil { + return + } + fn := filepath.Join(settings.Path, "tg_langs.json") + _ = os.WriteFile(fn, buf, 0o600) +} + +func cmdLang(c tele.Context) error { + uid := c.Sender().ID + args := c.Args() + if len(args) == 0 { + lang := getUserLang(uid) + if lang == LangEN { + return c.Send(tr(uid, "lang_current_en") + "\n/lang RU — " + tr(uid, "lang_switch_ru")) + } + return c.Send(tr(uid, "lang_current_ru") + "\n/lang EN — " + tr(uid, "lang_switch_en")) + } + lang := strings.ToUpper(strings.TrimSpace(args[0])) + if lang == "EN" { + setUserLang(uid, LangEN) + return c.Send(tr(uid, "lang_set_en")) + } + if lang == "RU" || lang == "РУ" { + setUserLang(uid, LangRU) + return c.Send(tr(uid, "lang_set")) + } + return c.Send(tr(uid, "lang_usage")) +} diff --git a/server/tgbot/link.go b/server/tgbot/link.go new file mode 100644 index 0000000..39ed55e --- /dev/null +++ b/server/tgbot/link.go @@ -0,0 +1,76 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func callbackLink(c tele.Context, data string) error { + uid := c.Sender().ID + index := 1 + hash := data + if idx := strings.Index(data, "|"); idx >= 0 && idx+1 < len(data) { + if i, err := strconv.Atoi(data[idx+1:]); err == nil && i > 0 { + index = i + hash = data[:idx] + } + } + t := torr.GetTorrent(hash) + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "torrent_not_found")}) + } + if !strings.Contains(data, "|") && t.WaitInfo() { + st := t.Status() + if st != nil && len(st.FileStats) > 1 { + maxFiles := 5 + if len(st.FileStats) < maxFiles { + maxFiles = len(st.FileStats) + } + var rows [][]tele.InlineButton + for i := 0; i < maxFiles; i++ { + f := st.FileStats[i] + btn := tele.InlineButton{Text: fmt.Sprintf("#%d", f.Id), Unique: "flink", Data: hash + "|" + strconv.Itoa(f.Id)} + rows = append(rows, []tele.InlineButton{btn}) + } + kbd := &tele.ReplyMarkup{InlineKeyboard: rows} + _ = c.Respond(&tele.CallbackResponse{}) + return c.Send("🔗 "+tr(uid, "btn_link")+":", kbd) + } + } + host := getHost() + url := fmt.Sprintf("%s/play/%s/%d", host, hash, index) + _ = c.Respond(&tele.CallbackResponse{}) + return c.Send(fmt.Sprintf(tr(uid, "link_play"), url)) +} + +func cmdLink(c tele.Context) error { + args := c.Args() + arg := "" + if len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + if hash == "" { + return c.Send(tr(c.Sender().ID, "link_usage")) + } + + index := 1 + if len(args) > 1 { + if i, err := strconv.Atoi(args[1]); err == nil && i > 0 { + index = i + } + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + host := getHost() + url := fmt.Sprintf("%s/play/%s/%d", host, hash, index) + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "link_play"), url)) +} diff --git a/server/tgbot/list.go b/server/tgbot/list.go new file mode 100644 index 0000000..2de2e16 --- /dev/null +++ b/server/tgbot/list.go @@ -0,0 +1,139 @@ +package tgbot + +import ( + "strconv" + "strings" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/log" + "server/torr" +) + +const listPageSize = 5 + +func list(c tele.Context) error { + args := c.Args() + compact := len(args) > 0 && strings.ToLower(args[0]) == "compact" + return sendListPage(c, 0, compact) +} + +func sendListPage(c tele.Context, page int, compact bool) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + totalPages := (len(torrents) + listPageSize - 1) / listPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * listPageSize + end := start + listPageSize + if end > len(torrents) { + end = len(torrents) + } + pageTorrents := torrents[start:end] + + uid := c.Sender().ID + for _, t := range pageTorrents { + hash := t.Hash().HexString() + var rows [][]tele.InlineButton + if compact { + rows = [][]tele.InlineButton{ + { + tele.InlineButton{Text: tr(uid, "btn_files"), Unique: "files", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_status"), Unique: "fstatus", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_delete"), Unique: "delete", Data: hash}, + }, + } + } else { + rows = [][]tele.InlineButton{ + { + tele.InlineButton{Text: tr(uid, "btn_files"), Unique: "files", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_delete"), Unique: "delete", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_status"), Unique: "fstatus", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_m3u"), Unique: "fm3u", Data: hash}, + }, + { + tele.InlineButton{Text: tr(uid, "btn_link"), Unique: "flink", Data: hash}, + tele.InlineButton{Text: tr(uid, "btn_drop"), Unique: "fdrop", Data: hash}, + }, + } + } + torrKbd := &tele.ReplyMarkup{InlineKeyboard: rows} + msg := "<b>" + escapeHtml(t.Title) + "</b>" + if t.Size > 0 { + msg += " <i>" + humanize.IBytes(uint64(t.Size)) + "</i>" + } + msg += "\n<code>" + hash + "</code>" + if err := c.Send(msg, torrKbd); err != nil { + log.TLogln("tg list send err", err) + return err + } + } + + compactStr := "0" + if compact { + compactStr = "1" + } + navRow := []tele.InlineButton{} + if totalPages > 1 { + if page > 0 { + navRow = append(navRow, tele.InlineButton{Text: "◀️", Unique: "flist", Data: strconv.Itoa(page-1) + "|" + compactStr}) + } + navRow = append(navRow, tele.InlineButton{Text: strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages), Unique: "fnop", Data: ""}) + if page < totalPages-1 { + navRow = append(navRow, tele.InlineButton{Text: "▶️", Unique: "flist", Data: strconv.Itoa(page+1) + "|" + compactStr}) + } + } + navRow = append(navRow, tele.InlineButton{Text: "🔄", Unique: "frefresh", Data: strconv.Itoa(page) + "|" + compactStr}) + if len(navRow) > 1 || totalPages == 1 { + if err := c.Send(tr(uid, "page")+" "+strconv.Itoa(page+1)+"/"+strconv.Itoa(totalPages), &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{navRow}}); err != nil { + log.TLogln("tg list nav err", err) + return err + } + } + return nil +} + +func callbackListPage(c tele.Context, data string) error { + parts := strings.Split(data, "|") + page := 0 + compact := false + if len(parts) > 0 && parts[0] != "" { + if p, err := strconv.Atoi(parts[0]); err == nil { + page = p + } + } + if len(parts) > 1 && parts[1] == "1" { + compact = true + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendListPage(c, page, compact) +} + +func callbackListRefresh(c tele.Context, data string) error { + parts := strings.Split(data, "|") + page := 0 + compact := false + if len(parts) > 0 && parts[0] != "" { + if p, err := strconv.Atoi(parts[0]); err == nil { + page = p + } + } + if len(parts) > 1 && parts[1] == "1" { + compact = true + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendListPage(c, page, compact) +} diff --git a/server/tgbot/locales.go b/server/tgbot/locales.go new file mode 100644 index 0000000..a5318fd --- /dev/null +++ b/server/tgbot/locales.go @@ -0,0 +1,14 @@ +package tgbot + +func tr(userID int64, key string) string { + lang := getUserLang(userID) + if lang == LangEN { + if s, ok := msgEN[key]; ok { + return s + } + } + if s, ok := msgRU[key]; ok { + return s + } + return key +} diff --git a/server/tgbot/locales_en.go b/server/tgbot/locales_en.go new file mode 100644 index 0000000..022f0b1 --- /dev/null +++ b/server/tgbot/locales_en.go @@ -0,0 +1,276 @@ +package tgbot + +var msgEN = map[string]string{ + "help": "TorrServer management bot", + "help_main": "Main", + "help_manage": "Management", + "help_status": "Status & links", + "help_search": "Search", + "help_other": "Other", + "help_server": "Server", + "help_use_index": "Use number from /list: /remove 1, /status 2", + "help_reply": "Or reply to torrent message with command", + "help_id": "Your id", + "no_torrents": "📭 No torrents", + "torrent_not_found": "❌ Torrent not found", + "invalid_hash": "❌ Invalid hash. Use 40 chars (a-f, 0-9)", + "invalid_index": "❌ Invalid index. Use number from /list", + "connecting": "⏳ Connecting to torrent...", + "add_magnet": "ℹ️ Paste magnet/hash/torrs:// to add torrent", + "range_error": "❌ Error, use numbers, e.g. 2-12", + "lang_set": "🌐 Language set: Russian", + "lang_set_en": "🌐 Language set: English", + "lang_current_ru": "🌐 Current language: Russian", + "lang_current_en": "🌐 Current language: English", + "lang_switch_ru": "switch to Russian", + "lang_switch_en": "switch to English", + "lang_usage": "ℹ️ Usage: /lang RU | /lang EN", + "admin_only": "🔒 Admin only", + "server_stopped": "🛑 Server stopped", + "searching": "🔍 Searching...", + "search_not_found": "🔍 Nothing found for «%s» (%s)", + "search_disabled_rutor": "ℹ️ RuTor search disabled in settings", + "search_disabled_torznab": "ℹ️ Torznab search disabled in settings", + "search_usage": "ℹ️ Usage: /search <query>", + "rutor_usage": "ℹ️ Usage: /rutor <query>", + "torznab_usage": "ℹ️ Usage: /torznab <query> [index]", + "clear_confirm": "🗑 Delete all %d torrents?", + "clear_done": "🗑 Deleted torrents: %d", + "shutdown_confirm": "⚠️ Shut down server?", + "canceled": "👌 Canceled", + "deleted": "✅ Deleted", + "callback_unknown": "❌ Error: unknown button", + "stats_title": "Summary statistics", + "page": "📄 Page", + "btn_add": "➕ Add", + "btn_files": "Files", + "btn_delete": "Delete", + "btn_status": "Status", + "btn_m3u": "M3U", + "btn_link": "Link", + "btn_drop": "Drop", + "btn_yes": "Yes", + "btn_no": "No", + "help_help": "This help", + "help_list": "/list [compact] - List (compact — fewer buttons)", + "help_clear": "/clear - Delete all torrents", + "help_add": "/add <link> - Add torrent", + "help_hash": "/hash [N] - Show torrent hashes", + "help_manage_desc": "(hash or number from /list)", + "help_remove": "/remove, /drop, /set, /status, /cache, /queue", + "help_links": "/link, /play, /m3u, /m3uall", + "help_server_cmd": "/server - Server info", + "help_echo": "/echo - Version", + "help_db": "/db - Torrents in DB", + "help_search_desc": "(with Add button)", + "help_search_cmd": "/search, /rutor, /torznab", + "help_other_cmd": "/viewed, /ffp, /speedtest, /preload, /snake", + "help_lang": "/lang RU|EN - Language", + "help_admin": "/shutdown, /settings, /preset - Admin", + "help_stats": "/stats - Summary statistics", + "help_stat": "/stat - Detailed status", + "help_export": "/export - Export magnet links", + "help_import": "/import <text> - Import from list", + "help_categories": "/categories - Torrent categories", + "help_rutor": "/rutor - Search RuTor", + "help_m3uall": "/m3uall - M3U of all torrents", + "help_play": "/play - Alias for /link", + "help_export_import": "Export / Import", + "help_categories_section": "Categories", + "settings_title": "Server settings", + "settings_error": "❌ Error: %s", + "settings_not_loaded": "❌ Settings not loaded", + "settings_export": "Export", + "settings_nav_cache": "Cache", + "settings_nav_paths": "Paths", + "settings_nav_storage": "Storage", + "settings_export_caption": "TorrServer settings", + "settings_exported": "✅ Settings exported", + "settings_saved": "✅ Saved", + "settings_readonly": "⚠️ Read-only mode", + "settings_more": "More", + "settings_back": "Back", + "settings_to_page2": "Cache", + "settings_page2": "Cache & limits", + "settings_page3": "Text parameters", + "settings_section_search": "Search", + "settings_section_network": "Network", + "settings_section_other": "Other", + "settings_section_limits": "Limits", + "settings_limits_cache": "Cache", + "settings_limits_connections": "Connections", + "settings_limits_speed": "Speed", + "settings_section_paths": "Paths & keys", + "settings_input_reply": "Reply to this message with new value", + "settings_input_done": "✅ %s: %s", + "settings_input_error": "❌ Error: %s", + "settings_input_torznab_usage": "Format: URL or URL|Key or URL|Key|Name", + "settings_input_torznab_added": "✅ Torznab added: %s", + "settings_set_friendlyname": "FriendlyName (DLNA)", + "settings_set_path": "TorrentsSavePath", + "settings_set_sslcert": "SslCert", + "settings_set_sslkey": "SslKey", + "settings_set_tmdbkey": "TMDB API Key", + "settings_add_torznab": "Add Torznab", + "settings_clear_torznab": "Clear Torznab", + "settings_set_proxyhosts": "ProxyHosts", + "settings_hint_friendlyname": "DLNA server name. clear — to clear", + "settings_hint_path": "Path to cache folder on server. clear — disable UseDisk", + "settings_hint_sslcert": "Path to SSL certificate. clear — to clear", + "settings_hint_sslkey": "Path to SSL key. clear — to clear", + "settings_hint_tmdbkey": "TMDB API Key. clear — to clear", + "settings_hint_proxyhosts": "Hosts comma-separated: host1, host2. clear — reset", + "settings_hint_torznab": "URL or URL|Key or URL|Key|Name", + "settings_page4": "Storage & TMDB", + "settings_section_storage": "Storage", + "settings_section_tmdb": "TMDB (read-only)", + "settings_storage_settings": "Settings", + "settings_storage_viewed": "Viewed", + "settings_torznab_test": "Test Torznab", + "settings_hint_torznab_test": "URL|Key — test indexer before adding", + "settings_torznab_test_ok": "✅ Torznab: connection successful", + "settings_torznab_test_fail": "❌ Torznab: %s", + "settings_reset": "Reset to defaults", + "settings_reset_confirm": "Reset to factory defaults?", + "settings_reset_done": "✅ Settings reset", + "preset_usage": "⚙️ /preset <name> or /preset <key> <value> ...\n\nNamed: performance, storage, streaming, low, default\n\nExamples:\n/preset performance\n/preset cache 256 preload 50\n/preset cache 512 conn 100 down 0", + "preset_confirm": "⚠️ Applying preset will reload TorrServer (torrents will be disconnected). Continue?", + "preset_applied": "✅ Preset applied: ", + "add_error": "❌ Connection error: %s", + "add_not_created": "❌ Error: torrent not created", + "add_timeout": "❌ Error adding torrent: timeout connection get torrent info", + "add_getting_meta": "⏳ Getting metadata...", + "add_success": "✅ Torrent added:\n<code>%s</code>", + "stats_torrents": "Torrents", + "stats_total_size": "Total size", + "stats_loaded": "Loaded", + "stats_peers": "Peers", + "stats_active": "active", + "stats_seeds": "seeders", + "stats_streams": "Streams", + "error": "❌ Error", + "search_expired": "ℹ️ Result expired, search again", + "search_more": "More", + "search_more_hint": "ℹ️ Showing %d of %d. Click for more results", + "search_no_link": "ℹ️ No link", + "search_adding": "⏳ Adding...", + "add_usage": "ℹ️ Usage: /add <magnet|hash|torrs://|url>\nPaste torrent link", + "add_no_link": "ℹ️ Specify torrent link", + "remove_usage": "ℹ️ Usage: /remove <hash|number>\nOr reply to torrent message", + "remove_done": "✅ Torrent removed:\n<code>%s</code>", + "status_waiting": "⏳ Waiting for torrent info...", + "status_stopped": "🛑 Auto-refresh stopped", + "status_stop_btn": "🛑 Stop", + "status_refresh_btn": "🔄 Refresh", + "status_auto_ended": "Auto-refresh ended", + "status_torrent_gone": "Torrent removed or disconnected", + "status_no_active": "📭 No active torrents", + "status_label": "Status", + "status_size": "Size", + "status_cache": "Cache", + "status_streams": "streams", + "status_download": "Download", + "status_upload": "Upload", + "status_peers": "Peers", + "speed_bps": "bps", + "speed_kbps": "kbps", + "speed_Mbps": "Mbps", + "speed_Gbps": "Gbps", + "speed_Tbps": "Tbps", + "link_usage": "ℹ️ Usage: /link <hash|number> [index]\nOr reply to torrent message", + "link_play": "🔗 Playback link:\n<code>%s</code>", + "server_title": "TorrServer", + "server_url": "URL", + "server_port": "Port", + "server_streams": "Active streams", + "m3u_usage": "ℹ️ Usage: /m3u <hash|number> [fromlast]\nOr reply to torrent message", + "m3u_playlist": "🎵 M3U playlist:\n<code>%s</code>", + "m3u_all": "🎵 All torrents M3U:\n<code>%s</code>", + "drop_done": "✅ Torrent disconnected", + "drop_done_hash": "✅ Torrent disconnected:\n<code>%s</code>", + "preload_usage": "ℹ️ Usage: /preload <hash|number> <index>\nOr reply to torrent message", + "preload_invalid": "❌ Specify valid file number (integer >= 1)", + "preload_started": "⏳ Preload started for file #%s", + "preload_btn": "Preload #%s", + "hash_title": "Torrent hashes", + "files_link": "Link", + "files_download_all": "Download all files", + "files_range_hint": "To download multiple files, reply with range, e.g. 2-12\n\nDownload all files? Total: %d", + "upload_queue_full": "⚠️ Queue full, try later\n\nItems in queue: %d", + "upload_connecting": "⏳ <b>Connecting to torrent</b>\n<code>%s</code>", + "upload_cancel": "Cancel", + "upload_queue_pos": "📋 Queue position %d", + "upload_error": "❌ Telegram upload error: %v", + "parse_range_err": "❌ Invalid format", + "cache_usage": "ℹ️ Usage: /cache <hash|number>\nOr reply to torrent message", + "cache_capacity": "Capacity", + "cache_filled": "Filled", + "cache_pieces": "Pieces", + "cache_readers": "Readers", + "cache_unavailable": "⚠️ Cache unavailable for torrent:\n<code>%s</code>", + "snake_usage": "ℹ️ Usage: /snake <hash|number> [cols] [rows]\n\nCache visualization. Position moves along snake.\nDefault: 20×3 (up to 50×15)", + "snake_cache": "Preload / Cache", + "snake_cached": "cached", + "snake_range": "buffer", + "snake_empty": "empty", + "snake_reader": "reader", + "snake_legend": "🟩cache 🟦buff 🔵pos ⬜empt", + "snake_pieces": "pieces", + "snake_no_data": "No cache data", + "set_done": "✅ Title updated:\n<code>%s</code>", + "set_usage": "ℹ️ Usage: /set <hash|index> <title>\nOr reply to torrent message", + "set_title_required": "❌ Specify new title", + "viewed_marked": "✅ Marked: <code>%s</code> file #%d", + "viewed_unmarked": "✅ Unmarked: <code>%s</code> file #%d", + "viewed_cleared": "✅ All marks cleared: <code>%s</code>", + "viewed_list": "📺 Viewed files", + "viewed_usage": "ℹ️ Usage:\n/viewed <hash|index> — list\n/viewed set <hash|index> <file> — mark\n/viewed rem <hash|index> [file] — unmark", + "viewed_usage_action": "ℹ️ Usage: /viewed %s <hash|index> [file]", + "viewed_usage_set": "ℹ️ Usage: /viewed set <hash|index> <file>", + "viewed_file_index": "❌ Specify file number (integer >= 1)", + "viewed_empty": "📭 No viewed files for this torrent", + "speedtest_msg": "⚡ Download test %d MB:\n<code>%s</code>\n\nDownload the file and measure speed", + "ffp_usage": "ℹ️ Usage: /ffp <hash|number> <id> [json]\nid — file number. json — raw output", + "ffp_file_index": "❌ Specify valid file number", + "ffp_error": "❌ FFprobe error: %s", + "ffp_format": "Format", + "ffp_container": "Container", + "ffp_duration": "Duration", + "ffp_size": "Size", + "ffp_bitrate": "Bitrate", + "ffp_streams": "Streams", + "ffp_video": "Video", + "ffp_audio": "Audio", + "ffp_subtitle": "Subtitle", + "ffp_codec": "Codec", + "ffp_resolution": "Resolution", + "ffp_pixel": "Pixel format", + "ffp_fps": "FPS", + "ffp_color": "Color", + "ffp_samplerate": "Sample rate", + "ffp_channels": "Channels", + "ffp_title": "Title", + "db_empty": "📭 Torrent database is empty", + "db_title": "Torrents in DB", + "export_title": "Export torrents", + "export_file_caption": "Magnet links in file", + "import_usage": "ℹ️ Usage: /import <text with magnet/hash/torrs>\nPaste multiple links separated by space or newline", + "import_no_links": "ℹ️ No links found. Paste magnet, hash or torrs://", + "import_done": "✅ Added: %d of %d", + "categories_title": "Categories", + "categories_uncategorized": "(uncategorized)", + "queue_empty": "📭 Queue empty", + "upload_working": "📥 Downloading", + "upload_in_queue": "📋 In queue", + "upload_stopping": "⏹ Stopping...", + "upload_title": "Downloading torrent", + "upload_hash": "Hash", + "upload_speed": "Speed", + "upload_remaining": "Remaining", + "upload_peers": "Peers", + "upload_progress": "Progress", + "upload_files": "Files", + "upload_finishing": "Finishing download, this may take a while", + "upload_file_too_large_2gb": "❌ File size must not exceed 2GB", + "upload_file_too_large_50mb": "❌ File size must not exceed 50MB. To upload files up to 2GB, specify host in tg.cfg to <a href='https://github.com/tdlib/telegram-bot-api'>telegram bot-api</a>", +} diff --git a/server/tgbot/locales_ru.go b/server/tgbot/locales_ru.go new file mode 100644 index 0000000..79d28e4 --- /dev/null +++ b/server/tgbot/locales_ru.go @@ -0,0 +1,276 @@ +package tgbot + +var msgRU = map[string]string{ + "help": "Бот для управления TorrServer", + "help_main": "Основные", + "help_manage": "Управление", + "help_status": "Статус и ссылки", + "help_search": "Поиск", + "help_other": "Прочее", + "help_server": "Сервер", + "help_use_index": "Можно использовать номер из /list: /remove 1, /status 2", + "help_reply": "Или ответьте на сообщение торрента командой", + "help_id": "Ваш id", + "no_torrents": "📭 Нет торрентов", + "torrent_not_found": "❌ Торрент не найден", + "invalid_hash": "❌ Некорректный хэш. Укажите 40 символов (a-f, 0-9)", + "invalid_index": "❌ Некорректный номер. Используйте число из /list", + "connecting": "⏳ Подключение к торренту...", + "add_magnet": "ℹ️ Вставьте магнет/хэш/torrs:// чтоб добавить торрент", + "range_error": "❌ Ошибка, нужно указывать числа, пример: 2-12", + "lang_set": "🌐 Язык установлен: Русский", + "lang_set_en": "🌐 Language set: English", + "lang_current_ru": "🌐 Текущий язык: Русский", + "lang_current_en": "🌐 Current language: English", + "lang_switch_ru": "переключить на русский", + "lang_switch_en": "switch to English", + "lang_usage": "ℹ️ Использование: /lang RU | /lang EN", + "admin_only": "🔒 Только для администратора", + "server_stopped": "🛑 Сервер остановлен", + "searching": "🔍 Поиск...", + "search_not_found": "🔍 По запросу «%s» ничего не найдено (%s)", + "search_disabled_rutor": "ℹ️ Поиск RuTor отключён в настройках", + "search_disabled_torznab": "ℹ️ Поиск Torznab отключён в настройках", + "search_usage": "ℹ️ Использование: /search <запрос>", + "rutor_usage": "ℹ️ Использование: /rutor <запрос>", + "torznab_usage": "ℹ️ Использование: /torznab <запрос> [индекс]", + "clear_confirm": "🗑 Удалить все %d торрентов?", + "clear_done": "🗑 Удалено торрентов: %d", + "shutdown_confirm": "⚠️ Остановить сервер?", + "canceled": "👌 Отменено", + "deleted": "✅ Удалено", + "callback_unknown": "❌ Ошибка: кнопка не распознана", + "stats_title": "Сводная статистика", + "page": "📄 Страница", + "btn_add": "➕ Добавить", + "btn_files": "Файлы", + "btn_delete": "Удалить", + "btn_status": "Статус", + "btn_m3u": "M3U", + "btn_link": "Ссылка", + "btn_drop": "Отключить", + "btn_yes": "Да", + "btn_no": "Нет", + "help_help": "Эта справка", + "help_list": "/list [compact] - Список (compact — меньше кнопок)", + "help_clear": "/clear - Удалить все торренты", + "help_add": "/add <ссылка> - Добавить торрент", + "help_hash": "/hash [N] - Показать hash торрентов", + "help_manage_desc": "(hash или номер из /list)", + "help_remove": "/remove, /drop, /set, /status, /cache, /queue", + "help_links": "/link, /play, /m3u, /m3uall", + "help_server_cmd": "/server - Информация о сервере", + "help_echo": "/echo - Версия", + "help_db": "/db - Торренты в БД", + "help_search_desc": "(с кнопкой Добавить)", + "help_search_cmd": "/search, /rutor, /torznab", + "help_other_cmd": "/viewed, /ffp, /speedtest, /preload, /snake", + "help_lang": "/lang RU|EN - Язык", + "help_admin": "/shutdown, /settings, /preset - Админ", + "help_stats": "/stats - Сводная статистика", + "help_stat": "/stat - Детальный статус", + "help_export": "/export - Экспорт магнет-ссылок", + "help_import": "/import <текст> - Импорт из списка", + "help_categories": "/categories - Категории торрентов", + "help_rutor": "/rutor - Поиск RuTor", + "help_m3uall": "/m3uall - M3U всех торрентов", + "help_play": "/play - Алиас /link", + "help_export_import": "Экспорт / Импорт", + "help_categories_section": "Категории", + "settings_title": "Настройки сервера", + "settings_error": "❌ Ошибка: %s", + "settings_not_loaded": "❌ Настройки не загружены", + "settings_export": "Экспорт", + "settings_nav_cache": "Кэш", + "settings_nav_paths": "Пути", + "settings_nav_storage": "Хранилище", + "settings_export_caption": "Настройки TorrServer", + "settings_exported": "✅ Настройки экспортированы", + "settings_saved": "✅ Сохранено", + "settings_readonly": "⚠️ Режим только чтение", + "settings_more": "Ещё", + "settings_back": "Назад", + "settings_to_page2": "Кэш", + "settings_page2": "Кэш и лимиты", + "settings_page3": "Текстовые параметры", + "settings_section_search": "Поиск", + "settings_section_network": "Сеть", + "settings_section_other": "Прочее", + "settings_section_limits": "Лимиты", + "settings_limits_cache": "Кэш", + "settings_limits_connections": "Подключения", + "settings_limits_speed": "Скорость", + "settings_section_paths": "Пути и ключи", + "settings_input_reply": "Ответьте на это сообщение новым значением", + "settings_input_done": "✅ %s: %s", + "settings_input_error": "❌ Ошибка: %s", + "settings_input_torznab_usage": "Формат: URL или URL|Key или URL|Key|Name", + "settings_input_torznab_added": "✅ Torznab добавлен: %s", + "settings_set_friendlyname": "FriendlyName (DLNA)", + "settings_set_path": "TorrentsSavePath", + "settings_set_sslcert": "SslCert", + "settings_set_sslkey": "SslKey", + "settings_set_tmdbkey": "TMDB API Key", + "settings_add_torznab": "Добавить Torznab", + "settings_clear_torznab": "Очистить Torznab", + "settings_set_proxyhosts": "ProxyHosts", + "settings_hint_friendlyname": "Имя DLNA-сервера. clear — очистить", + "settings_hint_path": "Путь к папке кэша на сервере. clear — отключить UseDisk", + "settings_hint_sslcert": "Путь к SSL-сертификату. clear — очистить", + "settings_hint_sslkey": "Путь к SSL-ключу. clear — очистить", + "settings_hint_tmdbkey": "TMDB API Key. clear — очистить", + "settings_hint_proxyhosts": "Хосты через запятую: host1, host2. clear — сброс", + "settings_hint_torznab": "URL или URL|Key или URL|Key|Name", + "settings_page4": "Хранилище и TMDB", + "settings_section_storage": "Хранилище", + "settings_section_tmdb": "TMDB (только просмотр)", + "settings_storage_settings": "Настройки", + "settings_storage_viewed": "Просмотренные", + "settings_torznab_test": "Тест Torznab", + "settings_hint_torznab_test": "URL|Key — проверка индексера до добавления", + "settings_torznab_test_ok": "✅ Torznab: подключение успешно", + "settings_torznab_test_fail": "❌ Torznab: %s", + "settings_reset": "Сброс настроек", + "settings_reset_confirm": "Сбросить на заводские настройки?", + "settings_reset_done": "✅ Настройки сброшены", + "preset_usage": "⚙️ /preset <имя> или /preset <ключ> <значение> ...\n\nИменованные: performance, storage, streaming, low, default\n\nПримеры:\n/preset performance\n/preset cache 256 preload 50\n/preset cache 512 conn 100 down 0", + "preset_confirm": "⚠️ Применение пресета перезагрузит TorrServer (торренты будут отключены). Продолжить?", + "preset_applied": "✅ Пресет применён: ", + "add_error": "❌ Ошибка при подключении: %s", + "add_not_created": "❌ Ошибка: торрент не создан", + "add_timeout": "❌ Ошибка при добавлении торрента: timeout connection get torrent info", + "add_getting_meta": "⏳ Получение метаданных...", + "add_success": "✅ Торрент добавлен:\n<code>%s</code>", + "stats_torrents": "Торрентов", + "stats_total_size": "Общий размер", + "stats_loaded": "Загружено", + "stats_peers": "Пиры", + "stats_active": "активных", + "stats_seeds": "сидов", + "stats_streams": "Потоков", + "error": "❌ Ошибка", + "search_expired": "ℹ️ Результат устарел, повторите поиск", + "search_more": "Ещё", + "search_more_hint": "ℹ️ Показано %d из %d. Нажмите для следующих результатов", + "search_no_link": "ℹ️ Нет ссылки", + "search_adding": "⏳ Добавление...", + "add_usage": "ℹ️ Использование: /add <magnet|hash|torrs://|url>\nВставьте ссылку на торрент", + "add_no_link": "ℹ️ Укажите ссылку на торрент", + "remove_usage": "ℹ️ Использование: /remove <hash|номер>\nИли ответьте на сообщение торрента", + "remove_done": "✅ Торрент удалён:\n<code>%s</code>", + "status_waiting": "⏳ Ожидание информации о торренте...", + "status_stopped": "🛑 Автообновление остановлено", + "status_stop_btn": "🛑 Стоп", + "status_refresh_btn": "🔄 Обновить", + "status_auto_ended": "Автообновление завершено", + "status_torrent_gone": "Торрент удалён или отключён", + "status_no_active": "📭 Нет активных торрентов", + "status_label": "Статус", + "status_size": "Размер", + "status_cache": "Кэш", + "status_streams": "потоков", + "status_download": "Скачивание", + "status_upload": "Раздача", + "status_peers": "Пиры", + "speed_bps": "бит/c", + "speed_kbps": "кбит/с", + "speed_Mbps": "Мбит/c", + "speed_Gbps": "Гбит/с", + "speed_Tbps": "Тбит/с", + "link_usage": "ℹ️ Использование: /link <hash|номер> [index]\nИли ответьте на сообщение торрента", + "link_play": "🔗 Ссылка для воспроизведения:\n<code>%s</code>", + "server_title": "Сервер TorrServer", + "server_url": "URL", + "server_port": "Порт", + "server_streams": "Активных потоков", + "m3u_usage": "ℹ️ Использование: /m3u <hash|номер> [fromlast]\nИли ответьте на сообщение торрента", + "m3u_playlist": "🎵 M3U плейлист:\n<code>%s</code>", + "m3u_all": "🎵 M3U всех торрентов:\n<code>%s</code>", + "drop_done": "✅ Торрент отключён", + "drop_done_hash": "✅ Торрент отключён:\n<code>%s</code>", + "preload_usage": "ℹ️ Использование: /preload <hash|номер> <index>\nИли ответьте на сообщение торрента", + "preload_invalid": "❌ Укажите корректный номер файла (целое число >= 1)", + "preload_started": "⏳ Предзагрузка запущена для файла #%s", + "preload_btn": "Предзагрузка #%s", + "hash_title": "Hash торрентов", + "files_link": "Ссылка", + "files_download_all": "Скачать все файлы", + "files_range_hint": "Чтобы скачать несколько файлов, ответьте на это сообщение, с какого файла скачать по какой, пример: 2-12\n\nСкачать все файлы? Всего: %d", + "upload_queue_full": "⚠️ Очередь переполнена, попробуйте попозже\n\nЭлементов в очереди: %d", + "upload_connecting": "⏳ <b>Подключение к торренту</b>\n<code>%s</code>", + "upload_cancel": "Отмена", + "upload_queue_pos": "📋 Номер в очереди %d", + "upload_error": "❌ Ошибка загрузки в телеграм: %v", + "parse_range_err": "❌ Неверный формат строки", + "cache_usage": "ℹ️ Использование: /cache <hash|номер>\nИли ответьте на сообщение торрента", + "cache_capacity": "Ёмкость", + "cache_filled": "Заполнено", + "cache_pieces": "Пайсов", + "cache_readers": "Читателей", + "cache_unavailable": "⚠️ Кэш недоступен для торрента:\n<code>%s</code>", + "snake_usage": "ℹ️ Использование: /snake <hash|номер> [колонок] [строк]\n\nВизуализация кэша. Позиция движется по змейке.\nПо умолчанию: 20×3 (до 50×15)", + "snake_cache": "Предзагрузка / Кеш", + "snake_cached": "кэш", + "snake_range": "буфер", + "snake_empty": "пусто", + "snake_reader": "позиция", + "snake_legend": "🟩кэш 🟦буф 🔵поз ⬜пуст", + "snake_pieces": "пайсы", + "snake_no_data": "Нет данных кэша", + "set_done": "✅ Название обновлено:\n<code>%s</code>", + "set_usage": "ℹ️ Использование: /set <hash|номер> <название>\nИли ответьте на сообщение торрента", + "set_title_required": "❌ Укажите новое название", + "viewed_marked": "✅ Отмечено: <code>%s</code> файл #%d", + "viewed_unmarked": "✅ Снята отметка: <code>%s</code> файл #%d", + "viewed_cleared": "✅ Сняты все отметки: <code>%s</code>", + "viewed_list": "📺 Просмотренные файлы", + "viewed_usage": "ℹ️ Использование:\n/viewed <hash|номер> — список\n/viewed set <hash|номер> <index> — отметить\n/viewed rem <hash|номер> [index] — снять отметку", + "viewed_usage_action": "ℹ️ Использование: /viewed %s <hash|номер> [index]", + "viewed_usage_set": "ℹ️ Использование: /viewed set <hash|номер> <index>", + "viewed_file_index": "❌ Укажите номер файла (целое число >= 1)", + "viewed_empty": "📭 Нет просмотренных файлов для этого торрента", + "speedtest_msg": "⚡ Тест загрузки %d MB:\n<code>%s</code>\n\nСкачайте файл и замерьте скорость", + "ffp_usage": "ℹ️ Использование: /ffp <hash|номер> <id> [json]\nid — номер файла. json — сырой вывод", + "ffp_file_index": "❌ Укажите корректный номер файла", + "ffp_error": "❌ Ошибка ffprobe: %s", + "ffp_format": "Формат", + "ffp_container": "Контейнер", + "ffp_duration": "Длительность", + "ffp_size": "Размер", + "ffp_bitrate": "Битрейт", + "ffp_streams": "Дорожки", + "ffp_video": "Видео", + "ffp_audio": "Аудио", + "ffp_subtitle": "Субтитры", + "ffp_codec": "Кодек", + "ffp_resolution": "Разрешение", + "ffp_pixel": "Пиксели", + "ffp_fps": "FPS", + "ffp_color": "Цвет", + "ffp_samplerate": "Частота", + "ffp_channels": "Каналы", + "ffp_title": "Название", + "db_empty": "📭 База торрентов пуста", + "db_title": "Торренты в БД", + "export_title": "Экспорт торрентов", + "export_file_caption": "Магнет-ссылки в файле", + "import_usage": "ℹ️ Использование: /import <текст с magnet/hash/torrs>\nВставьте несколько ссылок через пробел или перенос строки", + "import_no_links": "ℹ️ Ссылки не найдены. Вставьте magnet, hash или torrs://", + "import_done": "✅ Добавлено: %d из %d", + "categories_title": "Категории", + "categories_uncategorized": "(без категории)", + "queue_empty": "📭 Очередь пуста", + "upload_working": "📥 Закачиваются", + "upload_in_queue": "📋 В очереди", + "upload_stopping": "⏹ Остановка...", + "upload_title": "Загрузка торрента", + "upload_hash": "Хэш", + "upload_speed": "Скорость", + "upload_remaining": "Осталось", + "upload_peers": "Пиры", + "upload_progress": "Загружено", + "upload_files": "Файлов", + "upload_finishing": "Завершение загрузки, это займёт некоторое время", + "upload_file_too_large_2gb": "❌ Размер файла не должен превышать 2GB", + "upload_file_too_large_50mb": "❌ Размер файла не должен превышать 50MB. Чтобы закачивать файлы до 2GB, укажите host в tg.cfg к <a href='https://github.com/tdlib/telegram-bot-api'>telegram bot-api</a>", +} diff --git a/server/tgbot/m3u.go b/server/tgbot/m3u.go new file mode 100644 index 0000000..67574f0 --- /dev/null +++ b/server/tgbot/m3u.go @@ -0,0 +1,51 @@ +package tgbot + +import ( + "fmt" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func callbackM3u(c tele.Context, hash string) error { + uid := c.Sender().ID + t := torr.GetTorrent(hash) + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "torrent_not_found")}) + } + host := getHost() + url := fmt.Sprintf("%s/playlist?hash=%s", host, hash) + _ = c.Respond(&tele.CallbackResponse{}) + return c.Send(fmt.Sprintf(tr(uid, "m3u_playlist"), url)) +} + +func cmdM3u(c tele.Context) error { + args := c.Args() + arg := "" + if len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + if hash == "" { + return c.Send(tr(c.Sender().ID, "m3u_usage")) + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + host := getHost() + url := fmt.Sprintf("%s/playlist?hash=%s", host, hash) + if len(args) > 1 && strings.ToLower(args[1]) == "fromlast" { + url += "&fromlast=1" + } + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "m3u_playlist"), url)) +} + +func cmdM3uAll(c tele.Context) error { + host := getHost() + url := host + "/playlistall/all.m3u" + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "m3u_all"), url)) +} diff --git a/server/tgbot/middleware.go b/server/tgbot/middleware.go new file mode 100644 index 0000000..d8f8eda --- /dev/null +++ b/server/tgbot/middleware.go @@ -0,0 +1,16 @@ +package tgbot + +import tele "gopkg.in/telebot.v4" + +// adminOnly wraps a handler to allow only admin users (when whitelist is used) +func adminOnly(h tele.HandlerFunc) tele.HandlerFunc { + return func(c tele.Context) error { + if c.Sender() == nil { + return nil + } + if !isAdmin(c.Sender().ID) { + return c.Send(tr(c.Sender().ID, "admin_only")) + } + return h(c) + } +} diff --git a/server/tgbot/preload.go b/server/tgbot/preload.go new file mode 100644 index 0000000..13bbeca --- /dev/null +++ b/server/tgbot/preload.go @@ -0,0 +1,48 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdPreload(c tele.Context) error { + args := c.Args() + if len(args) < 2 { + return c.Send(tr(c.Sender().ID, "preload_usage")) + } + uid := c.Sender().ID + hash := resolveHash(c, args[0]) + if hash == "" { + return c.Send(tr(uid, "invalid_hash")) + } + index, err := strconv.Atoi(strings.TrimSpace(args[1])) + if err != nil || index < 1 { + return c.Send(tr(uid, "preload_invalid")) + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(uid, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + torr.Preload(t, index) + return c.Send(fmt.Sprintf(tr(uid, "preload_started"), args[1])) +} + +func callbackPreload(c tele.Context, hash, indexStr string) error { + uid := c.Sender().ID + index, err := strconv.Atoi(indexStr) + if err != nil || index < 1 { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "error")}) + } + t := torr.GetTorrent(hash) + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "torrent_not_found")}) + } + torr.Preload(t, index) + return c.Respond(&tele.CallbackResponse{Text: fmt.Sprintf(tr(uid, "preload_btn"), indexStr)}) +} diff --git a/server/tgbot/remove.go b/server/tgbot/remove.go new file mode 100644 index 0000000..2c63527 --- /dev/null +++ b/server/tgbot/remove.go @@ -0,0 +1,34 @@ +package tgbot + +import ( + "fmt" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdRemove(c tele.Context) error { + arg := "" + if args := c.Args(); len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + if hash == "" { + return c.Send(tr(c.Sender().ID, "remove_usage")) + } + + torrents := torr.ListTorrent() + var found bool + for _, t := range torrents { + if t.Hash().HexString() == hash { + found = true + break + } + } + if !found { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + torr.RemTorrent(hash) + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "remove_done"), hash)) +} diff --git a/server/tgbot/search.go b/server/tgbot/search.go new file mode 100644 index 0000000..b8be2cc --- /dev/null +++ b/server/tgbot/search.go @@ -0,0 +1,186 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + "server/rutor" + "server/rutor/models" + sets "server/settings" + "server/torznab" +) + +func cmdSearch(c tele.Context) error { + if sets.BTsets == nil || (!sets.BTsets.EnableRutorSearch && !sets.BTsets.EnableTorznabSearch) { + return c.Send(tr(c.Sender().ID, "search_disabled_rutor")) + } + + args := c.Args() + if len(args) == 0 { + return c.Send(tr(c.Sender().ID, "search_usage")) + } + query := strings.Join(args, " ") + uid := c.Sender().ID + statusMsg, err := c.Bot().Send(c.Sender(), tr(uid, "searching")) + if err != nil { + return err + } + go func() { + var list []*models.TorrentDetails + if sets.BTsets != nil && sets.BTsets.EnableRutorSearch { + list = append(list, rutor.Search(query)...) + } + if sets.BTsets != nil && sets.BTsets.EnableTorznabSearch { + list = append(list, torznab.Search(query, -1)...) + } + source := "RuTor+Torznab" + sendSearchResultsAsync(c.Bot(), c.Sender(), statusMsg, uid, query, list, source) + }() + return nil +} + +func cmdSearchRutor(c tele.Context) error { + if sets.BTsets == nil || !sets.BTsets.EnableRutorSearch { + return c.Send(tr(c.Sender().ID, "search_disabled_rutor")) + } + + args := c.Args() + if len(args) == 0 { + return c.Send(tr(c.Sender().ID, "rutor_usage")) + } + query := strings.Join(args, " ") + uid := c.Sender().ID + statusMsg, err := c.Bot().Send(c.Sender(), tr(uid, "searching")) + if err != nil { + return err + } + go func() { + list := rutor.Search(query) + sendSearchResultsAsync(c.Bot(), c.Sender(), statusMsg, uid, query, list, "RuTor") + }() + return nil +} + +func cmdTorznab(c tele.Context) error { + if sets.BTsets == nil || !sets.BTsets.EnableTorznabSearch { + return c.Send(tr(c.Sender().ID, "search_disabled_torznab")) + } + + args := c.Args() + if len(args) == 0 { + return c.Send(tr(c.Sender().ID, "torznab_usage")) + } + query := strings.Join(args, " ") + index := -1 + if len(args) > 1 { + if i, err := strconv.Atoi(args[len(args)-1]); err == nil && i >= 0 && i < 100 { + index = i + query = strings.Join(args[:len(args)-1], " ") + } + } + uid := c.Sender().ID + statusMsg, err := c.Bot().Send(c.Sender(), tr(uid, "searching")) + if err != nil { + return err + } + go func() { + list := torznab.Search(query, index) + sendSearchResultsAsync(c.Bot(), c.Sender(), statusMsg, uid, query, list, "Torznab") + }() + return nil +} + +func sendSearchResultsAsync(api tele.API, recipient tele.Recipient, statusMsg *tele.Message, userID int64, query string, list []*models.TorrentDetails, source string) { + if len(list) == 0 { + _, _ = api.Edit(statusMsg, fmt.Sprintf(tr(userID, "search_not_found"), query, source)) + return + } + _ = api.Delete(statusMsg) + _ = sendSearchResultsToRecipient(api, recipient, userID, 0, list, source) +} + +func sendSearchResultsToRecipient(api tele.API, recipient tele.Recipient, userID int64, offset int, list []*models.TorrentDetails, source string) error { + const pageSize = 10 + if offset == 0 { + storeSearchResults(userID, list) + } + start := offset + end := offset + pageSize + if end > len(list) { + end = len(list) + } + page := list[start:end] + + for i, item := range page { + idx := offset + i + link := item.Magnet + if link == "" { + link = item.Link + } + if link == "" { + continue + } + size := item.Size + if size == "" { + size = "?" + } + txt := fmt.Sprintf("%d. <b>%s</b> (%s) S:%d P:%d", idx+1, escapeHtml(item.Title), size, item.Seed, item.Peer) + btnAdd := tele.InlineButton{Text: tr(userID, "btn_add"), Unique: "fadd", Data: strconv.Itoa(idx)} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnAdd}}} + _, _ = api.Send(recipient, txt, kbd) + } + + if end < len(list) { + btnMore := tele.InlineButton{Text: "🔍 " + tr(userID, "search_more"), Unique: "fmore", Data: strconv.Itoa(end)} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnMore}}} + _, _ = api.Send(recipient, fmt.Sprintf(tr(userID, "search_more_hint"), end, len(list)), kbd) + } + return nil +} + +func callbackSearchMore(c tele.Context, offsetStr string) error { + uid := c.Sender().ID + offset, err := strconv.Atoi(offsetStr) + if err != nil || offset < 0 { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "error")}) + } + slice, total := getSearchResultsSlice(uid, offset, 10) + if len(slice) == 0 { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "search_expired")}) + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendSearchResultsPage(c.Bot(), c.Sender(), uid, offset, slice, total) +} + +func sendSearchResultsPage(api tele.API, recipient tele.Recipient, userID int64, offset int, page []*models.TorrentDetails, total int) error { + for i, item := range page { + idx := offset + i + link := item.Magnet + if link == "" { + link = item.Link + } + if link == "" { + continue + } + size := item.Size + if size == "" { + size = "?" + } + txt := fmt.Sprintf("%d. <b>%s</b> (%s) S:%d P:%d", idx+1, escapeHtml(item.Title), size, item.Seed, item.Peer) + btnAdd := tele.InlineButton{Text: tr(userID, "btn_add"), Unique: "fadd", Data: strconv.Itoa(idx)} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnAdd}}} + _, _ = api.Send(recipient, txt, kbd) + } + nextOffset := offset + len(page) + if nextOffset < total { + btnMore := tele.InlineButton{Text: "🔍 " + tr(userID, "search_more"), Unique: "fmore", Data: strconv.Itoa(nextOffset)} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnMore}}} + _, _ = api.Send(recipient, fmt.Sprintf(tr(userID, "search_more_hint"), nextOffset, total), kbd) + } + return nil +} diff --git a/server/tgbot/search_cache.go b/server/tgbot/search_cache.go new file mode 100644 index 0000000..edf4ca2 --- /dev/null +++ b/server/tgbot/search_cache.go @@ -0,0 +1,138 @@ +package tgbot + +import ( + "fmt" + "strconv" + "sync" + "time" + + tele "gopkg.in/telebot.v4" + "server/rutor/models" +) + +type searchCacheEntry struct { + results []*models.TorrentDetails + expires time.Time +} + +var ( + searchCache = make(map[int64]*searchCacheEntry) + searchCacheMu sync.RWMutex + cacheTTL = 10 * time.Minute + cacheMaxSize = 1000 +) + +func init() { + go searchCacheCleanup() +} + +func searchCacheCleanup() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + searchCacheMu.Lock() + now := time.Now() + for id, entry := range searchCache { + if entry == nil || now.After(entry.expires) { + delete(searchCache, id) + } + } + if len(searchCache) > cacheMaxSize { + evict := len(searchCache) - cacheMaxSize + if evict < len(searchCache)/10 { + evict = len(searchCache) / 10 + } + if evict < 1 { + evict = 1 + } + type kv struct { + id int64 + exp time.Time + } + var entries []kv + for id, entry := range searchCache { + if entry != nil { + entries = append(entries, kv{id, entry.expires}) + } + } + for evict > 0 && len(entries) > 0 { + oldest := 0 + for j := 1; j < len(entries); j++ { + if entries[j].exp.Before(entries[oldest].exp) { + oldest = j + } + } + delete(searchCache, entries[oldest].id) + entries[oldest] = entries[len(entries)-1] + entries = entries[:len(entries)-1] + evict-- + } + } + searchCacheMu.Unlock() + } +} + +func storeSearchResults(userID int64, results []*models.TorrentDetails) { + searchCacheMu.Lock() + defer searchCacheMu.Unlock() + searchCache[userID] = &searchCacheEntry{ + results: results, + expires: time.Now().Add(cacheTTL), + } +} + +func getSearchResult(userID int64, index int) *models.TorrentDetails { + searchCacheMu.Lock() + defer searchCacheMu.Unlock() + entry, ok := searchCache[userID] + if !ok || entry == nil || time.Now().After(entry.expires) { + return nil + } + if index < 0 || index >= len(entry.results) { + return nil + } + return entry.results[index] +} + +func getSearchResultsSlice(userID int64, offset, limit int) ([]*models.TorrentDetails, int) { + searchCacheMu.Lock() + defer searchCacheMu.Unlock() + entry, ok := searchCache[userID] + if !ok || entry == nil || time.Now().After(entry.expires) { + return nil, 0 + } + total := len(entry.results) + if offset >= total { + return nil, total + } + end := offset + limit + if end > total { + end = total + } + slice := make([]*models.TorrentDetails, end-offset) + copy(slice, entry.results[offset:end]) + return slice, total +} + +func callbackSearchAdd(c tele.Context, indexStr string) error { + uid := c.Sender().ID + index, parseErr := strconv.Atoi(indexStr) + if parseErr != nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "error")}) + } + item := getSearchResult(uid, index) + if item == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "search_expired")}) + } + link := item.Magnet + if link == "" { + link = item.Link + } + if link == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "search_no_link")}) + } + _ = c.Respond(&tele.CallbackResponse{Text: tr(uid, "search_adding")}) + if err := addTorrent(c, link); err != nil { + return c.Send(fmt.Sprintf(tr(uid, "add_error"), err.Error())) + } + return list(c) +} diff --git a/server/tgbot/server.go b/server/tgbot/server.go new file mode 100644 index 0000000..eaf2ca7 --- /dev/null +++ b/server/tgbot/server.go @@ -0,0 +1,28 @@ +package tgbot + +import ( + "fmt" + "strings" + + tele "gopkg.in/telebot.v4" + "server/settings" + "server/torr" +) + +func cmdServer(c tele.Context) error { + uid := c.Sender().ID + host := getHost() + torrents := torr.ListTorrent() + streams := torr.GetActiveStreams() + + var sb strings.Builder + sb.WriteString("🖥 <b>" + tr(uid, "server_title") + "</b>\n\n") + fmt.Fprintf(&sb, "%s: <code>%s</code>\n", tr(uid, "server_url"), host) + fmt.Fprintf(&sb, "%s: %s\n", tr(uid, "server_port"), settings.Port) + if settings.Ssl { + fmt.Fprintf(&sb, "SSL %s: %s\n", tr(uid, "server_port"), settings.SslPort) + } + fmt.Fprintf(&sb, "%s: %d\n", tr(uid, "stats_torrents"), len(torrents)) + fmt.Fprintf(&sb, "%s: %d\n", tr(uid, "server_streams"), streams) + return c.Send(sb.String()) +} diff --git a/server/tgbot/set.go b/server/tgbot/set.go new file mode 100644 index 0000000..6a1acfe --- /dev/null +++ b/server/tgbot/set.go @@ -0,0 +1,27 @@ +package tgbot + +import ( + "fmt" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdSet(c tele.Context) error { + args := c.Args() + if len(args) < 2 { + return c.Send(tr(c.Sender().ID, "set_usage")) + } + hash := resolveHash(c, args[0]) + if hash == "" { + return c.Send(tr(c.Sender().ID, "invalid_hash")) + } + title := strings.TrimSpace(strings.Join(args[1:], " ")) + if title == "" { + return c.Send(tr(c.Sender().ID, "set_title_required")) + } + + torr.SetTorrent(hash, title, "", "", "") + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "set_done"), escapeHtml(title))) +} diff --git a/server/tgbot/settings_input.go b/server/tgbot/settings_input.go new file mode 100644 index 0000000..61c210c --- /dev/null +++ b/server/tgbot/settings_input.go @@ -0,0 +1,218 @@ +package tgbot + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + tele "gopkg.in/telebot.v4" + "server/dlna" + "server/rutor" + "server/settings" + "server/torr" + "server/torznab" +) + +const pendingInputTTL = 30 * time.Minute + +type pendingInput struct { + Setting string + UserID int64 + CreatedAt time.Time +} + +var ( + pendingInputMu sync.Mutex + pendingInputs = make(map[string]pendingInput) +) + +func init() { + go pendingInputCleanup() +} + +func pendingInputCleanup() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + pendingInputMu.Lock() + now := time.Now() + for key, p := range pendingInputs { + if now.Sub(p.CreatedAt) > pendingInputTTL { + delete(pendingInputs, key) + } + } + pendingInputMu.Unlock() + } +} + +func sendSettingsInputPrompt(c tele.Context, uid int64, setting, hint string) error { + msg := fmt.Sprintf("✏️ %s\n\n%s", tr(uid, "settings_input_reply"), hint) + btnCancel := tele.InlineButton{Text: "❌ " + tr(uid, "canceled"), Unique: "fset", Data: "input_cancel"} + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{{btnCancel}}} + sent, err := c.Bot().Send(c.Chat(), msg, kbd) + if err != nil { + return err + } + pendingInputMu.Lock() + pendingInputs[chatMsgKey(sent.Chat.ID, sent.ID)] = pendingInput{Setting: setting, UserID: uid, CreatedAt: time.Now()} + pendingInputMu.Unlock() + return c.Respond(&tele.CallbackResponse{}) +} + +func handleSettingsInputReply(c tele.Context) (handled bool) { + msg := c.Message() + if msg.ReplyTo == nil { + return false + } + key := chatMsgKey(msg.ReplyTo.Chat.ID, msg.ReplyTo.ID) + pendingInputMu.Lock() + pending, ok := pendingInputs[key] + delete(pendingInputs, key) + pendingInputMu.Unlock() + if !ok || pending.UserID != msg.Sender.ID { + return false + } + if time.Since(pending.CreatedAt) > pendingInputTTL { + _ = c.Send(tr(msg.Sender.ID, "canceled")) + return true + } + applySettingsInput(c, pending.Setting, strings.TrimSpace(msg.Text)) + return true +} + +func cancelSettingsInput(c tele.Context) error { + key := chatMsgKey(c.Callback().Message.Chat.ID, c.Callback().Message.ID) + pendingInputMu.Lock() + delete(pendingInputs, key) + pendingInputMu.Unlock() + _ = c.Bot().Delete(c.Callback().Message) + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "canceled")}) +} + +func applySettingsInput(c tele.Context, setting, value string) { + uid := c.Sender().ID + if !isAdmin(uid) { + _ = c.Send(tr(uid, "admin_only")) + return + } + if settings.ReadOnly { + _ = c.Send(tr(uid, "settings_readonly")) + return + } + if settings.BTsets == nil { + _ = c.Send(tr(uid, "settings_not_loaded")) + return + } + + clear := strings.ToLower(value) == "clear" || strings.ToLower(value) == "очистить" || value == "-" + if clear { + value = "" + } + + sets := new(settings.BTSets) + *sets = *settings.BTsets + + switch setting { + case "friendlyname": + sets.FriendlyName = value + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "FriendlyName", valueOrClear(value))) + case "torrentssavepath": + if value != "" { + abs, err := filepath.Abs(value) + if err != nil { + abs = value + } + if _, err := os.Stat(abs); err != nil && !os.IsNotExist(err) { + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_error"), err.Error())) + return + } + sets.TorrentsSavePath = abs + sets.UseDisk = true + } else { + sets.TorrentsSavePath = "" + sets.UseDisk = false + } + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "TorrentsSavePath", valueOrClear(value))) + case "sslcert": + sets.SslCert = value + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "SslCert", valueOrClear(value))) + case "sslkey": + sets.SslKey = value + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "SslKey", valueOrClear(value))) + case "tmdbkey": + sets.TMDBSettings.APIKey = value + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "TMDB API Key", valueOrClear(value))) + case "proxyhosts": + if value == "" { + sets.ProxyHosts = nil + } else { + parts := strings.Split(value, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + sets.ProxyHosts = parts + } + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_done"), "ProxyHosts", valueOrClear(value))) + case "torznab_add": + if value == "" { + _ = c.Send(tr(uid, "settings_input_torznab_usage")) + return + } + parts := strings.SplitN(value, "|", 3) + cfg := settings.TorznabConfig{Host: strings.TrimSpace(parts[0])} + if len(parts) > 1 { + cfg.Key = strings.TrimSpace(parts[1]) + } + if len(parts) > 2 { + cfg.Name = strings.TrimSpace(parts[2]) + } + if !strings.HasPrefix(cfg.Host, "http") { + cfg.Host = "https://" + cfg.Host + } + sets.TorznabUrls = append(sets.TorznabUrls, cfg) + _ = c.Send(fmt.Sprintf(tr(uid, "settings_input_torznab_added"), cfg.Host)) + case "torznab_test": + if value == "" { + _ = c.Send(tr(uid, "settings_input_torznab_usage")) + return + } + parts := strings.SplitN(value, "|", 3) + host := strings.TrimSpace(parts[0]) + key := "" + if len(parts) > 1 { + key = strings.TrimSpace(parts[1]) + } + if !strings.HasPrefix(host, "http") { + host = "https://" + host + } + if err := torznab.Test(host, key); err != nil { + _ = c.Send(fmt.Sprintf(tr(uid, "settings_torznab_test_fail"), err.Error())) + return + } + _ = c.Send(tr(uid, "settings_torznab_test_ok")) + return + default: + _ = c.Send(tr(uid, "callback_unknown")) + return + } + + torr.SetSettings(sets) + dlna.Stop() + if sets.EnableDLNA { + dlna.Start() + } + rutor.Stop() + rutor.Start() +} + +func valueOrClear(v string) string { + if v == "" { + return "(cleared)" + } + if len(v) > 50 { + return v[:47] + "..." + } + return v +} diff --git a/server/tgbot/snake.go b/server/tgbot/snake.go new file mode 100644 index 0000000..913f957 --- /dev/null +++ b/server/tgbot/snake.go @@ -0,0 +1,435 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/log" + "server/torr" + cacheSt "server/torr/storage/state" +) + +var ( + snakeStopChans = make(map[int]chan struct{}) + snakeStopChansMu sync.Mutex + snakeWindowStart = make(map[string]int) + snakeWindowStartMu sync.Mutex +) + +const ( + snakeBlockFilled = "🟩" + snakeBlockEmpty = "⬜" + snakeBlockReader = "🔵" + snakeBlockInRange = "🟦" + snakeTitleMaxLen = 55 + snakeHashDisplayLen = 8 +) + +func cmdSnake(c tele.Context) error { + args := c.Args() + hash := "" + cols, rows := 20, 3 + + if len(args) > 0 { + hash = resolveHash(c, args[0]) + } + if len(args) > 1 { + if n, err := strconv.Atoi(args[1]); err == nil && n > 0 && n <= 50 { + cols = n + } + } + if len(args) > 2 { + if n, err := strconv.Atoi(args[2]); err == nil && n > 0 && n <= 15 { + rows = n + } + } + + if hash == "" { + return c.Send(tr(c.Sender().ID, "snake_usage")) + } + + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + + st := t.CacheState() + if st == nil { + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "cache_unavailable"), hash)) + } + + uid := c.Sender().ID + txt := formatSnake(uid, st, hash, cols, rows) + kbd := snakeKeyboard(uid, hash, cols, rows, true) + msg, err := c.Bot().Send(c.Sender(), txt, kbd) + if err != nil { + return err + } + log.TLogln("tg snake sent", logUserID(uid), logSafeStr(st.Torrent.Title, 40), hash) + go snakeRefreshLoop(c.Bot(), msg, hash, uid, cols, rows) + return nil +} + +func formatSnake(uid int64, st *cacheSt.CacheState, hash string, cols, rows int) string { + totalBlocks := cols * rows + if totalBlocks <= 0 { + return tr(uid, "snake_no_data") + } + if st.PiecesCount <= 0 { + title := "" + if st.Torrent != nil { + title = escapeHtml(st.Torrent.Title) + } + txt := "📊 <b>" + title + "</b>\n" + txt += fmt.Sprintf("%s: %s / %s\n", tr(uid, "snake_cache"), + humanize.IBytes(uint64(st.Filled)), humanize.IBytes(uint64(st.Capacity))) + dispHash := st.Hash + if len(dispHash) > snakeHashDisplayLen { + dispHash = dispHash[:snakeHashDisplayLen] + } + txt += tr(uid, "snake_no_data") + " <code>" + dispHash + "</code>" + return txt + } + + pieceFilled := make(map[int]bool) + for id, p := range st.Pieces { + if id >= 0 && id < st.PiecesCount && p.Size > 0 { + pieceFilled[id] = true + } + } + + readerPositions := make(map[int]bool) + readerRanges := make(map[int]bool) + for _, r := range st.Readers { + readerPositions[r.Reader] = true + for p := r.Start; p < r.End && p < st.PiecesCount; p++ { + readerRanges[p] = true + } + } + + cacheWindowPieces := int64(totalBlocks) * 2 + if st.PiecesLength > 0 { + cacheWindowPieces = st.Capacity / st.PiecesLength + } + if cacheWindowPieces < int64(totalBlocks) { + cacheWindowPieces = int64(totalBlocks) + } + + startPiece, endPiece := 0, st.PiecesCount + if len(st.Readers) > 0 { + minReader, maxReader := st.PiecesCount, 0 + for _, r := range st.Readers { + if r.Reader < minReader { + minReader = r.Reader + } + if r.Reader > maxReader { + maxReader = r.Reader + } + } + windowSize := int(cacheWindowPieces) + snakeWindowStartMu.Lock() + lastStart := snakeWindowStart[hash] + scrollThreshold := windowSize * 3 / 4 + if lastStart == 0 || minReader < lastStart { + lastStart = minReader + } else if minReader >= lastStart+scrollThreshold { + lastStart = minReader - windowSize/5 + } + if lastStart < 0 { + lastStart = 0 + } + snakeWindowStart[hash] = lastStart + snakeWindowStartMu.Unlock() + startPiece = lastStart + endPiece = startPiece + windowSize + if endPiece > st.PiecesCount { + endPiece = st.PiecesCount + startPiece = endPiece - windowSize + if startPiece < 0 { + startPiece = 0 + } + } + } else if len(pieceFilled) > 0 { + minP, maxP := st.PiecesCount, 0 + for id := range pieceFilled { + if id < minP { + minP = id + } + if id > maxP { + maxP = id + } + } + window := maxP - minP + 1 + if window > int(cacheWindowPieces) { + window = int(cacheWindowPieces) + } + startPiece = minP + endPiece = minP + window + if endPiece > st.PiecesCount { + endPiece = st.PiecesCount + } + } + + windowSize := endPiece - startPiece + if windowSize <= 0 { + windowSize = 1 + } + + blocks := make([]string, totalBlocks) + piecesPerBlock := (windowSize + totalBlocks - 1) / totalBlocks + if piecesPerBlock < 1 { + piecesPerBlock = 1 + } + + for i := 0; i < totalBlocks; i++ { + start := startPiece + i*piecesPerBlock + end := start + piecesPerBlock + if end > endPiece { + end = endPiece + } + if start >= end { + blocks[i] = snakeBlockEmpty + continue + } + + blockFilled := false + blockHasReader := false + blockInRange := false + for p := start; p < end; p++ { + if pieceFilled[p] { + blockFilled = true + } + if readerPositions[p] { + blockHasReader = true + } + if readerRanges[p] { + blockInRange = true + } + } + + switch { + case blockHasReader: + blocks[i] = snakeBlockReader + case blockFilled: + blocks[i] = snakeBlockFilled + case blockInRange: + blocks[i] = snakeBlockInRange + default: + blocks[i] = snakeBlockEmpty + } + } + + var sb strings.Builder + title := "" + if st.Torrent != nil { + title = st.Torrent.Title + } + if len([]rune(title)) > snakeTitleMaxLen { + title = string([]rune(title)[:snakeTitleMaxLen]) + "…" + } + title = escapeHtml(title) + sb.WriteString("📊 <b>") + sb.WriteString(title) + sb.WriteString("</b>\n") + fmt.Fprintf(&sb, "%s: %s / %s", + tr(uid, "snake_cache"), + humanize.IBytes(uint64(st.Filled)), + humanize.IBytes(uint64(st.Capacity))) + if len(st.Readers) > 1 { + fmt.Fprintf(&sb, " · %d %s", len(st.Readers), tr(uid, "status_streams")) + } + if endPiece-startPiece < st.PiecesCount { + fmt.Fprintf(&sb, " · %s %d-%d", tr(uid, "snake_pieces"), startPiece+1, endPiece) + } + sb.WriteString("\n") + + for r := 0; r < rows; r++ { + for c := 0; c < cols; c++ { + var idx int + if r%2 == 0 { + idx = r*cols + c + } else { + idx = r*cols + (cols - 1 - c) + } + if idx < len(blocks) { + sb.WriteString(blocks[idx]) + } + } + sb.WriteString("\n") + } + dispHash := st.Hash + if len(dispHash) > snakeHashDisplayLen { + dispHash = dispHash[:snakeHashDisplayLen] + } + sb.WriteString(tr(uid, "snake_legend")) + sb.WriteString(" <code>") + sb.WriteString(dispHash) + sb.WriteString("</code>") + return sb.String() +} + +func snakeData(hash string, cols, rows int) string { + return fmt.Sprintf("%s|%d|%d", hash, cols, rows) +} + +func parseSnakeData(data string) (hash string, cols, rows int) { + cols, rows = 20, 3 + parts := strings.Split(data, "|") + if len(parts) > 0 { + hash = parts[0] + } + if len(parts) > 1 { + if n, err := strconv.Atoi(parts[1]); err == nil && n > 0 { + cols = n + } + } + if len(parts) > 2 { + if n, err := strconv.Atoi(parts[2]); err == nil && n > 0 { + rows = n + } + } + return hash, cols, rows +} + +func snakeKeyboard(uid int64, hash string, cols, rows int, active bool) *tele.ReplyMarkup { + data := snakeData(hash, cols, rows) + if active { + return &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + { + {Text: "🔄", Unique: "fsnakerefresh", Data: data}, + {Text: tr(uid, "status_stop_btn"), Unique: "fsnakestop", Data: data}, + }, + }} + } + return &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + {{Text: tr(uid, "status_refresh_btn"), Unique: "fsnakerefresh", Data: data}}, + }} +} + +func snakeRefreshLoop(api tele.API, msg *tele.Message, hash string, uid int64, cols, rows int) { + const interval = 2 * time.Second + const duration = 2 * time.Minute + stopCh := make(chan struct{}) + snakeStopChansMu.Lock() + snakeStopChans[msg.ID] = stopCh + snakeStopChansMu.Unlock() + defer func() { + snakeStopChansMu.Lock() + delete(snakeStopChans, msg.ID) + snakeStopChansMu.Unlock() + }() + + ticker := time.NewTicker(interval) + defer ticker.Stop() + deadline := time.Now().Add(duration) + for { + select { + case <-stopCh: + return + case <-ticker.C: + if time.Now().After(deadline) { + t := torr.GetTorrent(hash) + if t != nil { + if st := t.CacheState(); st != nil { + txt := formatSnake(uid, st, hash, cols, rows) + "\n" + tr(uid, "status_auto_ended") + _, _ = api.Edit(msg, txt, snakeKeyboard(uid, hash, cols, rows, false), tele.ModeHTML) + } + } + return + } + t := torr.GetTorrent(hash) + if t == nil { + return + } + st := t.CacheState() + if st == nil { + return + } + txt := formatSnake(uid, st, hash, cols, rows) + if _, err := api.Edit(msg, txt, snakeKeyboard(uid, hash, cols, rows, true), tele.ModeHTML); err != nil { + errStr := err.Error() + if strings.Contains(errStr, "message is not modified") { + continue + } + if strings.Contains(errStr, "message to edit not found") { + return + } + log.TLogln("tg snake refresh err", err) + return + } + } + } +} + +func stopSnakeRefresh(msgID int) { + snakeStopChansMu.Lock() + ch := snakeStopChans[msgID] + delete(snakeStopChans, msgID) + snakeStopChansMu.Unlock() + if ch != nil { + close(ch) + } +} + +func callbackSnakeRefresh(c tele.Context, data string) error { + hash, cols, rows := parseSnakeData(data) + if hash == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + t := torr.GetTorrent(hash) + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "torrent_not_found")}) + } + st := t.CacheState() + if st == nil { + return c.Respond(&tele.CallbackResponse{Text: fmt.Sprintf(tr(c.Sender().ID, "cache_unavailable"), hash)}) + } + if c.Callback().Message != nil { + stopSnakeRefresh(c.Callback().Message.ID) + _ = c.Bot().Delete(c.Callback().Message) + } + _ = c.Respond(&tele.CallbackResponse{}) + uid := c.Sender().ID + txt := formatSnake(uid, st, hash, cols, rows) + kbd := snakeKeyboard(uid, hash, cols, rows, true) + msg, err := c.Bot().Send(c.Sender(), txt, kbd) + if err != nil { + return err + } + go snakeRefreshLoop(c.Bot(), msg, hash, uid, cols, rows) + return nil +} + +func callbackSnakeStop(c tele.Context, data string) error { + uid := c.Sender().ID + hash, cols, rows := parseSnakeData(data) + if hash != "" { + if t := torr.GetTorrent(hash); t != nil { + log.TLogln("tg snake stop", logUserID(uid), logSafeStr(t.Title, 40), hash) + } + } + if c.Callback().Message != nil { + stopSnakeRefresh(c.Callback().Message.ID) + if hash != "" { + msg := c.Callback().Message + t := torr.GetTorrent(hash) + txt := "" + if t != nil { + if st := t.CacheState(); st != nil { + txt = formatSnake(uid, st, hash, cols, rows) + } + } + if txt == "" { + txt = "<code>" + hash + "</code>" + } + txt += "\n" + tr(uid, "status_stopped") + _, _ = c.Bot().Edit(msg, txt, snakeKeyboard(uid, hash, cols, rows, false), tele.ModeHTML) + } + } + return c.Respond(&tele.CallbackResponse{Text: "🛑"}) +} diff --git a/server/tgbot/speedtest.go b/server/tgbot/speedtest.go new file mode 100644 index 0000000..83e64af --- /dev/null +++ b/server/tgbot/speedtest.go @@ -0,0 +1,23 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" +) + +func cmdSpeedtest(c tele.Context) error { + args := c.Args() + size := 10 + if len(args) > 0 { + if s, err := strconv.Atoi(strings.TrimSpace(args[0])); err == nil && s > 0 && s <= 100 { + size = s + } + } + + host := getHost() + url := fmt.Sprintf("%s/download/%d", host, size) + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "speedtest_msg"), size, url)) +} diff --git a/server/tgbot/stat.go b/server/tgbot/stat.go new file mode 100644 index 0000000..14d2638 --- /dev/null +++ b/server/tgbot/stat.go @@ -0,0 +1,21 @@ +package tgbot + +import ( + "bytes" + "strings" + + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdStat(c tele.Context) error { + var buf bytes.Buffer + torr.WriteStatus(&buf) + msg := buf.String() + msg = strings.ReplaceAll(msg, "<", "<") + msg = strings.ReplaceAll(msg, ">", ">") + if len(msg) > 4000 { + msg = msg[:4000] + "\n..." + } + return c.Send("📋 <b>" + tr(c.Sender().ID, "help_stat") + "</b>\n\n<pre>" + msg + "</pre>") +} diff --git a/server/tgbot/stats.go b/server/tgbot/stats.go new file mode 100644 index 0000000..ad2e612 --- /dev/null +++ b/server/tgbot/stats.go @@ -0,0 +1,50 @@ +package tgbot + +import ( + "fmt" + "strings" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/torr" +) + +func cmdStats(c tele.Context) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + var totalSize, loadedSize int64 + var totalPeers, activePeers, seeders int + for _, t := range torrents { + st := t.Status() + if st != nil { + totalSize += st.TorrentSize + loadedSize += st.LoadedSize + totalPeers += st.TotalPeers + activePeers += st.ActivePeers + seeders += st.ConnectedSeeders + } else { + totalSize += t.Size + } + } + + streams := torr.GetActiveStreams() + + uid := c.Sender().ID + var sb strings.Builder + sb.WriteString("📊 <b>" + tr(uid, "stats_title") + "</b>\n\n") + fmt.Fprintf(&sb, "%s: %d\n", tr(uid, "stats_torrents"), len(torrents)) + fmt.Fprintf(&sb, "%s: %s\n", tr(uid, "stats_total_size"), humanize.IBytes(uint64(totalSize))) + progress := 0.0 + if totalSize > 0 { + progress = float64(loadedSize) / float64(totalSize) * 100 + } + fmt.Fprintf(&sb, "%s: %s (%.1f%%)\n", + tr(uid, "stats_loaded"), humanize.IBytes(uint64(loadedSize)), progress) + fmt.Fprintf(&sb, "%s: %d %s, %d %s\n", + tr(uid, "stats_peers"), activePeers, tr(uid, "stats_active"), seeders, tr(uid, "stats_seeds")) + fmt.Fprintf(&sb, "%s: %d\n", tr(uid, "stats_streams"), streams) + return c.Send(sb.String()) +} diff --git a/server/tgbot/status.go b/server/tgbot/status.go new file mode 100644 index 0000000..be38a53 --- /dev/null +++ b/server/tgbot/status.go @@ -0,0 +1,395 @@ +package tgbot + +import ( + "fmt" + "math" + "strconv" + "strings" + "sync" + "time" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/log" + "server/torr" +) + +// humanizeSpeedBits formats bytes/s as bits/s (bps, kbps, Mbps, Gbps, Tbps) — same as web mode. +func humanizeSpeedBits(uid int64, bytesPerSec float64) string { + if bytesPerSec <= 0 { + return "0 " + tr(uid, "speed_bps") + } + bits := bytesPerSec * 8 + i := int(math.Floor(math.Log(bits) / math.Log(1000))) + if i < 0 { + i = 0 + } + units := []string{"speed_bps", "speed_kbps", "speed_Mbps", "speed_Gbps", "speed_Tbps"} + if i >= len(units) { + i = len(units) - 1 + } + val := bits / math.Pow(1000, float64(i)) + return fmt.Sprintf("%.0f %s", val, tr(uid, units[i])) +} + +var ( + statusStopChans = make(map[int]chan struct{}) + statusStopChansMu sync.Mutex +) + +func cmdStatus(c tele.Context) error { + arg := "" + if args := c.Args(); len(args) > 0 { + arg = args[0] + } + hash := resolveHash(c, arg) + + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + if hash != "" { + t := torr.GetTorrent(hash) + if t == nil { + return c.Send(tr(c.Sender().ID, "torrent_not_found") + ":\n<code>" + hash + "</code>") + } + log.TLogln("tg status cmd", logUser(c.Sender()), logSafeStr(t.Title, 40), hash) + if !t.WaitInfo() { + msg, err := c.Bot().Send(c.Sender(), tr(c.Sender().ID, "status_waiting")) + if err != nil { + return err + } + go waitForInfoAndUpdateStatus(c.Bot(), msg, hash, c.Sender().ID) + return nil + } + return sendStatus(c, t) + } + + return sendStatusAllPage(c, 0) +} + +const statusAllPageSize = 5 + +func sendStatusAllPage(c tele.Context, page int) error { + torrents := torr.ListTorrent() + if len(torrents) == 0 { + return c.Send(tr(c.Sender().ID, "no_torrents")) + } + + totalPages := (len(torrents) + statusAllPageSize - 1) / statusAllPageSize + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } + start := page * statusAllPageSize + end := start + statusAllPageSize + if end > len(torrents) { + end = len(torrents) + } + pageTorrents := torrents[start:end] + + uid := c.Sender().ID + var sb strings.Builder + for _, t := range pageTorrents { + txt := formatTorrentStatus(uid, t) + if txt != "" { + sb.WriteString(txt) + sb.WriteString("\n\n") + } + } + if sb.Len() == 0 { + return c.Send(tr(uid, "status_no_active")) + } + msg := strings.TrimSuffix(sb.String(), "\n\n") + + navRow := []tele.InlineButton{} + if totalPages > 1 { + if page > 0 { + navRow = append(navRow, tele.InlineButton{Text: "◀️", Unique: "fstatusall", Data: strconv.Itoa(page - 1)}) + } + navRow = append(navRow, tele.InlineButton{Text: strconv.Itoa(page+1) + "/" + strconv.Itoa(totalPages), Unique: "fnop", Data: ""}) + if page < totalPages-1 { + navRow = append(navRow, tele.InlineButton{Text: "▶️", Unique: "fstatusall", Data: strconv.Itoa(page + 1)}) + } + } + navRow = append(navRow, tele.InlineButton{Text: "🔄", Unique: "fstatusallrefresh", Data: strconv.Itoa(page)}) + + kbd := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{navRow}} + if err := c.Send(msg, kbd); err != nil { + log.TLogln("tg status all send err", err) + return err + } + return nil +} + +func callbackStatusAllPage(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendStatusAllPage(c, page) +} + +func callbackStatusAllRefresh(c tele.Context, data string) error { + page := 0 + if data != "" { + if p, err := strconv.Atoi(data); err == nil { + page = p + } + } + _ = c.Respond(&tele.CallbackResponse{Text: "🔄"}) + if c.Callback().Message != nil { + _ = c.Bot().Delete(c.Callback().Message) + } + return sendStatusAllPage(c, page) +} + +func sendStatus(c tele.Context, t *torr.Torrent) error { + uid := c.Sender().ID + txt := formatTorrentStatus(uid, t) + if txt == "" && t != nil { + txt = "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + hash := "" + if t != nil { + hash = t.Hash().HexString() + } + kbd := statusKeyboard(uid, hash, true) + msg, err := c.Bot().Send(c.Sender(), txt, kbd) + if err != nil { + return err + } + if t != nil { + log.TLogln("tg status sent", logUserID(uid), logSafeStr(t.Title, 40), hash) + go refreshStatusLoop(c.Bot(), msg, hash, uid) + } + return nil +} + +func statusKeyboard(uid int64, hash string, active bool) *tele.ReplyMarkup { + if active { + return &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + { + {Text: "🔄", Unique: "fstatusrefresh", Data: hash}, + {Text: tr(uid, "status_stop_btn"), Unique: "fstatusstop", Data: hash}, + }, + }} + } + return &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + {{Text: tr(uid, "status_refresh_btn"), Unique: "fstatusrefresh", Data: hash}}, + }} +} + +func refreshStatusLoop(api tele.API, msg *tele.Message, hash string, uid int64) { + const interval = 5 * time.Second + const duration = 2 * time.Minute + stopCh := make(chan struct{}) + statusStopChansMu.Lock() + statusStopChans[msg.ID] = stopCh + statusStopChansMu.Unlock() + defer func() { + statusStopChansMu.Lock() + delete(statusStopChans, msg.ID) + statusStopChansMu.Unlock() + }() + + ticker := time.NewTicker(interval) + defer ticker.Stop() + deadline := time.Now().Add(duration) + for { + select { + case <-stopCh: + return + case <-ticker.C: + if time.Now().After(deadline) { + t := torr.GetTorrent(hash) + txt := "" + if t != nil { + txt = formatTorrentStatus(uid, t) + if txt == "" { + txt = "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + txt += "\n\n" + tr(uid, "status_auto_ended") + } else { + txt = "<code>" + hash + "</code>\n\n" + tr(uid, "status_torrent_gone") + } + _, _ = api.Edit(msg, txt, statusKeyboard(uid, hash, false), tele.ModeHTML) + return + } + t := torr.GetTorrent(hash) + if t == nil { + txt := "<code>" + hash + "</code>\n\n" + tr(uid, "status_torrent_gone") + _, _ = api.Edit(msg, txt, statusKeyboard(uid, hash, false), tele.ModeHTML) + return + } + txt := formatTorrentStatus(uid, t) + if txt == "" { + txt = "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + if _, err := api.Edit(msg, txt, statusKeyboard(uid, hash, true), tele.ModeHTML); err != nil { + errStr := err.Error() + if strings.Contains(errStr, "message is not modified") { + continue + } + if strings.Contains(errStr, "message to edit not found") { + return + } + log.TLogln("tg status refresh err", err) + return + } + } + } +} + +func stopStatusRefresh(msgID int) { + statusStopChansMu.Lock() + ch := statusStopChans[msgID] + delete(statusStopChans, msgID) + statusStopChansMu.Unlock() + if ch != nil { + close(ch) + } +} + +const waitForInfoTimeout = 2 * time.Minute + +func waitForInfoAndUpdateStatus(api tele.API, msg *tele.Message, hash string, uid int64) { + deadline := time.Now().Add(waitForInfoTimeout) + for { + t := torr.GetTorrent(hash) + if t == nil { + _, _ = api.Edit(msg, tr(uid, "torrent_not_found")+":\n<code>"+hash+"</code>", tele.ModeHTML) + return + } + if t.WaitInfo() { + break + } + if time.Now().After(deadline) { + _, _ = api.Edit(msg, tr(uid, "status_waiting")+"\n\n"+tr(uid, "status_auto_ended"), tele.ModeHTML) + return + } + time.Sleep(time.Second) + } + t := torr.GetTorrent(hash) + if t == nil { + _, _ = api.Edit(msg, tr(uid, "torrent_not_found")+":\n<code>"+hash+"</code>", tele.ModeHTML) + return + } + txt := formatTorrentStatus(uid, t) + if txt == "" { + txt = "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + if _, err := api.Edit(msg, txt, statusKeyboard(uid, hash, true), tele.ModeHTML); err != nil { + log.TLogln("tg status wait edit err", err) + return + } + go refreshStatusLoop(api, msg, hash, uid) +} + +func callbackStatusRefresh(c tele.Context, hash string) error { + uid := c.Sender().ID + if hash == "" { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "callback_unknown")}) + } + t := torr.GetTorrent(hash) + if t != nil { + log.TLogln("tg status refresh", logUserID(uid), logSafeStr(t.Title, 40), hash) + } + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "torrent_not_found")}) + } + if c.Callback().Message != nil { + stopStatusRefresh(c.Callback().Message.ID) + _ = c.Bot().Delete(c.Callback().Message) + } + _ = c.Respond(&tele.CallbackResponse{}) + return sendStatus(c, t) +} + +func callbackStatusStop(c tele.Context, hash string) error { + uid := c.Sender().ID + if hash != "" { + if t := torr.GetTorrent(hash); t != nil { + log.TLogln("tg status stop", logUserID(uid), logSafeStr(t.Title, 40), hash) + } + } + if c.Callback().Message != nil { + stopStatusRefresh(c.Callback().Message.ID) + if hash != "" { + msg := c.Callback().Message + t := torr.GetTorrent(hash) + txt := "" + if t != nil { + txt = formatTorrentStatus(uid, t) + if txt == "" { + txt = "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + } else { + txt = "<code>" + hash + "</code>" + } + txt += "\n\n" + tr(uid, "status_stopped") + _, _ = c.Bot().Edit(msg, txt, statusKeyboard(uid, hash, false), tele.ModeHTML) + } + } + return c.Respond(&tele.CallbackResponse{Text: "🛑"}) +} + +func callbackStatus(c tele.Context, hash string) error { + uid := c.Sender().ID + t := torr.GetTorrent(hash) + if t == nil { + return c.Respond(&tele.CallbackResponse{Text: tr(uid, "torrent_not_found")}) + } + _ = c.Respond(&tele.CallbackResponse{}) + if !t.WaitInfo() { + msg, err := c.Bot().Send(c.Sender(), tr(uid, "status_waiting")) + if err != nil { + return err + } + go waitForInfoAndUpdateStatus(c.Bot(), msg, hash, uid) + return nil + } + return sendStatus(c, t) +} + +func formatTorrentStatus(uid int64, t *torr.Torrent) string { + if t == nil { + return "" + } + st := t.Status() + if st == nil { + return "<b>" + escapeHtml(t.Title) + "</b>\n" + tr(uid, "status_label") + ": " + t.Stat.String() + } + + // For streaming: size + cache info (progress is misleading — we stream, not download sequentially) + sizeLine := fmt.Sprintf("%s: %s", tr(uid, "status_size"), humanize.IBytes(uint64(st.TorrentSize))) + if cache := t.CacheState(); cache != nil { + sizeLine += fmt.Sprintf(" | %s: %s / %s · %d %s", + tr(uid, "status_cache"), + humanize.IBytes(uint64(cache.Filled)), + humanize.IBytes(uint64(cache.Capacity)), + len(cache.Readers), + tr(uid, "status_streams")) + } + + txt := fmt.Sprintf("<b>%s</b>\n", escapeHtml(st.Title)) + txt += fmt.Sprintf("%s: %s\n", tr(uid, "status_label"), st.StatString) + txt += sizeLine + "\n" + txt += fmt.Sprintf("%s: %s | %s: %s\n", + tr(uid, "status_download"), humanizeSpeedBits(uid, st.DownloadSpeed), + tr(uid, "status_upload"), humanizeSpeedBits(uid, st.UploadSpeed)) + txt += fmt.Sprintf("%s: %d %s, %d %s\n", + tr(uid, "stats_peers"), st.ActivePeers, tr(uid, "stats_active"), + st.ConnectedSeeders, tr(uid, "stats_seeds")) + txt += fmt.Sprintf("<code>%s</code>", st.Hash) + return txt +} diff --git a/server/tgbot/upload.go b/server/tgbot/upload.go new file mode 100644 index 0000000..d144eb7 --- /dev/null +++ b/server/tgbot/upload.go @@ -0,0 +1,44 @@ +package tgbot + +import ( + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + up "server/tgbot/upload" +) + +func upload(c tele.Context) error { + args := c.Args() + if len(args) < 3 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + hash := args[1] + if !isHash(hash) { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + id, err := strconv.Atoi(args[2]) + if err != nil { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + up.AddRange(c, hash, id, id) + return nil +} + +func uploadall(c tele.Context) error { + args := c.Args() + if len(args) < 2 { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + hash := "" + if len(args) >= 3 && isHash(args[2]) { + hash = args[2] + } else { + hash = strings.TrimPrefix(args[1], "all|") + } + if !isHash(hash) { + return c.Respond(&tele.CallbackResponse{Text: tr(c.Sender().ID, "callback_unknown")}) + } + up.AddRange(c, hash, 1, -1) + return nil +} diff --git a/server/tgbot/upload/manager.go b/server/tgbot/upload/manager.go new file mode 100644 index 0000000..03a6559 --- /dev/null +++ b/server/tgbot/upload/manager.go @@ -0,0 +1,288 @@ +package upload + +import ( + "errors" + "fmt" + "math" + "path/filepath" + "strconv" + "sync" + "time" + + tele "gopkg.in/telebot.v4" + + "server/log" + "server/torr" + "server/torr/state" +) + +// TrFunc is set by tgbot for localization (avoids circular import) +var TrFunc func(int64, string) string + +// EscapeFunc is set by tgbot for HTML escaping (avoids circular import) +var EscapeFunc func(string) string + +func tr(uid int64, key string) string { + if TrFunc != nil { + return TrFunc(uid, key) + } + return key +} + +func escapeHtml(s string) string { + if EscapeFunc != nil { + return EscapeFunc(s) + } + return s +} + +type Worker struct { + id int + c tele.Context + msg *tele.Message + torrentHash string + isCancelled bool + from int + to int + ti *state.TorrentStatus +} + +type Manager struct { + queue []*Worker + working map[int]*Worker + ids int + wrkSync sync.Mutex + queueLock sync.Mutex +} + +func (m *Manager) Start() { + m.working = make(map[int]*Worker) + go m.work() +} + +func (m *Manager) AddRange(c tele.Context, hash string, from, to int) { + m.queueLock.Lock() + defer m.queueLock.Unlock() + + if len(m.queue) > 50 { + c.Bot().Send(c.Recipient(), fmt.Sprintf(tr(c.Sender().ID, "upload_queue_full"), len(m.queue))) + return + } + + m.ids++ + if m.ids > math.MaxInt { + m.ids = 0 + } + + var msg *tele.Message + var err error + + for i := 0; i < 20; i++ { + msg, err = c.Bot().Send(c.Recipient(), fmt.Sprintf(tr(c.Sender().ID, "upload_connecting"), hash)) + if err == nil { + break + } + log.TLogln("tg upload retry", i+1, "/", 20) + if i < 19 { + backoff := time.Duration(1<<uint(i)) * 100 * time.Millisecond + if backoff > 5*time.Second { + backoff = 5 * time.Second + } + time.Sleep(backoff) + } + } + + if err != nil { + log.TLogln("tg upload send err", err) + return + } + + t := torr.GetTorrent(hash) + if t == nil { + c.Bot().Edit(msg, tr(c.Sender().ID, "torrent_not_found")+":\n<code>"+hash+"</code>") + return + } + t.WaitInfo() + for t.Status().Stat != state.TorrentWorking { + time.Sleep(time.Second) + t = torr.GetTorrent(hash) + if t == nil { + return + } + } + ti := t.Status() + + if from == 1 && to == -1 { + to = len(ti.FileStats) + } + if from < 1 { + from = 1 + } + if to > len(ti.FileStats) { + to = len(ti.FileStats) + } + if from > to { + from, to = to, from + } + if to > len(ti.FileStats) { + to = len(ti.FileStats) + } + + w := &Worker{ + id: m.ids, + c: c, + torrentHash: hash, + msg: msg, + ti: ti, + from: from, + to: to, + } + + m.queue = append(m.queue, w) +} + +func (m *Manager) Cancel(id int) { + m.queueLock.Lock() + defer m.queueLock.Unlock() + for i, w := range m.queue { + if w.id == id { + w.isCancelled = true + w.c.Bot().Delete(w.msg) + m.queue = append(m.queue[:i], m.queue[i+1:]...) + return + } + } + if wrk, ok := m.working[id]; ok { + wrk.isCancelled = true + return + } +} + +func (m *Manager) work() { + for { + m.queueLock.Lock() + if len(m.working) > 0 { + m.queueLock.Unlock() + m.sendQueueStatus() + time.Sleep(time.Second) + continue + } + if len(m.queue) == 0 { + m.queueLock.Unlock() + time.Sleep(time.Second) + continue + } + wrk := m.queue[0] + m.queue = m.queue[1:] + m.working[wrk.id] = wrk + m.queueLock.Unlock() + + m.sendQueueStatus() + + loading(wrk) + + m.queueLock.Lock() + delete(m.working, wrk.id) + m.queueLock.Unlock() + } +} + +func (m *Manager) sendQueueStatus() { + m.queueLock.Lock() + defer m.queueLock.Unlock() + for i, wrk := range m.queue { + if wrk.msg == nil || wrk.c.Sender() == nil { + continue + } + torrKbd := &tele.ReplyMarkup{} + torrKbd.Inline([]tele.Row{torrKbd.Row(torrKbd.Data(tr(wrk.c.Sender().ID, "upload_cancel"), "cancel", strconv.Itoa(wrk.id)))}...) + + msg := fmt.Sprintf(tr(wrk.c.Sender().ID, "upload_queue_pos"), i+1) + + wrk.c.Bot().Edit(wrk.msg, msg, torrKbd) + } +} + +func loading(wrk *Worker) { + iserr := false + + t := torr.GetTorrent(wrk.torrentHash) + if t == nil { + wrk.c.Bot().Edit(wrk.msg, tr(wrk.c.Sender().ID, "torrent_not_found")+":\n<code>"+wrk.torrentHash+"</code>") + return + } + t.WaitInfo() + for t.Status().Stat != state.TorrentWorking { + time.Sleep(time.Second) + t = torr.GetTorrent(wrk.torrentHash) + if t == nil { + return + } + } + wrk.ti = t.Status() + + for i := wrk.from - 1; i <= wrk.to-1; i++ { + file := wrk.ti.FileStats[i] + if wrk.isCancelled { + return + } + + err := uploadFile(wrk, file, i+1, len(wrk.ti.FileStats)) + if err != nil { + errstr := fmt.Sprintf(tr(wrk.c.Sender().ID, "upload_error"), err) + wrk.c.Bot().Edit(wrk.msg, errstr) + iserr = true + break + } + } + if !iserr { + wrk.c.Bot().Delete(wrk.msg) + } +} + +func uploadFile(wrk *Worker, file *state.TorrentFileStat, fi, fc int) error { + caption := filepath.Base(file.Path) + torrFile, err := NewTorrFile(wrk, file) + if err != nil { + return err + } + + var wa sync.WaitGroup + wa.Add(1) + complete := false + go func() { + for !complete { + updateLoadStatus(wrk, torrFile, fi, fc) + time.Sleep(1 * time.Second) + } + wa.Done() + }() + + d := &tele.Document{} + d.FileName = file.Path + d.Caption = caption + d.File.FileReader = torrFile + + for i := 0; i < 20; i++ { + err = wrk.c.Send(d) + if err == nil || errors.Is(err, ERR_STOPPED) { + break + } + log.TLogln("tg upload retry", i+1, "/", 20) + if i < 19 { + backoff := time.Duration(1<<uint(i)) * 100 * time.Millisecond + if backoff > 5*time.Second { + backoff = 5 * time.Second + } + time.Sleep(backoff) + } + } + + complete = true + wa.Wait() + torrFile.Close() + if errors.Is(err, ERR_STOPPED) { + err = nil + } + return err +} diff --git a/server/tgbot/upload/queue.go b/server/tgbot/upload/queue.go new file mode 100644 index 0000000..d25d06c --- /dev/null +++ b/server/tgbot/upload/queue.go @@ -0,0 +1,130 @@ +package upload + +import ( + "fmt" + "strconv" + "time" + + "github.com/dustin/go-humanize" + tele "gopkg.in/telebot.v4" + "server/torr" +) + +type DLQueue struct { + id int + c tele.Context + hash string + fileID string + fileName string + updateMsg *tele.Message +} + +var manager = &Manager{} + +func Start() { + manager.Start() +} + +func ShowQueue(c tele.Context) error { + msg := "" + manager.queueLock.Lock() + defer manager.queueLock.Unlock() + if len(manager.queue) == 0 && len(manager.working) == 0 { + return c.Send(tr(c.Sender().ID, "queue_empty")) + } + if len(manager.working) > 0 { + msg += tr(c.Sender().ID, "upload_working") + ":\n" + i := 0 + for _, dlQueue := range manager.working { + s := "#" + strconv.Itoa(i+1) + ": <code>" + dlQueue.torrentHash + "</code>\n" + if len(msg+s) > 1024 { + c.Send(msg) + msg = "" + } + msg += s + i++ + } + if len(msg) > 0 { + c.Send(msg) + msg = "" + } + } + if len(manager.queue) > 0 { + msg = tr(c.Sender().ID, "upload_in_queue") + ":\n" + for i, dlQueue := range manager.queue { + s := "#" + strconv.Itoa(i+1) + ": <code>" + dlQueue.torrentHash + "</code>\n" + if len(msg+s) > 1024 { + c.Send(msg) + msg = "" + } + msg += s + } + if len(msg) > 0 { + c.Send(msg) + msg = "" + } + } + return nil +} + +func AddRange(c tele.Context, hash string, from, to int) { + manager.AddRange(c, hash, from, to) +} + +func Cancel(id int) { + manager.Cancel(id) +} + +func updateLoadStatus(wrk *Worker, file *TorrFile, fi, fc int) { + if wrk.msg == nil { + return + } + t := torr.GetTorrent(wrk.torrentHash) + if t == nil { + return + } + ti := t.Status() + if wrk.isCancelled { + wrk.c.Bot().Edit(wrk.msg, tr(wrk.c.Sender().ID, "upload_stopping")) + } else { + wrk.c.Send(tele.UploadingVideo) + if ti.DownloadSpeed == 0 { + ti.DownloadSpeed = 1.0 + } + wait := time.Duration(float64(file.Remaining())/ti.DownloadSpeed) * time.Second + speed := humanize.IBytes(uint64(ti.DownloadSpeed)) + "/sec" + peers := fmt.Sprintf("%v · %v/%v", ti.ConnectedSeeders, ti.ActivePeers, ti.TotalPeers) + prc := fmt.Sprintf("%.2f%% %v / %v", float64(file.offset)*100.0/float64(file.size), humanize.IBytes(uint64(file.offset)), humanize.IBytes(uint64(file.size))) + + name := file.name + if name == ti.Title { + name = "" + } + + uid := wrk.c.Sender().ID + msg := tr(uid, "upload_title") + ":\n" + + "<b>" + escapeHtml(ti.Title) + "</b>\n" + if name != "" { + msg += "<i>" + escapeHtml(name) + "</i>\n" + } + msg += "<b>" + tr(uid, "upload_hash") + ":</b> <code>" + file.hash + "</code>\n" + if file.offset < file.size { + msg += "<b>" + tr(uid, "upload_speed") + ": </b>" + speed + "\n" + + "<b>" + tr(uid, "upload_remaining") + ": </b>" + wait.String() + "\n" + + "<b>" + tr(uid, "upload_peers") + ": </b>" + peers + "\n" + + "<b>" + tr(uid, "upload_progress") + ": </b>" + prc + } + if fc > 1 { + msg += "\n<b>" + tr(uid, "upload_files") + ": </b>" + strconv.Itoa(fi) + "/" + strconv.Itoa(fc) + } + if file.offset >= file.size { + msg += "\n<b>" + tr(uid, "upload_finishing") + "</b>" + wrk.c.Bot().Edit(wrk.msg, msg) + return + } + + torrKbd := &tele.ReplyMarkup{} + torrKbd.Inline([]tele.Row{torrKbd.Row(torrKbd.Data(tr(wrk.c.Sender().ID, "upload_cancel"), "cancel", strconv.Itoa(wrk.id)))}...) + wrk.c.Bot().Edit(wrk.msg, msg, torrKbd) + } +} diff --git a/server/tgbot/upload/torrfile.go b/server/tgbot/upload/torrfile.go new file mode 100644 index 0000000..7cbfe06 --- /dev/null +++ b/server/tgbot/upload/torrfile.go @@ -0,0 +1,98 @@ +package upload + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/anacrolix/torrent" + + sets "server/settings" + "server/log" + "server/tgbot/config" + "server/torr" + "server/torr/state" + "server/torr/storage/torrstor" +) + +var ERR_STOPPED = errors.New("stopped") + +type TorrFile struct { + hash string + name string + wrk *Worker + offset int64 + size int64 + id int + + reader *torrstor.Reader +} + +func NewTorrFile(wrk *Worker, stFile *state.TorrentFileStat) (*TorrFile, error) { + uid := int64(0) + if wrk.c != nil && wrk.c.Sender() != nil { + uid = wrk.c.Sender().ID + } + if config.Cfg != nil && config.Cfg.HostTG != "" && stFile.Length > 2*1024*1024*1024 { + return nil, errors.New(tr(uid, "upload_file_too_large_2gb")) + } + if (config.Cfg == nil || config.Cfg.HostTG == "") && stFile.Length > 50*1024*1024 { + return nil, errors.New(tr(uid, "upload_file_too_large_50mb")) + } + + tf := new(TorrFile) + tf.hash = wrk.torrentHash + tf.name = filepath.Base(stFile.Path) + tf.wrk = wrk + tf.size = stFile.Length + + t := torr.GetTorrent(wrk.torrentHash) + t.WaitInfo() + + files := t.Files() + var file *torrent.File + for _, tfile := range files { + if tfile.Path() == stFile.Path { + file = tfile + break + } + } + if file == nil { + return nil, fmt.Errorf("file with id %v not found", stFile.Id) + } + if int64(sets.MaxSize) > 0 && file.Length() > int64(sets.MaxSize) { + log.TLogln("tg upload err size", file.DisplayPath(), "max", sets.MaxSize) + return nil, fmt.Errorf("file size exceeded max allowed %d bytes", sets.MaxSize) + } + + reader := t.NewReader(file) + if reader == nil { + return nil, errors.New("cannot create torrent reader") + } + if sets.BTsets != nil && sets.BTsets.ResponsiveMode { + reader.SetResponsive() + } + tf.reader = reader + + return tf, nil +} + +func (t *TorrFile) Read(p []byte) (n int, err error) { + if t.wrk.isCancelled { + return 0, ERR_STOPPED + } + n, err = t.reader.Read(p) + t.offset += int64(n) + return +} + +func (t *TorrFile) Remaining() int64 { + return t.size - t.offset +} + +func (t *TorrFile) Close() { + if t.reader != nil { + t.reader.Close() + t.reader = nil + } +} diff --git a/server/tgbot/utils.go b/server/tgbot/utils.go new file mode 100644 index 0000000..4bf089f --- /dev/null +++ b/server/tgbot/utils.go @@ -0,0 +1,141 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + "unicode" + + tele "gopkg.in/telebot.v4" + + "server/settings" + "server/tgbot/config" + "server/web" +) + +func chatMsgKey(chatID int64, msgID int) string { + return fmt.Sprintf("%d_%d", chatID, msgID) +} + +// escapeHtml escapes <, >, &, " for Telegram HTML parse mode to prevent "Unsupported start tag" errors +func escapeHtml(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + return s +} + +// logSafeStr truncates by runes, strips emojis/symbols for clean laconic logs +func logSafeStr(s string, maxRunes int) string { + var b strings.Builder + n := 0 + lastSpace := true + for _, r := range s { + if n >= maxRunes { + break + } + switch { + case r == '\n' || r == '\r' || r == '\t': + if !lastSpace { + b.WriteRune(' ') + n++ + lastSpace = true + } + case r < 32 || r == 127: + case logIsEmojiOrSymbol(r): + case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '/' || r == '-' || r == '_' || r == '|' || r == ':' || r == '.' || r == ',' || r == ' ' || r == '?' || r == '!' || r == '@': + b.WriteRune(r) + n++ + lastSpace = (r == ' ') + default: + b.WriteRune(r) + n++ + lastSpace = false + } + } + return strings.TrimSpace(b.String()) +} + +func logIsEmojiOrSymbol(r rune) bool { + if unicode.IsSymbol(r) { + return true + } + u := uint32(r) + return (u >= 0x1F300 && u <= 0x1F9FF) || (u >= 0x2600 && u <= 0x26FF) || + (u >= 0x2700 && u <= 0x27BF) || (u >= 0x1F600 && u <= 0x1F64F) || + (u >= 0x1F680 && u <= 0x1F6FF) || (u >= 0x1F1E0 && u <= 0x1F1FF) || + (u >= 0xFE00 && u <= 0xFE0F) || (u >= 0x1F000 && u <= 0x1F02F) +} + +// logUser formats uid and optional username for logs +func logUser(u *tele.User) string { + if u == nil { + return "uid=?" + } + return logUserID(u.ID) + logUsername(u.Username) +} + +// logUserID formats uid for logs when User is not available +func logUserID(uid int64) string { + return "uid=" + strconv.FormatInt(uid, 10) +} + +func logUsername(username string) string { + if username == "" { + return "" + } + return " @" + username +} + +// logHashOrTruncate returns hash for logging if link is hash or magnet with btih, else truncated link +func logHashOrTruncate(link string) string { + if isHash(link) { + return link + } + if idx := strings.Index(link, "btih:"); idx >= 0 && idx+45 <= len(link) { + if h := link[idx+5 : idx+45]; isHash(h) { + return h + } + } + if strings.HasPrefix(strings.ToLower(link), "torrs://") && len(link) >= 48 { + if h := link[8:48]; isHash(h) { + return h + } + } + if len(link) > 64 { + return link[:64] + "..." + } + return link +} + +// getHost returns the base URL for stream/play links (e.g. http://192.168.1.1:8090) +func getHost() string { + host := config.Cfg.HostWeb + if host == "" { + host = settings.PubIPv4 + if host == "" { + ips := web.GetLocalIps() + if len(ips) == 0 { + host = "127.0.0.1" + } else { + host = ips[0] + } + } + } + if !strings.Contains(host, ":") { + if settings.Ssl { + host += ":" + settings.SslPort + } else { + host += ":" + settings.Port + } + } + if !strings.HasPrefix(host, "http") { + if settings.Ssl { + host = "https://" + host + } else { + host = "http://" + host + } + } + return host +} diff --git a/server/tgbot/viewed.go b/server/tgbot/viewed.go new file mode 100644 index 0000000..99489de --- /dev/null +++ b/server/tgbot/viewed.go @@ -0,0 +1,68 @@ +package tgbot + +import ( + "fmt" + "strconv" + "strings" + + tele "gopkg.in/telebot.v4" + sets "server/settings" +) + +func cmdViewed(c tele.Context) error { + args := c.Args() + if len(args) == 0 { + return c.Send(tr(c.Sender().ID, "viewed_usage")) + } + + action := strings.ToLower(args[0]) + if action == "set" || action == "rem" { + if len(args) < 2 { + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "viewed_usage_action"), action)) + } + hash := resolveHash(c, args[1]) + if hash == "" { + return c.Send(tr(c.Sender().ID, "invalid_hash")) + } + if action == "set" { + if len(args) < 3 { + return c.Send(tr(c.Sender().ID, "viewed_usage_set")) + } + index, err := strconv.Atoi(args[2]) + if err != nil || index < 1 { + return c.Send(tr(c.Sender().ID, "viewed_file_index")) + } + sets.SetViewed(&sets.Viewed{Hash: hash, FileIndex: index}) + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "viewed_marked"), hash, index)) + } + index := -1 + if len(args) >= 3 { + if i, err := strconv.Atoi(args[2]); err == nil && i >= 1 { + index = i + } + } + sets.RemViewed(&sets.Viewed{Hash: hash, FileIndex: index}) + if index >= 1 { + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "viewed_unmarked"), hash, index)) + } + return c.Send(fmt.Sprintf(tr(c.Sender().ID, "viewed_cleared"), hash)) + } + + hash := resolveHash(c, args[0]) + if hash == "" { + return c.Send(tr(c.Sender().ID, "viewed_usage")) + } + + list := sets.ListViewed(hash) + if len(list) == 0 { + return c.Send(tr(c.Sender().ID, "viewed_empty")) + } + + var sb strings.Builder + sb.WriteString("<b>" + tr(c.Sender().ID, "viewed_list") + "</b>\n\n") + fmt.Fprintf(&sb, "<code>%s</code>\n\n", hash) + for _, v := range list { + fmt.Fprintf(&sb, " #%d\n", v.FileIndex) + } + return c.Send(sb.String()) +} diff --git a/server/torr/apihelper.go b/server/torr/apihelper.go new file mode 100644 index 0000000..ec7b640 --- /dev/null +++ b/server/torr/apihelper.go @@ -0,0 +1,277 @@ +package torr + +import ( + "io" + "os" + "path/filepath" + "sort" + "time" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + + "server/log" + sets "server/settings" +) + +var bts *BTServer + +func InitApiHelper(bt *BTServer) { + bts = bt +} + +func LoadTorrent(tor *Torrent) *Torrent { + if tor.TorrentSpec == nil { + return nil + } + tr, err := NewTorrent(tor.TorrentSpec, bts) + if err != nil { + return nil + } + if !tr.WaitInfo() { + return nil + } + tr.Title = tor.Title + tr.Poster = tor.Poster + tr.Data = tor.Data + return tr +} + +func AddTorrent(spec *torrent.TorrentSpec, title, poster string, data string, category string) (*Torrent, error) { + torr, err := NewTorrent(spec, bts) + if err != nil { + log.TLogln("error add torrent:", err) + return nil, err + } + + torDB := GetTorrentDB(spec.InfoHash) + + if torr.Title == "" { + torr.Title = title + if title == "" && torDB != nil { + torr.Title = torDB.Title + } + if torr.Title == "" && torr.Torrent != nil && torr.Torrent.Info() != nil { + torr.Title = torr.Info().Name + } + } + + if torr.Category == "" { + torr.Category = category + if torr.Category == "" && torDB != nil { + torr.Category = torDB.Category + } + } + + if torr.Poster == "" { + torr.Poster = poster + if torr.Poster == "" && torDB != nil { + torr.Poster = torDB.Poster + } + } + + if torr.Data == "" { + torr.Data = data + if torr.Data == "" && torDB != nil { + torr.Data = torDB.Data + } + } + + return torr, nil +} + +func SaveTorrentToDB(torr *Torrent) { + log.TLogln("save to db:", torr.Hash()) + AddTorrentDB(torr) +} + +func GetTorrent(hashHex string) *Torrent { + hash := metainfo.NewHashFromHex(hashHex) + timeout := time.Second * time.Duration(sets.BTsets.TorrentDisconnectTimeout) + if timeout > time.Minute { + timeout = time.Minute + } + tor := bts.GetTorrent(hash) + if tor != nil { + tor.AddExpiredTime(timeout) + return tor + } + + tr := GetTorrentDB(hash) + if tr != nil { + tor = tr + go func() { + log.TLogln("New torrent", tor.Hash()) + tr, _ := NewTorrent(tor.TorrentSpec, bts) + if tr != nil { + tr.Title = tor.Title + tr.Poster = tor.Poster + tr.Data = tor.Data + tr.Size = tor.Size + tr.Timestamp = tor.Timestamp + tr.Category = tor.Category + tr.GotInfo() + } + }() + } + return tor +} + +func SetTorrent(hashHex, title, poster, category string, data string) *Torrent { + hash := metainfo.NewHashFromHex(hashHex) + torr := bts.GetTorrent(hash) + torrDb := GetTorrentDB(hash) + + if title == "" && torr == nil && torrDb != nil { + torr = GetTorrent(hashHex) + torr.GotInfo() + if torr.Torrent != nil && torr.Torrent.Info() != nil { + title = torr.Info().Name + } + } + + if torr != nil { + if title == "" && torr.Torrent != nil && torr.Torrent.Info() != nil { + title = torr.Info().Name + } + torr.Title = title + torr.Poster = poster + torr.Category = category + if data != "" { + torr.Data = data + } + } + // update torrent data in DB + if torrDb != nil { + torrDb.Title = title + torrDb.Poster = poster + torrDb.Category = category + if data != "" { + torrDb.Data = data + } + AddTorrentDB(torrDb) + } + if torr != nil { + return torr + } else { + return torrDb + } +} + +func RemTorrent(hashHex string) { + if sets.ReadOnly { + log.TLogln("API RemTorrent: Read-only DB mode!", hashHex) + return + } + hash := metainfo.NewHashFromHex(hashHex) + if bts.RemoveTorrent(hash) { + if sets.BTsets.UseDisk && hashHex != "" && hashHex != "/" { + name := filepath.Join(sets.BTsets.TorrentsSavePath, hashHex) + ff, _ := os.ReadDir(name) + for _, f := range ff { + os.Remove(filepath.Join(name, f.Name())) + } + err := os.Remove(name) + if err != nil { + log.TLogln("Error remove cache:", err) + } + } + } + RemTorrentDB(hash) +} + +func ListTorrent() []*Torrent { + btlist := bts.ListTorrents() + dblist := ListTorrentsDB() + + for hash, t := range dblist { + if _, ok := btlist[hash]; !ok { + btlist[hash] = t + } + } + var ret []*Torrent + + for _, t := range btlist { + ret = append(ret, t) + } + + sort.Slice(ret, func(i, j int) bool { + if ret[i].Timestamp != ret[j].Timestamp { + return ret[i].Timestamp > ret[j].Timestamp + } else { + return ret[i].Title > ret[j].Title + } + }) + + return ret +} + +func DropTorrent(hashHex string) { + hash := metainfo.NewHashFromHex(hashHex) + bts.RemoveTorrent(hash) +} + +func SetSettings(set *sets.BTSets) { + if sets.ReadOnly { + log.TLogln("API SetSettings: Read-only DB mode!") + return + } + sets.SetBTSets(set) + log.TLogln("drop all torrents") + dropAllTorrent() + time.Sleep(time.Second * 1) + log.TLogln("disconect") + bts.Disconnect() + log.TLogln("connect") + bts.Connect() + time.Sleep(time.Second * 1) + log.TLogln("end set settings") +} + +func SetDefSettings() { + if sets.ReadOnly { + log.TLogln("API SetDefSettings: Read-only DB mode!") + return + } + sets.SetDefaultConfig() + log.TLogln("drop all torrents") + dropAllTorrent() + time.Sleep(time.Second * 1) + log.TLogln("disconect") + bts.Disconnect() + log.TLogln("connect") + bts.Connect() + time.Sleep(time.Second * 1) + log.TLogln("end set default settings") +} + +func dropAllTorrent() { + for _, torr := range bts.torrents { + torr.drop() + <-torr.closed + } +} + +func Shutdown() { + bts.Disconnect() + sets.CloseDB() + log.TLogln("Received shutdown. Quit") + os.Exit(0) +} + +func WriteStatus(w io.Writer) { + bts.client.WriteStatus(w) +} + +func Preload(torr *Torrent, index int) { + cache := float32(sets.BTsets.CacheSize) + preload := float32(sets.BTsets.PreloadCache) + size := int64((cache / 100.0) * preload) + if size <= 0 { + return + } + if size > sets.BTsets.CacheSize { + size = sets.BTsets.CacheSize + } + torr.Preload(index, size) +} diff --git a/server/torr/btserver.go b/server/torr/btserver.go new file mode 100644 index 0000000..ea4129e --- /dev/null +++ b/server/torr/btserver.go @@ -0,0 +1,338 @@ +package torr + +import ( + "context" + "fmt" + "log" + "maps" + "net" + "net/http" + "net/url" + "server/proxy" + "sync" + + "github.com/anacrolix/publicip" + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + "github.com/wlynxg/anet" + + "server/settings" + "server/torr/storage/torrstor" + "server/torr/utils" + "server/version" +) + +type BTServer struct { + config *torrent.ClientConfig + client *torrent.Client + + storage *torrstor.Storage + + torrents map[metainfo.Hash]*Torrent + + mu sync.Mutex +} + +var privateIPBlocks []*net.IPNet + +func init() { + for _, cidr := range []string{ + "127.0.0.0/8", // IPv4 loopback + "10.0.0.0/8", // RFC1918 + "172.16.0.0/12", // RFC1918 + "192.168.0.0/16", // RFC1918 + "169.254.0.0/16", // RFC3927 link-local + "::1/128", // IPv6 loopback + "fe80::/10", // IPv6 link-local + "fc00::/7", // IPv6 unique local addr + } { + _, block, err := net.ParseCIDR(cidr) + if err != nil { + panic(fmt.Errorf("parse error on %q: %v", cidr, err)) + } + privateIPBlocks = append(privateIPBlocks, block) + } +} + +func NewBTS() *BTServer { + bts := new(BTServer) + bts.torrents = make(map[metainfo.Hash]*Torrent) + return bts +} + +func (bt *BTServer) Connect() error { + bt.mu.Lock() + defer bt.mu.Unlock() + var err error + bt.configure(context.TODO()) + bt.client, err = torrent.NewClient(bt.config) + bt.torrents = make(map[metainfo.Hash]*Torrent) + InitApiHelper(bt) + + proxy.Start() + return err +} + +func (bt *BTServer) Disconnect() { + bt.mu.Lock() + defer bt.mu.Unlock() + if bt.client != nil { + bt.client.Close() + bt.client = nil + utils.FreeOSMemGC() + } + proxy.Stop() +} + +func (bt *BTServer) configure(ctx context.Context) { + blocklist, _ := utils.ReadBlockedIP() + bt.config = torrent.NewDefaultClientConfig() + + bt.storage = torrstor.NewStorage(settings.BTsets.CacheSize) + bt.config.DefaultStorage = bt.storage + + userAgent := "qBittorrent/4.3.9" + peerID := "-qB4390-" + upnpID := "TorrServer/" + version.Version + cliVers := userAgent + + bt.config.Debug = settings.BTsets.EnableDebug + bt.config.DisableIPv6 = !settings.BTsets.EnableIPv6 + bt.config.DisableTCP = settings.BTsets.DisableTCP + bt.config.DisableUTP = settings.BTsets.DisableUTP + // https://github.com/anacrolix/torrent/issues/703 + // bt.config.DisableWebtorrent = true // NE + // bt.config.DisableWebseeds = false // NE + bt.config.NoDefaultPortForwarding = settings.BTsets.DisableUPNP + bt.config.NoDHT = settings.BTsets.DisableDHT + bt.config.DisablePEX = settings.BTsets.DisablePEX + bt.config.NoUpload = settings.BTsets.DisableUpload + bt.config.IPBlocklist = blocklist + bt.config.Bep20 = peerID + bt.config.PeerID = utils.PeerIDRandom(peerID) + bt.config.UpnpID = upnpID + bt.config.HTTPUserAgent = userAgent + bt.config.ExtendedHandshakeClientVersion = cliVers + bt.config.EstablishedConnsPerTorrent = settings.BTsets.ConnectionsLimit + bt.config.TotalHalfOpenConns = 500 + // Encryption/Obfuscation + bt.config.EncryptionPolicy = torrent.EncryptionPolicy{ // OE + ForceEncryption: settings.BTsets.ForceEncrypt, // OE + } // OE + // bt.config.HeaderObfuscationPolicy = torrent.HeaderObfuscationPolicy{ // NE + // RequirePreferred: settings.BTsets.ForceEncrypt, // NE + // Preferred: true, // NE + // } // NE + if settings.BTsets.DownloadRateLimit > 0 { + bt.config.DownloadRateLimiter = utils.Limit(settings.BTsets.DownloadRateLimit * 1024) + } + if settings.BTsets.UploadRateLimit > 0 { + bt.config.Seed = true + bt.config.UploadRateLimiter = utils.Limit(settings.BTsets.UploadRateLimit * 1024) + } + if settings.TorAddr != "" { + log.Println("Set listen addr", settings.TorAddr) + bt.config.SetListenAddr(settings.TorAddr) + } else { + if settings.BTsets.PeersListenPort > 0 { + log.Println("Set listen port", settings.BTsets.PeersListenPort) + bt.config.ListenPort = settings.BTsets.PeersListenPort + } else { + log.Println("Set listen port to random autoselect (0)") + bt.config.ListenPort = 0 + } + } + + // Configure proxy if enabled + if err := bt.configureProxy(); err != nil { + log.Println("Proxy configuration error:", err) + } + + log.Println("Client config:", settings.BTsets) + + var err error + + // set public IPv4 + if settings.PubIPv4 != "" { + if ip4 := net.ParseIP(settings.PubIPv4); ip4.To4() != nil && !isPrivateIP(ip4) { + bt.config.PublicIp4 = ip4 + } + } + if bt.config.PublicIp4 == nil { + bt.config.PublicIp4, err = publicip.Get4(ctx) + if err != nil { + log.Printf("error getting public ipv4 address: %v", err) + } + } + if bt.config.PublicIp4.To4() == nil { // possible IPv6 from publicip.Get4(ctx) + bt.config.PublicIp4 = nil + } + if bt.config.PublicIp4 != nil { + log.Println("PublicIp4:", bt.config.PublicIp4) + } + + // set public IPv6 + if settings.PubIPv6 != "" { + if ip6 := net.ParseIP(settings.PubIPv6); ip6.To16() != nil && ip6.To4() == nil && !isPrivateIP(ip6) { + bt.config.PublicIp6 = ip6 + } + } + if bt.config.PublicIp6 == nil && settings.BTsets.EnableIPv6 { + bt.config.PublicIp6, err = publicip.Get6(ctx) + if err != nil { + log.Printf("error getting public ipv6 address: %v", err) + } + } + if bt.config.PublicIp6.To16() == nil { // just 4 sure it's valid IPv6 + bt.config.PublicIp6 = nil + } + if bt.config.PublicIp6 != nil { + log.Println("PublicIp6:", bt.config.PublicIp6) + } +} + +func (bt *BTServer) configureProxy() error { + proxyURL := settings.Args.ProxyURL + + if proxyURL == "" { + return nil // No proxy configured + } + + proxyMode := settings.Args.ProxyMode + if proxyMode == "" { + proxyMode = "tracker" // default + } + + // Parse and validate proxy URL + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return fmt.Errorf("invalid proxy URL: %w", err) + } + + scheme := parsedURL.Scheme + // Validate proxy protocol + switch scheme { + case "socks5", "socks5h", "socks4", "socks4a", "http", "https": + // Supported protocols + default: + return fmt.Errorf("unsupported proxy protocol: %s (supported: http, https, socks4, socks4a, socks5, socks5h)", scheme) + } + + if proxyMode == "full" { + log.Printf("Configuring proxy for all BitTorrent traffic: %s://%s", scheme, parsedURL.Host) + + // Set ProxyURL - this will be used by anacrolix/torrent for all BitTorrent traffic + bt.config.ProxyURL = proxyURL + + // Also set HTTPProxy explicitly for HTTP tracker requests + bt.config.HTTPProxy = func(req *http.Request) (*url.URL, error) { + return parsedURL, nil + } + + log.Println("Proxy configured successfully for all BitTorrent connections (tracker, DHT, peers)") + } else if proxyMode == "peers" { + log.Printf("Configuring proxy for peer connections only: %s://%s", scheme, parsedURL.Host) + + // Set ProxyURL for peer connections, but don't set HTTPProxy + // This routes DHT and peer connections through proxy, but not HTTP tracker requests + bt.config.ProxyURL = proxyURL + + log.Println("Proxy configured successfully for peer and DHT connections only") + } else { + log.Printf("Configuring proxy for HTTP tracker requests only: %s://%s", scheme, parsedURL.Host) + + // Only set HTTPProxy for tracker requests, don't set ProxyURL + bt.config.HTTPProxy = func(req *http.Request) (*url.URL, error) { + return parsedURL, nil + } + + log.Println("Proxy configured successfully for HTTP tracker connections only") + } + + return nil +} + +func (bt *BTServer) GetTorrent(hash torrent.InfoHash) *Torrent { + if torr, ok := bt.torrents[hash]; ok { + return torr + } + return nil +} + +func (bt *BTServer) ListTorrents() map[metainfo.Hash]*Torrent { + list := make(map[metainfo.Hash]*Torrent) + maps.Copy(list, bt.torrents) + return list +} + +func (bt *BTServer) RemoveTorrent(hash torrent.InfoHash) bool { + if torr, ok := bt.torrents[hash]; ok { + return torr.Close() + } + return false +} + +func isPrivateIP(ip net.IP) bool { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + return true + } + + for _, block := range privateIPBlocks { + if block.Contains(ip) { + return true + } + } + return false +} + +func getPublicIp4() net.IP { + ifaces, err := anet.Interfaces() + if err != nil { + log.Println("Error get public IPv4") + return nil + } + for _, i := range ifaces { + addrs, _ := anet.InterfaceAddrsByInterface(&i) + if i.Flags&net.FlagUp == net.FlagUp { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !isPrivateIP(ip) && ip.To4() != nil { + return ip + } + } + } + } + return nil +} + +func getPublicIp6() net.IP { + ifaces, err := anet.Interfaces() + if err != nil { + log.Println("Error get public IPv6") + return nil + } + for _, i := range ifaces { + addrs, _ := anet.InterfaceAddrsByInterface(&i) + if i.Flags&net.FlagUp == net.FlagUp { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !isPrivateIP(ip) && ip.To16() != nil && ip.To4() == nil { + return ip + } + } + } + } + return nil +} diff --git a/server/torr/dbwrapper.go b/server/torr/dbwrapper.go new file mode 100644 index 0000000..3a2080a --- /dev/null +++ b/server/torr/dbwrapper.go @@ -0,0 +1,88 @@ +package torr + +import ( + "encoding/json" + + "server/settings" + "server/torr/state" + "server/torr/utils" + + "github.com/anacrolix/torrent/metainfo" +) + +type tsFiles struct { + TorrServer struct { + Files []*state.TorrentFileStat `json:"Files"` + } `json:"TorrServer"` +} + +func AddTorrentDB(torr *Torrent) { + t := new(settings.TorrentDB) + t.TorrentSpec = torr.TorrentSpec + t.Title = torr.Title + t.Category = torr.Category + if torr.Data == "" { + files := new(tsFiles) + files.TorrServer.Files = torr.Status().FileStats + buf, err := json.Marshal(files) + if err == nil { + t.Data = string(buf) + torr.Data = t.Data + } + } else { + t.Data = torr.Data + } + + if torr.Poster != "" && utils.CheckImgUrl(torr.Poster) { + t.Poster = torr.Poster + } + t.Size = torr.Size + if t.Size == 0 && torr.Torrent != nil { + t.Size = torr.Torrent.Length() + } + // don't override timestamp from DB on edit + t.Timestamp = torr.Timestamp // time.Now().Unix() + + settings.AddTorrent(t) +} + +func GetTorrentDB(hash metainfo.Hash) *Torrent { + list := settings.ListTorrent() + for _, db := range list { + if hash == db.InfoHash { + torr := new(Torrent) + torr.TorrentSpec = db.TorrentSpec + torr.Title = db.Title + torr.Poster = db.Poster + torr.Category = db.Category + torr.Timestamp = db.Timestamp + torr.Size = db.Size + torr.Data = db.Data + torr.Stat = state.TorrentInDB + return torr + } + } + return nil +} + +func RemTorrentDB(hash metainfo.Hash) { + settings.RemTorrent(hash) +} + +func ListTorrentsDB() map[metainfo.Hash]*Torrent { + ret := make(map[metainfo.Hash]*Torrent) + list := settings.ListTorrent() + for _, db := range list { + torr := new(Torrent) + torr.TorrentSpec = db.TorrentSpec + torr.Title = db.Title + torr.Poster = db.Poster + torr.Category = db.Category + torr.Timestamp = db.Timestamp + torr.Size = db.Size + torr.Data = db.Data + torr.Stat = state.TorrentInDB + ret[torr.TorrentSpec.InfoHash] = torr + } + return ret +} diff --git a/server/torr/preload.go b/server/torr/preload.go new file mode 100644 index 0000000..462f2d5 --- /dev/null +++ b/server/torr/preload.go @@ -0,0 +1,293 @@ +package torr + +import ( + "fmt" + "io" + "strconv" + "sync" + "time" + + "server/ffprobe" + + "server/log" + "server/settings" + "server/torr/state" + utils2 "server/utils" + + "github.com/anacrolix/torrent" +) + +func (t *Torrent) Preload(index int, size int64) { + if size <= 0 { + return + } + t.PreloadSize = size + + if t.Stat == state.TorrentGettingInfo { + if !t.WaitInfo() { + return + } + // wait change status + time.Sleep(100 * time.Millisecond) + } + + t.muTorrent.Lock() + if t.Stat != state.TorrentWorking { + t.muTorrent.Unlock() + return + } + + t.Stat = state.TorrentPreload + t.muTorrent.Unlock() + + defer func() { + t.muTorrent.Lock() + if t.Stat == state.TorrentPreload { + t.Stat = state.TorrentWorking + } + t.muTorrent.Unlock() + // Очистка по окончании прелоада + t.BitRate = "" + t.DurationSeconds = 0 + }() + + file := t.findFileIndex(index) + if file == nil { + file = t.Files()[0] + } + + if size > file.Length() { + size = file.Length() + } + + if t.Info() == nil { + return + } + + timeout := time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout) + if timeout > time.Minute { + timeout = time.Minute + } + + // Create a stop channel for the logging goroutine + logStopChan := make(chan struct{}) + defer close(logStopChan) // Ensure logging stops when function returns + + // Запуск лога в отдельном потоке + go func(stopChan <-chan struct{}) { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + t.muTorrent.Lock() + stat := t.Stat + t.muTorrent.Unlock() + + if stat != state.TorrentPreload { + return + } + + statStr := fmt.Sprint(file.Torrent().InfoHash().HexString(), " ", + utils2.Format(float64(t.PreloadedBytes)), "/", + utils2.Format(float64(t.PreloadSize)), " Speed:", + utils2.Format(t.DownloadSpeed), " Peers:", + t.Torrent.Stats().ActivePeers, "/", + t.Torrent.Stats().TotalPeers, " [Seeds:", + t.Torrent.Stats().ConnectedSeeders, "]") + log.TLogln("Preload:", statStr) + t.AddExpiredTime(timeout) + case <-stopChan: + return + } + } + }(logStopChan) + + if ffprobe.Exists() { + link := "http://127.0.0.1:" + settings.Port + "/play/" + t.Hash().HexString() + "/" + strconv.Itoa(index) + if settings.Ssl { + link = "https://127.0.0.1:" + settings.SslPort + "/play/" + t.Hash().HexString() + "/" + strconv.Itoa(index) + } + if data, err := ffprobe.ProbeUrl(link); err == nil { + t.BitRate = data.Format.BitRate + t.DurationSeconds = data.Format.DurationSeconds + } + } + + // Check if torrent was closed + t.muTorrent.Lock() + isClosed := t.Stat == state.TorrentClosed + t.muTorrent.Unlock() + + if isClosed { + log.TLogln("End preload: torrent closed") + return + } + + // startend -> 8/16 MB + startend := t.Info().PieceLength + if startend < 8<<20 { + startend = 8 << 20 + } + + readerStart := file.NewReader() + if readerStart == nil { + log.TLogln("End preload: null reader") + return + } + defer readerStart.Close() + + readerStart.SetResponsive() + readerStart.SetReadahead(0) + readerStartEnd := size - startend + + if readerStartEnd < 0 { + // Если конец начального ридера оказался за началом + readerStartEnd = size + } + if readerStartEnd > file.Length() { + // Если конец начального ридера оказался после конца файла + readerStartEnd = file.Length() + } + + readerEndStart := file.Length() - startend + readerEndEnd := file.Length() + + var wg sync.WaitGroup + var preloadErr error + + // Start end range preload if needed + if readerEndStart > readerStartEnd { + wg.Add(1) + go func() { + defer wg.Done() + + // Check if we should still preload + t.muTorrent.Lock() + shouldPreload := t.Stat == state.TorrentPreload + t.muTorrent.Unlock() + + if !shouldPreload { + return + } + + readerEnd := file.NewReader() + if readerEnd == nil { + log.TLogln("Err preload: null reader") + preloadErr = fmt.Errorf("null reader for end range") + return + } + defer readerEnd.Close() // Ensure reader is always closed + + readerEnd.SetResponsive() + readerEnd.SetReadahead(0) + + _, err := readerEnd.Seek(readerEndStart, io.SeekStart) + if err != nil { + log.TLogln("Err preload seek:", err) + preloadErr = err + return + } + + offset := readerEndStart + tmp := make([]byte, 32768) + for offset+int64(len(tmp)) < readerEndEnd { + n, err := readerEnd.Read(tmp) + if err != nil { + if err != io.EOF { + log.TLogln("Err preload read:", err) + preloadErr = err + } + break + } + offset += int64(n) + + // Check if we should continue + t.muTorrent.Lock() + shouldContinue := t.Stat == state.TorrentPreload + t.muTorrent.Unlock() + + if !shouldContinue { + break + } + } + }() + } + + // Main preload section + pieceLength := t.Info().PieceLength + readahead := pieceLength * 4 + if readerStartEnd < readahead { + readahead = 0 + } + readerStart.SetReadahead(readahead) + + offset := int64(0) + tmp := make([]byte, 32768) + for offset+int64(len(tmp)) < readerStartEnd { + // Check if we should continue + t.muTorrent.Lock() + shouldContinue := t.Stat == state.TorrentPreload + t.muTorrent.Unlock() + + if !shouldContinue { + log.TLogln("Preload cancelled") + break + } + + n, err := readerStart.Read(tmp) + if err != nil { + if err != io.EOF { + log.TLogln("Error preload:", err) + } + break + } + offset += int64(n) + + if readahead > 0 && readerStartEnd-(offset+int64(len(tmp))) < readahead { + readahead = 0 + readerStart.SetReadahead(0) + } + } + + // Wait for end range preload to complete + wg.Wait() + + // Check if end range preload failed + if preloadErr != nil { + log.TLogln("End range preload failed:", preloadErr) + } + + // Final log + t.muTorrent.Lock() + finalStat := t.Stat + t.muTorrent.Unlock() + + if finalStat == state.TorrentPreload { + log.TLogln("End preload:", file.Torrent().InfoHash().HexString(), + "Peers:", t.Torrent.Stats().ActivePeers, "/", + t.Torrent.Stats().TotalPeers, "[ Seeds:", + t.Torrent.Stats().ConnectedSeeders, "]") + } +} + +func (t *Torrent) findFileIndex(index int) *torrent.File { + st := t.Status() + var stFile *state.TorrentFileStat + for _, f := range st.FileStats { + if index == f.Id { + stFile = f + break + } + } + if stFile == nil { + return nil + } + for _, file := range t.Files() { + if file.Path() == stFile.Path { + return file + } + } + return nil +} diff --git a/server/torr/state/state.go b/server/torr/state/state.go new file mode 100644 index 0000000..17b0f2c --- /dev/null +++ b/server/torr/state/state.go @@ -0,0 +1,76 @@ +package state + +type TorrentStat int + +func (t TorrentStat) String() string { + switch t { + case TorrentAdded: + return "Torrent added" + case TorrentGettingInfo: + return "Torrent getting info" + case TorrentPreload: + return "Torrent preload" + case TorrentWorking: + return "Torrent working" + case TorrentClosed: + return "Torrent closed" + case TorrentInDB: + return "Torrent in db" + default: + return "Torrent unknown status" + } +} + +const ( + TorrentAdded = TorrentStat(iota) + TorrentGettingInfo + TorrentPreload + TorrentWorking + TorrentClosed + TorrentInDB +) + +type TorrentStatus struct { + Title string `json:"title"` + Category string `json:"category"` + Poster string `json:"poster"` + Data string `json:"data,omitempty"` + Timestamp int64 `json:"timestamp"` + Name string `json:"name,omitempty"` + Hash string `json:"hash,omitempty"` + TorrsHash string `json:"torrs_hash,omitempty"` + Stat TorrentStat `json:"stat"` + StatString string `json:"stat_string"` + LoadedSize int64 `json:"loaded_size,omitempty"` + TorrentSize int64 `json:"torrent_size,omitempty"` + PreloadedBytes int64 `json:"preloaded_bytes,omitempty"` + PreloadSize int64 `json:"preload_size,omitempty"` + DownloadSpeed float64 `json:"download_speed,omitempty"` + UploadSpeed float64 `json:"upload_speed,omitempty"` + TotalPeers int `json:"total_peers,omitempty"` + PendingPeers int `json:"pending_peers,omitempty"` + ActivePeers int `json:"active_peers,omitempty"` + ConnectedSeeders int `json:"connected_seeders,omitempty"` + HalfOpenPeers int `json:"half_open_peers,omitempty"` + BytesWritten int64 `json:"bytes_written,omitempty"` + BytesWrittenData int64 `json:"bytes_written_data,omitempty"` + BytesRead int64 `json:"bytes_read,omitempty"` + BytesReadData int64 `json:"bytes_read_data,omitempty"` + BytesReadUsefulData int64 `json:"bytes_read_useful_data,omitempty"` + ChunksWritten int64 `json:"chunks_written,omitempty"` + ChunksRead int64 `json:"chunks_read,omitempty"` + ChunksReadUseful int64 `json:"chunks_read_useful,omitempty"` + ChunksReadWasted int64 `json:"chunks_read_wasted,omitempty"` + PiecesDirtiedGood int64 `json:"pieces_dirtied_good,omitempty"` + PiecesDirtiedBad int64 `json:"pieces_dirtied_bad,omitempty"` + DurationSeconds float64 `json:"duration_seconds,omitempty"` + BitRate string `json:"bit_rate,omitempty"` + + FileStats []*TorrentFileStat `json:"file_stats,omitempty"` +} + +type TorrentFileStat struct { + Id int `json:"id,omitempty"` + Path string `json:"path,omitempty"` + Length int64 `json:"length,omitempty"` +} diff --git a/server/torr/storage/state/state.go b/server/torr/storage/state/state.go new file mode 100644 index 0000000..301c2ed --- /dev/null +++ b/server/torr/storage/state/state.go @@ -0,0 +1,30 @@ +package state + +import ( + "server/torr/state" +) + +type CacheState struct { + Hash string + Capacity int64 + Filled int64 + PiecesLength int64 + PiecesCount int + Torrent *state.TorrentStatus + Pieces map[int]ItemState + Readers []*ReaderState +} + +type ItemState struct { + Id int + Length int64 + Size int64 + Completed bool + Priority int +} + +type ReaderState struct { + Start int + End int + Reader int +} diff --git a/server/torr/storage/storage.go b/server/torr/storage/storage.go new file mode 100644 index 0000000..87dfbd5 --- /dev/null +++ b/server/torr/storage/storage.go @@ -0,0 +1,12 @@ +package storage + +import ( + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +type Storage interface { + storage.ClientImpl + + CloseHash(hash metainfo.Hash) +} diff --git a/server/torr/storage/torrstor/cache.go b/server/torr/storage/torrstor/cache.go new file mode 100644 index 0000000..5b8080a --- /dev/null +++ b/server/torr/storage/torrstor/cache.go @@ -0,0 +1,389 @@ +package torrstor + +import ( + "os" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/anacrolix/torrent" + + "server/log" + "server/settings" + "server/torr/storage/state" + "server/torr/utils" + + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +type Cache struct { + storage.TorrentImpl + storage *Storage + + capacity int64 + filled int64 + hash metainfo.Hash + + pieceLength int64 + pieceCount int + + pieces map[int]*Piece + + readers map[*Reader]struct{} + muReaders sync.Mutex + + isRemove bool + isClosed bool + muRemove sync.Mutex + torrent *torrent.Torrent +} + +func NewCache(capacity int64, storage *Storage) *Cache { + ret := &Cache{ + capacity: capacity, + filled: 0, + pieces: make(map[int]*Piece), + storage: storage, + readers: make(map[*Reader]struct{}), + } + + return ret +} + +func (c *Cache) Init(info *metainfo.Info, hash metainfo.Hash) { + log.TLogln("Create cache for:", info.Name, hash.HexString()) + if c.capacity == 0 { + c.capacity = info.PieceLength * 4 + } + + c.pieceLength = info.PieceLength + c.pieceCount = info.NumPieces() + c.hash = hash + + if settings.BTsets.UseDisk { + name := filepath.Join(settings.BTsets.TorrentsSavePath, hash.HexString()) + err := os.MkdirAll(name, 0o777) + if err != nil { + log.TLogln("Error create dir:", err) + } + } + + for i := 0; i < c.pieceCount; i++ { + c.pieces[i] = NewPiece(i, c) + } +} + +func (c *Cache) SetTorrent(torr *torrent.Torrent) { + c.torrent = torr +} + +func (c *Cache) Piece(m metainfo.Piece) storage.PieceImpl { + if val, ok := c.pieces[m.Index()]; ok { + return val + } + return &PieceFake{} +} + +func (c *Cache) Close() error { + if c.torrent != nil { + log.TLogln("Close cache for:", c.torrent.Name(), c.hash) + } else { + log.TLogln("Close cache for:", c.hash) + } + c.isClosed = true + + delete(c.storage.caches, c.hash) + + if settings.BTsets.RemoveCacheOnDrop { + name := filepath.Join(settings.BTsets.TorrentsSavePath, c.hash.HexString()) + if name != "" && name != "/" { + for _, v := range c.pieces { + if v.dPiece != nil { + os.Remove(v.dPiece.name) + } + } + os.Remove(name) + } + } + + c.muReaders.Lock() + c.readers = nil + c.pieces = nil + c.muReaders.Unlock() + + utils.FreeOSMemGC() + return nil +} + +func (c *Cache) removePiece(piece *Piece) { + if !c.isClosed { + piece.Release() + } +} + +func (c *Cache) AdjustRA(readahead int64) { + if settings.BTsets.CacheSize == 0 { + c.capacity = readahead * 3 + } + if c.Readers() > 0 { + c.muReaders.Lock() + for r := range c.readers { + r.SetReadahead(readahead) + } + c.muReaders.Unlock() + } +} + +func (c *Cache) GetState() *state.CacheState { + cState := new(state.CacheState) + + piecesState := make(map[int]state.ItemState, 0) + var fill int64 = 0 + + if len(c.pieces) > 0 { + for _, p := range c.pieces { + if p.Size > 0 { + fill += p.Size + piecesState[p.Id] = state.ItemState{ + Id: p.Id, + Size: p.Size, + Length: c.pieceLength, + Completed: p.Complete, + Priority: int(c.torrent.PieceState(p.Id).Priority), + } + } + } + } + + readersState := make([]*state.ReaderState, 0) + + if c.Readers() > 0 { + c.muReaders.Lock() + for r := range c.readers { + rng := r.getPiecesRange() + pc := r.getReaderPiece() + readersState = append(readersState, &state.ReaderState{ + Start: rng.Start, + End: rng.End, + Reader: pc, + }) + } + c.muReaders.Unlock() + } + + c.filled = fill + cState.Capacity = c.capacity + cState.PiecesLength = c.pieceLength + cState.PiecesCount = c.pieceCount + cState.Hash = c.hash.HexString() + cState.Filled = fill + cState.Pieces = piecesState + cState.Readers = readersState + return cState +} + +func (c *Cache) cleanPieces() { + if c.isRemove || c.isClosed { + return + } + c.muRemove.Lock() + if c.isRemove { + c.muRemove.Unlock() + return + } + c.isRemove = true + defer func() { c.isRemove = false }() + c.muRemove.Unlock() + + remPieces := c.getRemPieces() + if c.filled > c.capacity { + rems := (c.filled-c.capacity)/c.pieceLength + 1 + for _, p := range remPieces { + c.removePiece(p) + rems-- + if rems <= 0 { + utils.FreeOSMemGC() + return + } + } + } +} + +func (c *Cache) getRemPieces() []*Piece { + piecesRemove := make([]*Piece, 0) + fill := int64(0) + + ranges := make([]Range, 0) + c.muReaders.Lock() + for r := range c.readers { + r.checkReader() + if r.isUse { + ranges = append(ranges, r.getPiecesRange()) + } + } + c.muReaders.Unlock() + ranges = mergeRange(ranges) + + for id, p := range c.pieces { + if p.Size > 0 { + fill += p.Size + } + if len(ranges) > 0 { + if !inRanges(ranges, id) { + if p.Size > 0 && !c.isIdInFileBE(ranges, id) { + piecesRemove = append(piecesRemove, p) + } + } + } else { + // on preload clean + if p.Size > 0 && !c.isIdInFileBE(ranges, id) { + piecesRemove = append(piecesRemove, p) + } + } + } + + c.clearPriority() + c.setLoadPriority(ranges) + + sort.Slice(piecesRemove, func(i, j int) bool { + return piecesRemove[i].Accessed < piecesRemove[j].Accessed + }) + + c.filled = fill + return piecesRemove +} + +func (c *Cache) setLoadPriority(ranges []Range) { + c.muReaders.Lock() + for r := range c.readers { + if !r.isUse { + continue + } + if c.isIdInFileBE(ranges, r.getReaderPiece()) { + continue + } + readerPos := r.getReaderPiece() + readerRAHPos := r.getReaderRAHPiece() + end := r.getPiecesRange().End + count := settings.BTsets.ConnectionsLimit / len(c.readers) // max concurrent loading blocks + limit := 0 + for i := readerPos; i < end && limit < count; i++ { + if !c.pieces[i].Complete { + if i == readerPos { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNow) + } else if i == readerPos+1 { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNext) + } else if i > readerPos && i <= readerRAHPos { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityReadahead) + } else if i > readerRAHPos && i <= readerRAHPos+5 && c.torrent.PieceState(i).Priority != torrent.PiecePriorityHigh { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityHigh) + } else if i > readerRAHPos+5 && c.torrent.PieceState(i).Priority != torrent.PiecePriorityNormal { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNormal) + } + limit++ + } + } + } + c.muReaders.Unlock() +} + +func (c *Cache) isIdInFileBE(ranges []Range, id int) bool { + // keep 8/16 MB + FileRangeNotDelete := int64(c.pieceLength) + if FileRangeNotDelete < 8<<20 { + FileRangeNotDelete = 8 << 20 + } + + for _, rng := range ranges { + ss := int(rng.File.Offset() / c.pieceLength) + se := int((rng.File.Offset() + FileRangeNotDelete) / c.pieceLength) + + es := int((rng.File.Offset() + rng.File.Length() - FileRangeNotDelete) / c.pieceLength) + ee := int((rng.File.Offset() + rng.File.Length()) / c.pieceLength) + + if id >= ss && id < se || id > es && id <= ee { + return true + } + } + return false +} + +////////////////// +// Reader section +//////// + +func (c *Cache) NewReader(file *torrent.File) *Reader { + return newReader(file, c) +} + +func (c *Cache) GetUseReaders() int { + if c == nil { + return 0 + } + c.muReaders.Lock() + defer c.muReaders.Unlock() + readers := 0 + for reader := range c.readers { + if reader.isUse { + readers++ + } + } + return readers +} + +func (c *Cache) Readers() int { + if c == nil { + return 0 + } + c.muReaders.Lock() + defer c.muReaders.Unlock() + if c.readers == nil { + return 0 + } + return len(c.readers) +} + +func (c *Cache) CloseReader(r *Reader) { + r.cache.muReaders.Lock() + r.Close() + delete(r.cache.readers, r) + r.cache.muReaders.Unlock() + go c.clearPriority() +} + +func (c *Cache) clearPriority() { + time.Sleep(time.Second) + ranges := make([]Range, 0) + c.muReaders.Lock() + for r := range c.readers { + r.checkReader() + if r.isUse { + ranges = append(ranges, r.getPiecesRange()) + } + } + c.muReaders.Unlock() + ranges = mergeRange(ranges) + + for id := range c.pieces { + if len(ranges) > 0 { + if !inRanges(ranges, id) { + if c.torrent.PieceState(id).Priority != torrent.PiecePriorityNone { + c.torrent.Piece(id).SetPriority(torrent.PiecePriorityNone) + } + } + } else { + if c.torrent.PieceState(id).Priority != torrent.PiecePriorityNone { + c.torrent.Piece(id).SetPriority(torrent.PiecePriorityNone) + } + } + } +} + +func (c *Cache) GetCapacity() int64 { + if c == nil { + return 0 + } + return c.capacity +} diff --git a/server/torr/storage/torrstor/diskpiece.go b/server/torr/storage/torrstor/diskpiece.go new file mode 100644 index 0000000..822a68e --- /dev/null +++ b/server/torr/storage/torrstor/diskpiece.go @@ -0,0 +1,85 @@ +package torrstor + +import ( + "io" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "server/log" + "server/settings" +) + +type DiskPiece struct { + piece *Piece + + name string + + mu sync.RWMutex +} + +func NewDiskPiece(p *Piece) *DiskPiece { + name := filepath.Join(settings.BTsets.TorrentsSavePath, p.cache.hash.HexString(), strconv.Itoa(p.Id)) + ff, err := os.Stat(name) + if err == nil { + p.Size = ff.Size() + p.Complete = ff.Size() == p.cache.pieceLength + p.Accessed = ff.ModTime().Unix() + } + return &DiskPiece{piece: p, name: name} +} + +func (p *DiskPiece) WriteAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + ff, err := os.OpenFile(p.name, os.O_RDWR|os.O_CREATE, 0o666) + if err != nil { + log.TLogln("Error open file:", err) + return 0, err + } + defer ff.Close() + n, err = ff.WriteAt(b, off) + + p.piece.Size += int64(n) + if p.piece.Size > p.piece.cache.pieceLength { + p.piece.Size = p.piece.cache.pieceLength + } + p.piece.Accessed = time.Now().Unix() + return +} + +func (p *DiskPiece) ReadAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + ff, err := os.OpenFile(p.name, os.O_RDONLY, 0o666) + if os.IsNotExist(err) { + return 0, io.EOF + } + if err != nil { + log.TLogln("Error open file:", err) + return 0, err + } + defer ff.Close() + + n, err = ff.ReadAt(b, off) + + p.piece.Accessed = time.Now().Unix() + if int64(len(b))+off >= p.piece.Size { + go p.piece.cache.cleanPieces() + } + return n, nil +} + +func (p *DiskPiece) Release() { + p.mu.Lock() + defer p.mu.Unlock() + + p.piece.Size = 0 + p.piece.Complete = false + + os.Remove(p.name) +} diff --git a/server/torr/storage/torrstor/mempiece.go b/server/torr/storage/torrstor/mempiece.go new file mode 100644 index 0000000..acb3074 --- /dev/null +++ b/server/torr/storage/torrstor/mempiece.go @@ -0,0 +1,70 @@ +package torrstor + +import ( + "io" + "sync" + "time" +) + +type MemPiece struct { + piece *Piece + + buffer []byte + mu sync.RWMutex +} + +func NewMemPiece(p *Piece) *MemPiece { + return &MemPiece{piece: p} +} + +func (p *MemPiece) WriteAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.buffer == nil { + go p.piece.cache.cleanPieces() + p.buffer = make([]byte, p.piece.cache.pieceLength, p.piece.cache.pieceLength) + } + n = copy(p.buffer[off:], b[:]) + p.piece.Size += int64(n) + if p.piece.Size > p.piece.cache.pieceLength { + p.piece.Size = p.piece.cache.pieceLength + } + p.piece.Accessed = time.Now().Unix() + return +} + +func (p *MemPiece) ReadAt(b []byte, off int64) (n int, err error) { + p.mu.RLock() + defer p.mu.RUnlock() + + size := len(b) + if size+int(off) > len(p.buffer) { + size = len(p.buffer) - int(off) + if size < 0 { + size = 0 + } + } + if len(p.buffer) < int(off) || len(p.buffer) < int(off)+size { + return 0, io.EOF + } + n = copy(b, p.buffer[int(off) : int(off)+size][:]) + p.piece.Accessed = time.Now().Unix() + if int64(len(b))+off >= p.piece.Size { + go p.piece.cache.cleanPieces() + } + if n == 0 { + return 0, io.EOF + } + return n, nil +} + +func (p *MemPiece) Release() { + p.mu.Lock() + defer p.mu.Unlock() + if p.buffer != nil { + p.buffer = nil + } + p.piece.Size = 0 + p.piece.Complete = false +} diff --git a/server/torr/storage/torrstor/piece.go b/server/torr/storage/torrstor/piece.go new file mode 100644 index 0000000..f91aac4 --- /dev/null +++ b/server/torr/storage/torrstor/piece.go @@ -0,0 +1,81 @@ +package torrstor + +import ( + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/storage" + "server/settings" +) + +type Piece struct { + storage.PieceImpl `json:"-"` + + Id int `json:"-"` + Size int64 `json:"size"` + + Complete bool `json:"complete"` + Accessed int64 `json:"accessed"` + + mPiece *MemPiece `json:"-"` + dPiece *DiskPiece `json:"-"` + + cache *Cache `json:"-"` +} + +func NewPiece(id int, cache *Cache) *Piece { + p := &Piece{ + Id: id, + cache: cache, + } + + if !settings.BTsets.UseDisk { + p.mPiece = NewMemPiece(p) + } else { + p.dPiece = NewDiskPiece(p) + } + return p +} + +func (p *Piece) WriteAt(b []byte, off int64) (n int, err error) { + if !settings.BTsets.UseDisk { + return p.mPiece.WriteAt(b, off) + } else { + return p.dPiece.WriteAt(b, off) + } +} + +func (p *Piece) ReadAt(b []byte, off int64) (n int, err error) { + if !settings.BTsets.UseDisk { + return p.mPiece.ReadAt(b, off) + } else { + return p.dPiece.ReadAt(b, off) + } +} + +func (p *Piece) MarkComplete() error { + p.Complete = true + return nil +} + +func (p *Piece) MarkNotComplete() error { + p.Complete = false + return nil +} + +func (p *Piece) Completion() storage.Completion { + return storage.Completion{ + Complete: p.Complete, + Ok: true, + } +} + +func (p *Piece) Release() { + if !settings.BTsets.UseDisk { + p.mPiece.Release() + } else { + p.dPiece.Release() + } + if !p.cache.isClosed { + p.cache.torrent.Piece(p.Id).SetPriority(torrent.PiecePriorityNone) + p.cache.torrent.Piece(p.Id).UpdateCompletion() + } +} diff --git a/server/torr/storage/torrstor/piecefake.go b/server/torr/storage/torrstor/piecefake.go new file mode 100644 index 0000000..7bcff21 --- /dev/null +++ b/server/torr/storage/torrstor/piecefake.go @@ -0,0 +1,34 @@ +package torrstor + +import ( + "errors" + + "github.com/anacrolix/torrent/storage" +) + +type PieceFake struct{} + +func (PieceFake) ReadAt(p []byte, off int64) (n int, err error) { + err = errors.New("fake") + return +} + +func (PieceFake) WriteAt(p []byte, off int64) (n int, err error) { + err = errors.New("fake") + return +} + +func (PieceFake) MarkComplete() error { + return errors.New("fake") +} + +func (PieceFake) MarkNotComplete() error { + return errors.New("fake") +} + +func (PieceFake) Completion() storage.Completion { + return storage.Completion{ + Complete: false, + Ok: true, + } +} diff --git a/server/torr/storage/torrstor/ranges.go b/server/torr/storage/torrstor/ranges.go new file mode 100644 index 0000000..4b28621 --- /dev/null +++ b/server/torr/storage/torrstor/ranges.go @@ -0,0 +1,52 @@ +package torrstor + +import ( + "sort" + + "github.com/anacrolix/torrent" +) + +type Range struct { + Start, End int + File *torrent.File +} + +func inRanges(ranges []Range, ind int) bool { + for _, r := range ranges { + if ind >= r.Start && ind <= r.End { + return true + } + } + return false +} + +func mergeRange(ranges []Range) []Range { + if len(ranges) <= 1 { + return ranges + } + // copy ranges + merged := append([]Range(nil), ranges...) + + sort.Slice(merged, func(i, j int) bool { + if merged[i].Start < merged[j].Start { + return true + } + if merged[i].Start == merged[j].Start && merged[i].End < merged[j].End { + return true + } + return false + }) + + j := 0 + for i := 1; i < len(merged); i++ { + if merged[j].End >= merged[i].Start { + if merged[j].End < merged[i].End { + merged[j].End = merged[i].End + } + } else { + j++ + merged[j] = merged[i] + } + } + return merged[:j+1] +} diff --git a/server/torr/storage/torrstor/reader.go b/server/torr/storage/torrstor/reader.go new file mode 100644 index 0000000..350d472 --- /dev/null +++ b/server/torr/storage/torrstor/reader.go @@ -0,0 +1,205 @@ +package torrstor + +import ( + "io" + "sync" + "time" + + "github.com/anacrolix/torrent" + + "server/log" + "server/settings" +) + +type Reader struct { + torrent.Reader + offset int64 + readahead int64 + file *torrent.File + + cache *Cache + isClosed bool + + ///Preload + lastAccess int64 + isUse bool + mu sync.Mutex +} + +func newReader(file *torrent.File, cache *Cache) *Reader { + r := new(Reader) + r.file = file + r.Reader = file.NewReader() + + r.SetReadahead(0) + r.cache = cache + r.isUse = true + + cache.muReaders.Lock() + cache.readers[r] = struct{}{} + cache.muReaders.Unlock() + return r +} + +func (r *Reader) Seek(offset int64, whence int) (n int64, err error) { + if r.isClosed { + return 0, io.EOF + } + switch whence { + case io.SeekStart: + r.offset = offset + case io.SeekCurrent: + r.offset += offset + case io.SeekEnd: + r.offset = r.file.Length() + offset + } + r.readerOn() + n, err = r.Reader.Seek(offset, whence) + r.offset = n + r.lastAccess = time.Now().Unix() + return +} + +func (r *Reader) Read(p []byte) (n int, err error) { + err = io.EOF + if r.isClosed { + return + } + if r.file.Torrent() != nil && r.file.Torrent().Info() != nil { + r.readerOn() + n, err = r.Reader.Read(p) + + // samsung tv fix xvid/divx + //if r.offset == 0 && len(p) >= 192 { + // str := strings.ToLower(string(p[112:116])) + // if str == "xvid" || str == "divx" { + // p[112] = 0x4D // M + // p[113] = 0x50 // P + // p[114] = 0x34 // 4 + // p[115] = 0x56 // V + // } + // str = strings.ToLower(string(p[188:192])) + // if str == "xvid" || str == "divx" { + // p[188] = 0x4D // M + // p[189] = 0x50 // P + // p[190] = 0x34 // 4 + // p[191] = 0x56 // V + // } + //} + + r.offset += int64(n) + r.lastAccess = time.Now().Unix() + } else { + log.TLogln("Torrent closed and readed") + } + return +} + +func (r *Reader) SetReadahead(length int64) { + if r.cache != nil && length > r.cache.capacity { + length = r.cache.capacity + } + if r.isUse { + r.Reader.SetReadahead(length) + } + r.readahead = length +} + +func (r *Reader) Offset() int64 { + return r.offset +} + +func (r *Reader) Readahead() int64 { + return r.readahead +} + +func (r *Reader) Close() { + // file reader close in gotorrent + // this struct close in cache + r.isClosed = true + if len(r.file.Torrent().Files()) > 0 { + r.Reader.Close() + } + go r.cache.getRemPieces() +} + +func (r *Reader) getPiecesRange() Range { + startOff, endOff := r.getOffsetRange() + return Range{r.getPieceNum(startOff), r.getPieceNum(endOff), r.file} +} + +func (r *Reader) getReaderPiece() int { + return r.getPieceNum(r.offset) +} + +func (r *Reader) getReaderRAHPiece() int { + return r.getPieceNum(r.offset + r.readahead) +} + +func (r *Reader) getPieceNum(offset int64) int { + return int((offset + r.file.Offset()) / r.cache.pieceLength) +} + +func (r *Reader) getOffsetRange() (int64, int64) { + prc := int64(settings.BTsets.ReaderReadAHead) + readers := int64(r.getUseReaders()) + if readers == 0 { + readers = 1 + } + + beginOffset := r.offset - (r.cache.capacity/readers)*(100-prc)/100 + endOffset := r.offset + (r.cache.capacity/readers)*prc/100 + + if beginOffset < 0 { + beginOffset = 0 + } + + if endOffset > r.file.Length() { + endOffset = r.file.Length() + } + return beginOffset, endOffset +} + +func (r *Reader) checkReader() { + if time.Now().Unix() > r.lastAccess+60 && len(r.cache.readers) > 1 { + r.readerOff() + } else { + r.readerOn() + } +} + +func (r *Reader) readerOn() { + r.mu.Lock() + defer r.mu.Unlock() + if !r.isUse { + if pos, err := r.Reader.Seek(0, io.SeekCurrent); err == nil && pos == 0 { + r.Reader.Seek(r.offset, io.SeekStart) + } + r.SetReadahead(r.readahead) + r.isUse = true + } +} + +func (r *Reader) readerOff() { + r.mu.Lock() + defer r.mu.Unlock() + if r.isUse { + r.SetReadahead(0) + r.isUse = false + if r.offset > 0 { + r.Reader.Seek(0, io.SeekStart) + } + } +} + +func (r *Reader) getUseReaders() int { + readers := 0 + if r.cache != nil { + for reader := range r.cache.readers { + if reader.isUse { + readers++ + } + } + } + return readers +} diff --git a/server/torr/storage/torrstor/storage.go b/server/torr/storage/torrstor/storage.go new file mode 100644 index 0000000..195fbb7 --- /dev/null +++ b/server/torr/storage/torrstor/storage.go @@ -0,0 +1,72 @@ +package torrstor + +import ( + "sync" + + "server/torr/storage" + + "github.com/anacrolix/torrent/metainfo" + ts "github.com/anacrolix/torrent/storage" +) + +type Storage struct { + storage.Storage + + caches map[metainfo.Hash]*Cache + capacity int64 + mu sync.Mutex +} + +func NewStorage(capacity int64) *Storage { + stor := new(Storage) + stor.capacity = capacity + stor.caches = make(map[metainfo.Hash]*Cache) + return stor +} + +func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (ts.TorrentImpl, error) { + // capFunc := func() (int64, bool) { // NE + // return s.capacity, true // NE + // } // NE + s.mu.Lock() + defer s.mu.Unlock() + ch := NewCache(s.capacity, s) + ch.Init(info, infoHash) + s.caches[infoHash] = ch + return ch, nil // OE + // return ts.TorrentImpl{ // NE + // Piece: ch.Piece, // NE + // Close: ch.Close, // NE + // Capacity: &capFunc, // NE + // }, nil // NE +} + +func (s *Storage) CloseHash(hash metainfo.Hash) { + if s.caches == nil { + return + } + s.mu.Lock() + defer s.mu.Unlock() + if ch, ok := s.caches[hash]; ok { + ch.Close() + delete(s.caches, hash) + } +} + +func (s *Storage) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + for _, ch := range s.caches { + ch.Close() + } + return nil +} + +func (s *Storage) GetCache(hash metainfo.Hash) *Cache { + s.mu.Lock() + defer s.mu.Unlock() + if cache, ok := s.caches[hash]; ok { + return cache + } + return nil +} diff --git a/server/torr/stream.go b/server/torr/stream.go new file mode 100644 index 0000000..02f4d8c --- /dev/null +++ b/server/torr/stream.go @@ -0,0 +1,170 @@ +package torr + +import ( + // "context" + "encoding/hex" + "errors" + "fmt" + "log" + "net" + "net/http" + "sync/atomic" + "time" + + "github.com/anacrolix/dms/dlna" + "github.com/anacrolix/missinggo/v2/httptoo" + "github.com/anacrolix/torrent" + + mt "server/mimetype" + sets "server/settings" + "server/torr/state" +) + +// Add atomic counter for concurrent streams +var activeStreams int32 + +// type contextResponseWriter struct { +// http.ResponseWriter +// ctx context.Context +// } + +// func (w *contextResponseWriter) Write(p []byte) (n int, err error) { +// // Check context before each write +// select { +// case <-w.ctx.Done(): +// return 0, w.ctx.Err() +// default: +// return w.ResponseWriter.Write(p) +// } +// } + +func (t *Torrent) Stream(fileID int, req *http.Request, resp http.ResponseWriter) error { + // Increment active streams counter + streamID := atomic.AddInt32(&activeStreams, 1) + defer atomic.AddInt32(&activeStreams, -1) + // Stream disconnect timeout (same as torrent) + streamTimeout := sets.BTsets.TorrentDisconnectTimeout + + if !t.GotInfo() { + http.NotFound(resp, req) + return errors.New("torrent doesn't have info yet") + } + // Get file information + st := t.Status() + var stFile *state.TorrentFileStat + for _, fileStat := range st.FileStats { + if fileStat.Id == fileID { + stFile = fileStat + break + } + } + if stFile == nil { + return fmt.Errorf("file with id %v not found", fileID) + } + // Find the actual torrent file + files := t.Files() + var file *torrent.File + for _, tfile := range files { + if tfile.Path() == stFile.Path { + file = tfile + break + } + } + if file == nil { + return fmt.Errorf("file with id %v not found", fileID) + } + // Check file size limit + if int64(sets.MaxSize) > 0 && file.Length() > int64(sets.MaxSize) { + err := fmt.Errorf("file size exceeded max allowed %d bytes", sets.MaxSize) + log.Printf("File %s size (%d) exceeded max allowed %d bytes", file.DisplayPath(), file.Length(), sets.MaxSize) + http.Error(resp, err.Error(), http.StatusForbidden) + return err + } + // Create reader with context for timeout + reader := t.NewReader(file) + if reader == nil { + return errors.New("cannot create torrent reader") + } + // Ensure reader is always closed + defer t.CloseReader(reader) + + if sets.BTsets.ResponsiveMode { + reader.SetResponsive() + } + // Log connection + host, port, clerr := net.SplitHostPort(req.RemoteAddr) + + if sets.BTsets.EnableDebug { + if clerr != nil { + log.Printf("[Stream:%d] Connect client (Active streams: %d)", streamID, atomic.LoadInt32(&activeStreams)) + } else { + log.Printf("[Stream:%d] Connect client %s:%s (Active streams: %d)", + streamID, host, port, atomic.LoadInt32(&activeStreams)) + } + } + + // Mark as viewed + sets.SetViewed(&sets.Viewed{ + Hash: t.Hash().HexString(), + FileIndex: fileID, + }) + + // Set response headers + resp.Header().Set("Connection", "close") + // Add timeout header if configured + if streamTimeout > 0 { + resp.Header().Set("X-Stream-Timeout", fmt.Sprintf("%d", streamTimeout)) + } + // Add ETag + etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", t.Hash().HexString(), file.Path()))) + resp.Header().Set("ETag", httptoo.EncodeQuotedString(etag)) + // DLNA headers + resp.Header().Set("transferMode.dlna.org", "Streaming") + // add MimeType + mime, err := mt.MimeTypeByPath(file.Path()) + if err == nil && mime.IsMedia() { + resp.Header().Set("content-type", mime.String()) + } + // DLNA Seek + if req.Header.Get("getContentFeatures.dlna.org") != "" { + resp.Header().Set("contentFeatures.dlna.org", dlna.ContentFeatures{ + SupportRange: true, + SupportTimeSeek: true, + }.String()) + } + // Add support for range requests + if req.Header.Get("Range") != "" { + resp.Header().Set("Accept-Ranges", "bytes") + } + // // Create a context with timeout if configured + // ctx := req.Context() + // if streamTimeout > 0 { + // var cancel context.CancelFunc + // ctx, cancel = context.WithTimeout(ctx, time.Duration(streamTimeout)*time.Second) + // defer cancel() + // } + // // Update request with new context + // req = req.WithContext(ctx) + // // Handle client disconnections better + // wrappedResp := &contextResponseWriter{ + // ResponseWriter: resp, + // ctx: ctx, + // } + // http.ServeContent(wrappedResp, req, file.Path(), time.Unix(t.Timestamp, 0), reader) + + http.ServeContent(resp, req, file.Path(), time.Unix(t.Timestamp, 0), reader) + + if sets.BTsets.EnableDebug { + if clerr != nil { + log.Printf("[Stream:%d] Disconnect client", streamID) + } else { + log.Printf("[Stream:%d] Disconnect client %s:%s", streamID, host, port) + } + } + return nil +} + +// GetActiveStreams returns number of currently active streams +func GetActiveStreams() int32 { + return atomic.LoadInt32(&activeStreams) +} diff --git a/server/torr/torrent.go b/server/torr/torrent.go new file mode 100644 index 0000000..8731027 --- /dev/null +++ b/server/torr/torrent.go @@ -0,0 +1,395 @@ +package torr + +import ( + "errors" + "server/torrshash" + "sort" + "strconv" + "sync" + "time" + + utils2 "server/utils" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + + "server/log" + "server/settings" + "server/torr/state" + cacheSt "server/torr/storage/state" + "server/torr/storage/torrstor" + "server/torr/utils" +) + +type Torrent struct { + Title string + Category string + Poster string + Data string + *torrent.TorrentSpec + + Stat state.TorrentStat + Timestamp int64 + Size int64 + + *torrent.Torrent + muTorrent sync.Mutex + + bt *BTServer + cache *torrstor.Cache + + lastTimeSpeed time.Time + DownloadSpeed float64 + UploadSpeed float64 + BytesReadUsefulData int64 + BytesWrittenData int64 + + PreloadSize int64 + PreloadedBytes int64 + + DurationSeconds float64 + BitRate string + + expiredTime time.Time + + closed <-chan struct{} + + progressTicker *time.Ticker +} + +func NewTorrent(spec *torrent.TorrentSpec, bt *BTServer) (*Torrent, error) { + // https://github.com/anacrolix/torrent/issues/747 + if bt == nil || bt.client == nil { + return nil, errors.New("BT client not connected") + } + switch settings.BTsets.RetrackersMode { + case 1: + spec.Trackers = append(spec.Trackers, [][]string{utils.GetDefTrackers()}...) + case 2: + spec.Trackers = nil + case 3: + spec.Trackers = [][]string{utils.GetDefTrackers()} + } + + trackers := utils.GetTrackerFromFile() + if len(trackers) > 0 { + spec.Trackers = append(spec.Trackers, [][]string{trackers}...) + } + + goTorrent, _, err := bt.client.AddTorrentSpec(spec) + if err != nil { + return nil, err + } + + bt.mu.Lock() + defer bt.mu.Unlock() + if tor, ok := bt.torrents[spec.InfoHash]; ok { + return tor, nil + } + + timeout := time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout) + if timeout > time.Minute { + timeout = time.Minute + } + + torr := new(Torrent) + torr.Torrent = goTorrent + torr.Stat = state.TorrentAdded + torr.lastTimeSpeed = time.Now() + torr.bt = bt + torr.closed = goTorrent.Closed() + torr.TorrentSpec = spec + torr.AddExpiredTime(timeout) + torr.Timestamp = time.Now().Unix() + + go torr.watch() + + bt.torrents[spec.InfoHash] = torr + return torr, nil +} + +func (t *Torrent) WaitInfo() bool { + if t == nil || t.Torrent == nil { + return false + } + + // Close torrent if no info in 1 minute + TorrentDisconnectTimeout config option + tm := time.NewTimer(time.Minute + time.Second*time.Duration(settings.BTsets.TorrentDisconnectTimeout)) + + select { + case <-t.Torrent.GotInfo(): + if t.bt != nil && t.bt.storage != nil { + t.cache = t.bt.storage.GetCache(t.Hash()) + t.cache.SetTorrent(t.Torrent) + } + return true + case <-t.closed: + return false + case <-tm.C: + return false + } +} + +func (t *Torrent) GotInfo() bool { + // log.TLogln("GotInfo state:", t.Stat) + if t == nil || t.Stat == state.TorrentClosed { + return false + } + // assume we have info in preload state + // and dont override with TorrentWorking + if t.Stat == state.TorrentPreload { + return true + } + t.Stat = state.TorrentGettingInfo + if t.WaitInfo() { + t.Stat = state.TorrentWorking + t.AddExpiredTime(time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout)) + return true + } else { + t.Close() + return false + } +} + +func (t *Torrent) AddExpiredTime(duration time.Duration) { + newExpiredTime := time.Now().Add(duration) + if t.expiredTime.Before(newExpiredTime) { + t.expiredTime = newExpiredTime + } +} + +func (t *Torrent) watch() { + t.progressTicker = time.NewTicker(time.Second) + defer t.progressTicker.Stop() + + for { + select { + case <-t.progressTicker.C: + go t.progressEvent() + case <-t.closed: + return + } + } +} + +func (t *Torrent) progressEvent() { + if t.expired() { + if t.TorrentSpec != nil { + log.TLogln("Torrent close by timeout", t.TorrentSpec.InfoHash.HexString()) + } + t.bt.RemoveTorrent(t.Hash()) + return + } + + t.muTorrent.Lock() + if t.Torrent != nil && t.Torrent.Info() != nil { + st := t.Torrent.Stats() + deltaDlBytes := st.BytesRead.Int64() - t.BytesReadUsefulData + deltaUpBytes := st.BytesWritten.Int64() - t.BytesWrittenData + deltaTime := time.Since(t.lastTimeSpeed).Seconds() + + t.DownloadSpeed = float64(deltaDlBytes) / deltaTime + t.UploadSpeed = float64(deltaUpBytes) / deltaTime + + t.BytesReadUsefulData = st.BytesRead.Int64() + t.BytesWrittenData = st.BytesWritten.Int64() + + if t.cache != nil { + t.PreloadedBytes = t.cache.GetState().Filled + } + } else { + t.DownloadSpeed = 0 + t.UploadSpeed = 0 + } + t.muTorrent.Unlock() + + t.lastTimeSpeed = time.Now() + t.updateRA() +} + +func (t *Torrent) updateRA() { + // t.muTorrent.Lock() + // defer t.muTorrent.Unlock() + // if t.Torrent != nil && t.Torrent.Info() != nil { + // pieceLen := t.Torrent.Info().PieceLength + // adj := pieceLen * int64(t.Torrent.Stats().ActivePeers) / int64(1+t.cache.Readers()) + // switch { + // case adj < pieceLen: + // adj = pieceLen + // case adj > pieceLen*4: + // adj = pieceLen * 4 + // } + // go t.cache.AdjustRA(adj) + // } + adj := int64(16 << 20) // 16 MB fixed RA + go t.cache.AdjustRA(adj) +} + +func (t *Torrent) expired() bool { + return t.cache.Readers() == 0 && t.expiredTime.Before(time.Now()) && (t.Stat == state.TorrentWorking || t.Stat == state.TorrentClosed) +} + +func (t *Torrent) Files() []*torrent.File { + if t.Torrent != nil && t.Torrent.Info() != nil { + files := t.Torrent.Files() + return files + } + return nil +} + +func (t *Torrent) Hash() metainfo.Hash { + if t.Torrent != nil { + return t.Torrent.InfoHash() + } + if t.TorrentSpec != nil { + return t.TorrentSpec.InfoHash + } + return [20]byte{} +} + +func (t *Torrent) Length() int64 { + if t.Info() == nil { + return 0 + } + return t.Torrent.Length() +} + +func (t *Torrent) NewReader(file *torrent.File) *torrstor.Reader { + if t.Stat == state.TorrentClosed { + return nil + } + reader := t.cache.NewReader(file) + return reader +} + +func (t *Torrent) CloseReader(reader *torrstor.Reader) { + t.cache.CloseReader(reader) + t.AddExpiredTime(time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout)) +} + +func (t *Torrent) GetCache() *torrstor.Cache { + return t.cache +} + +func (t *Torrent) drop() { + t.muTorrent.Lock() + defer t.muTorrent.Unlock() + if t.Torrent != nil { + t.Torrent.Drop() + t.Torrent = nil + } +} + +func (t *Torrent) Close() bool { + if t == nil { + return false + } + if settings.ReadOnly && t.cache != nil && t.cache.GetUseReaders() > 0 { + return false + } + t.Stat = state.TorrentClosed + + if t.bt != nil { + t.bt.mu.Lock() + delete(t.bt.torrents, t.Hash()) + t.bt.mu.Unlock() + } + + t.drop() + return true +} + +func (t *Torrent) Status() *state.TorrentStatus { + t.muTorrent.Lock() + defer t.muTorrent.Unlock() + + st := new(state.TorrentStatus) + + st.Stat = t.Stat + st.StatString = t.Stat.String() + st.Title = t.Title + st.Category = t.Category + st.Poster = t.Poster + st.Data = t.Data + st.Timestamp = t.Timestamp + st.TorrentSize = t.Size + st.BitRate = t.BitRate + st.DurationSeconds = t.DurationSeconds + + if t.TorrentSpec != nil { + st.Hash = t.TorrentSpec.InfoHash.HexString() + } + if t.Torrent != nil { + st.Name = t.Torrent.Name() + st.Hash = t.Torrent.InfoHash().HexString() + st.LoadedSize = t.Torrent.BytesCompleted() + + st.PreloadedBytes = t.PreloadedBytes + st.PreloadSize = t.PreloadSize + st.DownloadSpeed = t.DownloadSpeed + st.UploadSpeed = t.UploadSpeed + + tst := t.Torrent.Stats() + st.BytesWritten = tst.BytesWritten.Int64() + st.BytesWrittenData = tst.BytesWrittenData.Int64() + st.BytesRead = tst.BytesRead.Int64() + st.BytesReadData = tst.BytesReadData.Int64() + st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64() + st.ChunksWritten = tst.ChunksWritten.Int64() + st.ChunksRead = tst.ChunksRead.Int64() + st.ChunksReadUseful = tst.ChunksReadUseful.Int64() + st.ChunksReadWasted = tst.ChunksReadWasted.Int64() + st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64() + st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64() + st.TotalPeers = tst.TotalPeers + st.PendingPeers = tst.PendingPeers + st.ActivePeers = tst.ActivePeers + st.ConnectedSeeders = tst.ConnectedSeeders + st.HalfOpenPeers = tst.HalfOpenPeers + + if t.Torrent.Info() != nil { + st.TorrentSize = t.Torrent.Length() + + files := t.Files() + sort.Slice(files, func(i, j int) bool { + return utils2.CompareStrings(files[i].Path(), files[j].Path()) + }) + for i, f := range files { + st.FileStats = append(st.FileStats, &state.TorrentFileStat{ + Id: i + 1, // in web id 0 is undefined + Path: f.Path(), + Length: f.Length(), + }) + } + + th := torrshash.New(st.Hash) + th.AddField(torrshash.TagTitle, st.Title) + th.AddField(torrshash.TagPoster, st.Poster) + th.AddField(torrshash.TagCategory, st.Category) + th.AddField(torrshash.TagSize, strconv.FormatInt(st.TorrentSize, 10)) + + if t.TorrentSpec != nil { + if len(t.TorrentSpec.Trackers) > 0 && len(t.TorrentSpec.Trackers[0]) > 0 { + for _, tr := range t.TorrentSpec.Trackers[0] { + th.AddField(torrshash.TagTracker, tr) + } + } + } + token, err := torrshash.Pack(th) + if err == nil { + st.TorrsHash = token + } + } + } + + return st +} + +func (t *Torrent) CacheState() *cacheSt.CacheState { + if t.Torrent != nil && t.cache != nil { + st := t.cache.GetState() + st.Torrent = t.Status() + return st + } + return nil +} diff --git a/server/torr/utils/blockedIP.go b/server/torr/utils/blockedIP.go new file mode 100644 index 0000000..8745f05 --- /dev/null +++ b/server/torr/utils/blockedIP.go @@ -0,0 +1,39 @@ +package utils + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "server/log" + + "server/settings" + + "github.com/anacrolix/torrent/iplist" +) + +func ReadBlockedIP() (ranger iplist.Ranger, err error) { + buf, err := os.ReadFile(filepath.Join(settings.Path, "blocklist")) + if err != nil { + return nil, err + } + log.TLogln("Read block list...") + scanner := bufio.NewScanner(strings.NewReader(string(buf))) + var ranges []iplist.Range + for scanner.Scan() { + r, ok, err := iplist.ParseBlocklistP2PLine(scanner.Bytes()) + if err != nil { + return nil, err + } + if ok { + ranges = append(ranges, r) + } + } + err = scanner.Err() + if len(ranges) > 0 { + ranger = iplist.New(ranges) + log.TLogln("Readed ranges:", len(ranges)) + } + return +} diff --git a/server/torr/utils/freemem.go b/server/torr/utils/freemem.go new file mode 100644 index 0000000..00b6a1e --- /dev/null +++ b/server/torr/utils/freemem.go @@ -0,0 +1,15 @@ +package utils + +import ( + "runtime" + "runtime/debug" +) + +func FreeOSMem() { + debug.FreeOSMemory() +} + +func FreeOSMemGC() { + runtime.GC() + debug.FreeOSMemory() +} diff --git a/server/torr/utils/torrent.go b/server/torr/utils/torrent.go new file mode 100644 index 0000000..c922500 --- /dev/null +++ b/server/torr/utils/torrent.go @@ -0,0 +1,123 @@ +package utils + +import ( + "encoding/base32" + "io" + "math/rand" + "net/http" + "os" + "path/filepath" + "strings" + + "server/settings" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + "golang.org/x/time/rate" +) + +var defTrackers = []string{ + "http://retracker.local/announce", + "http://bt4.t-ru.org/ann?magnet", + "http://retracker.mgts.by:80/announce", + "http://tracker.city9x.com:2710/announce", + "http://tracker.electro-torrent.pl:80/announce", + "http://tracker.internetwarriors.net:1337/announce", + "http://tracker2.itzmx.com:6961/announce", + "udp://opentor.org:2710", + "udp://public.popcorn-tracker.org:6969/announce", + "udp://tracker.opentrackr.org:1337/announce", + "http://bt.svao-ix.ru/announce", + "udp://explodie.org:6969/announce", + "wss://tracker.btorrent.xyz", + "wss://tracker.openwebtorrent.com", +} + +var loadedTrackers []string + +func GetTrackerFromFile() []string { + name := filepath.Join(settings.Path, "trackers.txt") + buf, err := os.ReadFile(name) + if err == nil { + list := strings.Split(string(buf), "\n") + var ret []string + for _, l := range list { + if strings.HasPrefix(l, "udp") || strings.HasPrefix(l, "http") { + ret = append(ret, l) + } + } + return ret + } + return nil +} + +func GetDefTrackers() []string { + loadNewTracker() + if len(loadedTrackers) == 0 { + return defTrackers + } + return loadedTrackers +} + +func loadNewTracker() { + if len(loadedTrackers) > 0 { + return + } + resp, err := http.Get("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt") + if err == nil { + defer resp.Body.Close() + buf, err := io.ReadAll(resp.Body) + if err == nil { + arr := strings.Split(string(buf), "\n") + var ret []string + for _, s := range arr { + s = strings.TrimSpace(s) + if len(s) > 0 { + ret = append(ret, s) + } + } + loadedTrackers = append(ret, defTrackers...) + } + } +} + +func PeerIDRandom(peer string) string { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + return peer + base32.StdEncoding.EncodeToString(randomBytes)[:20-len(peer)] +} + +func Limit(i int) *rate.Limiter { + l := rate.NewLimiter(rate.Inf, 0) + if i > 0 { + b := i + if b < 16*1024 { + b = 16 * 1024 + } + l = rate.NewLimiter(rate.Limit(i), b) + } + return l +} + +func OpenTorrentFile(path string) (*torrent.TorrentSpec, error) { + minfo, err := metainfo.LoadFromFile(path) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} diff --git a/server/torr/utils/webImageChecker.go b/server/torr/utils/webImageChecker.go new file mode 100644 index 0000000..66bbec5 --- /dev/null +++ b/server/torr/utils/webImageChecker.go @@ -0,0 +1,56 @@ +package utils + +import ( + "context" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" + "net/http" + "strings" + "time" + + "golang.org/x/image/webp" + + "server/log" +) + +func CheckImgUrl(link string) bool { + if link == "" { + return false + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", link, nil) + if err != nil { + log.TLogln("Error create request for image:", err) + return false + } + + client := &http.Client{ + Timeout: 5 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + log.TLogln("Error check image:", err) + return false + } + defer resp.Body.Close() + + limitedReader := io.LimitReader(resp.Body, 2*1024*1024) + + if strings.HasSuffix(link, ".webp") { + _, err = webp.Decode(limitedReader) + } else { + _, _, err = image.Decode(limitedReader) + } + if err != nil { + log.TLogln("Error decode image:", err) + return false + } + return true +} diff --git a/server/torrfs/asfs.go b/server/torrfs/asfs.go new file mode 100644 index 0000000..b4dce24 --- /dev/null +++ b/server/torrfs/asfs.go @@ -0,0 +1,24 @@ +package torrfs + +import ( + "io/fs" + "path" + "strings" +) + +type ioFSAdapter struct { + root *RootDir +} + +func AsFS(root *RootDir) fs.FS { + return &ioFSAdapter{root: root} +} + +func (a *ioFSAdapter) Open(name string) (fs.File, error) { + name = path.Clean(name) + if name == "." || name == "/" || name == "" { + return a.root.Open(".") + } + name = strings.TrimPrefix(name, "/") + return a.root.Open(name) +} diff --git a/server/torrfs/categorydir.go b/server/torrfs/categorydir.go new file mode 100644 index 0000000..e5659f6 --- /dev/null +++ b/server/torrfs/categorydir.go @@ -0,0 +1,72 @@ +package torrfs + +import ( + "io/fs" + "time" + + "server/settings" + + "server/torr" +) + +type CategoryDir struct { + info fs.FileInfo +} + +func NewCategoryDir(category string) *CategoryDir { + if category == "" { + category = "other" + } + d := &CategoryDir{ + info: info{ + name: category, + size: 4096, + mode: 0o555, + mtime: time.Unix(477033666, 0), + isDir: true, + }, + } + return d +} + +func (d *CategoryDir) Stat() (fs.FileInfo, error) { + return d.info, nil +} + +func (d *CategoryDir) ReadDir(n int) ([]fs.DirEntry, error) { + nodes := []fs.DirEntry{} + torrs := torr.ListTorrent() + for _, t := range torrs { + if t.Category == "" { + t.Category = "other" + } + if t.Category == d.Name() { + if settings.BTsets.ShowFSActiveTorr && !t.GotInfo() { + continue + } + td := NewTorrDir(nil, t.Title, t) + nodes = append(nodes, td) + } + } + + return nodes, nil +} + +// INode +func (d *CategoryDir) Open(name string) (fs.File, error) { return Open(d, name) } +func (d *CategoryDir) Parent() INode { return nil } +func (d *CategoryDir) Torrent() *torr.Torrent { return nil } +func (d *CategoryDir) SetTorrent(torr *torr.Torrent) {} + +// DirEntry +func (d *CategoryDir) Name() string { return d.info.Name() } +func (d *CategoryDir) IsDir() bool { return true } +func (d *CategoryDir) Type() fs.FileMode { + s, _ := d.Stat() + return s.Mode() +} +func (d *CategoryDir) Info() (fs.FileInfo, error) { return d.info, nil } + +// File +func (d *CategoryDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid } +func (d *CategoryDir) Close() error { return nil } diff --git a/server/torrfs/fuse/fuse.go b/server/torrfs/fuse/fuse.go new file mode 100644 index 0000000..3fb791b --- /dev/null +++ b/server/torrfs/fuse/fuse.go @@ -0,0 +1,383 @@ +//go:build !windows +// +build !windows + +package fuse + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "sync" + "syscall" + "time" + + "server/log" + "server/settings" + torrfs "server/torrfs" + + gofusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +type FuseStatus struct { + Enabled bool `json:"enabled"` + MountPath string `json:"mount_path"` +} + +type FuseFS struct { + gofusefs.Inode + + mountPath string + server *fuse.Server + + mu sync.RWMutex + enabled bool + + tfs fs.FS + p string // "." +} + +var ( + globalFuseFS *FuseFS + fuseMutex sync.Mutex +) + +func NewFuseFS() *FuseFS { return &FuseFS{enabled: false} } + +func FuseAutoMount() { + if settings.Args.FusePath != "" { + ffs := GetFuseFS() + if !ffs.enabled { + log.TLogln("FUSE mount") + err := ffs.Mount(settings.Args.FusePath) + if err != nil { + log.TLogln("Failed to auto-mount FUSE filesystem:", err) + os.Exit(1) + } + } + } +} + +func FuseCleanup() { + ffs := GetFuseFS() + if ffs.enabled { + _ = ffs.Unmount() + } +} + +func GetFuseFS() *FuseFS { + fuseMutex.Lock() + defer fuseMutex.Unlock() + if globalFuseFS == nil { + globalFuseFS = NewFuseFS() + } + return globalFuseFS +} + +func (ffs *FuseFS) GetMountPath() string { + ffs.mu.RLock() + defer ffs.mu.RUnlock() + return ffs.mountPath +} + +func (ffs *FuseFS) Mount(mountPath string) error { + ffs.mu.Lock() + defer ffs.mu.Unlock() + + if ffs.enabled { + return errors.New("FUSE filesystem is already mounted") + } + if err := os.MkdirAll(mountPath, 0o755); err != nil { + log.TLogln("Error create FUSE mount point dir:", err) + return err + } + + ffs.mountPath = mountPath + ffs.tfs = torrfs.AsFS(torrfs.New()) + ffs.p = "." + + entryTimeout := time.Second + attrTimeout := time.Second + + opts := &gofusefs.Options{ + MountOptions: fuse.MountOptions{ + AllowOther: true, + Name: "torrserver", + FsName: "torrserver-fuse", + Debug: settings.BTsets.EnableDebug, + }, + EntryTimeout: &entryTimeout, + AttrTimeout: &attrTimeout, + UID: uint32(os.Getuid()), + GID: uint32(os.Getgid()), + } + + srv, err := gofusefs.Mount(mountPath, ffs, opts) + if err != nil { + log.TLogln("Error mount FUSE filesystem:", err) + return err + } + + ffs.server = srv + ffs.enabled = true + log.TLogln("FUSE filesystem mounted at", mountPath) + go ffs.server.Wait() + return nil +} + +func (ffs *FuseFS) Unmount() error { + ffs.mu.Lock() + defer ffs.mu.Unlock() + + if !ffs.enabled { + return errors.New("FUSE filesystem is not mounted") + } + if err := ffs.server.Unmount(); err != nil { + return err + } + + ffs.enabled = false + ffs.server = nil + ffs.mountPath = "" + ffs.tfs = nil + ffs.p = "" + + log.TLogln("FUSE filesystem unmounted") + return nil +} + +// ----- go-fuse integration ----- + +var ( + _ = (gofusefs.InodeEmbedder)((*FuseFS)(nil)) + _ = (gofusefs.NodeOnAdder)((*FuseFS)(nil)) + _ = (gofusefs.NodeGetattrer)((*FuseFS)(nil)) + _ = (gofusefs.NodeReaddirer)((*FuseFS)(nil)) + _ = (gofusefs.NodeLookuper)((*FuseFS)(nil)) +) + +func (ffs *FuseFS) EmbeddedInode() *gofusefs.Inode { return &ffs.Inode } + +func (ffs *FuseFS) OnAdd(ctx context.Context) { + if ffs.p == "" { + ffs.p = "." + } +} + +// ----- Root ops ----- + +func (ffs *FuseFS) Getattr(ctx context.Context, fh gofusefs.FileHandle, out *fuse.AttrOut) syscall.Errno { + fi, err := fs.Stat(ffs.tfs, ".") + if err != nil { + return errno(err) + } + fillAttr(&out.Attr, fi) + return 0 +} + +func (ffs *FuseFS) Readdir(ctx context.Context) (gofusefs.DirStream, syscall.Errno) { + des, err := fs.ReadDir(ffs.tfs, ".") + if err != nil { + log.TLogln("FUSE root Readdir error:", err) + return nil, errno(err) + } + + out := make([]fuse.DirEntry, 0, len(des)) + for _, de := range des { + fi, err := de.Info() + if err != nil { + continue + } + out = append(out, fuse.DirEntry{ + Name: de.Name(), + Mode: fuseModeFromInfo(fi), + }) + } + return gofusefs.NewListDirStream(out), 0 +} + +func (ffs *FuseFS) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*gofusefs.Inode, syscall.Errno) { + childPath := path.Join(".", name) + + fi, err := fs.Stat(ffs.tfs, childPath) + if err != nil { + return nil, errno(err) + } + + fillAttr(&out.Attr, fi) + out.AttrValid = 1 + out.AttrValidNsec = 0 + out.EntryValid = 1 + out.EntryValidNsec = 0 + + mode := fuseModeFromInfo(fi) + ch := ffs.NewInode(ctx, &tfsNode{tfs: ffs.tfs, p: childPath}, gofusefs.StableAttr{Mode: mode}) + return ch, 0 +} + +// ----- Regular nodes ----- + +type tfsNode struct { + gofusefs.Inode + tfs fs.FS + p string +} + +var ( + _ = (gofusefs.NodeGetattrer)((*tfsNode)(nil)) + _ = (gofusefs.NodeReaddirer)((*tfsNode)(nil)) + _ = (gofusefs.NodeLookuper)((*tfsNode)(nil)) + _ = (gofusefs.NodeOpener)((*tfsNode)(nil)) +) + +func (n *tfsNode) full(name string) string { return path.Join(n.p, name) } + +func (n *tfsNode) Getattr(ctx context.Context, fh gofusefs.FileHandle, out *fuse.AttrOut) syscall.Errno { + fi, err := fs.Stat(n.tfs, n.p) + if err != nil { + return errno(err) + } + fillAttr(&out.Attr, fi) + return 0 +} + +func (n *tfsNode) Readdir(ctx context.Context) (gofusefs.DirStream, syscall.Errno) { + des, err := fs.ReadDir(n.tfs, n.p) + if err != nil { + return nil, errno(err) + } + + out := make([]fuse.DirEntry, 0, len(des)) + for _, de := range des { + fi, err := de.Info() + if err != nil { + continue + } + out = append(out, fuse.DirEntry{ + Name: de.Name(), + Mode: fuseModeFromInfo(fi), + }) + } + return gofusefs.NewListDirStream(out), 0 +} + +func (n *tfsNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*gofusefs.Inode, syscall.Errno) { + childPath := n.full(name) + + fi, err := fs.Stat(n.tfs, childPath) + if err != nil { + return nil, errno(err) + } + + fillAttr(&out.Attr, fi) + out.AttrValid = 1 + out.AttrValidNsec = 0 + out.EntryValid = 1 + out.EntryValidNsec = 0 + + mode := fuseModeFromInfo(fi) + ch := n.NewInode(ctx, &tfsNode{tfs: n.tfs, p: childPath}, gofusefs.StableAttr{Mode: mode}) + return ch, 0 +} + +func (n *tfsNode) Open(ctx context.Context, flags uint32) (gofusefs.FileHandle, uint32, syscall.Errno) { + if flags&(fuse.O_ANYWRITE) != 0 { + return nil, 0, syscall.EROFS + } + + f, err := n.tfs.Open(n.p) + if err != nil { + return nil, 0, errno(err) + } + if _, ok := f.(io.ReadSeeker); !ok { + _ = f.Close() + return nil, 0, syscall.ENOSYS + } + + return &tfsHandle{f: f}, fuse.FOPEN_DIRECT_IO, 0 +} + +// ----- File handle ----- + +type tfsHandle struct { + f fs.File // must implement io.ReadSeeker +} + +var ( + _ = (gofusefs.FileReader)((*tfsHandle)(nil)) + _ = (gofusefs.FileReleaser)((*tfsHandle)(nil)) +) + +func (h *tfsHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + rs := h.f.(io.ReadSeeker) + + if _, err := rs.Seek(off, io.SeekStart); err != nil { + return nil, syscall.EIO + } + n, err := rs.Read(dest) + if err != nil && err != io.EOF { + return nil, syscall.EIO + } + return fuse.ReadResultData(dest[:n]), 0 +} + +func (h *tfsHandle) Release(ctx context.Context) syscall.Errno { + _ = h.f.Close() + return 0 +} + +// ----- Attribute helpers ----- + +func fuseModeFromInfo(fi fs.FileInfo) uint32 { + if fi.IsDir() { + return fuse.S_IFDIR | uint32(fi.Mode().Perm()) + } + return fuse.S_IFREG | uint32(fi.Mode().Perm()) +} + +func fillAttr(a *fuse.Attr, fi fs.FileInfo) { + a.Mode = fuseModeFromInfo(fi) + + if fi.IsDir() { + a.Size = 4096 + } else { + a.Size = uint64(fi.Size()) + } + + mt := fi.ModTime() + if mt.IsZero() { + mt = time.Now() + } + a.Mtime = uint64(mt.Unix()) + a.Mtimensec = uint32(mt.Nanosecond()) + + a.Ctime = a.Mtime + a.Ctimensec = a.Mtimensec + + a.Atime = a.Mtime + a.Atimensec = a.Mtimensec +} + +// ----- errno mapping ----- + +func errno(err error) syscall.Errno { + if err == nil { + return 0 + } + if pe, ok := err.(*fs.PathError); ok { + return errno(pe.Err) + } + switch err { + case fs.ErrNotExist: + return syscall.ENOENT + case fs.ErrPermission: + return syscall.EPERM + case fs.ErrInvalid: + return syscall.EINVAL + default: + return syscall.EIO + } +} diff --git a/server/torrfs/fuse/fuse_win.go b/server/torrfs/fuse/fuse_win.go new file mode 100644 index 0000000..effee67 --- /dev/null +++ b/server/torrfs/fuse/fuse_win.go @@ -0,0 +1,18 @@ +//go:build windows +// +build windows + +package fuse + +import ( + "server/log" + "server/settings" +) + +func FuseAutoMount() { + if settings.Args.FusePath != "" { + log.TLogln("Windows not support FUSE") + } +} + +func FuseCleanup() { +} diff --git a/server/torrfs/info.go b/server/torrfs/info.go new file mode 100644 index 0000000..3830920 --- /dev/null +++ b/server/torrfs/info.go @@ -0,0 +1,21 @@ +package torrfs + +import ( + "io/fs" + "time" +) + +type info struct { + name string + size int64 + mode fs.FileMode + mtime time.Time + isDir bool +} + +func (i info) Name() string { return i.name } +func (i info) Size() int64 { return i.size } +func (i info) Mode() fs.FileMode { return i.mode } +func (i info) ModTime() time.Time { return i.mtime } +func (i info) IsDir() bool { return i.isDir } +func (i info) Sys() any { return nil } diff --git a/server/torrfs/node.go b/server/torrfs/node.go new file mode 100644 index 0000000..70e769e --- /dev/null +++ b/server/torrfs/node.go @@ -0,0 +1,43 @@ +package torrfs + +import ( + "io/fs" + "strings" + + "server/torr" +) + +type INode interface { + fs.ReadDirFile + fs.DirEntry + + Open(name string) (fs.File, error) + + Parent() INode + + Torrent() *torr.Torrent + SetTorrent(torr *torr.Torrent) +} + +func Open(d INode, name string) (fs.File, error) { + trimPath := strings.TrimPrefix(name, d.Name()) + trimPath = strings.TrimSuffix(trimPath, "/") + trimPath = strings.TrimPrefix(trimPath, "/") + if trimPath == "" { + return d, nil + } + arr := strings.Split(trimPath, "/") + if len(arr) == 0 { + return nil, fs.ErrNotExist + } + + dirs, _ := d.ReadDir(-1) + + for _, dir := range dirs { + if dir.Name() == arr[0] { + return dir.(INode).Open(trimPath) + } + } + + return nil, fs.ErrNotExist +} diff --git a/server/torrfs/rootdir.go b/server/torrfs/rootdir.go new file mode 100644 index 0000000..79ee426 --- /dev/null +++ b/server/torrfs/rootdir.go @@ -0,0 +1,91 @@ +package torrfs + +import ( + "io/fs" + "path" + "strings" + "time" + + "server/torr" +) + +type RootDir struct { + info fs.FileInfo +} + +func NewRootDir() *RootDir { + return &RootDir{ + info: info{ + name: "/", + size: 4096, + mode: 0o555, + mtime: time.Unix(477033600, 0), + isDir: true, + }, + } +} + +func (d *RootDir) Open(name string) (fs.File, error) { + name = path.Clean(name) + if !fs.ValidPath(name) { + return nil, &fs.PathError{Path: name, Err: fs.ErrInvalid} + } + + if name == "." || name == "/" { + return d, nil + } + + if !strings.HasPrefix(name, "/") { + name = "/" + name + } + + return Open(d, name) +} + +func (d *RootDir) Stat() (fs.FileInfo, error) { + return d.info, nil +} + +func (d *RootDir) ReadDir(n int) ([]fs.DirEntry, error) { + torrs := torr.ListTorrent() + cats := map[string]struct{}{} + nodes := map[string]INode{} + + for _, torrent := range torrs { + cats[torrent.Category] = struct{}{} + } + + for cat := range cats { + if cat == "" { + cat = "other" + } + nodes[cat] = NewCategoryDir(cat) + } + + var entries []fs.DirEntry + for _, c := range nodes { + entries = append(entries, c) + } + if n > 0 && len(entries) > n { + entries = entries[:n] + } + return entries, nil +} + +// INode +func (d *RootDir) Parent() INode { return nil } +func (d *RootDir) Torrent() *torr.Torrent { return nil } +func (d *RootDir) SetTorrent(torr *torr.Torrent) {} + +// DirEntry +func (d *RootDir) Name() string { return d.info.Name() } +func (d *RootDir) IsDir() bool { return true } +func (d *RootDir) Type() fs.FileMode { + s, _ := d.Stat() + return s.Mode() +} +func (d *RootDir) Info() (fs.FileInfo, error) { return d.info, nil } + +// File +func (d *RootDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid } +func (d *RootDir) Close() error { return nil } diff --git a/server/torrfs/torrdir.go b/server/torrfs/torrdir.go new file mode 100644 index 0000000..05e9bd2 --- /dev/null +++ b/server/torrfs/torrdir.go @@ -0,0 +1,142 @@ +package torrfs + +import ( + "io/fs" + "path" + "strings" + "time" + + "server/settings" + "server/torr" +) + +type TorrDir struct { + parent INode + children map[string]INode + + info fs.FileInfo + + torr *torr.Torrent +} + +func NewTorrDir(parent INode, name string, torrent *torr.Torrent) *TorrDir { + d := &TorrDir{ + parent: parent, + torr: torrent, + info: info{ + name: name, + size: 4096, + mode: 0o555, + mtime: time.Unix(torrent.Timestamp, 0), + isDir: true, + }, + } + return d +} + +func (d *TorrDir) ReadDir(n int) ([]fs.DirEntry, error) { + if d.Torrent() == nil { + return nil, fs.ErrInvalid + } + // соединяемся с торрентом при чтении директории торрента + if !d.Torrent().GotInfo() { + hash := d.Torrent().Hash().String() + for i := 0; i < settings.BTsets.TorrentDisconnectTimeout*2; i++ { + tor := torr.GetTorrent(hash) + if tor.GotInfo() { + d.SetTorrent(tor) + break + } + + time.Sleep(time.Millisecond * 500) + } + if d.Torrent() == nil { + return nil, fs.ErrNotExist + } + } + + files := d.Torrent().Files() + nodes := map[string]fs.DirEntry{} + + currTorrPath := d.getTorrPath() + + for _, file := range files { + dp := file.DisplayPath() + + var rel string + if currTorrPath == "" { + rel = dp + } else { + prefix := currTorrPath + "/" + if !strings.HasPrefix(dp, prefix) { + continue + } + rel = strings.TrimPrefix(dp, prefix) + } + + if rel == "" { + continue + } + + arr := strings.SplitN(rel, "/", 2) + name := arr[0] + if name == "" { + continue + } + + if len(arr) == 1 { + nodes[name] = NewTorrFile(d, name, file) + } else if _, ok := nodes[name]; !ok { + nodes[name] = NewTorrDir(d, name, d.Torrent()) + } + } + + var entries []fs.DirEntry + for _, c := range nodes { + entries = append(entries, c) + } + if n > 0 && len(entries) > n { + entries = entries[:n] + } + return entries, nil +} + +func (d *TorrDir) getTorrPath() string { + parts := []string{} + + for n := INode(d); n != nil && n.Torrent() != nil; n = n.Parent() { + if n.Parent() != nil && n.Parent().Torrent() == nil { + continue + } + parts = append([]string{n.Name()}, parts...) + } + + // отдаем без самого названия торрента + if len(parts) > 0 { + return path.Join(parts[1:]...) + } + return "" +} + +func (d *TorrDir) Open(name string) (fs.File, error) { + return Open(d, name) +} + +// INode +func (d *TorrDir) Parent() INode { return d.parent } +func (d *TorrDir) Torrent() *torr.Torrent { return d.torr } +func (d *TorrDir) SetTorrent(torr *torr.Torrent) { d.torr = torr } + +// DirEntry +func (d *TorrDir) Name() string { return d.info.Name() } +func (d *TorrDir) IsDir() bool { return true } +func (d *TorrDir) Type() fs.FileMode { + s, _ := d.Stat() + return s.Mode() +} +func (d *TorrDir) Info() (fs.FileInfo, error) { return d.info, nil } +func (d *TorrDir) Stat() (fs.FileInfo, error) { return d.info, nil } + +// File +func (d *TorrDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid } +func (d *TorrDir) Close() error { return nil } diff --git a/server/torrfs/torrfile.go b/server/torrfs/torrfile.go new file mode 100644 index 0000000..1db382e --- /dev/null +++ b/server/torrfs/torrfile.go @@ -0,0 +1,85 @@ +package torrfs + +import ( + "io/fs" + "time" + + sets "server/settings" + "server/torr" + "server/torr/storage/torrstor" + + "github.com/anacrolix/torrent" +) + +type TorrFile struct { + parent INode + + info fs.FileInfo + + torr *torr.Torrent + file *torrent.File + reader *torrstor.Reader +} + +type TorrFileHandle struct { + *TorrFile + r *torrstor.Reader +} + +func NewTorrFile(parent INode, name string, file *torrent.File) *TorrFile { + f := &TorrFile{ + file: file, + parent: parent, + torr: parent.Torrent(), + info: info{ + name: name, + size: file.Length(), + mode: 0o444, + mtime: time.Unix(parent.Torrent().Timestamp, 0), + isDir: false, + }, + } + return f +} + +func (f *TorrFile) Open(name string) (fs.File, error) { + r := f.Torrent().NewReader(f.file) + if r == nil { + return nil, fs.ErrInvalid + } + if sets.BTsets.ResponsiveMode { + r.SetResponsive() + } + return &TorrFileHandle{TorrFile: f, r: r}, nil +} + +// INode +func (f *TorrFile) Parent() INode { return f.parent } +func (f *TorrFile) Torrent() *torr.Torrent { return f.torr } +func (f *TorrFile) SetTorrent(torr *torr.Torrent) { f.torr = torr } + +// DirEntry +func (f *TorrFile) Name() string { return f.info.Name() } +func (f *TorrFile) IsDir() bool { return false } +func (f *TorrFile) Type() fs.FileMode { + s, _ := f.Stat() + return s.Mode() +} +func (f *TorrFile) Info() (fs.FileInfo, error) { return f.info, nil } +func (f *TorrFile) Stat() (fs.FileInfo, error) { return f.info, nil } +func (f *TorrFile) Read(p []byte) (int, error) { return 0, fs.ErrInvalid } +func (f *TorrFile) Close() error { return nil } +func (f *TorrFile) ReadDir(n int) ([]fs.DirEntry, error) { return nil, fs.ErrInvalid } + +func (h *TorrFileHandle) Read(p []byte) (int, error) { + return h.r.Read(p) +} + +func (h *TorrFileHandle) Seek(off int64, whence int) (int64, error) { + return h.r.Seek(off, whence) +} + +func (h *TorrFileHandle) Close() error { + h.torr.CloseReader(h.r) + return nil +} diff --git a/server/torrfs/torrfs.go b/server/torrfs/torrfs.go new file mode 100644 index 0000000..2dc5886 --- /dev/null +++ b/server/torrfs/torrfs.go @@ -0,0 +1,6 @@ +package torrfs + +func New() *RootDir { + r := NewRootDir() + return r +} diff --git a/server/torrfs/webdav/webdav.go b/server/torrfs/webdav/webdav.go new file mode 100644 index 0000000..d775a55 --- /dev/null +++ b/server/torrfs/webdav/webdav.go @@ -0,0 +1,232 @@ +package webdav + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "sync" + "time" + + "server/log" + + "server/torrfs" + + "github.com/gin-gonic/gin" + "golang.org/x/net/webdav" +) + +var missingMethods = []string{ + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", +} + +func MountWebDAV(r *gin.Engine) { + log.TLogln("Starting WebDAV") + tfs := torrfs.AsFS(torrfs.New()) + + h := &webdav.Handler{ + Prefix: "/dav", + FileSystem: &ReadOnlyFS{FS: tfs}, + LockSystem: webdav.NewMemLS(), + } + + grp := r.Group("/dav") + + handler := func(c *gin.Context) { + h.ServeHTTP(c.Writer, c.Request) + } + + grp.Any("/*webdav", handler) + for _, m := range missingMethods { + grp.Handle(m, "/*webdav", handler) + } + + grp.Any("", handler) + for _, m := range missingMethods { + grp.Handle(m, "", handler) + } +} + +type ReadOnlyFS struct { + FS fs.FS +} + +var _ webdav.FileSystem = (*ReadOnlyFS)(nil) + +func (ro *ReadOnlyFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + return os.ErrPermission +} + +func (ro *ReadOnlyFS) RemoveAll(ctx context.Context, name string) error { + return os.ErrPermission +} + +func (ro *ReadOnlyFS) Rename(ctx context.Context, oldName, newName string) error { + return os.ErrPermission +} + +func (ro *ReadOnlyFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { + name = cleanWebDAVPath(name) + return fs.Stat(ro.FS, name) +} + +func (ro *ReadOnlyFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + if flag&(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 { + return nil, os.ErrPermission + } + + name = cleanWebDAVPath(name) + + f, err := ro.FS.Open(name) + if err != nil { + return nil, err + } + + return newROFile(ro.FS, name, f), nil +} + +// --- file wrapper --- + +type roFile struct { + fsys fs.FS + name string + + mu sync.Mutex + f fs.File + + dirPos int + dirList []fs.DirEntry +} + +func newROFile(fsys fs.FS, name string, f fs.File) *roFile { + return &roFile{fsys: fsys, name: name, f: f} +} + +var _ webdav.File = (*roFile)(nil) + +func (f *roFile) Write(p []byte) (n int, err error) { + return 0, fs.ErrPermission +} + +func (f *roFile) Close() error { + f.mu.Lock() + defer f.mu.Unlock() + if f.f == nil { + return nil + } + err := f.f.Close() + f.f = nil + f.dirList = nil + f.dirPos = 0 + return err +} + +func (f *roFile) Read(p []byte) (int, error) { + f.mu.Lock() + defer f.mu.Unlock() + + if f.f == nil { + return 0, fs.ErrClosed + } + r, ok := f.f.(io.Reader) + if !ok { + return 0, fs.ErrInvalid + } + return r.Read(p) +} + +func (f *roFile) Seek(offset int64, whence int) (int64, error) { + f.mu.Lock() + defer f.mu.Unlock() + + if f.f == nil { + return 0, fs.ErrClosed + } + rs, ok := f.f.(io.Seeker) + if !ok { + return 0, errors.New("seek not supported") + } + return rs.Seek(offset, whence) +} + +func (f *roFile) Stat() (os.FileInfo, error) { + return fs.Stat(f.fsys, f.name) +} + +func (f *roFile) Readdir(count int) ([]os.FileInfo, error) { + f.mu.Lock() + defer f.mu.Unlock() + + if f.f == nil { + return nil, fs.ErrClosed + } + + fi, err := fs.Stat(f.fsys, f.name) + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fs.ErrInvalid + } + + if f.dirList == nil { + des, err := fs.ReadDir(f.fsys, f.name) + if err != nil { + return nil, err + } + f.dirList = des + f.dirPos = 0 + } + + if count <= 0 { + out := make([]os.FileInfo, 0, len(f.dirList)-f.dirPos) + for f.dirPos < len(f.dirList) { + de := f.dirList[f.dirPos] + f.dirPos++ + info, err := de.Info() + if err != nil { + continue + } + out = append(out, info) + } + return out, nil + } + + out := make([]os.FileInfo, 0, count) + for f.dirPos < len(f.dirList) && len(out) < count { + de := f.dirList[f.dirPos] + f.dirPos++ + info, err := de.Info() + if err != nil { + continue + } + out = append(out, info) + } + + if len(out) == 0 { + return nil, io.EOF + } + return out, nil +} + +// --- path helpers --- +func cleanWebDAVPath(name string) string { + if name == "" || name == "/" { + return "." + } + name = path.Clean("/" + name) + name = name[1:] + if name == "" { + return "." + } + return name +} + +func nonZeroTime(t time.Time) time.Time { + if t.IsZero() { + return time.Unix(0, 0) + } + return t +} diff --git a/server/torrshash/base62.go b/server/torrshash/base62.go new file mode 100644 index 0000000..faf7fe5 --- /dev/null +++ b/server/torrshash/base62.go @@ -0,0 +1,43 @@ +package torrshash + +import ( + "bytes" + "math/big" + "regexp" + "strings" +) + +const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +var base62Regex = regexp.MustCompile(`^[a-zA-Z0-9]+$`) + +func IsBase62(s string) bool { + return base62Regex.MatchString(strings.TrimSpace(s)) +} + +func Encode62(b []byte) string { + x := new(big.Int).SetBytes(b) + base := big.NewInt(62) + zero := big.NewInt(0) + mod := new(big.Int) + var res []byte + for x.Cmp(zero) > 0 { + x.QuoRem(x, base, mod) + res = append(res, base62Alphabet[mod.Int64()]) + } + for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 { + res[i], res[j] = res[j], res[i] + } + return string(res) +} + +func Decode62(s string) []byte { + res := new(big.Int) + base := big.NewInt(62) + for _, char := range s { + val := bytes.IndexByte([]byte(base62Alphabet), byte(char)) + res.Mul(res, base) + res.Add(res, big.NewInt(int64(val))) + } + return res.Bytes() +} diff --git a/server/torrshash/fields.go b/server/torrshash/fields.go new file mode 100644 index 0000000..d812156 --- /dev/null +++ b/server/torrshash/fields.go @@ -0,0 +1,130 @@ +package torrshash + +import ( + "encoding/binary" + "fmt" + "io" + "strconv" + "strings" +) + +type Field struct { + Tag Tag `json:"tag"` + Value string `json:"value"` +} + +type Tag int + +const ( + TagTitle = iota // string + TagPoster // string + TagTracker // string + TagCategory // string + TagSize // binary +) + +func (tag Tag) String() string { + switch tag { + case TagTitle: + return "Title" + case TagPoster: + return "Poster" + case TagTracker: + return "Tracker" + case TagCategory: + return "Category" + case TagSize: + return "Size" + default: + return "Unknown" + } +} + +func NewField(tag Tag, value string) *Field { + return &Field{Tag: tag, Value: value} +} + +func ReadField(reader io.Reader) (*Field, error) { + tagb := make([]byte, 1) + if _, err := reader.Read(tagb); err == io.EOF { + return nil, nil + } + tag := Tag(tagb[0]) + + if isBinary(tag) { + var value int64 + err := binary.Read(reader, binary.LittleEndian, &value) + if err != nil { + return nil, err + } + return NewField(tag, strconv.FormatInt(value, 10)), nil + } + + var length uint16 + err := binary.Read(reader, binary.LittleEndian, &length) + if err != nil { + return nil, err + } + + valBytes := make([]byte, length) + n, err := io.ReadFull(reader, valBytes) + + if err != nil { + return nil, err + } + + if n != int(length) { + return nil, fmt.Errorf("expected to read %v bytes, read %v", length, n) + } + + return NewField(tag, string(valBytes)), nil +} + +func (f *Field) write(writer io.Writer) error { + value := strings.TrimSpace(f.Value) + if len(value) == 0 { + return nil + } + if isBinary(f.Tag) && value == "0" { + return nil + } + + _, err := writer.Write([]byte{byte(f.Tag)}) + if err != nil { + return err + } + + if isBinary(f.Tag) { + ii, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + if ii == 0 { + return nil + } + return binary.Write(writer, binary.LittleEndian, ii) + } + + strBytes := []byte(value) + err = binary.Write(writer, binary.LittleEndian, uint16(len(strBytes))) + if err != nil { + return err + } + _, err = writer.Write(strBytes) + return err +} + +func isBinary(t Tag) bool { + switch t { + case TagTitle: + case TagPoster: + case TagTracker: + case TagCategory: + return false + case TagSize: + return true + default: + return false + } + return false +} diff --git a/server/torrshash/pack.go b/server/torrshash/pack.go new file mode 100644 index 0000000..e6b5cc6 --- /dev/null +++ b/server/torrshash/pack.go @@ -0,0 +1,83 @@ +package torrshash + +import ( + "bytes" + "compress/zlib" + "encoding/hex" + "io" + "strings" +) + +func Pack(hash *TorrsHash) (string, error) { + data, err := pack(hash) + if err != nil { + return "", err + } + return Encode62(data), nil +} + +func PackBytes(hash *TorrsHash) ([]byte, error) { + return pack(hash) +} + +func Unpack(token string) (*TorrsHash, error) { + data := Decode62(strings.TrimSpace(token)) + return UnpackBytes(data) +} + +func UnpackBytes(data []byte) (*TorrsHash, error) { + return unpack(data) +} + +func pack(t *TorrsHash) ([]byte, error) { + buf := new(bytes.Buffer) + zw, err := zlib.NewWriterLevel(buf, zlib.BestCompression) + if err != nil { + return nil, err + } + + hashBytes, _ := hex.DecodeString(strings.TrimSpace(t.Hash)) + _, err = zw.Write(hashBytes) + if err != nil { + return nil, err + } + + for _, f := range t.Fields { + err = f.write(zw) + if err != nil { + return nil, err + } + } + + err = zw.Close() + return buf.Bytes(), err +} + +func unpack(data []byte) (*TorrsHash, error) { + zr, err := zlib.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer zr.Close() + + hashBuf := make([]byte, 20) + if _, err = io.ReadFull(zr, hashBuf); err != nil { + return nil, err + } + + th := &TorrsHash{} + th.Hash = hex.EncodeToString(hashBuf) + + for { + field, err := ReadField(zr) + if err == nil && field == nil { + //End on read + return th, nil + } + if err != nil { + return th, err + } + + th.Fields = append(th.Fields, field) + } +} diff --git a/server/torrshash/torrshash.go b/server/torrshash/torrshash.go new file mode 100644 index 0000000..8f7471c --- /dev/null +++ b/server/torrshash/torrshash.go @@ -0,0 +1,62 @@ +package torrshash + +type TorrsHash struct { + Hash string `json:"hash"` + Fields []*Field `json:"fields"` +} + +func New(hash string) *TorrsHash { + th := &TorrsHash{} + th.Hash = hash + return th +} + +func (th *TorrsHash) AddField(tag Tag, value string) { + th.Fields = append(th.Fields, &Field{tag, value}) +} + +func (h *TorrsHash) Title() string { + for _, f := range h.Fields { + if f.Tag == TagTitle { + return f.Value + } + } + return "" +} + +func (h *TorrsHash) Poster() string { + for _, f := range h.Fields { + if f.Tag == TagPoster { + return f.Value + } + } + return "" +} + +func (h *TorrsHash) Category() string { + for _, f := range h.Fields { + if f.Tag == TagCategory { + return f.Value + } + } + return "" +} + +func (h *TorrsHash) Trackers() []string { + var list []string + for _, f := range h.Fields { + if f.Tag == TagTracker { + list = append(list, f.Value) + } + } + return list +} + +func (h *TorrsHash) String() string { + str := "Hash: " + h.Hash + + for _, f := range h.Fields { + str += " " + f.Tag.String() + ": " + f.Value + } + return str +} diff --git a/server/torznab/torznab.go b/server/torznab/torznab.go new file mode 100644 index 0000000..b8eadcb --- /dev/null +++ b/server/torznab/torznab.go @@ -0,0 +1,239 @@ +package torznab + +import ( + "encoding/xml" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "server/log" + "server/rutor/models" + "server/settings" +) + +type TorznabAttribute struct { + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} + +type TorznabEnclosure struct { + URL string `xml:"url,attr"` + Length int64 `xml:"length,attr"` + Type string `xml:"type,attr"` +} + +type TorznabItem struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate string `xml:"pubDate"` + Size int64 `xml:"size"` + Enclosure []TorznabEnclosure `xml:"enclosure"` + Attributes []TorznabAttribute `xml:"attr"` +} + +type TorznabChannel struct { + Items []TorznabItem `xml:"item"` +} + +type TorznabResponse struct { + Channel TorznabChannel `xml:"channel"` +} + +func Search(query string, index int) []*models.TorrentDetails { + if !settings.BTsets.EnableTorznabSearch || len(settings.BTsets.TorznabUrls) == 0 { + return nil + } + + var allResults []*models.TorrentDetails + if index >= 0 && index < len(settings.BTsets.TorznabUrls) { + config := settings.BTsets.TorznabUrls[index] + if config.Host != "" && config.Key != "" { + return searchOne(config.Host, config.Key, query) + } + return nil + } + + for _, config := range settings.BTsets.TorznabUrls { + if config.Host == "" || config.Key == "" { + continue + } + results := searchOne(config.Host, config.Key, query) + if results != nil { + allResults = append(allResults, results...) + } + } + return allResults +} + +func searchOne(host, key, query string) []*models.TorrentDetails { + if !strings.HasSuffix(host, "/") { + host += "/" + } + + u, err := url.Parse(host + "api") + if err != nil { + log.TLogln("Error parsing Torznab host:", err) + return nil + } + + q := u.Query() + q.Set("apikey", key) + q.Set("t", "search") + q.Set("q", query) + q.Set("cat", "5000,2000") // Movies and TV + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + log.TLogln("Error connecting to Torznab:", err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.TLogln("Torznab returned status:", resp.Status) + return nil + } + + var torznabResp TorznabResponse + if err := xml.NewDecoder(resp.Body).Decode(&torznabResp); err != nil { + log.TLogln("Error decoding Torznab response:", err) + return nil + } + + var results []*models.TorrentDetails + for _, item := range torznabResp.Channel.Items { + detail := &models.TorrentDetails{ + Title: item.Title, + Name: item.Title, // Use Title as Name for now + Link: item.Link, + CreateDate: parseDate(item.PubDate), + } + + if len(item.Enclosure) > 0 { + detail.Link = item.Enclosure[0].URL + detail.Size = formatSize(item.Enclosure[0].Length) + } else { + detail.Size = formatSize(item.Size) + } + + for _, attr := range item.Attributes { + if attr.Name == "magneturl" { + detail.Magnet = attr.Value + detail.Hash = extractHash(detail.Magnet) + } + if attr.Name == "seeders" { + detail.Seed, _ = strconv.Atoi(attr.Value) + } + if attr.Name == "peers" { + detail.Peer, _ = strconv.Atoi(attr.Value) + } + } + + // Fallback if magnet not in attributes but link is a magnet + if detail.Magnet == "" && strings.HasPrefix(detail.Link, "magnet:") { + detail.Magnet = detail.Link + detail.Hash = extractHash(detail.Magnet) + } + + results = append(results, detail) + } + + return results +} + +func Test(host, key string) error { + if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { + host = "http://" + host + } + if !strings.HasSuffix(host, "/") { + host += "/" + } + + u, err := url.Parse(host + "api") + if err != nil { + return err + } + + q := u.Query() + q.Set("apikey", key) + q.Set("t", "caps") + u.RawQuery = q.Encode() + + resp, err := http.Get(u.String()) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status: %s", resp.Status) + } + + var probe struct { + XMLName xml.Name + Code string `xml:"code,attr"` + Description string `xml:"description,attr"` + } + + if err := xml.NewDecoder(resp.Body).Decode(&probe); err != nil { + return fmt.Errorf("invalid xml response: %v", err) + } + + if probe.XMLName.Local == "error" { + msg := probe.Description + if msg == "" { + msg = probe.Code + } + return fmt.Errorf("api error: %s", msg) + } + + if probe.XMLName.Local != "caps" { + return fmt.Errorf("unexpected xml root: %s", probe.XMLName.Local) + } + + return nil +} + +func parseDate(dateStr string) time.Time { + // RFC1123 is common in RSS + t, err := time.Parse(time.RFC1123, dateStr) + if err != nil { + // Try RFC1123Z + t, err = time.Parse(time.RFC1123Z, dateStr) + if err != nil { + return time.Now() + } + } + return t +} + +func formatSize(bytes int64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cCiB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +func extractHash(magnet string) string { + if strings.HasPrefix(magnet, "magnet:?") { + u, err := url.Parse(magnet) + if err == nil { + xt := u.Query().Get("xt") + if strings.HasPrefix(xt, "urn:btih:") { + return strings.TrimPrefix(xt, "urn:btih:") + } + } + } + return "" +} diff --git a/server/utils/filetypes.go b/server/utils/filetypes.go new file mode 100644 index 0000000..10e3e15 --- /dev/null +++ b/server/utils/filetypes.go @@ -0,0 +1,99 @@ +package utils + +import ( + "path/filepath" + "strings" + + "server/torr/state" +) + +var extVideo = map[string]interface{}{ + ".3g2": nil, + ".3gp": nil, + ".aaf": nil, + ".asf": nil, + ".avchd": nil, + ".avi": nil, + ".drc": nil, + ".flv": nil, + ".m2ts": nil, + ".m2v": nil, + ".m4p": nil, + ".m4v": nil, + ".mkv": nil, + ".mng": nil, + ".mov": nil, + ".mp2": nil, + ".mp4": nil, + ".mpe": nil, + ".mpeg": nil, + ".mpg": nil, + ".mpv": nil, + ".mts": nil, + ".mxf": nil, + ".nsv": nil, + ".ogg": nil, + ".ogv": nil, + ".qt": nil, + ".rm": nil, + ".rmvb": nil, + ".roq": nil, + ".svi": nil, + ".ts": nil, + ".vob": nil, + ".webm": nil, + ".wmv": nil, + ".yuv": nil, +} + +var extAudio = map[string]interface{}{ + ".aac": nil, + ".aiff": nil, + ".ape": nil, + ".au": nil, + ".dff": nil, + ".dsd": nil, + ".dsf": nil, + ".flac": nil, + ".gsm": nil, + ".it": nil, + ".m3u": nil, + ".m4a": nil, + ".mid": nil, + ".mod": nil, + ".mp3": nil, + ".mpa": nil, + ".mpga": nil, + ".oga": nil, + ".ogg": nil, + ".opus": nil, + ".pls": nil, + ".ra": nil, + ".s3m": nil, + ".sid": nil, + ".spx": nil, + ".wav": nil, + ".wma": nil, + ".xm": nil, +} + +func GetMimeType(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + if _, ok := extVideo[ext]; ok { + return "video/*" + } + if _, ok := extAudio[ext]; ok { + return "audio/*" + } + return "*/*" +} + +func GetPlayableFiles(st state.TorrentStatus) []*state.TorrentFileStat { + files := make([]*state.TorrentFileStat, 0) + for _, f := range st.FileStats { + if GetMimeType(f.Path) != "*/*" { + files = append(files, f) + } + } + return files +} diff --git a/server/utils/location.go b/server/utils/location.go new file mode 100644 index 0000000..7cbe832 --- /dev/null +++ b/server/utils/location.go @@ -0,0 +1,22 @@ +package utils + +import ( + "github.com/gin-contrib/location/v2" + "github.com/gin-gonic/gin" +) + +func GetScheme(c *gin.Context) string { + url := location.Get(c) + if url == nil { + return "http" + } + return url.Scheme +} + +func GetHost(c *gin.Context) string { + url := location.Get(c) + if url == nil { + return c.Request.Host + } + return url.Host +} diff --git a/server/utils/prallel.go b/server/utils/prallel.go new file mode 100644 index 0000000..bba4311 --- /dev/null +++ b/server/utils/prallel.go @@ -0,0 +1,17 @@ +package utils + +import ( + "sync" +) + +func ParallelFor(begin, end int, fn func(i int)) { + var wg sync.WaitGroup + wg.Add(end - begin) + for i := begin; i < end; i++ { + go func(i int) { + fn(i) + wg.Done() + }(i) + } + wg.Wait() +} diff --git a/server/utils/strings.go b/server/utils/strings.go new file mode 100644 index 0000000..5767f54 --- /dev/null +++ b/server/utils/strings.go @@ -0,0 +1,99 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +const ( + _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier + KB + MB + GB + TB + PB + EB +) + +func Format(b float64) string { + multiple := "" + value := b + + switch { + case b >= EB: + value /= EB + multiple = "EB" + case b >= PB: + value /= PB + multiple = "PB" + case b >= TB: + value /= TB + multiple = "TB" + case b >= GB: + value /= GB + multiple = "GB" + case b >= MB: + value /= MB + multiple = "MB" + case b >= KB: + value /= KB + multiple = "KB" + case b == 0: + return "0" + default: + return strconv.FormatInt(int64(b), 10) + "B" + } + + return fmt.Sprintf("%.2f%s", value, multiple) +} + +func CommonPrefix(first, second string) string { + var result strings.Builder + + minLength := len(first) + if len(second) < minLength { + minLength = len(second) + } + + for i := 0; i < minLength; i++ { + if first[i] != second[i] { + break + } + result.WriteByte(first[i]) + } + + return result.String() +} + +func NumberPrefix(str string) (int, error) { + var result strings.Builder + + for i := 0; i < len(str); i++ { + if !unicode.IsDigit(rune(str[i])) { + break + } + result.WriteByte(str[i]) + } + + return strconv.Atoi(result.String()) +} + +func CompareStrings(first, second string) bool { + commonPrefix := CommonPrefix(first, second) + resultStr1 := strings.TrimPrefix(first, commonPrefix) + resultStr2 := strings.TrimPrefix(second, commonPrefix) + num1, err1 := NumberPrefix(resultStr1) + num2, err2 := NumberPrefix(resultStr2) + + if err1 == nil && err2 == nil { + return num1 < num2 + } + if err1 == nil { + return true + } else if err2 == nil { + return false + } + return resultStr1 < resultStr2 +} diff --git a/server/version/version.go b/server/version/version.go new file mode 100644 index 0000000..157d350 --- /dev/null +++ b/server/version/version.go @@ -0,0 +1,27 @@ +package version + +import ( + "log" + "runtime/debug" +) + +// Version is set at build time via -ldflags "-X server/version.Version=<tag>" +var Version = "MatriX.141" + +func GetTorrentVersion() string { + bi, ok := debug.ReadBuildInfo() + if !ok { + log.Printf("Failed to read build info") + return "" + } + for _, dep := range bi.Deps { + if dep.Path == "github.com/anacrolix/torrent" { + if dep.Replace != nil { + return dep.Replace.Version + } + + return dep.Version + } + } + return "" +} diff --git a/server/web/api/cache.go b/server/web/api/cache.go new file mode 100644 index 0000000..021ad4b --- /dev/null +++ b/server/web/api/cache.go @@ -0,0 +1,63 @@ +package api + +import ( + "net/http" + + "server/torr" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// Action: get +type cacheReqJS struct { + requestI + Hash string `json:"hash,omitempty"` +} + +// cache godoc +// +// @Summary Return cache stats +// @Description Return cache stats. +// +// @Tags API +// +// @Param request body cacheReqJS true "Cache stats request" +// +// @Produce json +// @Success 200 {object} state.CacheState "Cache stats" +// @Router /cache [post] +func cache(c *gin.Context) { + var req cacheReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + c.Status(http.StatusBadRequest) + switch req.Action { + case "get": + { + getCache(req, c) + } + } +} + +func getCache(req cacheReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + tor := torr.GetTorrent(req.Hash) + + if tor != nil { + st := tor.CacheState() + if st == nil { + c.JSON(200, struct{}{}) + } else { + c.JSON(200, st) + } + } else { + c.Status(http.StatusNotFound) + } +} diff --git a/server/web/api/download.go b/server/web/api/download.go new file mode 100644 index 0000000..06893d4 --- /dev/null +++ b/server/web/api/download.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" +) + +type fileReader struct { + pos int64 + size int64 + io.ReadSeeker +} + +func newFR(size int64) *fileReader { + return &fileReader{ + pos: 0, + size: size, + } +} + +func (f *fileReader) Read(p []byte) (n int, err error) { + f.pos = f.pos + int64(len(p)) + return len(p), nil +} + +func (f *fileReader) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + f.pos = offset + case 1: + f.pos += offset + case 2: + f.pos = f.size + offset + } + return f.pos, nil +} + +// download godoc +// +// @Summary Generates test file of given size +// @Description Download the test file of given size (for speed testing purpose). +// +// @Tags API +// +// @Param size path string true "Test file size (in MB)" +// +// @Produce application/octet-stream +// @Success 200 {file} file +// @Router /download/{size} [get] +func download(c *gin.Context) { + szStr := c.Param("size") + sz, err := strconv.Atoi(szStr) + if err != nil { + c.Error(err) + return + } + + http.ServeContent(c.Writer, c.Request, fmt.Sprintln(szStr)+"mb.bin", time.Now(), newFR(int64(sz*1024*1024))) +} diff --git a/server/web/api/ffprobe.go b/server/web/api/ffprobe.go new file mode 100644 index 0000000..5a7f2ce --- /dev/null +++ b/server/web/api/ffprobe.go @@ -0,0 +1,48 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "server/ffprobe" + sets "server/settings" + + "github.com/gin-gonic/gin" +) + +// ffp godoc +// +// @Summary Gather informations using ffprobe +// @Description Gather informations using ffprobe. +// +// @Tags API +// +// @Param hash path string true "Torrent hash" +// @Param id path string true "File index in torrent" +// +// @Produce json +// @Success 200 "Data returned from ffprobe" +// @Router /ffp/{hash}/{id} [get] +func ffp(c *gin.Context) { + hash := c.Param("hash") + indexStr := c.Param("id") + + if hash == "" || indexStr == "" { + c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty")) + return + } + + link := "http://127.0.0.1:" + sets.Port + "/play/" + hash + "/" + indexStr + if sets.Ssl { + link = "https://127.0.0.1:" + sets.SslPort + "/play/" + hash + "/" + indexStr + } + + data, err := ffprobe.ProbeUrl(link) + if err != nil { + c.AbortWithError(http.StatusBadRequest, fmt.Errorf("error getting data: %v", err)) + return + } + + c.JSON(200, data) +} diff --git a/server/web/api/m3u.go b/server/web/api/m3u.go new file mode 100644 index 0000000..814ad0f --- /dev/null +++ b/server/web/api/m3u.go @@ -0,0 +1,186 @@ +package api + +import ( + "bytes" + "encoding/hex" + "fmt" + "net/http" + "net/url" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/anacrolix/missinggo/v2/httptoo" + + sets "server/settings" + "server/torr" + "server/torr/state" + "server/utils" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// allPlayList godoc +// +// @Summary Get a M3U playlist with all torrents +// @Description Retrieve all torrents and generates a bundled M3U playlist. +// +// @Tags API +// +// @Produce audio/x-mpegurl +// @Success 200 {file} file +// @Router /playlistall/all.m3u [get] +func allPlayList(c *gin.Context) { + torrs := torr.ListTorrent() + + host := utils.GetScheme(c) + "://" + utils.GetHost(c) + list := "#EXTM3U\n" + hash := "" + // fn=file.m3u fix forkplayer bug with end .m3u in link + for _, tr := range torrs { + list += "#EXTINF:0" + if tr.Poster != "" { + list += " tvg-logo=\"" + tr.Poster + "\"" + } + list += " type=\"playlist\"," + tr.Title + "\n" + list += host + "/stream/" + url.PathEscape(tr.Title) + ".m3u?link=" + tr.TorrentSpec.InfoHash.HexString() + "&m3u&fn=file.m3u\n" + hash += tr.Hash().HexString() + } + + sendM3U(c, "all.m3u", hash, list) +} + +// playList godoc +// +// @Summary Get HTTP link of torrent in M3U list +// @Description Get HTTP link of torrent in M3U list. +// +// @Tags API +// +// @Param hash query string true "Torrent hash" +// @Param fromlast query bool false "From last play file" +// +// @Produce audio/x-mpegurl +// @Success 200 {file} file +// @Router /playlist [get] +func playList(c *gin.Context) { + hash, _ := c.GetQuery("hash") + _, fromlast := c.GetQuery("fromlast") + if hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + + tor := torr.GetTorrent(hash) + if tor == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + + if tor.Stat == state.TorrentInDB { + tor = torr.LoadTorrent(tor) + if tor == nil { + c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent info")) + return + } + } + + host := utils.GetScheme(c) + "://" + utils.GetHost(c) + list := getM3uList(tor.Status(), host, fromlast) + list = "#EXTM3U\n" + list + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + + sendM3U(c, name, tor.Hash().HexString(), list) +} + +func sendM3U(c *gin.Context, name, hash string, m3u string) { + c.Header("Content-Type", "audio/x-mpegurl") + c.Header("Connection", "close") + if hash != "" { + etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", hash, name))) + c.Header("ETag", httptoo.EncodeQuotedString(etag)) + } + if name == "" { + name = "playlist.m3u" + } + c.Header("Content-Disposition", `attachment; filename="`+name+`"`) + http.ServeContent(c.Writer, c.Request, name, time.Now(), bytes.NewReader([]byte(m3u))) +} + +func getM3uList(tor *state.TorrentStatus, host string, fromLast bool) string { + m3u := "" + from := 0 + if fromLast { + pos := searchLastPlayed(tor) + if pos != -1 { + from = pos + } + } + for i, f := range tor.FileStats { + if i >= from { + if utils.GetMimeType(f.Path) != "*/*" { + fn := filepath.Base(f.Path) + if fn == "" { + fn = f.Path + } + m3u += "#EXTINF:0," + fn + "\n" + fileNamesakes := findFileNamesakes(tor.FileStats, f) // find external media with same name (audio/subtiles tracks) + if fileNamesakes != nil { + m3u += "#EXTVLCOPT:input-slave=" // include VLC option for external media + for _, namesake := range fileNamesakes { // include play-links to external media, with # splitter + sname := filepath.Base(namesake.Path) + m3u += host + "/stream/" + url.PathEscape(sname) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(namesake.Id) + "&play#" + } + m3u += "\n" + } + name := filepath.Base(f.Path) + m3u += host + "/stream/" + url.PathEscape(name) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(f.Id) + "&play\n" + } + } + } + return m3u +} + +func findFileNamesakes(files []*state.TorrentFileStat, file *state.TorrentFileStat) []*state.TorrentFileStat { + // find files with the same name in torrent + name := filepath.Base(strings.TrimSuffix(file.Path, filepath.Ext(file.Path))) + var namesakes []*state.TorrentFileStat + for _, f := range files { + if strings.Contains(f.Path, name) { // external tracks always include name of videofile + if f != file { // exclude itself + namesakes = append(namesakes, f) + } + } + } + return namesakes +} + +func searchLastPlayed(tor *state.TorrentStatus) int { + viewed := sets.ListViewed(tor.Hash) + if len(viewed) == 0 { + return -1 + } + sort.Slice(viewed, func(i, j int) bool { + return viewed[i].FileIndex > viewed[j].FileIndex + }) + + lastViewedIndex := viewed[0].FileIndex + + for i, stat := range tor.FileStats { + if stat.Id == lastViewedIndex { + if i >= len(tor.FileStats) { + return -1 + } + return i + } + } + + return -1 +} diff --git a/server/web/api/play.go b/server/web/api/play.go new file mode 100644 index 0000000..153a048 --- /dev/null +++ b/server/web/api/play.go @@ -0,0 +1,85 @@ +package api + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "server/torr" + "server/torr/state" + "server/web/api/utils" +) + +// play godoc +// +// @Summary Play given torrent by infohash +// @Description Play given torrent referenced by infohash and file id. +// +// @Tags API +// +// @Param hash path string true "Torrent infohash" +// @Param id path string true "File index in torrent" +// +// @Produce application/octet-stream +// @Success 200 "Torrent data" +// @Router /play/{hash}/{id} [get] +func play(c *gin.Context) { + hash := c.Param("hash") + indexStr := c.Param("id") + notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == "" + + if hash == "" || indexStr == "" { + c.AbortWithError(http.StatusNotFound, errors.New("no infohash or file index in link")) + return + } + + spec, err := utils.ParseLink(hash) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor == nil && notAuth { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if tor == nil { + c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent")) + return + } + + if tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, tor.Title, tor.Poster, tor.Data, tor.Category) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout")) + return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 1 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 { // if file index not set and play file exec + c.AbortWithError(http.StatusBadRequest, errors.New("file \"index\" is wrong")) + return + } + + tor.Stream(index, c.Request, c.Writer) +} diff --git a/server/web/api/route.go b/server/web/api/route.go new file mode 100644 index 0000000..e5b349b --- /dev/null +++ b/server/web/api/route.go @@ -0,0 +1,67 @@ +package api + +import ( + config "server/settings" + "server/web/auth" + + "github.com/gin-gonic/gin" +) + +type requestI struct { + Action string `json:"action,omitempty"` +} + +func SetupRoute(route gin.IRouter) { + authorized := route.Group("/", auth.CheckAuth()) + + authorized.GET("/shutdown", shutdown) + authorized.GET("/shutdown/*reason", shutdown) + + authorized.POST("/settings", settings) + authorized.POST("/torznab/test", torznabTest) + + authorized.POST("/torrents", torrents) + + authorized.POST("/torrent/upload", torrentUpload) + + authorized.POST("/cache", cache) + + route.HEAD("/stream", stream) + route.GET("/stream", stream) + + route.HEAD("/stream/*fname", stream) + route.GET("/stream/*fname", stream) + + route.HEAD("/play/:hash/:id", play) + route.GET("/play/:hash/:id", play) + + authorized.POST("/viewed", viewed) + + authorized.GET("/playlistall/all.m3u", allPlayList) + + route.GET("/playlist", playList) + route.GET("/playlist/*fname", playList) + + authorized.GET("/download/:size", download) + + if config.SearchWA { + route.GET("/search/*query", rutorSearch) + } else { + authorized.GET("/search/*query", rutorSearch) + } + + if config.SearchWA { + route.GET("/torznab/search/*query", torznabSearch) + } else { + authorized.GET("/torznab/search/*query", torznabSearch) + } + + // Add storage settings endpoints + authorized.GET("/storage/settings", GetStorageSettings) + authorized.POST("/storage/settings", UpdateStorageSettings) + + // Add TMDB settings endpoint + authorized.GET("/tmdb/settings", tmdbSettings) + + authorized.GET("/ffp/:hash/:id", ffp) +} diff --git a/server/web/api/rutor.go b/server/web/api/rutor.go new file mode 100644 index 0000000..196791d --- /dev/null +++ b/server/web/api/rutor.go @@ -0,0 +1,38 @@ +package api + +import ( + "net/http" + "net/url" + + "github.com/gin-gonic/gin" + + "server/rutor" + "server/rutor/models" + sets "server/settings" +) + +// rutorSearch godoc +// +// @Summary Makes a rutor search +// @Description Makes a rutor search. +// +// @Tags API +// +// @Param query query string true "Rutor query" +// +// @Produce json +// @Success 200 {array} models.TorrentDetails "Rutor torrent search result(s)" +// @Router /search [get] +func rutorSearch(c *gin.Context) { + if !sets.BTsets.EnableRutorSearch { + c.JSON(http.StatusBadRequest, []string{}) + return + } + query := c.Query("query") + query, _ = url.QueryUnescape(query) + list := rutor.Search(query) + if list == nil { + list = []*models.TorrentDetails{} + } + c.JSON(200, list) +} diff --git a/server/web/api/settings.go b/server/web/api/settings.go new file mode 100644 index 0000000..16335e2 --- /dev/null +++ b/server/web/api/settings.go @@ -0,0 +1,65 @@ +package api + +import ( + "net/http" + + "server/rutor" + + "server/dlna" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + + sets "server/settings" + "server/torr" +) + +// Action: get, set, def +type setsReqJS struct { + requestI + Sets *sets.BTSets `json:"sets,omitempty"` +} + +// settings godoc +// +// @Summary Get / Set server settings +// @Description Allow to get or set server settings. +// +// @Tags API +// +// @Param request body setsReqJS true "Settings request. Available params for action: get, set, def" +// +// @Accept json +// @Produce json +// @Success 200 {object} sets.BTSets "Settings JSON or nothing. Depends on what action has been asked." +// @Router /settings [post] +func settings(c *gin.Context) { + var req setsReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + if req.Action == "get" { + c.JSON(200, sets.BTsets) + return + } else if req.Action == "set" { + torr.SetSettings(req.Sets) + dlna.Stop() + if req.Sets.EnableDLNA { + dlna.Start() + } + rutor.Stop() + rutor.Start() + c.Status(200) + return + } else if req.Action == "def" { + torr.SetDefSettings() + dlna.Stop() + rutor.Stop() + c.Status(200) + return + } + c.AbortWithError(http.StatusBadRequest, errors.New("action is empty")) +} diff --git a/server/web/api/shutdown.go b/server/web/api/shutdown.go new file mode 100644 index 0000000..71f54f9 --- /dev/null +++ b/server/web/api/shutdown.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "strings" + "time" + + sets "server/settings" + "server/torr" + + "github.com/gin-gonic/gin" +) + +// shutdown godoc +// @Summary Shuts down server +// @Description Gracefully shuts down server after 1 second. +// +// @Tags API +// +// @Success 200 +// @Router /shutdown [get] +func shutdown(c *gin.Context) { + reasonStr := strings.ReplaceAll(c.Param("reason"), `/`, "") + if sets.ReadOnly && reasonStr == "" { + c.Status(http.StatusForbidden) + return + } + c.Status(200) + go func() { + time.Sleep(1000) + torr.Shutdown() + }() +} diff --git a/server/web/api/storage.go b/server/web/api/storage.go new file mode 100644 index 0000000..b1aac38 --- /dev/null +++ b/server/web/api/storage.go @@ -0,0 +1,101 @@ +package api + +import ( + "net/http" + + sets "server/settings" + + "github.com/gin-gonic/gin" +) + +// GetStorageSettings godoc +// @Summary Get storage configuration settings +// @Description Retrieves the current storage preferences for settings and viewed history +// @Tags API +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} map[string]interface{} "Storage preferences" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /storage/settings [get] +func GetStorageSettings(c *gin.Context) { + prefs := sets.GetStoragePreferences() + c.JSON(http.StatusOK, prefs) +} + +// UpdateStorageSettings godoc +// @Summary Update storage configuration settings +// @Description Updates the storage preferences for settings and viewed history. Requires application restart for changes to take effect. +// @Tags API +// @Accept json,x-www-form-urlencoded +// @Produce json +// @Security ApiKeyAuth +// @Param request body map[string]interface{} true "Storage preferences to update" +// @Param settings formData string false "Settings storage type" Enums(json,bbolt) +// @Param viewed formData string false "Viewed history storage type" Enums(json,bbolt) +// @Success 200 {object} map[string]string "Update successful" +// @Failure 400 {object} map[string]string "Invalid input data" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Failure 403 {object} map[string]string "Read-only mode" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /storage/settings [post] +func UpdateStorageSettings(c *gin.Context) { + if sets.ReadOnly { + c.JSON(http.StatusForbidden, gin.H{"error": "Read-only mode"}) + return + } + + var prefs map[string]interface{} + + // Check Content-Type to handle both JSON and form data + contentType := c.GetHeader("Content-Type") + + if contentType == "application/x-www-form-urlencoded" { + // Handle form data + settings := c.PostForm("settings") + viewed := c.PostForm("viewed") + + prefs = make(map[string]interface{}) + if settings != "" { + prefs["settings"] = settings + } + if viewed != "" { + prefs["viewed"] = viewed + } + } else { + // Handle JSON (default) + if err := c.ShouldBindJSON(&prefs); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } + + // Validate preferences - only validate if provided + if settingsPref, ok := prefs["settings"].(string); ok && settingsPref != "" { + if settingsPref != "json" && settingsPref != "bbolt" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid settings storage value"}) + return + } + } + + if viewedPref, ok := prefs["viewed"].(string); ok && viewedPref != "" { + if viewedPref != "json" && viewedPref != "bbolt" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid viewed storage value"}) + return + } + } + + // Check if we have at least one value to update + if len(prefs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "No preferences provided"}) + return + } + + if err := sets.SetStoragePreferences(prefs); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/server/web/api/stream.go b/server/web/api/stream.go new file mode 100644 index 0000000..14d9f7b --- /dev/null +++ b/server/web/api/stream.go @@ -0,0 +1,322 @@ +package api + +import ( + "net/http" + "net/url" + "server/log" + "server/torrshash" + "strconv" + "strings" + + "server/torr" + "server/torr/state" + utils2 "server/utils" + "server/web/api/utils" + + "github.com/anacrolix/torrent" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// get stat +// http://127.0.0.1:8090/stream/fname?link=...&stat +// get m3u +// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u +// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u&fromlast +// stream torrent +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&preload +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save&title=...&poster=... +// only save +// http://127.0.0.1:8090/stream/fname?link=...&save&title=...&poster=... + +// stream godoc +// +// @Summary Multi usage endpoint +// @Description Multi usage endpoint. +// +// @Tags API +// +// @Param link query string true "Magnet/hash/link to torrent" +// @Param index query string false "File index in torrent" +// @Param preload query string false "Should preload torrent" +// @Param stat query string false "Get statistics from torrent" +// @Param save query string false "Should save torrent" +// @Param m3u query string false "Get torrent as M3U playlist" +// @Param fromlast query string false "Get M3U from last played file" +// @Param play query string false "Start stream torrent" +// @Param title query string false "Set title of torrent" +// @Param poster query string false "Set poster link of torrent" +// @Param category query string false "Set category of torrent, used in web: movie, tv, music, other" +// +// @Produce application/octet-stream +// @Success 200 "Data returned according to query" +// @Router /stream [get] +func stream(c *gin.Context) { + link := c.Query("link") + indexStr := c.Query("index") + _, preload := c.GetQuery("preload") + _, stat := c.GetQuery("stat") + _, save := c.GetQuery("save") + _, m3u := c.GetQuery("m3u") + _, fromlast := c.GetQuery("fromlast") + _, play := c.GetQuery("play") + title := c.Query("title") + poster := c.Query("poster") + category := c.Query("category") + + data := "" + + notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == "" + + if notAuth { + err := utils.TestLink(link, !notAuth) + if err != nil { + log.TLogln("Wrong link:", err) + c.AbortWithError(http.StatusBadRequest, errors.New("wrong link")) + return + } + } + + if notAuth && (play || m3u) { + streamNoAuth(c) + return + } + if notAuth { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty")) + return + } + + link, _ = url.QueryUnescape(link) + title, _ = url.QueryUnescape(title) + poster, _ = url.QueryUnescape(poster) + category, _ = url.QueryUnescape(category) + + var spec *torrent.TorrentSpec + var torrsHash *torrshash.TorrsHash + var err error + + if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) { + spec, torrsHash, err = utils.ParseTorrsHash(link) + if err != nil { + log.TLogln("error parse torrshash:", err) + c.AbortWithError(http.StatusBadRequest, err) + return + } + if title == "" { + title = torrsHash.Title() + } + if poster == "" { + poster = torrsHash.Poster() + } + if category == "" { + category = torrsHash.Category() + } + } else { + spec, err = utils.ParseLink(link) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor != nil { + title = tor.Title + poster = tor.Poster + data = tor.Data + category = tor.Category + } + if tor == nil || tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, title, poster, data, category) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout")) + return + } + + if tor.Title == "" { + tor.Title = tor.Name() + } + + // save to db + if save { + torr.SaveTorrentToDB(tor) + c.Status(200) // only set status, not return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 1 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 && play { // if file index not set and play file exec + c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong")) + return + } + // preload torrent + if preload { + torr.Preload(tor, index) + } + // return stat if query + if stat { + c.JSON(200, tor.Status()) + return + } else + // return m3u if query + if m3u { + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast) + sendM3U(c, name, tor.Hash().HexString(), m3ulist) + return + } else + // return play if query + if play { + tor.Stream(index, c.Request, c.Writer) + return + } +} + +func streamNoAuth(c *gin.Context) { + link := c.Query("link") + indexStr := c.Query("index") + _, preload := c.GetQuery("preload") + _, m3u := c.GetQuery("m3u") + _, fromlast := c.GetQuery("fromlast") + _, play := c.GetQuery("play") + title := c.Query("title") + poster := c.Query("poster") + category := c.Query("category") + + if link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty")) + return + } + + link, _ = url.QueryUnescape(link) + title, _ = url.QueryUnescape(title) + poster, _ = url.QueryUnescape(poster) + category, _ = url.QueryUnescape(category) + + var spec *torrent.TorrentSpec + var torrsHash *torrshash.TorrsHash + var err error + + if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) { + spec, torrsHash, err = utils.ParseTorrsHash(link) + if err != nil { + log.TLogln("error parse torrshash:", err) + c.AbortWithError(http.StatusBadRequest, err) + return + } + if title == "" { + title = torrsHash.Title() + } + if poster == "" { + poster = torrsHash.Poster() + } + if category == "" { + category = torrsHash.Category() + } + } else { + spec, err = utils.ParseLink(link) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor == nil { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if title == "" { + title = tor.Title + } + + if poster == "" { + poster = tor.Poster + } + + if category == "" { + category = tor.Category + } + + data := tor.Data + + if tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, title, poster, data, category) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout")) + return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 1 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 && play { // if file index not set and play file exec + c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong")) + return + } + // preload torrent + if preload { + torr.Preload(tor, index) + } + // return m3u if query + if m3u { + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast) + sendM3U(c, name, tor.Hash().HexString(), m3ulist) + return + } else + // return play if query + if play { + tor.Stream(index, c.Request, c.Writer) + return + } + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) +} diff --git a/server/web/api/tmdb.go b/server/web/api/tmdb.go new file mode 100644 index 0000000..c799895 --- /dev/null +++ b/server/web/api/tmdb.go @@ -0,0 +1,27 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + sets "server/settings" +) + +// tmdbSettings godoc +// +// @Summary Get TMDB settings +// @Description Get TMDB API configuration +// +// @Tags API +// +// @Produce json +// @Success 200 {object} sets.TMDBConfig "TMDB settings" +// @Router /tmdb/settings [get] +func tmdbSettings(c *gin.Context) { + if sets.BTsets == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Settings not initialized"}) + return + } + c.JSON(200, sets.BTsets.TMDBSettings) +} diff --git a/server/web/api/torrents.go b/server/web/api/torrents.go new file mode 100644 index 0000000..cdafeaf --- /dev/null +++ b/server/web/api/torrents.go @@ -0,0 +1,229 @@ +package api + +import ( + "net/http" + "server/torrshash" + "strings" + + "server/dlna" + "server/log" + set "server/settings" + "server/torr" + "server/torr/state" + "server/web/api/utils" + + "github.com/anacrolix/torrent" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// Action: add, get, set, rem, list, drop +type torrReqJS struct { + requestI + Link string `json:"link,omitempty"` + Hash string `json:"hash,omitempty"` + Title string `json:"title,omitempty"` + Category string `json:"category,omitempty"` + Poster string `json:"poster,omitempty"` + Data string `json:"data,omitempty"` + SaveToDB bool `json:"save_to_db,omitempty"` +} + +// torrents godoc +// +// @Summary Handle torrents informations +// @Description Allow to list, add, remove, get, set, drop, wipe torrents on server. The action depends of what has been asked. +// +// @Tags API +// +// @Param request body torrReqJS true "Torrent request. Available params for action: add, get, set, rem, list, drop, wipe. link required for add, hash required for get, set, rem, drop." +// +// @Accept json +// @Produce json +// @Success 200 +// @Router /torrents [post] +func torrents(c *gin.Context) { + var req torrReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + c.Status(http.StatusBadRequest) + switch req.Action { + case "add": + { + addTorrent(req, c) + } + case "get": + { + getTorrent(req, c) + } + case "set": + { + setTorrent(req, c) + } + case "rem": + { + remTorrent(req, c) + } + case "list": + { + listTorrents(c) + } + case "drop": + { + dropTorrent(req, c) + } + case "wipe": + { + wipeTorrents(c) + } + } +} + +func addTorrent(req torrReqJS, c *gin.Context) { + if req.Link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link is empty")) + return + } + + log.TLogln("add torrent", req.Link) + req.Link = strings.ReplaceAll(req.Link, "&", "&") + + var torrSpec *torrent.TorrentSpec + var torrsHash *torrshash.TorrsHash + var err error + + if strings.HasPrefix(req.Link, "torrs://") { + torrSpec, torrsHash, err = utils.ParseTorrsHash(req.Link) + if err != nil { + log.TLogln("error parse torrshash:", err) + c.AbortWithError(http.StatusBadRequest, err) + return + } + if req.Title == "" { + req.Title = torrsHash.Title() + } + if req.Poster == "" { + req.Poster = torrsHash.Poster() + } + if req.Category == "" { + req.Category = torrsHash.Category() + } + } else { + torrSpec, err = utils.ParseLink(req.Link) + if err != nil { + log.TLogln("error parse link:", err) + c.AbortWithError(http.StatusBadRequest, err) + return + } + } + + tor, err := torr.AddTorrent(torrSpec, req.Title, req.Poster, req.Data, req.Category) + if err != nil { + log.TLogln("error add torrent:", err) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + go func() { + if !tor.GotInfo() { + log.TLogln("error add torrent:", "timeout connection get torrent info") + return + } + + if tor.Title == "" { + tor.Title = torrSpec.DisplayName // prefer dn over name + tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "") + tor.Title = strings.ReplaceAll(tor.Title, "_", " ") + tor.Title = strings.Trim(tor.Title, " ") + if tor.Title == "" { + tor.Title = tor.Name() + } + } + + if req.SaveToDB { + torr.SaveTorrentToDB(tor) + } + }() + + if set.BTsets.EnableDLNA { + dlna.Stop() + dlna.Start() + } + c.JSON(200, tor.Status()) +} + +func getTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + tor := torr.GetTorrent(req.Hash) + + if tor != nil { + st := tor.Status() + c.JSON(200, st) + } else { + c.Status(http.StatusNotFound) + } +} + +func setTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.SetTorrent(req.Hash, req.Title, req.Poster, req.Category, req.Data) + c.Status(200) +} + +func remTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.RemTorrent(req.Hash) + // TODO: remove + if set.BTsets.EnableDLNA { + dlna.Stop() + dlna.Start() + } + c.Status(200) +} + +func listTorrents(c *gin.Context) { + list := torr.ListTorrent() + if len(list) == 0 { + c.JSON(200, []*state.TorrentStatus{}) + return + } + var stats []*state.TorrentStatus + for _, tr := range list { + stats = append(stats, tr.Status()) + } + c.JSON(200, stats) +} + +func dropTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.DropTorrent(req.Hash) + c.Status(200) +} + +func wipeTorrents(c *gin.Context) { + torrents := torr.ListTorrent() + for _, t := range torrents { + torr.RemTorrent(t.TorrentSpec.InfoHash.HexString()) + } + // TODO: remove (copied todo from remTorrent()) + if set.BTsets.EnableDLNA { + dlna.Stop() + dlna.Start() + } + c.Status(200) +} diff --git a/server/web/api/torznab.go b/server/web/api/torznab.go new file mode 100644 index 0000000..dbba534 --- /dev/null +++ b/server/web/api/torznab.go @@ -0,0 +1,64 @@ +package api + +import ( + "net/http" + "net/url" + "strconv" + + "github.com/gin-gonic/gin" + + "server/rutor/models" + sets "server/settings" + "server/torznab" +) + +// torznabSearch godoc +// +// @Summary Makes a torznab search +// @Description Makes a torznab search. +// +// @Tags API +// +// @Param query query string true "Torznab query" +// +// @Produce json +// @Success 200 {array} models.TorrentDetails "Torznab torrent search result(s)" +// @Router /torznab/search [get] +func torznabSearch(c *gin.Context) { + if !sets.BTsets.EnableTorznabSearch { + c.JSON(http.StatusBadRequest, []string{}) + return + } + query := c.Query("query") + indexStr := c.DefaultQuery("index", "-1") + index := -1 + if i, err := strconv.Atoi(indexStr); err == nil { + index = i + } + + query, _ = url.QueryUnescape(query) + list := torznab.Search(query, index) + if list == nil { + list = []*models.TorrentDetails{} + } + c.JSON(200, list) +} + +type torznabTestReq struct { + Host string `json:"host"` + Key string `json:"key"` +} + +func torznabTest(c *gin.Context) { + var req torznabTestReq + if err := c.ShouldBindJSON(&req); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + if err := torznab.Test(req.Host, req.Key); err != nil { + c.JSON(200, gin.H{"success": false, "error": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true}) +} diff --git a/server/web/api/upload.go b/server/web/api/upload.go new file mode 100644 index 0000000..983a0cc --- /dev/null +++ b/server/web/api/upload.go @@ -0,0 +1,114 @@ +package api + +import ( + "mime/multipart" + "net/http" + + "server/log" + set "server/settings" + "server/torr" + "server/torr/state" + "server/web/api/utils" + + "github.com/gin-gonic/gin" +) + +// torrentUpload godoc +// +// @Summary Add .torrent files +// @Description Supports multiple files. Returns array of statuses. +// +// @Tags API +// +// @Param file formData file true "Torrent file(s) to insert" +// @Param save formData string false "Save to DB" +// @Param title formData string false "Torrent title (single file only)" +// @Param category formData string false "Torrent category" +// @Param poster formData string false "Torrent poster (single file only)" +// @Param data formData string false "Torrent data" +// +// @Accept multipart/form-data +// +// @Produce json +// @Success 200 {array} state.TorrentStatus "Torrent statuses" +// @Router /torrent/upload [post] +func torrentUpload(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + defer form.RemoveAll() + + save := len(form.Value["save"]) > 0 + title := "" + if len(form.Value["title"]) > 0 { + title = form.Value["title"][0] + } + category := "" + if len(form.Value["category"]) > 0 { + category = form.Value["category"][0] + } + poster := "" + if len(form.Value["poster"]) > 0 { + poster = form.Value["poster"][0] + } + data := "" + if len(form.Value["data"]) > 0 { + data = form.Value["data"][0] + } + + var files []*multipart.FileHeader + for _, fh := range form.File { + files = append(files, fh...) + } + + var stats []*state.TorrentStatus + for _, fh := range files { + log.TLogln("add .torrent", fh.Filename) + + torrFile, err := fh.Open() + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + + spec, err := utils.ParseFile(torrFile) + torrFile.Close() + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + + tor, err := torr.AddTorrent(spec, title, poster, data, category) + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + + if tor.Data != "" && set.BTsets.EnableDebug { + log.TLogln("torrent data:", tor.Data) + } + if tor.Category != "" && set.BTsets.EnableDebug { + log.TLogln("torrent category:", tor.Category) + } + + go func(t *torr.Torrent) { + if !t.GotInfo() { + log.TLogln("error add torrent:", "torrent connection timeout") + return + } + + if t.Title == "" { + t.Title = t.Name() + } + + if save { + torr.SaveTorrentToDB(t) + } + }(tor) + + stats = append(stats, tor.Status()) + } + c.JSON(200, stats) +} diff --git a/server/web/api/utils/link.go b/server/web/api/utils/link.go new file mode 100644 index 0000000..2d9766a --- /dev/null +++ b/server/web/api/utils/link.go @@ -0,0 +1,183 @@ +package utils + +import ( + "bytes" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "runtime" + "server/torrshash" + "strings" + "time" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +func ParseFromBytes(data []byte) (*torrent.TorrentSpec, error) { + minfo, err := metainfo.Load(bytes.NewReader(data)) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} + +func ParseFile(file multipart.File) (*torrent.TorrentSpec, error) { + minfo, err := metainfo.Load(file) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} + +func ParseLink(link string) (*torrent.TorrentSpec, error) { + urlLink, err := url.Parse(link) + if err != nil { + return nil, err + } + + switch strings.ToLower(urlLink.Scheme) { + case "magnet": + return fromMagnet(urlLink.String()) + case "http", "https": + return fromHttp(urlLink.String()) + case "": + return fromMagnet("magnet:?xt=urn:btih:" + urlLink.Path) + case "file": + return fromFile(urlLink.Path) + default: + err = fmt.Errorf("unknown scheme:", urlLink, urlLink.Scheme) + } + return nil, err +} + +func fromMagnet(link string) (*torrent.TorrentSpec, error) { + mag, err := metainfo.ParseMagnetUri(link) + if err != nil { + return nil, err + } + + var trackers [][]string + if len(mag.Trackers) > 0 { + trackers = [][]string{mag.Trackers} + } + + return &torrent.TorrentSpec{ + InfoBytes: nil, + Trackers: trackers, + DisplayName: mag.DisplayName, + InfoHash: mag.InfoHash, + }, nil +} + +func ParseTorrsHash(token string) (*torrent.TorrentSpec, *torrshash.TorrsHash, error) { + if strings.HasPrefix(token, "torrs://") { + token = strings.TrimPrefix(token, "torrs://") + } + th, err := torrshash.Unpack(token) + if err != nil { + return nil, nil, err + } + + var trackers [][]string + if len(th.Trackers()) > 0 { + trackers = [][]string{th.Trackers()} + } + + return &torrent.TorrentSpec{ + InfoBytes: nil, + Trackers: trackers, + DisplayName: th.Title(), + InfoHash: metainfo.NewHashFromHex(th.Hash), + }, th, nil +} + +func fromHttp(link string) (*torrent.TorrentSpec, error) { + req, err := http.NewRequest("GET", link, nil) + if err != nil { + return nil, err + } + + client := new(http.Client) + client.Timeout = time.Duration(time.Second * 60) + req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)") + + resp, err := client.Do(req) + if er, ok := err.(*url.Error); ok { + if strings.HasPrefix(er.URL, "magnet:") { + return fromMagnet(er.URL) + } + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + minfo, err := metainfo.Load(resp.Body) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} + +func fromFile(path string) (*torrent.TorrentSpec, error) { + if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") { + path = strings.TrimPrefix(path, "/") + } + minfo, err := metainfo.LoadFromFile(path) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} diff --git a/server/web/api/utils/testlink.go b/server/web/api/utils/testlink.go new file mode 100644 index 0000000..3158331 --- /dev/null +++ b/server/web/api/utils/testlink.go @@ -0,0 +1,27 @@ +package utils + +import ( + "fmt" + "net/url" +) + +func TestLink(link string, auth bool) error { + link, err := url.QueryUnescape(link) + if err != nil { + return err + } + ur, err := url.Parse(link) + if err != nil { + return err + } + + if ur.Scheme == "magnet" || ur.Scheme == "torrs" || ur.Scheme == "" { + return nil + } + + if !auth { + return fmt.Errorf("auth required") + } + + return nil +} diff --git a/server/web/api/viewed.go b/server/web/api/viewed.go new file mode 100644 index 0000000..cd8a6b8 --- /dev/null +++ b/server/web/api/viewed.go @@ -0,0 +1,71 @@ +package api + +import ( + "net/http" + + sets "server/settings" + + "github.com/gin-gonic/gin" +) + +/* +file index starts from 1 +*/ + +// Action: set, rem, list +type viewedReqJS struct { + requestI + *sets.Viewed +} + +// viewed godoc +// +// @Summary Set / List / Remove viewed torrents +// @Description Allow to set, list or remove viewed torrents from server. +// +// @Tags API +// +// @Param request body viewedReqJS true "Viewed torrent request. Available params for action: set, rem, list" +// +// @Accept json +// @Produce json +// @Success 200 {array} sets.Viewed +// @Router /viewed [post] +func viewed(c *gin.Context) { + var req viewedReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + switch req.Action { + case "set": + { + setViewed(req, c) + } + case "rem": + { + remViewed(req, c) + } + case "list": + { + listViewed(req, c) + } + } +} + +func setViewed(req viewedReqJS, c *gin.Context) { + sets.SetViewed(req.Viewed) + c.Status(200) +} + +func remViewed(req viewedReqJS, c *gin.Context) { + sets.RemViewed(req.Viewed) + c.Status(200) +} + +func listViewed(req viewedReqJS, c *gin.Context) { + list := sets.ListViewed(req.Hash) + c.JSON(200, list) +} diff --git a/server/web/auth/auth.go b/server/web/auth/auth.go new file mode 100644 index 0000000..d810240 --- /dev/null +++ b/server/web/auth/auth.go @@ -0,0 +1,105 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + "path/filepath" + "unsafe" + + "github.com/gin-gonic/gin" + + "server/log" + "server/settings" +) + +func SetupAuth(engine *gin.Engine) { + if !settings.HttpAuth { + return + } + accs := getAccounts() + if accs == nil { + return + } + engine.Use(BasicAuth(accs)) +} + +func getAccounts() gin.Accounts { + buf, err := os.ReadFile(filepath.Join(settings.Path, "accs.db")) + if err != nil { + return nil + } + var accs gin.Accounts + err = json.Unmarshal(buf, &accs) + if err != nil { + log.TLogln("Error parse accs.db", err) + } + return accs +} + +type authPair struct { + value string + user string +} +type authPairs []authPair + +func (a authPairs) searchCredential(authValue string) (string, bool) { + if authValue == "" { + return "", false + } + for _, pair := range a { + if pair.value == authValue { + return pair.user, true + } + } + return "", false +} + +func BasicAuth(accounts gin.Accounts) gin.HandlerFunc { + pairs := processAccounts(accounts) + return func(c *gin.Context) { + c.Set("auth_required", true) + + user, found := pairs.searchCredential(c.Request.Header.Get("Authorization")) + if found { + c.Set(gin.AuthUserKey, user) + } + } +} + +func CheckAuth() gin.HandlerFunc { + return func(c *gin.Context) { + if !settings.HttpAuth { + return + } + + if _, ok := c.Get(gin.AuthUserKey); ok { + return + } + + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + } +} + +func processAccounts(accounts gin.Accounts) authPairs { + pairs := make(authPairs, 0, len(accounts)) + for user, password := range accounts { + value := authorizationHeader(user, password) + pairs = append(pairs, authPair{ + value: value, + user: user, + }) + } + return pairs +} + +func authorizationHeader(user, password string) string { + base := user + ":" + password + return "Basic " + base64.StdEncoding.EncodeToString(StringToBytes(base)) +} + +func StringToBytes(s string) (b []byte) { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} diff --git a/server/web/blocker/blocker.go b/server/web/blocker/blocker.go new file mode 100644 index 0000000..7bfbc10 --- /dev/null +++ b/server/web/blocker/blocker.go @@ -0,0 +1,115 @@ +package blocker + +import ( + "bufio" + "bytes" + "errors" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "server/log" + "server/settings" + + "github.com/gin-gonic/gin" +) + +func Blocker() gin.HandlerFunc { + emptyFN := func(c *gin.Context) { + c.Next() + } + + name := filepath.Join(settings.Path, "bip.txt") + buf, _ := os.ReadFile(name) + blackIpList := scanBuf(buf) + + name = filepath.Join(settings.Path, "wip.txt") + buf, _ = os.ReadFile(name) + whiteIpList := scanBuf(buf) + + if blackIpList.NumRanges() == 0 && whiteIpList.NumRanges() == 0 { + return emptyFN + } + + return func(c *gin.Context) { + arr := strings.Split(c.Request.RemoteAddr, ":") + if len(arr) > 0 { + ip := net.ParseIP(arr[0]) + minifyIP(&ip) + if whiteIpList.NumRanges() > 0 { + if _, ok := whiteIpList.Lookup(ip); !ok { + log.WebLogln("Block ip, not in white list", ip.String()) + c.String(http.StatusTeapot, "Banned") + c.Abort() + return + } + } + if blackIpList.NumRanges() > 0 { + if r, ok := blackIpList.Lookup(ip); ok { + log.WebLogln("Block ip, in black list:", ip.String(), "in range", r.Description, ":", r.First, "-", r.Last) + c.String(http.StatusTeapot, "Banned") + c.Abort() + return + } + } + } + c.Next() + } +} + +func scanBuf(buf []byte) Ranger { + if len(buf) == 0 { + return New(nil) + } + var ranges []Range + scanner := bufio.NewScanner(strings.NewReader(string(buf))) + for scanner.Scan() { + r, ok, err := parseLine(scanner.Bytes()) + if err != nil { + log.TLogln("Error scan ip list:", err) + return New(nil) + } + if ok { + ranges = append(ranges, r) + } + } + err := scanner.Err() + if err != nil { + log.TLogln("Error scan ip list:", err) + } + if len(ranges) > 0 { + return New(ranges) + } + return New(nil) +} + +func parseLine(l []byte) (r Range, ok bool, err error) { + l = bytes.TrimSpace(l) + if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) { + return + } + colon := bytes.LastIndexAny(l, ":") + hyphen := bytes.IndexByte(l[colon+1:], '-') + hyphen += colon + 1 + if colon >= 0 { + r.Description = string(l[:colon]) + } + if hyphen-(colon+1) >= 0 { + r.First = net.ParseIP(string(l[colon+1 : hyphen])) + minifyIP(&r.First) + r.Last = net.ParseIP(string(l[hyphen+1:])) + minifyIP(&r.Last) + } else { + r.First = net.ParseIP(string(l[colon+1:])) + minifyIP(&r.First) + r.Last = r.First + } + if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) { + err = errors.New("bad IP range") + return + } + ok = true + return +} diff --git a/server/web/blocker/iplist.go b/server/web/blocker/iplist.go new file mode 100644 index 0000000..690a7d0 --- /dev/null +++ b/server/web/blocker/iplist.go @@ -0,0 +1,87 @@ +package blocker + +import ( + "bytes" + "fmt" + "net" +) + +type Ranger interface { + Lookup(net.IP) (r Range, ok bool) + NumRanges() int +} + +type IPList struct { + ranges []Range +} + +type Range struct { + First, Last net.IP + Description string +} + +func (r Range) String() string { + return fmt.Sprintf("%s-%s: %s", r.First, r.Last, r.Description) +} + +// Create a new IP list. The given ranges must already sorted by the lower +// bound IP in each range. Behaviour is undefined for lists of overlapping +// ranges. +func New(initSorted []Range) *IPList { + return &IPList{ + ranges: initSorted, + } +} + +func (ipl *IPList) NumRanges() int { + if ipl == nil { + return 0 + } + return len(ipl.ranges) +} + +// Return the range the given IP is in. ok if false if no range is found. +func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) { + if ipl == nil { + return + } + v4 := ip.To4() + if v4 != nil { + r, ok = ipl.lookup(v4) + if ok { + return + } + } + v6 := ip.To16() + if v6 != nil { + return ipl.lookup(v6) + } + if v4 == nil && v6 == nil { + r = Range{ + Description: "bad IP", + } + ok = true + } + return +} + +// Return the range the given IP is in. Returns nil if no range is found. +func (ipl *IPList) lookup(ip net.IP) (Range, bool) { + var rng Range + ok := false + for _, r := range ipl.ranges { + ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0 + if ok { + rng = r + break + } + } + return rng, ok +} + +func minifyIP(ip *net.IP) { + v4 := ip.To4() + if v4 != nil { + *ip = append(make([]byte, 0, 4), v4...) + } +} diff --git a/server/web/https_redirect.go b/server/web/https_redirect.go new file mode 100644 index 0000000..122f66a --- /dev/null +++ b/server/web/https_redirect.go @@ -0,0 +1,48 @@ +package web + +import ( + "net" + "net/http" + "net/url" + + "server/log" + "server/settings" +) + +func runHTTPRedirectToHTTPS(addr string) error { + h := func(w http.ResponseWriter, r *http.Request) { + target := buildHTTPSRedirectTarget(r) + http.Redirect(w, r, target, http.StatusTemporaryRedirect) + } + log.TLogln("Start http server (redirect to https) at", addr) + return http.ListenAndServe(addr, http.HandlerFunc(h)) +} + +func buildHTTPSRedirectTarget(r *http.Request) string { + host := r.Host + hostName, _, err := net.SplitHostPort(host) + if err != nil { + hostName = host + } + sslPort := settings.SslPort + if sslPort == "" { + sslPort = "8091" + } + var httpsHost string + if sslPort == "443" { + httpsHost = hostName + } else { + httpsHost = net.JoinHostPort(hostName, sslPort) + } + path := r.URL.EscapedPath() + if path == "" { + path = "/" + } + u := &url.URL{ + Scheme: "https", + Host: httpsHost, + Path: path, + RawQuery: r.URL.RawQuery, + } + return u.String() +} diff --git a/server/web/msx/msx.go b/server/web/msx/msx.go new file mode 100644 index 0000000..eb991d3 --- /dev/null +++ b/server/web/msx/msx.go @@ -0,0 +1,179 @@ +package msx + +import ( + "encoding/json" + "errors" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "server/settings" + "server/torr" + "server/utils" + "server/version" + "server/web/auth" + + "github.com/gin-gonic/gin" +) + +const base, files = "tsmsx.yourok.ru", "media" + +var param = "menu:request:interaction:{SERVER}@{PREFIX}" + base + "/start.html" + +func trn(h string) (st, sc string) { + if h := torr.GetTorrent(h); h != nil { + if h := h.Status(); h != nil && h.Stat < 5 { + switch h.Stat { + case 4: + sc = "msx-red" + case 3: + sc = "msx-green" + default: + sc = "msx-yellow" + } + st = "{ico:north} " + strconv.Itoa(h.ActivePeers) + " / " + strconv.Itoa(h.TotalPeers) + " {ico:south} " + strconv.Itoa(h.ConnectedSeeders) + } + } + return +} + +func rsp(c *gin.Context, r *http.Response, e error) { + if e != nil { + c.AbortWithError(http.StatusInternalServerError, e) + } else { + defer r.Body.Close() + c.DataFromReader(r.StatusCode, r.ContentLength, r.Header.Get("Content-Type"), r.Body, nil) + } +} + +func SetupRoute(r gin.IRouter) { + authorized := r.Group("/", auth.CheckAuth()) + // MSX: + authorized.GET("/msx/", func(c *gin.Context) { + r, e := http.Get("http://" + base) + rsp(c, r, e) + }) + authorized.GET("/msx/start.json", func(c *gin.Context) { + c.JSON(http.StatusOK, map[string]any{ + "name": "TorrServer", + "version": version.Version, + "parameter": param, + "launcher": map[string]any{ + "type": "start", + "image": utils.GetScheme(c) + "://" + c.Request.Host + "/logo.png", + }, + }) + }) + authorized.POST("/msx/start.json", func(c *gin.Context) { + if e := c.BindJSON(¶m); e != nil { + c.AbortWithError(http.StatusBadRequest, e) + } + }) + authorized.GET("/msx/trn", func(c *gin.Context) { + r := false + if h := c.Query("hash"); h != "" { + for _, t := range settings.ListTorrent() { + if r = (t != nil && t.InfoHash.HexString() == h); r { + break + } + } + } + c.JSON(http.StatusOK, r) + }) + authorized.POST("/msx/trn", func(c *gin.Context) { + var r struct { + R struct { + S int `json:"status"` + T string `json:"text"` + M string `json:"message,omitempty"` + D map[string]any `json:"data,omitempty"` + } `json:"response"` + } + if j := struct{ Data string }{Data: c.Query("hash")}; j.Data != "" { + st, sc := trn(j.Data) + if sc != "" { + sc = "{col:" + sc + "}" + } + r.R.S, r.R.D = http.StatusOK, map[string]any{"action": "player:label:position:{VALUE}{tb}{tb}" + sc + st} + } else if e := c.BindJSON(&j); e != nil { + r.R.S, r.R.M = http.StatusBadRequest, e.Error() + } else if j.Data == "" { + r.R.S, r.R.M = http.StatusBadRequest, "data is not set" + } else { + st, sc := trn(j.Data[strings.LastIndexByte(j.Data, ':')+1:]) + r.R.D = map[string]any{"stamp": st, "stampColor": sc} + if sc != "" { + r.R.D["live"] = map[string]any{ + "type": "airtime", "duration": 3000, "over": map[string]any{ + "action": "execute:" + utils.GetScheme(c) + "://" + c.Request.Host + c.Request.URL.Path, "data": j.Data, + }, + } + } + r.R.S, r.R.D = http.StatusOK, map[string]any{"action": j.Data, "data": r.R.D} + } + r.R.T = http.StatusText(r.R.S) + c.JSON(http.StatusOK, &r) + }) + authorized.Any("/msx/proxy", func(c *gin.Context) { + if u := c.Query("url"); u == "" { + c.AbortWithStatus(http.StatusBadRequest) + } else if q, e := http.NewRequest(c.Request.Method, u, c.Request.Body); e != nil { + c.AbortWithError(http.StatusInternalServerError, e) + } else { + for _, v := range c.QueryArray("header") { + if v := strings.SplitN(v, ":", 2); len(v) == 2 { + q.Header.Add(v[0], v[1]) + } + } + r, e := http.DefaultClient.Do(q) + rsp(c, r, e) + } + }) + authorized.GET("/msx/imdb/:id", func(c *gin.Context) { + i, j := strings.TrimPrefix(c.Param("id"), "/"), false + if j = strings.HasSuffix(i, ".json"); !j { + i += ".json" + } + if r, e := http.Get("https://v2.sg.media-imdb.com/suggestion/h/" + i); e != nil || r.StatusCode != http.StatusOK || j { + rsp(c, r, e) + } else { + var j struct { + D []struct{ I struct{ ImageUrl string } } + } + if e = json.NewDecoder(r.Body).Decode(&j); e != nil { + c.AbortWithError(http.StatusInternalServerError, e) + } else if len(j.D) == 0 || j.D[0].I.ImageUrl == "" { + c.Status(http.StatusNotFound) + } else { + c.Redirect(http.StatusMovedPermanently, j.D[0].I.ImageUrl) + } + } + }) + // Files: + authorized.StaticFS("/files/", gin.Dir(filepath.Join(settings.Path, files), true)) + authorized.GET("/files", func(c *gin.Context) { + if l, e := os.Readlink(filepath.Join(settings.Path, files)); e == nil || os.IsNotExist(e) { + c.JSON(http.StatusOK, l) + } else { + c.JSON(http.StatusInternalServerError, e.Error) + } + }) + authorized.POST("/files", func(c *gin.Context) { + var l string + if e := c.BindJSON(&l); e != nil { + c.AbortWithError(http.StatusBadRequest, e) + } else if e = os.Remove(filepath.Join(settings.Path, files)); e != nil && !os.IsNotExist(e) { + c.AbortWithError(http.StatusInternalServerError, e) + } else if l != "" { + if f, e := os.Stat(l); e != nil { + c.AbortWithError(http.StatusBadRequest, e) + } else if !f.IsDir() { + c.AbortWithError(http.StatusBadRequest, errors.New(l+" is not a directory")) + } else if e = os.Symlink(l, filepath.Join(settings.Path, files)); e != nil { + c.AbortWithError(http.StatusInternalServerError, e) + } + } + }) +} diff --git a/server/web/pages/route.go b/server/web/pages/route.go new file mode 100644 index 0000000..34054cd --- /dev/null +++ b/server/web/pages/route.go @@ -0,0 +1,83 @@ +package pages + +import ( + "net/http" + "server/proxy" + + "github.com/anacrolix/torrent/metainfo" + "github.com/gin-gonic/gin" + + "server/settings" + "server/torr" + "server/web/auth" + "server/web/pages/template" + + "golang.org/x/exp/slices" +) + +func SetupRoute(route gin.IRouter) { + authorized := route.Group("/", auth.CheckAuth()) + + webPagesAuth := route.Group("/", func() gin.HandlerFunc { + return func(c *gin.Context) { + if slices.Contains([]string{"/site.webmanifest"}, c.FullPath()) { + return + } + auth.CheckAuth()(c) + } + }()) + + template.RouteWebPages(webPagesAuth) + authorized.GET("/stat", statPage) + authorized.GET("/magnets", getTorrents) + authorized.Any("/proxy/*url", proxyUrl) +} + +// stat godoc +// +// @Summary TorrServer Statistics +// @Description Show server and torrents statistics. +// +// @Tags Pages +// +// @Produce text/plain +// @Success 200 "TorrServer statistics" +// @Router /stat [get] +func statPage(c *gin.Context) { + torr.WriteStatus(c.Writer) + c.Status(200) +} + +// getTorrents godoc +// +// @Summary Get HTML of magnet links +// @Description Get HTML of magnet links. +// +// @Tags Pages +// +// @Produce text/html +// @Success 200 "HTML with Magnet links" +// @Router /magnets [get] +func getTorrents(c *gin.Context) { + list := settings.ListTorrent() + http := "<div>" + for _, db := range list { + ts := db.TorrentSpec + mi := metainfo.MetaInfo{ + AnnounceList: ts.Trackers, + } + // mag := mi.Magnet(ts.DisplayName, ts.InfoHash) + mag := mi.Magnet(&ts.InfoHash, &metainfo.Info{Name: ts.DisplayName}) + http += "<p><a href='" + mag.String() + "'>magnet:?xt=urn:btih:" + mag.InfoHash.HexString() + "</a></p>" + } + http += "</div>" + c.Data(200, "text/html; charset=utf-8", []byte(http)) +} + +func proxyUrl(c *gin.Context) { + if proxy.P2Proxy != nil { + proxy.P2Proxy.GinHandler(c) + return + } + c.AbortWithStatus(http.StatusNotFound) +} diff --git a/server/web/pages/template/html.go b/server/web/pages/template/html.go new file mode 100644 index 0000000..4730787 --- /dev/null +++ b/server/web/pages/template/html.go @@ -0,0 +1,149 @@ +package template + +import ( + _ "embed" +) + +//go:embed pages/apple-splash-1125-2436.jpg +var Applesplash11252436jpg []byte + +//go:embed pages/apple-splash-1136-640.jpg +var Applesplash1136640jpg []byte + +//go:embed pages/apple-splash-1170-2532.jpg +var Applesplash11702532jpg []byte + +//go:embed pages/apple-splash-1242-2208.jpg +var Applesplash12422208jpg []byte + +//go:embed pages/apple-splash-1242-2688.jpg +var Applesplash12422688jpg []byte + +//go:embed pages/apple-splash-1284-2778.jpg +var Applesplash12842778jpg []byte + +//go:embed pages/apple-splash-1334-750.jpg +var Applesplash1334750jpg []byte + +//go:embed pages/apple-splash-1536-2048.jpg +var Applesplash15362048jpg []byte + +//go:embed pages/apple-splash-1620-2160.jpg +var Applesplash16202160jpg []byte + +//go:embed pages/apple-splash-1668-2224.jpg +var Applesplash16682224jpg []byte + +//go:embed pages/apple-splash-1668-2388.jpg +var Applesplash16682388jpg []byte + +//go:embed pages/apple-splash-1792-828.jpg +var Applesplash1792828jpg []byte + +//go:embed pages/apple-splash-2048-1536.jpg +var Applesplash20481536jpg []byte + +//go:embed pages/apple-splash-2048-2732.jpg +var Applesplash20482732jpg []byte + +//go:embed pages/apple-splash-2160-1620.jpg +var Applesplash21601620jpg []byte + +//go:embed pages/apple-splash-2208-1242.jpg +var Applesplash22081242jpg []byte + +//go:embed pages/apple-splash-2224-1668.jpg +var Applesplash22241668jpg []byte + +//go:embed pages/apple-splash-2388-1668.jpg +var Applesplash23881668jpg []byte + +//go:embed pages/apple-splash-2436-1125.jpg +var Applesplash24361125jpg []byte + +//go:embed pages/apple-splash-2532-1170.jpg +var Applesplash25321170jpg []byte + +//go:embed pages/apple-splash-2688-1242.jpg +var Applesplash26881242jpg []byte + +//go:embed pages/apple-splash-2732-2048.jpg +var Applesplash27322048jpg []byte + +//go:embed pages/apple-splash-2778-1284.jpg +var Applesplash27781284jpg []byte + +//go:embed pages/apple-splash-640-1136.jpg +var Applesplash6401136jpg []byte + +//go:embed pages/apple-splash-750-1334.jpg +var Applesplash7501334jpg []byte + +//go:embed pages/apple-splash-828-1792.jpg +var Applesplash8281792jpg []byte + +//go:embed pages/asset-manifest.json +var Assetmanifestjson []byte + +//go:embed pages/browserconfig.xml +var Browserconfigxml []byte + +//go:embed pages/dlnaicon-120.png +var Dlnaicon120png []byte + +//go:embed pages/dlnaicon-48.png +var Dlnaicon48png []byte + +//go:embed pages/favicon-16x16.png +var Favicon16x16png []byte + +//go:embed pages/favicon-32x32.png +var Favicon32x32png []byte + +//go:embed pages/favicon.ico +var Faviconico []byte + +//go:embed pages/icon.png +var Iconpng []byte + +//go:embed pages/index.html +var Indexhtml []byte + +//go:embed pages/logo.png +var Logopng []byte + +//go:embed pages/lordicon/jkzgajyr.json +var Lordiconjkzgajyrjson []byte + +//go:embed pages/lordicon/lord-icon-2.0.2.js +var Lordiconlordicon202js []byte + +//go:embed pages/lordicon/wrprwmwt.json +var Lordiconwrprwmwtjson []byte + +//go:embed pages/mstile-150x150.png +var Mstile150x150png []byte + +//go:embed pages/site.webmanifest +var Sitewebmanifest []byte + +//go:embed pages/static/js/2.17225dbd.chunk.js +var Staticjs217225dbdchunkjs []byte + +//go:embed pages/static/js/2.17225dbd.chunk.js.LICENSE.txt +var Staticjs217225dbdchunkjsLICENSEtxt []byte + +//go:embed pages/static/js/2.17225dbd.chunk.js.map +var Staticjs217225dbdchunkjsmap []byte + +//go:embed pages/static/js/main.c7b9a3c5.chunk.js +var Staticjsmainc7b9a3c5chunkjs []byte + +//go:embed pages/static/js/main.c7b9a3c5.chunk.js.map +var Staticjsmainc7b9a3c5chunkjsmap []byte + +//go:embed pages/static/js/runtime-main.5ed86a79.js +var Staticjsruntimemain5ed86a79js []byte + +//go:embed pages/static/js/runtime-main.5ed86a79.js.map +var Staticjsruntimemain5ed86a79jsmap []byte diff --git a/server/web/pages/template/pages/apple-splash-1125-2436.jpg b/server/web/pages/template/pages/apple-splash-1125-2436.jpg new file mode 100644 index 0000000..5d9ce51 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1125-2436.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1136-640.jpg b/server/web/pages/template/pages/apple-splash-1136-640.jpg new file mode 100644 index 0000000..78fe6e0 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1136-640.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1170-2532.jpg b/server/web/pages/template/pages/apple-splash-1170-2532.jpg new file mode 100644 index 0000000..10cd777 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1170-2532.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1242-2208.jpg b/server/web/pages/template/pages/apple-splash-1242-2208.jpg new file mode 100644 index 0000000..5812ea0 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1242-2208.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1242-2688.jpg b/server/web/pages/template/pages/apple-splash-1242-2688.jpg new file mode 100644 index 0000000..57cf34f Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1242-2688.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1284-2778.jpg b/server/web/pages/template/pages/apple-splash-1284-2778.jpg new file mode 100644 index 0000000..1609ef3 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1284-2778.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1334-750.jpg b/server/web/pages/template/pages/apple-splash-1334-750.jpg new file mode 100644 index 0000000..71d1b75 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1334-750.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1536-2048.jpg b/server/web/pages/template/pages/apple-splash-1536-2048.jpg new file mode 100644 index 0000000..3e2d7aa Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1536-2048.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1620-2160.jpg b/server/web/pages/template/pages/apple-splash-1620-2160.jpg new file mode 100644 index 0000000..6293c63 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1620-2160.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1668-2224.jpg b/server/web/pages/template/pages/apple-splash-1668-2224.jpg new file mode 100644 index 0000000..4321611 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1668-2224.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1668-2388.jpg b/server/web/pages/template/pages/apple-splash-1668-2388.jpg new file mode 100644 index 0000000..79bf72e Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1668-2388.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-1792-828.jpg b/server/web/pages/template/pages/apple-splash-1792-828.jpg new file mode 100644 index 0000000..caeaaa8 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-1792-828.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2048-1536.jpg b/server/web/pages/template/pages/apple-splash-2048-1536.jpg new file mode 100644 index 0000000..a32092f Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2048-1536.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2048-2732.jpg b/server/web/pages/template/pages/apple-splash-2048-2732.jpg new file mode 100644 index 0000000..e1a776e Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2048-2732.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2160-1620.jpg b/server/web/pages/template/pages/apple-splash-2160-1620.jpg new file mode 100644 index 0000000..d8ed062 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2160-1620.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2208-1242.jpg b/server/web/pages/template/pages/apple-splash-2208-1242.jpg new file mode 100644 index 0000000..3b844f1 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2208-1242.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2224-1668.jpg b/server/web/pages/template/pages/apple-splash-2224-1668.jpg new file mode 100644 index 0000000..d8c0db2 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2224-1668.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2388-1668.jpg b/server/web/pages/template/pages/apple-splash-2388-1668.jpg new file mode 100644 index 0000000..fde19f2 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2388-1668.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2436-1125.jpg b/server/web/pages/template/pages/apple-splash-2436-1125.jpg new file mode 100644 index 0000000..665e09b Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2436-1125.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2532-1170.jpg b/server/web/pages/template/pages/apple-splash-2532-1170.jpg new file mode 100644 index 0000000..5da4dd8 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2532-1170.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2688-1242.jpg b/server/web/pages/template/pages/apple-splash-2688-1242.jpg new file mode 100644 index 0000000..270bedf Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2688-1242.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2732-2048.jpg b/server/web/pages/template/pages/apple-splash-2732-2048.jpg new file mode 100644 index 0000000..d5c52ed Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2732-2048.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-2778-1284.jpg b/server/web/pages/template/pages/apple-splash-2778-1284.jpg new file mode 100644 index 0000000..ec3e53c Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-2778-1284.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-640-1136.jpg b/server/web/pages/template/pages/apple-splash-640-1136.jpg new file mode 100644 index 0000000..16eb1db Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-640-1136.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-750-1334.jpg b/server/web/pages/template/pages/apple-splash-750-1334.jpg new file mode 100644 index 0000000..05c3d68 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-750-1334.jpg differ diff --git a/server/web/pages/template/pages/apple-splash-828-1792.jpg b/server/web/pages/template/pages/apple-splash-828-1792.jpg new file mode 100644 index 0000000..b8801b3 Binary files /dev/null and b/server/web/pages/template/pages/apple-splash-828-1792.jpg differ diff --git a/server/web/pages/template/pages/asset-manifest.json b/server/web/pages/template/pages/asset-manifest.json new file mode 100644 index 0000000..75f9e1a --- /dev/null +++ b/server/web/pages/template/pages/asset-manifest.json @@ -0,0 +1,17 @@ +{ + "files": { + "main.js": "./static/js/main.c7b9a3c5.chunk.js", + "main.js.map": "./static/js/main.c7b9a3c5.chunk.js.map", + "runtime-main.js": "./static/js/runtime-main.5ed86a79.js", + "runtime-main.js.map": "./static/js/runtime-main.5ed86a79.js.map", + "static/js/2.17225dbd.chunk.js": "./static/js/2.17225dbd.chunk.js", + "static/js/2.17225dbd.chunk.js.map": "./static/js/2.17225dbd.chunk.js.map", + "index.html": "./index.html", + "static/js/2.17225dbd.chunk.js.LICENSE.txt": "./static/js/2.17225dbd.chunk.js.LICENSE.txt" + }, + "entrypoints": [ + "static/js/runtime-main.5ed86a79.js", + "static/js/2.17225dbd.chunk.js", + "static/js/main.c7b9a3c5.chunk.js" + ] +} \ No newline at end of file diff --git a/server/web/pages/template/pages/browserconfig.xml b/server/web/pages/template/pages/browserconfig.xml new file mode 100644 index 0000000..cfe8930 --- /dev/null +++ b/server/web/pages/template/pages/browserconfig.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> + <msapplication> + <tile> + <square150x150logo src="/mstile-150x150.png"/> + <TileColor>#00a572</TileColor> + <TileImage src="/mstile-150x150.png" /> + </tile> + </msapplication> +</browserconfig> diff --git a/server/web/pages/template/pages/dlnaicon-120.png b/server/web/pages/template/pages/dlnaicon-120.png new file mode 100644 index 0000000..bf55d61 Binary files /dev/null and b/server/web/pages/template/pages/dlnaicon-120.png differ diff --git a/server/web/pages/template/pages/dlnaicon-48.png b/server/web/pages/template/pages/dlnaicon-48.png new file mode 100644 index 0000000..fbead48 Binary files /dev/null and b/server/web/pages/template/pages/dlnaicon-48.png differ diff --git a/server/web/pages/template/pages/favicon-16x16.png b/server/web/pages/template/pages/favicon-16x16.png new file mode 100644 index 0000000..27a879a Binary files /dev/null and b/server/web/pages/template/pages/favicon-16x16.png differ diff --git a/server/web/pages/template/pages/favicon-32x32.png b/server/web/pages/template/pages/favicon-32x32.png new file mode 100644 index 0000000..9c339e0 Binary files /dev/null and b/server/web/pages/template/pages/favicon-32x32.png differ diff --git a/server/web/pages/template/pages/favicon.ico b/server/web/pages/template/pages/favicon.ico new file mode 100644 index 0000000..0d793ce Binary files /dev/null and b/server/web/pages/template/pages/favicon.ico differ diff --git a/server/web/pages/template/pages/icon.png b/server/web/pages/template/pages/icon.png new file mode 100644 index 0000000..ae07815 Binary files /dev/null and b/server/web/pages/template/pages/icon.png differ diff --git a/server/web/pages/template/pages/index.html b/server/web/pages/template/pages/index.html new file mode 100644 index 0000000..e491f60 --- /dev/null +++ b/server/web/pages/template/pages/index.html @@ -0,0 +1 @@ +<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="node-browser-folder" content="/files/"/><link rel="manifest" href="/site.webmanifest" crossorigin="use-credentials"><meta name="msapplication-TileColor" content="#00a572"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="theme-color" content="#ffffff"><link rel="preconnect" href="https://fonts.gstatic.com"><link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&display=swap" rel="stylesheet"><meta name="viewport" content="width=device-width,shrink-to-fit=no,viewport-fit=cover,user-scalable=no"><meta name="description" content="TorrServer - torrent to http stream"><title>TorrServer MatriX
\ No newline at end of file diff --git a/server/web/pages/template/pages/logo.png b/server/web/pages/template/pages/logo.png new file mode 100644 index 0000000..fbf2cb0 Binary files /dev/null and b/server/web/pages/template/pages/logo.png differ diff --git a/server/web/pages/template/pages/lordicon/jkzgajyr.json b/server/web/pages/template/pages/lordicon/jkzgajyr.json new file mode 100644 index 0000000..428765d --- /dev/null +++ b/server/web/pages/template/pages/lordicon/jkzgajyr.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":201,"w":500,"h":500,"nm":"system-regular-44-folder","ddd":0,"assets":[{"id":"comp_0","nm":"in-folder","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36.021,46.831,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[-0.068,0],[-121,0],[0,-11.469],[0,0]],"o":[[0,0],[0.031,-0.061],[0,0],[13,0],[0,22.281],[0,0]],"v":[[-228.932,-136.624],[-228.185,-197.778],[-228.024,-197.876],[135.226,-197.919],[156.754,-176.365],[156.522,-136.624]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":23,"s":[{"i":[[0,0],[0,0],[-7.021,0],[0,0],[6.306,-12.408],[0,0]],"o":[[0,0],[3.181,-6.259],[0,0],[13.918,0],[0,0],[0,0]],"v":[[-228.728,82.376],[-150.185,-72.175],[-133.559,-82.376],[210.053,-82.376],[226.68,-55.276],[156.975,89.376]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,0],[0,0],[-6.996,0],[-0.421,0],[6.284,-12.404],[0,0]],"o":[[0,0],[3.17,-6.237],[0,0],[13.915,0],[0,0.077],[0,0]],"v":[[-228.728,82.376],[-154.821,-72.349],[-138.251,-82.515],[205.429,-82.516],[222.073,-55.434],[156.973,89.31]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[-6.852,0],[-2.926,0],[6.153,-12.385],[0,0]],"o":[[0,0],[3.104,-6.109],[0,0],[13.896,0],[0,0.539],[0,0]],"v":[[-228.728,82.376],[-161.56,-74.41],[-145.331,-84.367],[198.755,-84.368],[215.5,-57.401],[156.959,88.915]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":33,"s":[{"i":[[0,0],[0,0],[-6.369,0],[-11.332,0],[5.715,-12.32],[0,0]],"o":[[0,0],[2.886,-5.678],[0,0],[13.832,0],[0,2.087],[0,0]],"v":[[-228.728,82.376],[-171.001,-82.524],[-155.917,-91.779],[189.534,-91.783],[206.62,-65.201],[156.911,87.588]],"c":false}]},{"i":{"x":0.833,"y":0.758},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,0],[0,0],[-4.472,0],[-44.362,0],[3.994,-12.063],[0,0]],"o":[[0,0],[2.026,-3.986],[0,0],[13.581,0],[0,8.169],[0,0]],"v":[[-228.728,82.376],[-181.782,-106.585],[-171.192,-113.082],[179.619,-113.098],[198.043,-88.031],[156.725,82.376]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.242},"t":40,"s":[{"i":[[0,0],[0,0],[-0.821,0],[-107.898,0],[0.683,-11.57],[0,0]],"o":[[0,0],[0.372,-0.732],[0,0],[13.099,0],[0,19.869],[0,0]],"v":[[-228.728,82.376],[-219.739,-132.891],[-217.796,-134.083],[143.328,-134.121],[164.326,-111.967],[156.725,82.376]],"c":false}]},{"t":43,"s":[{"i":[[0,0],[0,0],[-0.068,0],[-121,0],[0,-11.469],[0,0]],"o":[[0,0],[0.031,-0.061],[0,0],[13,0],[0,22.281],[0,0]],"v":[[-228.728,82.376],[-228.185,-135.778],[-228.024,-135.876],[135.226,-135.919],[156.754,-114.365],[156.725,82.376]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":39,"s":[27]},{"t":40,"s":[48]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[215.503,171.002,0],"to":[-0.25,18.167,0],"ti":[0.338,-14.587,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.553,"y":0},"t":17,"s":[214.003,280.002,0],"to":[16,-16.708,0],"ti":[0.312,11.375,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":43,"s":[250.003,217.752,0],"to":[-0.125,3.565,0],"ti":[0,-2.375,0]},{"t":59,"s":[250.003,250.002,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[11.506,0],[0,0],[0,0],[0,0],[0,0],[-11.506,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,11.506],[0,0],[11.506,0],[0,0],[0,-11.505]],"v":[[171.544,-150.541],[-21.163,-150.541],[-52.083,-151.041],[-192.707,-151.041],[-192.911,-110.793],[-172.077,-89.959],[171.67,-89.959],[192.503,-110.793],[192.377,-129.708]],"c":true}]},{"t":23,"s":[{"i":[[11.506,0],[0,0],[0,0],[0,0],[0,0],[-11.506,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,11.506],[0,0],[11.506,0],[0,0],[0,-11.505]],"v":[[171.874,-88.541],[-20.833,-88.541],[-52.083,-151.041],[-192.707,-151.041],[-192.707,130.207],[-171.874,151.041],[171.874,151.041],[192.707,130.207],[192.707,-67.708]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[8.4]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[6]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34.725,"s":[8]},{"t":40.2421875,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[96.8]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[96]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0]},"t":23,"s":[92.5]},{"t":40.2421875,"s":[93.5]}],"ix":2},"o":{"a":0,"k":-39,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.14,0],[0,0],[0,0.14],[0,0],[0,0],[0,0],[-0.28,0],[0,0],[0,-0.14]],"o":[[0,0.14],[0,0],[-0.14,0],[0,0],[0,0],[0,0],[0.13,0.25],[0,0],[0.14,0],[0,0]],"v":[[8.5,6.25],[8.25,6.5],[-8.25,6.5],[-8.5,6.25],[-8.5,-6.5],[-2.96,-6.5],[-1.67,-3.91],[-1,-3.5],[8.25,-3.5],[8.5,-3.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.96,0],[0,0],[0,0],[0.28,0],[0,0],[0,-0.41],[0,0],[-0.96,0],[0,0],[0,0.96],[0,0]],"o":[[0,0],[0,0],[-0.13,-0.25],[0,0],[-0.41,0],[0,0],[0,0.96],[0,0],[0.96,0],[0,0],[0,-0.96]],"v":[[8.25,-5],[-0.54,-5],[-1.83,-7.59],[-2.5,-8],[-9.25,-8],[-10,-7.25],[-10,6.25],[-8.25,8],[8.25,8],[10,6.25],[10,-3.25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"hover-folder","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36.021,46.831,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[-0.068,0],[-121,0],[0,-11.469],[0,0]],"o":[[0,0],[0.031,-0.061],[0,0],[13,0],[0,22.281],[0,0]],"v":[[-228.728,82.376],[-228.185,-135.778],[-228.024,-135.876],[135.226,-135.919],[156.754,-114.365],[156.725,82.376]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[0,0],[-0.821,0],[-107.898,0],[0.683,-11.57],[0,0]],"o":[[0,0],[0.372,-0.732],[0,0],[13.099,0],[0,19.869],[0,0]],"v":[[-228.728,82.376],[-219.739,-134.891],[-217.796,-136.083],[143.328,-136.121],[164.326,-113.967],[156.725,82.376]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[0,0],[-3.893,0],[-54.433,0],[3.469,-11.985],[0,0]],"o":[[0,0],[1.764,-3.47],[0,0],[13.505,0],[0,10.023],[0,0]],"v":[[-228.728,82.376],[-179.275,-111.814],[-170.055,-117.471],[182.391,-117.49],[201.223,-92.884],[156.725,82.376]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":30,"s":[{"i":[[0,0],[0,0],[-7.021,0],[0,0],[6.306,-12.408],[0,0]],"o":[[0,0],[3.181,-6.259],[0,0],[13.918,0],[0,0],[0,0]],"v":[[-228.728,82.376],[-150.185,-72.175],[-133.559,-82.376],[210.053,-82.376],[226.68,-55.276],[156.975,89.376]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0],[-4.472,0],[-44.362,0],[3.994,-12.063],[0,0]],"o":[[0,0],[2.026,-3.986],[0,0],[13.581,0],[0,8.169],[0,0]],"v":[[-228.728,82.376],[-170.782,-113.138],[-160.192,-119.635],[190.619,-119.651],[209.043,-94.584],[156.725,82.376]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":55,"s":[{"i":[[0,0],[0,0],[-0.821,0],[-107.898,0],[0.683,-11.57],[0,0]],"o":[[0,0],[0.372,-0.732],[0,0],[13.099,0],[0,19.869],[0,0]],"v":[[-228.728,82.376],[-219.739,-132.891],[-217.796,-134.083],[143.328,-134.121],[164.326,-111.967],[156.725,82.376]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0],[-0.068,0],[-121,0],[0,-11.469],[0,0]],"o":[[0,0],[0.031,-0.061],[0,0],[13,0],[0,22.281],[0,0]],"v":[[-228.728,82.376],[-228.185,-135.778],[-228.024,-135.876],[135.226,-135.919],[156.754,-114.365],[156.725,82.376]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[48]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[27]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":53,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":54,"s":[27]},{"t":55,"s":[48]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.003,250.002,0],"to":[-6,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":30,"s":[214.003,250.002,0],"to":[0,0,0],"ti":[-6,0,0]},{"t":59,"s":[250.003,250.002,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.506,0],[0,0],[0,0],[0,0],[0,0],[-11.506,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,11.506],[0,0],[11.506,0],[0,0],[0,-11.505]],"v":[[171.874,-88.541],[-20.833,-88.541],[-52.083,-151.041],[-192.707,-151.041],[-192.707,130.207],[-171.874,151.041],[171.874,151.041],[192.707,130.207],[192.707,-67.708]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[6]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[8]},{"t":55,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[93.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0]},"t":30,"s":[92.5]},{"t":55,"s":[93.5]}],"ix":2},"o":{"a":0,"k":-39,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.14,0],[0,0],[0,0.14],[0,0],[0,0],[0,0],[-0.28,0],[0,0],[0,-0.14]],"o":[[0,0.14],[0,0],[-0.14,0],[0,0],[0,0],[0,0],[0.13,0.25],[0,0],[0.14,0],[0,0]],"v":[[8.5,6.25],[8.25,6.5],[-8.25,6.5],[-8.5,6.25],[-8.5,-6.5],[-2.96,-6.5],[-1.67,-3.91],[-1,-3.5],[8.25,-3.5],[8.5,-3.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.96,0],[0,0],[0,0],[0.28,0],[0,0],[0,-0.41],[0,0],[-0.96,0],[0,0],[0,0.96],[0,0]],"o":[[0,0],[0,0],[-0.13,-0.25],[0,0],[-0.41,0],[0,0],[0,0.96],[0,0],[0.96,0],[0,0],[0,-0.96]],"v":[[8.25,-5],[-0.54,-5],[-1.83,-7.59],[-2.5,-8],[-9.25,-8],[-10,-7.25],[-10,6.25],[-8.25,8],[8.25,8],[10,6.25],[10,-3.25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.14,0],[0,0],[0,0.14],[0,0],[0,0],[0,0],[-0.28,0],[0,0],[0,-0.14]],"o":[[0,0.14],[0,0],[-0.14,0],[0,0],[0,0],[0,0],[0.13,0.25],[0,0],[0.14,0],[0,0]],"v":[[8.5,6.25],[8.25,6.5],[-8.25,6.5],[-8.5,6.25],[-8.5,-6.5],[-2.96,-6.5],[-1.67,-3.91],[-1,-3.5],[8.25,-3.5],[8.5,-3.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.96,0],[0,0],[0,0],[0.28,0],[0,0],[0,-0.41],[0,0],[-0.96,0],[0,0],[0,0.96],[0,0]],"o":[[0,0],[0,0],[-0.13,-0.25],[0,0],[-0.41,0],[0,0],[0,0.96],[0,0],[0.96,0],[0,0],[0,-0.96]],"v":[[8.25,-5],[-0.54,-5],[-1.83,-7.59],[-2.5,-8],[-9.25,-8],[-10,-7.25],[-10,6.25],[-8.25,8],[8.25,8],[10,6.25],[10,-3.25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"morph-folder","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36.021,46.831,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[-0.068,0],[-121,0],[0,-11.469],[0,0]],"o":[[0,0],[0.031,-0.061],[0,0],[13,0],[0,22.281],[0,0]],"v":[[-228.728,82.376],[-228.185,-135.778],[-228.024,-135.876],[135.226,-135.919],[156.754,-114.365],[156.725,82.376]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9.139,"s":[{"i":[[0,0],[0,0],[-0.821,0],[-107.898,0],[0.683,-11.57],[0,0]],"o":[[0,0],[0.372,-0.732],[0,0],[13.099,0],[0,19.869],[0,0]],"v":[[-228.728,82.376],[-219.739,-134.891],[-217.796,-136.083],[143.328,-136.121],[164.326,-113.967],[156.725,82.376]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":17.275,"s":[{"i":[[0,0],[0,0],[-3.893,0],[-54.433,0],[3.469,-11.985],[0,0]],"o":[[0,0],[1.764,-3.47],[0,0],[13.505,0],[0,10.023],[0,0]],"v":[[-228.728,82.376],[-179.275,-111.814],[-170.055,-117.471],[182.391,-117.49],[201.223,-92.884],[156.725,82.376]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0],[-7.021,0],[0,0],[6.306,-12.408],[0,0]],"o":[[0,0],[3.181,-6.259],[0,0],[13.918,0],[0,0],[0,0]],"v":[[-228.728,82.376],[-150.185,-72.175],[-133.559,-82.376],[210.053,-82.376],[226.68,-55.276],[156.975,89.376]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"t":8,"s":[48],"h":1},{"t":9,"s":[0],"h":1}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":182,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.003,250.002,0],"to":[-6,0,0],"ti":[6,0,0]},{"t":60,"s":[214.003,250.002,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.506,0],[0,0],[0,0],[0,0],[0,0],[-11.506,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,11.506],[0,0],[11.506,0],[0,0],[0,-11.505]],"v":[[171.874,-88.541],[-20.833,-88.541],[-52.083,-151.041],[-192.707,-151.041],[-192.707,130.207],[-171.874,151.041],[171.874,151.041],[192.707,130.207],[192.707,-67.708]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":23.379,"s":[8]},{"t":60,"s":[6]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[93.5]},{"t":60,"s":[92.5]}],"ix":2},"o":{"a":0,"k":-39,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":182,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.14,0],[0,0],[0,0.14],[0,0],[0,0],[0,0],[-0.28,0],[0,0],[0,-0.14]],"o":[[0,0.14],[0,0],[-0.14,0],[0,0],[0,0],[0,0],[0.13,0.25],[0,0],[0.14,0],[0,0]],"v":[[8.5,6.25],[8.25,6.5],[-8.25,6.5],[-8.5,6.25],[-8.5,-6.5],[-2.96,-6.5],[-1.67,-3.91],[-1,-3.5],[8.25,-3.5],[8.5,-3.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.96,0],[0,0],[0,0],[0.28,0],[0,0],[0,-0.41],[0,0],[-0.96,0],[0,0],[0,0.96],[0,0]],"o":[[0,0],[0,0],[-0.13,-0.25],[0,0],[-0.41,0],[0,0],[0,0.96],[0,0],[0.96,0],[0,0],[0,-0.96]],"v":[[8.25,-5],[-0.54,-5],[-1.83,-7.59],[-2.5,-8],[-9.25,-8],[-10,-7.25],[-10,6.25],[-8.25,8],[8.25,8],[10,6.25],[10,-3.25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-44-folder').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":1}}]}],"ip":0,"op":201,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"in-folder","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-folder","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":70,"op":140,"st":70,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"morph-folder","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":140,"op":210,"st":140,"bm":0}],"markers":[{"tm":0,"cm":"in-folder","dr":60},{"tm":70,"cm":"default:hover-folder","dr":60},{"tm":140,"cm":"morph-folder","dr":60}],"props":{}} \ No newline at end of file diff --git a/server/web/pages/template/pages/lordicon/lord-icon-2.0.2.js b/server/web/pages/template/pages/lordicon/lord-icon-2.0.2.js new file mode 100644 index 0000000..1c12bca --- /dev/null +++ b/server/web/pages/template/pages/lordicon/lord-icon-2.0.2.js @@ -0,0 +1 @@ +(function(){"use strict";function e(t){return JSON.parse(JSON.stringify(t))}function f(t){return null==t}function r(t){return null!==t&&"object"==typeof t}function t(t,e,i){const s=Array.isArray(e)?e:e.split(".");let a=t;for(const t of s){if(!r(a))return i;if(!(t in a))return i;a=a[t]}return void 0===a?i:a}function a(t,e,i){let r=t;const s=Array.isArray(e)?e:e.split(".");for(let t=0;tt)).map((t=>t.split(":"))).filter((t=>2==t.length)).reduce(((t,e)=>(t[e[0].toLowerCase()]=i(e[1]),t)),{})}function d(t){return"light"===t||1===t||"1"===t?1:"regular"===t||2===t||"2"===t?2:"bold"===t||3===t||"3"===t?3:"number"==typeof t||"string"==typeof t?+t:void 0}function c(t){if("string"==typeof t)return t}function u(t){const e=t.toString(16);return 1==e.length?"0"+e:e}function s(t){return Math.round(t/255*1e3)/1e3}function b(t){return Math.round(255*t)}function g(t){const{r:e,g:i,b:r}=function(t){let e=parseInt("#"!=t[0]?t:t.substring(1),16);return{r:e>>16&255,g:e>>8&255,b:255&e}}(t);return[s(e),s(i),s(r)]}function h(t){return function(t){return"#"+u(t.r)+u(t.g)+u(t.b)}({r:b(t[0]),g:b(t[1]),b:b(t[2])})}function p(t,{lottieInstance:e}={}){const i=[];return t&&t.layers?(t.layers.forEach(((t,r)=>{t.nm&&t.ef&&t.ef.forEach(((t,s)=>{const a=t?.ef?.[0]?.v?.k;if(void 0===a)return;let n,o;if(n=e?`renderer.elements.${r}.effectsManager.effectElements.${s}.effectElements.0.p.v`:`layers.${r}.ef.${s}.ef.0.v.k`,"ADBE Color Control"===t.mn?o="color":"ADBE Slider Control"===t.mn?o="slider":"ADBE Point Control"===t.mn?o="point":"ADBE Checkbox Control"===t.mn?o="checkbox":t.mn.startsWith("Pseudo/")&&(o="feature"),!o)return;const h=t.nm.toLowerCase();i.push({name:h,path:n,value:a,type:o})}))})),i):i}function y(t,e){for(const i of e)a(t,i.path,i.value)}function m(t,e,r){for(const n of e)"color"===n.type?"object"==typeof r&&"r"in r&&"g"in r&&"b"in r?a(t,n.path,[s(r.r),s(r.g),s(r.b)]):Array.isArray(r)?a(t,n.path,r):"string"==typeof r&&a(t,n.path,g(i(r))):"point"===n.type?"object"==typeof r&&"x"in r&&"y"in r?(a(t,n.path+".0",r.x),a(t,n.path+".1",r.y)):Array.isArray(r)&&(a(t,n.path+".0",r[0]),a(t,n.path+".1",r[1])):a(t,n.path,r)}const INTERSECTION_LOADING_EVENTS=["click","mouseenter","mouseleave"],SUPPORTS_ADOPTING_STYLE_SHEETS="adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ELEMENT_STYLE="\n :host {\n position: relative;\n display: inline-block;\n width: 32px;\n height: 32px;\n transform: translate3d(0px, 0px, 0px);\n }\n\n :host(.current-color) svg path[fill] {\n fill: currentColor;\n }\n\n :host(.current-color) svg path[stroke] {\n stroke: currentColor;\n }\n\n svg {\n position: absolute;\n pointer-events: none;\n display: block;\n transform: unset!important;\n }\n\n ::slotted(*) {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n height: 100%;\n }\n\n .body.ready ::slotted(*) {\n display: none;\n }\n";let styleSheet=null;const OBSERVED_ATTRIBUTES=["colors","src","icon","state","trigger","loading","target","stroke"];class Element extends HTMLElement{static _iconLoader;static _playerFactory;static _definedTriggers=new Map;static get version(){return"1.10.1"}static get observedAttributes(){return OBSERVED_ATTRIBUTES}static setIconLoader(t){Element._iconLoader=t}static setPlayerFactory(t){Element._playerFactory=t}static defineTrigger(t,e){Element._definedTriggers.set(t,e)}_root;_isConnected=!1;_isReady=!1;_assignedIconData;_loadedIconData;_triggerInstance;_playerInstance;delayedLoading=null;attributeChangedCallback(t,e,i){this[`${t}Changed`].call(this)}connectedCallback(){if(this._root||this.createElements(),"lazy"===this.loading){let t;this.delayedLoading=e=>{t.unobserve(this),t=void 0,this.delayedLoading=null,e||this.createPlayer()};t=new IntersectionObserver(((e,i)=>{e.forEach((e=>{e.isIntersecting&&t&&this.delayedLoading&&this.delayedLoading()}))})),t.observe(this)}else if("interaction"===this.loading){let t;this.delayedLoading=r=>{for(const t of INTERSECTION_LOADING_EVENTS)(e||this).removeEventListener(t,i);this.delayedLoading=null,r||this.createPlayer().then((()=>{t&&(e||this).dispatchEvent(new Event(t))}))};const e=this.target?this.closest(this.target):null;let i=e=>{const i=e?.type;t?t=i:(t=i,this.delayedLoading&&this.delayedLoading())};i=i.bind(this);for(const t of INTERSECTION_LOADING_EVENTS)(e||this).addEventListener(t,i)}else if("delay"===this.loading){this.delayedLoading=t=>{this.delayedLoading=null,t||this.createPlayer()};const t=this.hasAttribute("delay")?+this.getAttribute("delay"):0;setTimeout((()=>{this.delayedLoading&&this.delayedLoading()}),t)}else this.createPlayer();this._isConnected=!0}disconnectedCallback(){this.delayedLoading&&this.delayedLoading(!0),this.destroyPlayer(),this._isConnected=!1}createElements(){if(this._root=this.attachShadow({mode:"open"}),SUPPORTS_ADOPTING_STYLE_SHEETS)styleSheet||(styleSheet=new CSSStyleSheet,styleSheet.replaceSync(ELEMENT_STYLE)),this._root.adoptedStyleSheets=[styleSheet];else{const t=document.createElement("style");t.innerHTML=ELEMENT_STYLE,this._root.appendChild(t)}const t=document.createElement("div");t.classList.add("body"),this._root.appendChild(t);const e=document.createElement("slot");t.appendChild(e)}async createPlayer(){if(!Element._playerFactory)throw new Error("Missing player loader!");if(this.delayedLoading)return;const t=await this.loadIconData();if(!t)return;this._playerInstance=Element._playerFactory(this.animationContainer,t,{state:c(this.state),stroke:d(this.stroke),colors:l(this.colors),scale:parseFloat(""+this.getAttribute("scale")||""),axisX:parseFloat(""+this.getAttribute("axis-x")||""),axisY:parseFloat(""+this.getAttribute("axis-y")||"")});const e=Object.entries(this._playerInstance.colors||{});if(e.length){let t="";for(const[i,r]of e)t+=`\n :host(:not(.current-color)) svg path[fill].${i} {\n fill: var(--lord-icon-${i}, var(--lord-icon-${i}-base, #000));\n }\n \n :host(:not(.current-color)) svg path[stroke].${i} {\n stroke: var(--lord-icon-${i}, var(--lord-icon-${i}-base, #000));\n }\n `;const i=document.createElement("style");i.innerHTML=t,this.animationContainer.appendChild(i)}this._playerInstance.connect(),this._playerInstance.addEventListener("ready",(()=>{this._triggerInstance&&this._triggerInstance.onReady&&this._triggerInstance.onReady()})),this._playerInstance.addEventListener("refresh",(()=>{this.refresh(),this._triggerInstance&&this._triggerInstance.onRefresh&&this._triggerInstance.onRefresh()})),this._playerInstance.addEventListener("complete",(()=>{this._triggerInstance&&this._triggerInstance.onComplete&&this._triggerInstance.onComplete()})),this._playerInstance.addEventListener("frame",(()=>{this._triggerInstance&&this._triggerInstance.onFrame&&this._triggerInstance.onFrame()})),this.refresh(),this.triggerChanged(),await new Promise(((t,e)=>{this._playerInstance.isReady?t():this._playerInstance.addEventListener("ready",t)})),this.animationContainer.classList.add("ready"),this._isReady=!0,this.dispatchEvent(new CustomEvent("ready"))}destroyPlayer(){this._isReady=!1,this._loadedIconData=void 0,this._triggerInstance&&(this._triggerInstance.onDisconnected&&this._triggerInstance.onDisconnected(),this._triggerInstance=void 0),this._playerInstance&&(this._playerInstance.disconnect(),this._playerInstance=void 0,this.animationContainer.classList.remove("ready"))}async loadIconData(){let t=this.iconData;if(!t)if(this.icon&&Element._iconLoader)this._loadedIconData=t=await Element._iconLoader(this.icon);else if(this.src){const e=await fetch(this.src);this._loadedIconData=t=await e.json()}return t}refresh(){this.movePaletteToCssVariables()}movePaletteToCssVariables(){for(const[t,e]of Object.entries(this._playerInstance.colors||{}))e?this.animationContainer.style.setProperty(`--lord-icon-${t}-base`,e):this.animationContainer.style.removeProperty(`--lord-icon-${t}-base`)}targetChanged(){this.triggerChanged()}loadingChanged(){}triggerChanged(){if(this._triggerInstance&&(this._triggerInstance.onDisconnected&&this._triggerInstance.onDisconnected(),this._triggerInstance=void 0,this._playerInstance?.pause()),!this.trigger||!this._playerInstance)return;const t=Element._definedTriggers.get(this.trigger);if(!t)throw new Error("Can't use unregistered trigger!");const e=this.target?this.closest(this.target):null;this._triggerInstance=new t(this._playerInstance,this,e||this),this._triggerInstance.onConnected&&this._triggerInstance.onConnected(),this._playerInstance.isReady&&this._triggerInstance.onReady&&this._triggerInstance.onReady()}colorsChanged(){this._playerInstance&&(this._playerInstance.colors=l(this.colors)||null)}strokeChanged(){this._playerInstance&&(this._playerInstance.stroke=d(this.stroke)||null)}stateChanged(){this._playerInstance&&(this._playerInstance.state=this.state)}iconChanged(){this._isConnected&&(this.destroyPlayer(),this.createPlayer())}srcChanged(){this._isConnected&&(this.destroyPlayer(),this.createPlayer())}set icon(t){const e=this._assignedIconData;t&&r(t)?(this._assignedIconData=t,e!==t&&(this.hasAttribute("icon")?this.removeAttribute("icon"):this.iconChanged())):(this._assignedIconData=void 0,t&&"string"==typeof t?this.setAttribute("icon",t):this.hasAttribute("icon")?this.removeAttribute("icon"):e&&this.iconChanged())}get icon(){return this._assignedIconData||this.getAttribute("icon")}set src(t){t?this.setAttribute("src",t):this.removeAttribute("src")}get src(){return this.getAttribute("src")}set state(t){t?this.setAttribute("state",t):this.removeAttribute("state")}get state(){return this.getAttribute("state")}set colors(t){t?this.setAttribute("colors",t):this.removeAttribute("colors")}get colors(){return this.getAttribute("colors")}set trigger(t){t?this.setAttribute("trigger",t):this.removeAttribute("trigger")}get trigger(){return this.getAttribute("trigger")}set loading(t){t?this.setAttribute("loading",t):this.removeAttribute("loading")}get loading(){if(this.getAttribute("loading")){const t=this.getAttribute("loading").toLowerCase();if("lazy"===t)return"lazy";if("interaction"===t)return"interaction";if("delay"===t)return"delay"}return null}set target(t){t?this.setAttribute("target",t):this.removeAttribute("target")}get target(){return this.getAttribute("target")}set stroke(t){t?this.setAttribute("stroke",t):this.removeAttribute("stroke")}get stroke(){return this.hasAttribute("stroke")?this.getAttribute("stroke"):null}set iconData(t){t!==this._assignedIconData&&(this._assignedIconData=t,this.iconChanged())}get iconData(){return this._assignedIconData||this._loadedIconData}get isReady(){return this._isReady}get playerInstance(){return this._playerInstance}get triggerInstance(){return this._triggerInstance}get animationContainer(){return this._root.lastElementChild}}const DEFAULT_LOTTIE_WEB_OPTIONS={renderer:"svg",loop:!1,autoplay:!1,rendererSettings:{preserveAspectRatio:"xMidYMid meet",progressiveLoad:!0,hideOnTransparent:!0}};function createColorsProxy(){return new Proxy(this,{set:(t,e,i,r)=>("string"==typeof e&&(i?m(this.lottie,this.rawProperties.filter((t=>"color"===t.type&&t.name===e)),i):y(this.lottie,this.rawProperties.filter((t=>"color"===t.type&&t.name===e))),t.refresh()),!0),get:(e,i,r)=>{for(const r of e.rawProperties)if("color"==r.type&&"string"==typeof i&&i==r.name){const e=t(this.lottie,r.path);if(e)return h(e)}},deleteProperty:(t,e)=>("string"==typeof e&&(y(this.lottie,this.rawProperties.filter((t=>"color"===t.type&&t.name===e))),t.refresh()),!0),ownKeys:t=>t.rawProperties.filter((t=>"color"==t.type)).map((t=>t.name)),has:(t,e)=>{for(const i of t.rawProperties)if("color"==i.type&&"string"==typeof e&&e==i.name)return!0;return!1},getOwnPropertyDescriptor:t=>({enumerable:!0,configurable:!0})})}class Player{_animationLoader;_container;_iconData;_initial;_options;_lottie;_isReady=!1;_colorsProxy;_direction=1;_speed=1;_rawProperties;_eventCallbacks={};_state;_states;constructor(t,i,r,s,n){if(this._animationLoader=t,this._container=i,this._iconData=r,this._initial=s||{},this._options=n||DEFAULT_LOTTIE_WEB_OPTIONS,this._states=(r.markers||[]).map((t=>{const[e,i]=t.cm.split(":"),r={time:t.tm,duration:t.dr,name:i||e,default:!(!i||!e.includes("default"))};return(r.name===this._initial.state||r.default&&f(this._initial.state))&&(this._state=r),r})).filter((t=>t.duration>0)),this._states.length&&(this._initial.stroke&&![1,2,3,"light","regular","bold"].includes(this._initial.stroke)&&delete this._initial.stroke,this._initial.state&&!this._state&&(this._state=this._states.filter((t=>t.default))[0])),!this._states.length){this._iconData=e(this._iconData);const t=p(this._iconData,{lottieInstance:!1});if(t&&this._initial.state){const e=`state-${this._initial.state.toLowerCase()}`;m(this._iconData,t.filter((t=>t.name.startsWith("state-"))),0),m(this._iconData,t.filter((t=>t.name===e)),1)}if(t&&this._initial.stroke){const e=t.filter((t=>"stroke"===t.name))[0];if(e){const t=e.value/50,i=this._initial.stroke*t;a(this._iconData,e.path,i)}}if(t&&this._initial.scale){const e=t.filter((t=>"scale"===t.name))[0];if(e){const t=e.value/50,i=this._initial.scale*t;a(this._iconData,e.path,i)}}if(t&&this._initial.axisX&&this._initial.axisY){const e=t.filter((t=>"axis"===t.name))[0];if(e){const t=(e.value[0]+e.value[1])/2/50;a(this._iconData,e.path+".0",this._initial.axisX*t),a(this._iconData,e.path+".1",this._initial.axisY*t)}}}}connect(){if(this._lottie)throw new Error("Already connected player!");const t={},i={};if(this._state&&(i.initialSegment=[this._state.time,this._state.time+this._state.duration+1]),this._states.length){const e=this._states[0],i=this._states[this._states.length-1];t.ip=e.time,t.op=i.time+i.duration+1}this._lottie=this._animationLoader({...this._options,...i,container:this._container,animationData:Object.assign(e(this._iconData),t)}),this._initial.colors&&(this.colors=this._initial.colors),this._initial.stroke&&(this.stroke=this._initial.stroke),this._lottie.addEventListener("complete",(t=>{this.triggerEvent("complete")})),this._lottie.addEventListener("loopComplete",(()=>{this.triggerEvent("complete")})),this._lottie.addEventListener("enterFrame",(t=>{this.triggerEvent("frame")})),this._lottie.isLoaded?(this._isReady=!0,this.triggerEvent("ready")):this._lottie.addEventListener("config_ready",(()=>{this._isReady=!0,this.triggerEvent("ready")}))}disconnect(){if(!this._lottie)throw new Error("Not connected player!");this._isReady=!1,this._lottie.destroy(),this._lottie=void 0,this._colorsProxy=void 0,this._rawProperties=void 0}addEventListener(t,e){return this._eventCallbacks[t]||(this._eventCallbacks[t]=[]),this._eventCallbacks[t].push(e),()=>{this.removeEventListener(t,e)}}removeEventListener(t,e){if(e){if(this._eventCallbacks[t]){let i=0,r=this._eventCallbacks[t].length;for(;i"color"===t.type)).length&&(t.colors={...this.colors}),this.rawProperties.filter((t=>"stroke"===t.name||"stroke-layers"===t.name)).length&&(t.stroke=this.stroke),this._states.length&&(t.state=this.state),t}set colors(t){if(y(this._lottie,this.rawProperties.filter((t=>"color"===t.type))),t)for(const[e,i]of Object.entries(t))m(this._lottie,this.rawProperties.filter((t=>"color"===t.type&&t.name===e)),i);this.refresh()}get colors(){return this._colorsProxy||(this._colorsProxy=createColorsProxy.call(this)),this._colorsProxy}set stroke(t){y(this._lottie,this.rawProperties.filter((t=>"stroke"===t.name||"stroke-layers"===t.name)));const e=d(t);e&&m(this._lottie,this.rawProperties.filter((t=>"stroke"===t.name||"stroke-layers"===t.name)),e),this.refresh()}get stroke(){const e=this.rawProperties.filter((t=>"stroke"===t.name||"stroke-layers"===t.name))[0];if(e){return d(+t(this._lottie,e.path))||null}return null}set state(t){if(t===this.state)return;const e=this.isPlaying;this._state=void 0,f(t)?this._state=this._states.filter((t=>t.default))[0]:t&&(this._state=this._states.filter((e=>e.name===t))[0],this._state||(this._state=this._states.filter((t=>t.default))[0])),this._state?this._lottie?.setSegment(this._state.time,this._state.time+this._state.duration+1):this._lottie.resetSegments(!0),this.goToFirstFrame(),e&&(this.pause(),this.play())}get state(){return this._state?this._state.name:""}set speed(t){this._speed=t,this._lottie?.setSpeed(t)}get speed(){return this._speed}set direction(t){this._direction=t,this._lottie.setDirection(t)}get direction(){return this._direction}set loop(t){this._lottie.loop=t}get loop(){return!!this._lottie.loop}set frame(t){this.goToFrame(Math.max(0,Math.min(this.frames,t)))}get frame(){return this._lottie.currentFrame}get states(){return this._states}get isPlaying(){return!this._lottie.isPaused}get isReady(){return this._isReady}get frames(){return this._lottie.getDuration(!0)-1}get duration(){return this._lottie.getDuration(!1)}get lottie(){return this._lottie}get rawProperties(){return this._rawProperties||(this._rawProperties=p(this._iconData,{lottieInstance:!0}),!this._states.length&&this._rawProperties&&(this._rawProperties=this._rawProperties.filter((t=>"scale"!==t.name&&"axis"!==t.name&&"stroke"!==t.name&&!t.name.startsWith("state-"))))),this._rawProperties||[]}}class Boomerang{player;element;targetElement;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.onHover=this.onHover.bind(this)}onConnected(){this.targetElement.addEventListener("mouseenter",this.onHover)}onDisconnected(){this.targetElement.removeEventListener("mouseenter",this.onHover),this.player.direction=1}onComplete(){this.player.direction=-1,this.player.play()}onHover(){this.player.direction=1,this.player.play()}}const CLICK_EVENTS=[{name:"mousedown"},{name:"touchstart",options:{passive:!0}}];class Click{player;element;targetElement;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.onClick=this.onClick.bind(this)}onConnected(){for(const t of CLICK_EVENTS)this.targetElement.addEventListener(t.name,this.onClick,t.options)}onDisconnected(){for(const t of CLICK_EVENTS)this.targetElement.removeEventListener(t.name,this.onClick)}onClick(){this.player.isPlaying||this.player.playFromBeginning()}}class Hover{player;element;targetElement;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.onHover=this.onHover.bind(this)}onConnected(){this.targetElement.addEventListener("mouseenter",this.onHover)}onDisconnected(){this.targetElement.removeEventListener("mouseenter",this.onHover)}onHover(){this.player.isPlaying||this.player.playFromBeginning()}}class In{player;element;targetElement;playTimeout=null;played=!1;intersectionObserver;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i}onConnected(){if(this.loading)this.play();else{const t=(t,e)=>{t.forEach((t=>{t.isIntersecting&&(this.play(),this.resetIntersectionObserver())}))};this.intersectionObserver=new IntersectionObserver(t),this.intersectionObserver.observe(this.element)}}onDisconnected(){this.played=!1,this.resetIntersectionObserver(),this.resetPlayDelayTimer()}play(){this.played||(this.played=!0,this.resetPlayDelayTimer(),this.delay>0?this.playTimeout=setTimeout((()=>{this.player.playFromBeginning(),this.playTimeout=null}),this.delay):this.player.playFromBeginning())}resetIntersectionObserver(){this.intersectionObserver&&(this.intersectionObserver.unobserve(this.element),this.intersectionObserver=void 0)}resetPlayDelayTimer(){this.playTimeout&&(clearTimeout(this.playTimeout),this.playTimeout=null)}get delay(){const t=this.element.hasAttribute("delay")?+(this.element.getAttribute("delay")||0):0;return Math.max(t,0)}get loading(){return this.element.hasAttribute("loading")}}class Loop{player;element;targetElement;playTimeout=null;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i}onReady(){this.play()}onComplete(){this.play()}onDisconnected(){this.resetPlayDelayTimer()}play(){this.resetPlayDelayTimer(),this.delay>0?this.playTimeout=setTimeout((()=>{this.player.playFromBeginning()}),this.delay):this.player.playFromBeginning()}resetPlayDelayTimer(){this.playTimeout&&(clearTimeout(this.playTimeout),this.playTimeout=null)}get delay(){const t=this.element.hasAttribute("delay")?+(this.element.getAttribute("delay")||0):0;return Math.max(t,0)}}class LoopOnHover{player;element;targetElement;playTimeout=null;mouseIn=!1;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this)}onConnected(){this.targetElement.addEventListener("mouseenter",this.onMouseEnter),this.targetElement.addEventListener("mouseleave",this.onMouseLeave)}onDisconnected(){this.targetElement.removeEventListener("mouseenter",this.onMouseEnter),this.targetElement.removeEventListener("mouseleave",this.onMouseLeave),this.resetPlayDelayTimer()}onMouseEnter(){this.mouseIn=!0,this.player.isPlaying||this.play()}onMouseLeave(){this.mouseIn=!1,this.resetPlayDelayTimer()}onComplete(){this.play()}play(){this.resetPlayDelayTimer(),this.mouseIn&&(this.delay>0?this.playTimeout=setTimeout((()=>{this.player.playFromBeginning()}),this.delay):this.player.playFromBeginning())}resetPlayDelayTimer(){this.playTimeout&&(clearTimeout(this.playTimeout),this.playTimeout=null)}get delay(){const t=this.element.hasAttribute("delay")?+(this.element.getAttribute("delay")||0):0;return Math.max(t,0)}}class Morph{player;element;targetElement;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this)}onConnected(){this.targetElement.addEventListener("mouseenter",this.onMouseEnter),this.targetElement.addEventListener("mouseleave",this.onMouseLeave)}onDisconnected(){this.targetElement.removeEventListener("mouseenter",this.onMouseEnter),this.targetElement.removeEventListener("mouseleave",this.onMouseLeave),this.player.direction=1}onMouseEnter(){this.player.direction=1,this.player.play()}onMouseLeave(){this.player.direction=-1,this.player.play()}}const NUMBER_REGEX=/^\d*(\.\d+)?$/,MUTATION_OBSERVER_CONFIG={attributes:!0,childList:!1,subtree:!1};class Sequence{player;element;targetElement;sequenceIndex=0;frameState=null;frameDelayFirst=null;frameDelayLast=null;timer;observer;constructor(t,e,i){this.player=t,this.element=e,this.targetElement=i,this.observer=new MutationObserver(((t,e)=>{for(const e of t)"attributes"===e.type&&"sequence"===e.attributeName&&(this.reset(),this.step())}))}onReady(){this.step()}onComplete(){this.timer=setTimeout((()=>{this.timer=null,this.frameDelayLast=null,this.step()}),this.frameDelayLast||0)}onConnected(){this.observer.observe(this.element,MUTATION_OBSERVER_CONFIG)}onDisconnected(){this.observer.disconnect(),this.timer&&(clearTimeout(this.timer),this.timer=null)}reset(){this.player.pause(),this.sequenceIndex=0,this.frameState=this.frameDelayFirst=this.frameDelayLast=null,this.timer&&(clearTimeout(this.timer),this.timer=null)}takeStep(){const t=this.sequence.split(","),e=t[this.sequenceIndex];this.sequenceIndex++,this.sequenceIndex>=t.length&&(this.sequenceIndex=0);const[i,...r]=e.split(":");return{action:i,params:r}}handleStep(t,e){if("play"===t){this.frameState&&(this.player.state=this.frameState,this.frameState=null);e.includes("reverse")?(this.player.goToLastFrame(),this.player.direction=-1):(this.player.goToFirstFrame(),this.player.direction=1),this.timer=setTimeout((()=>{this.timer=null,this.frameDelayFirst=null,this.player.play()}),this.frameDelayFirst||0)}else if("frame"===t){let t=0;e.length&&e[0].match(NUMBER_REGEX)&&(t=Math.max(0,Math.min(this.player.frames,+e[0]))),this.player.frame=t,this.timer=setTimeout((()=>{this.timer=null,this.frameDelayFirst=null,this.step()}),this.frameDelayFirst||0)}else if("state"===t)this.frameState=e[0],this.step();else if("delay"===t){let t=null;for(const i of e)i&&i.match(NUMBER_REGEX)&&(t=+i);t&&t>0&&(e.includes("first")&&e.includes("last")?(this.frameDelayFirst=t,this.frameDelayLast=t):e.includes("first")?this.frameDelayFirst=t:e.includes("last")?this.frameDelayLast=t:this.frameDelayFirst=t),this.step()}else if("idle"!==t)throw new Error(`Invalid sequence action: ${t}`)}step(){const{action:t,params:e}=this.takeStep();t&&this.handleStep(t,e)}get sequence(){return this.element.getAttribute("sequence")||""}}function defineElement(t){Element.setPlayerFactory(((e,i,r)=>new Player(t,e,i,r))),Element.defineTrigger("in",In),Element.defineTrigger("click",Click),Element.defineTrigger("hover",Hover),Element.defineTrigger("loop",Loop),Element.defineTrigger("loop-on-hover",LoopOnHover),Element.defineTrigger("morph",Morph),Element.defineTrigger("boomerang",Boomerang),Element.defineTrigger("sequence",Sequence),Element.defineTrigger("morph-two-way",Boomerang),customElements.get&&customElements.get("lord-icon")||customElements.define("lord-icon",Element)}var commonjsGlobal="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function getDefaultExportFromCjs(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var lottie$1={exports:{}};(function(module,exports){var factory;"undefined"!=typeof navigator&&(factory=function(){var svgNS="http://www.w3.org/2000/svg",locationHref="",_useWebWorker=!1,initialDefaultFrame=-999999,setWebWorker=function(t){_useWebWorker=!!t},getWebWorker=function(){return _useWebWorker},setLocationHref=function(t){locationHref=t},getLocationHref=function(){return locationHref};function createTag(t){return document.createElement(t)}function extendPrototype(t,e){var i,r,s=t.length;for(i=0;i1?i[1]=1:i[1]<=0&&(i[1]=0),HSVtoRGB(i[0],i[1],i[2])}function addBrightnessToRGB(t,e){var i=RGBtoHSV(255*t[0],255*t[1],255*t[2]);return i[2]+=e,i[2]>1?i[2]=1:i[2]<0&&(i[2]=0),HSVtoRGB(i[0],i[1],i[2])}function addHueToRGB(t,e){var i=RGBtoHSV(255*t[0],255*t[1],255*t[2]);return i[0]+=e/360,i[0]>1?i[0]-=1:i[0]<0&&(i[0]+=1),HSVtoRGB(i[0],i[1],i[2])}var rgbToHex=function(){var t,e,i=[];for(t=0;t<256;t+=1)e=t.toString(16),i[t]=1===e.length?"0"+e:e;return function(t,e,r){return t<0&&(t=0),e<0&&(e=0),r<0&&(r=0),"#"+i[t]+i[e]+i[r]}}(),setSubframeEnabled=function(t){subframeEnabled=!!t},getSubframeEnabled=function(){return subframeEnabled},setExpressionsPlugin=function(t){expressionsPlugin=t},getExpressionsPlugin=function(){return expressionsPlugin},setExpressionInterfaces=function(t){expressionsInterfaces=t},getExpressionInterfaces=function(){return expressionsInterfaces},setDefaultCurveSegments=function(t){defaultCurveSegments=t},getDefaultCurveSegments=function(){return defaultCurveSegments},setIdPrefix=function(t){idPrefix$1=t};function createNS(t){return document.createElementNS(svgNS,t)}function _typeof$5(t){return _typeof$5="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof$5(t)}var dataManager=function(){var t,e,i=1,r=[],s={onmessage:function(){},postMessage:function(e){t({data:e})}},a={postMessage:function(t){s.onmessage({data:t})}};function n(){e||(e=function(e){if(window.Worker&&window.Blob&&getWebWorker()){var i=new Blob(["var _workerSelf = self; self.onmessage = ",e.toString()],{type:"text/javascript"}),r=URL.createObjectURL(i);return new Worker(r)}return t=e,s}((function(t){if(a.dataManager||(a.dataManager=function(){function t(s,a){var n,o,h,l,p,c,m=s.length;for(o=0;o=0;e-=1)if("sh"===t[e].ty)if(t[e].ks.k.i)r(t[e].ks.k);else for(a=t[e].ks.k.length,s=0;si[0]||!(i[0]>t[0])&&(t[1]>i[1]||!(i[1]>t[1])&&(t[2]>i[2]||!(i[2]>t[2])&&null))}var a,n=function(){var t=[4,4,14];function e(t){var e,i,r,s=t.length;for(e=0;e=0;i-=1)if("sh"===t[i].ty)if(t[i].ks.k.i)t[i].ks.k.c=t[i].closed;else for(s=t[i].ks.k.length,r=0;r500)&&(this._imageLoaded(),clearInterval(i)),e+=1}.bind(this),50)}function a(t){var e={assetData:t},i=r(t,this.assetsPath,this.path);return dataManager.loadData(i,function(t){e.img=t,this._footageLoaded()}.bind(this),function(){e.img={},this._footageLoaded()}.bind(this)),e}function n(){this._imageLoaded=e.bind(this),this._footageLoaded=i.bind(this),this.testImageLoaded=s.bind(this),this.createFootageData=a.bind(this),this.assetsPath="",this.path="",this.totalImages=0,this.totalFootages=0,this.loadedAssets=0,this.loadedFootagesCount=0,this.imagesLoadedCb=null,this.images=[]}return n.prototype={loadAssets:function(t,e){var i;this.imagesLoadedCb=e;var r=t.length;for(i=0;ithis.animationData.op&&(this.animationData.op=t.op,this.totalFrames=Math.floor(t.op-this.animationData.ip));var e,i,r=this.animationData.layers,s=r.length,a=t.layers,n=a.length;for(i=0;ithis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger("enterFrame"),this.renderFrame(),this.trigger("drawnFrame")},AnimationItem.prototype.renderFrame=function(){if(!1!==this.isLoaded&&this.renderer)try{this.expressionsPlugin&&this.expressionsPlugin.resetFrame(),this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(t){this.triggerRenderFrameError(t)}},AnimationItem.prototype.play=function(t){t&&this.name!==t||!0===this.isPaused&&(this.isPaused=!1,this.trigger("_play"),this.audioController.resume(),this._idle&&(this._idle=!1,this.trigger("_active")))},AnimationItem.prototype.pause=function(t){t&&this.name!==t||!1===this.isPaused&&(this.isPaused=!0,this.trigger("_pause"),this._idle=!0,this.trigger("_idle"),this.audioController.pause())},AnimationItem.prototype.togglePause=function(t){t&&this.name!==t||(!0===this.isPaused?this.play():this.pause())},AnimationItem.prototype.stop=function(t){t&&this.name!==t||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},AnimationItem.prototype.getMarkerData=function(t){for(var e,i=0;i=this.totalFrames-1&&this.frameModifier>0?this.loop&&this.playCount!==this.loop?e>=this.totalFrames?(this.playCount+=1,this.checkSegments(e%this.totalFrames)||(this.setCurrentRawFrameValue(e%this.totalFrames),this._completedLoop=!0,this.trigger("loopComplete"))):this.setCurrentRawFrameValue(e):this.checkSegments(e>this.totalFrames?e%this.totalFrames:0)||(i=!0,e=this.totalFrames-1):e<0?this.checkSegments(e%this.totalFrames)||(!this.loop||this.playCount--<=0&&!0!==this.loop?(i=!0,e=0):(this.setCurrentRawFrameValue(this.totalFrames+e%this.totalFrames),this._completedLoop?this.trigger("loopComplete"):this._completedLoop=!0)):this.setCurrentRawFrameValue(e),i&&(this.setCurrentRawFrameValue(e),this.pause(),this.trigger("complete"))}},AnimationItem.prototype.adjustSegment=function(t,e){this.playCount=0,t[1]0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(-1)),this.totalFrames=t[0]-t[1],this.timeCompleted=this.totalFrames,this.firstFrame=t[1],this.setCurrentRawFrameValue(this.totalFrames-.001-e)):t[1]>t[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.totalFrames=t[1]-t[0],this.timeCompleted=this.totalFrames,this.firstFrame=t[0],this.setCurrentRawFrameValue(.001+e)),this.trigger("segmentStart")},AnimationItem.prototype.setSegment=function(t,e){var i=-1;this.isPaused&&(this.currentRawFrame+this.firstFramee&&(i=e-t)),this.firstFrame=t,this.totalFrames=e-t,this.timeCompleted=this.totalFrames,-1!==i&&this.goToAndStop(i,!0)},AnimationItem.prototype.playSegments=function(t,e){if(e&&(this.segments.length=0),"object"===_typeof$4(t[0])){var i,r=t.length;for(i=0;i=0;i-=1)e[i].animation.destroy(t)},t.freeze=function(){n=!0},t.unfreeze=function(){n=!1,d()},t.setVolume=function(t,i){var s;for(s=0;s=.001?function(t,e,i,r){for(var s=0;s<4;++s){var a=l(e,i,r);if(0===a)return e;e-=(h(e,i,r)-t)/a}return e}(t,f,e,s):0===c?f:function(t,e,i,r,s){var a,n,o=0;do{(a=h(n=e+(i-e)/2,r,s)-t)>0?i=n:e=n}while(Math.abs(a)>1e-7&&++o<10);return n}(t,n,n+r,e,s)}},t}(),pooling={double:function(t){return t.concat(createSizedArray(t.length))}},poolFactory=function(t,e,i){var r=0,s=t,a=createSizedArray(s);return{newElement:function(){return r?a[r-=1]:e()},release:function(t){r===s&&(a=pooling.double(a),s*=2),i&&i(t),a[r]=t,r+=1}}},bezierLengthPool=poolFactory(8,(function(){return{addedLength:0,percents:createTypedArray("float32",getDefaultCurveSegments()),lengths:createTypedArray("float32",getDefaultCurveSegments())}})),segmentsLengthPool=poolFactory(8,(function(){return{lengths:[],totalLength:0}}),(function(t){var e,i=t.lengths.length;for(e=0;e-.001&&n<.001}var i=function(t,e,i,r){var s,a,n,o,h,l,p=getDefaultCurveSegments(),f=0,c=[],m=[],d=bezierLengthPool.newElement();for(n=i.length,s=0;sn?-1:1,l=!0;l;)if(r[a]<=n&&r[a+1]>n?(o=(n-r[a])/(r[a+1]-r[a]),l=!1):a+=h,a<0||a>=s-1){if(a===s-1)return i[a];l=!1}return i[a]+(i[a+1]-i[a])*o}var h=createTypedArray("float32",8);return{getSegmentsLength:function(t){var e,r=segmentsLengthPool.newElement(),s=t.c,a=t.v,n=t.o,o=t.i,h=t._length,l=r.lengths,p=0;for(e=0;e1&&(a=1);var p,f=o(a,l),c=o(n=n>1?1:n,l),m=e.length,d=1-f,u=1-c,y=d*d*d,g=f*d*d*3,v=f*f*d*3,b=f*f*f,E=d*d*u,x=f*d*u+d*f*u+d*d*c,P=f*f*u+d*f*c+f*d*c,S=f*f*c,_=d*u*u,C=f*u*u+d*c*u+d*u*c,T=f*c*u+d*c*c+f*u*c,A=f*c*c,k=u*u*u,M=c*u*u+u*c*u+u*u*c,D=c*c*u+u*c*c+c*u*c,F=c*c*c;for(p=0;pm?c>d?c-m-d:d-m-c:d>m?d-m-c:m-c-d)>-1e-4&&f<1e-4}}}var bez=bezFunction(),initFrame=initialDefaultFrame,mathAbs=Math.abs;function interpolateValue(t,e){var i,r=this.offsetTime;"multidimensional"===this.propType&&(i=createTypedArray("float32",this.pv.length));for(var s,a,n,o,h,l,p,f,c,m=e.lastIndex,d=m,u=this.keyframes.length-1,y=!0;y;){if(s=this.keyframes[d],a=this.keyframes[d+1],d===u-1&&t>=a.t-r){s.h&&(s=a),m=0;break}if(a.t-r>t){m=d;break}d=v||t=v?E.points.length-1:0;for(h=E.points[x].point.length,o=0;o=_&&S<_+E.points[f+1].partialLength){for(P=(S-_)/E.points[f+1].partialLength,h=E.points[f].point.length,o=0;o=v?(i[0]=g[0],i[1]=g[1],i[2]=g[2]):t<=b?(i[0]=s.s[0],i[1]=s.s[1],i[2]=s.s[2]):quaternionToEuler(i,slerp(createQuaternion(s.s),createQuaternion(g),(t-b)/(v-b)));else for(d=0;d=v?l=1:t1e-6?(r=Math.acos(s),a=Math.sin(r),n=Math.sin((1-i)*r)/a,o=Math.sin(i*r)/a):(n=1-i,o=i),h[0]=n*l+o*m,h[1]=n*p+o*d,h[2]=n*f+o*u,h[3]=n*c+o*y,h}function quaternionToEuler(t,e){var i=e[0],r=e[1],s=e[2],a=e[3],n=Math.atan2(2*r*a-2*i*s,1-2*r*r-2*s*s),o=Math.asin(2*i*r+2*s*a),h=Math.atan2(2*i*a-2*r*s,1-2*i*i-2*s*s);t[0]=n/degToRads,t[1]=o/degToRads,t[2]=h/degToRads}function createQuaternion(t){var e=t[0]*degToRads,i=t[1]*degToRads,r=t[2]*degToRads,s=Math.cos(e/2),a=Math.cos(i/2),n=Math.cos(r/2),o=Math.sin(e/2),h=Math.sin(i/2),l=Math.sin(r/2);return[o*h*n+s*a*l,o*a*n+s*h*l,s*h*n-o*a*l,s*a*n-o*h*l]}function getValueAtCurrentTime(){var t=this.comp.renderedFrame-this.offsetTime,e=this.keyframes[0].t-this.offsetTime,i=this.keyframes[this.keyframes.length-1].t-this.offsetTime;if(!(t===this._caching.lastFrame||this._caching.lastFrame!==initFrame&&(this._caching.lastFrame>=i&&t>=i||this._caching.lastFrame=t&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var r=this.interpolateValue(t,this._caching);this.pv=r}return this._caching.lastFrame=t,this.pv}function setVValue(t){var e;if("unidimensional"===this.propType)e=t*this.mult,mathAbs(this.v-e)>1e-5&&(this.v=e,this._mdf=!0);else for(var i=0,r=this.v.length;i1e-5&&(this.v[i]=e,this._mdf=!0),i+=1}function processEffectsSequence(){if(this.elem.globalData.frameId!==this.frameId&&this.effectsSequence.length)if(this.lock)this.setVValue(this.pv);else{var t;this.lock=!0,this._mdf=this._isFirstFrame;var e=this.effectsSequence.length,i=this.kf?this.pv:this.data.k;for(t=0;t=this._maxLength&&this.doubleArrayLength(),i){case"v":a=this.v;break;case"i":a=this.i;break;case"o":a=this.o;break;default:a=[]}(!a[r]||a[r]&&!s)&&(a[r]=pointPool.newElement()),a[r][0]=t,a[r][1]=e},ShapePath.prototype.setTripleAt=function(t,e,i,r,s,a,n,o){this.setXYAt(t,e,"v",n,o),this.setXYAt(i,r,"o",n,o),this.setXYAt(s,a,"i",n,o)},ShapePath.prototype.reverse=function(){var t=new ShapePath;t.setPathData(this.c,this._length);var e=this.v,i=this.o,r=this.i,s=0;this.c&&(t.setTripleAt(e[0][0],e[0][1],r[0][0],r[0][1],i[0][0],i[0][1],0,!1),s=1);var a,n=this._length-1,o=this._length;for(a=s;a=m[m.length-1].t-this.offsetTime)r=m[m.length-1].s?m[m.length-1].s[0]:m[m.length-2].e[0],a=!0;else{for(var d,u,y,g=c,v=m.length-1,b=!0;b&&(d=m[g],!((u=m[g+1]).t-this.offsetTime>t));)g=u.t-this.offsetTime)p=1;else if(tr&&e>r)||(this._caching.lastIndex=s0||t>-1e-6&&t<0?r(1e4*t)/1e4:t}function I(){var t=this.props;return"matrix("+w(t[0])+","+w(t[1])+","+w(t[4])+","+w(t[5])+","+w(t[12])+","+w(t[13])+")"}return function(){this.reset=s,this.rotate=a,this.rotateX=n,this.rotateY=o,this.rotateZ=h,this.skew=p,this.skewFromAxis=f,this.shear=l,this.scale=c,this.setTransform=m,this.translate=d,this.transform=u,this.multiply=y,this.applyToPoint=x,this.applyToX=P,this.applyToY=S,this.applyToZ=_,this.applyToPointArray=M,this.applyToTriplePoints=k,this.applyToPointStringified=D,this.toCSS=F,this.to2dCSS=I,this.clone=b,this.cloneFromProps=E,this.equals=v,this.inversePoints=A,this.inversePoint=T,this.getInverseMatrix=C,this._t=this.transform,this.isIdentity=g,this._identity=!0,this._identityCalculated=!1,this.props=createTypedArray("float32",16),this.reset()}}();function _typeof$3(t){return _typeof$3="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof$3(t)}var lottie={};function setLocation(t){setLocationHref(t)}function searchAnimations(){animationManager.searchAnimations()}function setSubframeRendering(t){setSubframeEnabled(t)}function setPrefix(t){setIdPrefix(t)}function loadAnimation(t){return animationManager.loadAnimation(t)}function setQuality(t){if("string"==typeof t)switch(t){case"high":setDefaultCurveSegments(200);break;default:case"medium":setDefaultCurveSegments(50);break;case"low":setDefaultCurveSegments(10)}else!isNaN(t)&&t>1&&setDefaultCurveSegments(t)}function inBrowser(){return"undefined"!=typeof navigator}function installPlugin(t,e){"expressions"===t&&setExpressionsPlugin(e)}function getFactory(t){switch(t){case"propertyFactory":return PropertyFactory;case"shapePropertyFactory":return ShapePropertyFactory;case"matrix":return Matrix;default:return null}}function checkReady(){"complete"===document.readyState&&(clearInterval(readyStateCheckInterval),searchAnimations())}function getQueryVariable(t){for(var e=queryString.split("&"),i=0;i=1?a.push({s:t-1,e:e-1}):(a.push({s:t,e:1}),a.push({s:0,e:e-1}));var n,o,h=[],l=a.length;for(n=0;nr+i||(p=o.s*s<=r?0:(o.s*s-r)/i,f=o.e*s>=r+i?1:(o.e*s-r)/i,h.push([p,f]))}return h.length||h.push([0,0]),h},TrimModifier.prototype.releasePathsData=function(t){var e,i=t.length;for(e=0;e1?1+a:this.s.v<0?0+a:this.s.v+a)>(i=this.e.v>1?1+a:this.e.v<0?0+a:this.e.v+a)){var n=e;e=i,i=n}e=1e-4*Math.round(1e4*e),i=1e-4*Math.round(1e4*i),this.sValue=e,this.eValue=i}else e=this.sValue,i=this.eValue;var o,h,l,p,f,c=this.shapes.length,m=0;if(i===e)for(s=0;s=0;s-=1)if((d=this.shapes[s]).shape._mdf){for((u=d.localShapeCollection).releaseShapes(),2===this.m&&c>1?(g=this.calculateShapeEdges(e,i,d.totalShapeLength,E,m),E+=d.totalShapeLength):g=[[v,b]],h=g.length,o=0;o=1?y.push({s:d.totalShapeLength*(v-1),e:d.totalShapeLength*(b-1)}):(y.push({s:d.totalShapeLength*v,e:d.totalShapeLength}),y.push({s:0,e:d.totalShapeLength*(b-1)}));var x=this.addShapes(d,y[0]);if(y[0].s!==y[0].e){if(y.length>1)if(d.shape.paths.shapes[d.shape.paths._length-1].c){var P=x.pop();this.addPaths(x,u),x=this.addShapes(d,y[1],P)}else this.addPaths(x,u),x=this.addShapes(d,y[1]);this.addPaths(x,u)}}d.shape.paths=u}}},TrimModifier.prototype.addPaths=function(t,e){var i,r=t.length;for(i=0;ie.e){i.c=!1;break}e.s<=d&&e.e>=d+n.addedLength?(this.addSegment(c[r].v[s-1],c[r].o[s-1],c[r].i[s],c[r].v[s],i,o,y),y=!1):(l=bez.getNewSegment(c[r].v[s-1],c[r].v[s],c[r].o[s-1],c[r].i[s],(e.s-d)/n.addedLength,(e.e-d)/n.addedLength,h[s-1]),this.addSegmentFromArray(l,i,o,y),y=!1,i.c=!1),d+=n.addedLength,o+=1}if(c[r].c&&h.length){if(n=h[s-1],d<=e.e){var g=h[s-1].addedLength;e.s<=d&&e.e>=d+g?(this.addSegment(c[r].v[s-1],c[r].o[s-1],c[r].i[0],c[r].v[0],i,o,y),y=!1):(l=bez.getNewSegment(c[r].v[s-1],c[r].v[0],c[r].o[s-1],c[r].i[0],(e.s-d)/g,(e.e-d)/g,h[s-1]),this.addSegmentFromArray(l,i,o,y),y=!1,i.c=!1)}else i.c=!1;d+=n.addedLength,o+=1}if(i._length&&(i.setXYAt(i.v[p][0],i.v[p][1],"i",p),i.setXYAt(i.v[i._length-1][0],i.v[i._length-1][1],"o",i._length-1)),d>e.e)break;r=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/i,0),s=this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/i,0)):(r=this.p.pv,s=this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/i,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],s=[];var a=this.px,n=this.py;a._caching.lastFrame+a.offsetTime<=a.keyframes[0].t?(r[0]=a.getValueAtTime((a.keyframes[0].t+.01)/i,0),r[1]=n.getValueAtTime((n.keyframes[0].t+.01)/i,0),s[0]=a.getValueAtTime(a.keyframes[0].t/i,0),s[1]=n.getValueAtTime(n.keyframes[0].t/i,0)):a._caching.lastFrame+a.offsetTime>=a.keyframes[a.keyframes.length-1].t?(r[0]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/i,0),r[1]=n.getValueAtTime(n.keyframes[n.keyframes.length-1].t/i,0),s[0]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/i,0),s[1]=n.getValueAtTime((n.keyframes[n.keyframes.length-1].t-.01)/i,0)):(r=[a.pv,n.pv],s[0]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/i,a.offsetTime),s[1]=n.getValueAtTime((n._caching.lastFrame+n.offsetTime-.01)/i,n.offsetTime))}else r=s=t;this.v.rotate(-Math.atan2(r[1]-s[1],r[0]-s[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}},precalculateMatrix:function(){if(this.appliedTransformations=0,this.pre.reset(),!this.a.effectsSequence.length&&(this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1,!this.s.effectsSequence.length)){if(this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2,this.sk){if(this.sk.effectsSequence.length||this.sa.effectsSequence.length)return;this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3}this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):this.rz.effectsSequence.length||this.ry.effectsSequence.length||this.rx.effectsSequence.length||this.or.effectsSequence.length||(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}},autoOrient:function(){}},extendPrototype([DynamicPropertyContainer],e),e.prototype.addDynamicProperty=function(t){this._addDynamicProperty(t),this.elem.addDynamicProperty(t),this._isDirty=!0},e.prototype._addDynamicProperty=DynamicPropertyContainer.prototype.addDynamicProperty,{getTransformProperty:function(t,i,r){return new e(t,i,r)}}}();function RepeaterModifier(){}function RoundCornersModifier(){}function floatEqual(t,e){return 1e5*Math.abs(t-e)<=Math.min(Math.abs(t),Math.abs(e))}function floatZero(t){return Math.abs(t)<=1e-5}function lerp(t,e,i){return t*(1-i)+e*i}function lerpPoint(t,e,i){return[lerp(t[0],e[0],i),lerp(t[1],e[1],i)]}function quadRoots(t,e,i){if(0===t)return[];var r=e*e-4*t*i;if(r<0)return[];var s=-e/(2*t);if(0===r)return[s];var a=Math.sqrt(r)/(2*t);return[s-a,s+a]}function polynomialCoefficients(t,e,i,r){return[3*e-t-3*i+r,3*t-6*e+3*i,-3*t+3*e,t]}function singlePoint(t){return new PolynomialBezier(t,t,t,t,!1)}function PolynomialBezier(t,e,i,r,s){s&&pointEqual(t,e)&&(e=lerpPoint(t,r,1/3)),s&&pointEqual(i,r)&&(i=lerpPoint(t,r,2/3));var a=polynomialCoefficients(t[0],e[0],i[0],r[0]),n=polynomialCoefficients(t[1],e[1],i[1],r[1]);this.a=[a[0],n[0]],this.b=[a[1],n[1]],this.c=[a[2],n[2]],this.d=[a[3],n[3]],this.points=[t,e,i,r]}function extrema(t,e){var i=t.points[0][e],r=t.points[t.points.length-1][e];if(i>r){var s=r;r=i,i=s}for(var a=quadRoots(3*t.a[e],2*t.b[e],t.c[e]),n=0;n0&&a[n]<1){var o=t.point(a[n])[e];or&&(r=o)}return{min:i,max:r}}function intersectData(t,e,i){var r=t.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:t,t:(e+i)/2,t1:e,t2:i}}function splitData(t){var e=t.bez.split(.5);return[intersectData(e[0],t.t1,t.t),intersectData(e[1],t.t,t.t2)]}function boxIntersect(t,e){return 2*Math.abs(t.cx-e.cx)=a||t.width<=r&&t.height<=r&&e.width<=r&&e.height<=r)s.push([t.t,e.t]);else{var n=splitData(t),o=splitData(e);intersectsImpl(n[0],o[0],i+1,r,s,a),intersectsImpl(n[0],o[1],i+1,r,s,a),intersectsImpl(n[1],o[0],i+1,r,s,a),intersectsImpl(n[1],o[1],i+1,r,s,a)}}function crossProduct(t,e){return[t[1]*e[2]-t[2]*e[1],t[2]*e[0]-t[0]*e[2],t[0]*e[1]-t[1]*e[0]]}function lineIntersection(t,e,i,r){var s=[t[0],t[1],1],a=[e[0],e[1],1],n=[i[0],i[1],1],o=[r[0],r[1],1],h=crossProduct(crossProduct(s,a),crossProduct(n,o));return floatZero(h[2])?null:[h[0]/h[2],h[1]/h[2]]}function polarOffset(t,e,i){return[t[0]+Math.cos(e)*i,t[1]-Math.sin(e)*i]}function pointDistance(t,e){return Math.hypot(t[0]-e[0],t[1]-e[1])}function pointEqual(t,e){return floatEqual(t[0],e[0])&&floatEqual(t[1],e[1])}function ZigZagModifier(){}function setPoint(t,e,i,r,s,a,n){var o=i-Math.PI/2,h=i+Math.PI/2,l=e[0]+Math.cos(i)*r*s,p=e[1]-Math.sin(i)*r*s;t.setTripleAt(l,p,l+Math.cos(o)*a,p-Math.sin(o)*a,l+Math.cos(h)*n,p-Math.sin(h)*n,t.length())}function getPerpendicularVector(t,e){var i=[e[0]-t[0],e[1]-t[1]],r=.5*-Math.PI;return[Math.cos(r)*i[0]-Math.sin(r)*i[1],Math.sin(r)*i[0]+Math.cos(r)*i[1]]}function getProjectingAngle(t,e){var i=0===e?t.length()-1:e-1,r=(e+1)%t.length(),s=getPerpendicularVector(t.v[i],t.v[r]);return Math.atan2(0,1)-Math.atan2(s[1],s[0])}function zigZagCorner(t,e,i,r,s,a,n){var o=getProjectingAngle(e,i),h=e.v[i%e._length],l=e.v[0===i?e._length-1:i-1],p=e.v[(i+1)%e._length],f=2===a?Math.sqrt(Math.pow(h[0]-l[0],2)+Math.pow(h[1]-l[1],2)):0,c=2===a?Math.sqrt(Math.pow(h[0]-p[0],2)+Math.pow(h[1]-p[1],2)):0;setPoint(t,e.v[i%e._length],o,n,r,c/(2*(s+1)),f/(2*(s+1)))}function zigZagSegment(t,e,i,r,s,a){for(var n=0;n1&&e.length>1&&(s=getIntersection(t[0],e[e.length-1]))?[[t[0].split(s[0])[0]],[e[e.length-1].split(s[1])[1]]]:[i,r]}function pruneIntersections(t){for(var e,i=1;i1&&(e=pruneSegmentIntersection(t[t.length-1],t[0]),t[t.length-1]=e[0],t[0]=e[1]),t}function offsetSegmentSplit(t,e){var i,r,s,a,n=t.inflectionPoints();if(0===n.length)return[offsetSegment(t,e)];if(1===n.length||floatEqual(n[1],1))return i=(s=t.split(n[0]))[0],r=s[1],[offsetSegment(i,e),offsetSegment(r,e)];i=(s=t.split(n[0]))[0];var o=(n[1]-n[0])/(1-n[0]);return a=(s=s[1].split(o))[0],r=s[1],[offsetSegment(i,e),offsetSegment(a,e),offsetSegment(r,e)]}function OffsetPathModifier(){}function getFontProperties(t){for(var e=t.fStyle?t.fStyle.split(" "):[],i="normal",r="normal",s=e.length,a=0;a0;)i-=1,this._elements.unshift(e[i]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},RepeaterModifier.prototype.resetElements=function(t){var e,i=t.length;for(e=0;e0?Math.floor(c):Math.ceil(c),u=this.pMatrix.props,y=this.rMatrix.props,g=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v,b,E=0;if(c>0){for(;Ed;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),E-=1;m&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-m,!0),E-=m)}for(r=1===this.data.m?0:this._currentCopies-1,s=1===this.data.m?1:-1,a=this._currentCopies;a;){if(b=(i=(e=this.elemsData[r].it)[e.length-1].transform.mProps.v.props).length,e[e.length-1].transform.mProps._mdf=!0,e[e.length-1].transform.op._mdf=!0,e[e.length-1].transform.op.v=1===this._currentCopies?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),0!==E){for((0!==r&&1===s||r!==this._currentCopies-1&&-1===s)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(y[0],y[1],y[2],y[3],y[4],y[5],y[6],y[7],y[8],y[9],y[10],y[11],y[12],y[13],y[14],y[15]),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(u[0],u[1],u[2],u[3],u[4],u[5],u[6],u[7],u[8],u[9],u[10],u[11],u[12],u[13],u[14],u[15]),v=0;v0&&r<1?[e]:[]:[e-r,e+r].filter((function(t){return t>0&&t<1}))},PolynomialBezier.prototype.split=function(t){if(t<=0)return[singlePoint(this.points[0]),this];if(t>=1)return[this,singlePoint(this.points[this.points.length-1])];var e=lerpPoint(this.points[0],this.points[1],t),i=lerpPoint(this.points[1],this.points[2],t),r=lerpPoint(this.points[2],this.points[3],t),s=lerpPoint(e,i,t),a=lerpPoint(i,r,t),n=lerpPoint(s,a,t);return[new PolynomialBezier(this.points[0],e,s,n,!0),new PolynomialBezier(n,a,r,this.points[3],!0)]},PolynomialBezier.prototype.bounds=function(){return{x:extrema(this,0),y:extrema(this,1)}},PolynomialBezier.prototype.boundingBox=function(){var t=this.bounds();return{left:t.x.min,right:t.x.max,top:t.y.min,bottom:t.y.max,width:t.x.max-t.x.min,height:t.y.max-t.y.min,cx:(t.x.max+t.x.min)/2,cy:(t.y.max+t.y.min)/2}},PolynomialBezier.prototype.intersections=function(t,e,i){void 0===e&&(e=2),void 0===i&&(i=7);var r=[];return intersectsImpl(intersectData(this,0,1),intersectData(t,0,1),0,e,r,i),r},PolynomialBezier.shapeSegment=function(t,e){var i=(e+1)%t.length();return new PolynomialBezier(t.v[e],t.o[e],t.i[i],t.v[i],!0)},PolynomialBezier.shapeSegmentInverted=function(t,e){var i=(e+1)%t.length();return new PolynomialBezier(t.v[i],t.i[i],t.o[e],t.v[e],!0)},extendPrototype([ShapeModifier],ZigZagModifier),ZigZagModifier.prototype.initModifierProperties=function(t,e){this.getValue=this.processKeys,this.amplitude=PropertyFactory.getProp(t,e.s,0,null,this),this.frequency=PropertyFactory.getProp(t,e.r,0,null,this),this.pointsType=PropertyFactory.getProp(t,e.pt,0,null,this),this._isAnimated=0!==this.amplitude.effectsSequence.length||0!==this.frequency.effectsSequence.length||0!==this.pointsType.effectsSequence.length},ZigZagModifier.prototype.processPath=function(t,e,i,r){var s=t._length,a=shapePool.newElement();if(a.c=t.c,t.c||(s-=1),0===s)return a;var n=-1,o=PolynomialBezier.shapeSegment(t,0);zigZagCorner(a,t,0,e,i,r,n);for(var h=0;h=0;a-=1)o=PolynomialBezier.shapeSegmentInverted(t,a),l.push(offsetSegmentSplit(o,e));l=pruneIntersections(l);var p=null,f=null;for(a=0;a=55296&&i<=56319){var r=t.charCodeAt(1);r>=56320&&r<=57343&&(e=1024*(i-55296)+r-56320+65536)}return e}function l(t){var e=h(t);return e>=r&&e<=s}var p=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};p.isModifier=function(t,e){var i=t.toString(16)+e.toString(16);return-1!==a.indexOf(i)},p.isZeroWidthJoiner=function(t){return 8205===t},p.isFlagEmoji=function(t){return l(t.substr(0,2))&&l(t.substr(2,2))},p.isRegionalCode=l,p.isCombinedCharacter=function(t){return-1!==e.indexOf(t)},p.isRegionalFlag=function(t,e){var r=h(t.substr(e,2));if(r!==i)return!1;var s=0;for(e+=2;s<5;){if((r=h(t.substr(e,2)))<917601||r>917626)return!1;s+=1,e+=2}return 917631===h(t.substr(e,2))},p.isVariationSelector=function(t){return 65039===t},p.BLACK_FLAG_CODE_POINT=i;var f={addChars:function(t){if(t){var e;this.chars||(this.chars=[]);var i,r,s=t.length,a=this.chars.length;for(e=0;e0&&(p=!1),p){var f=createTag("style");f.setAttribute("f-forigin",r[i].fOrigin),f.setAttribute("f-origin",r[i].origin),f.setAttribute("f-family",r[i].fFamily),f.type="text/css",f.innerText="@font-face {font-family: "+r[i].fFamily+"; font-style: normal; src: url('"+r[i].fPath+"');}",e.appendChild(f)}}else if("g"===r[i].fOrigin||1===r[i].origin){for(h=document.querySelectorAll('link[f-forigin="g"], link[f-origin="1"]'),l=0;lt?!0!==this.isInRange&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):!1!==this.isInRange&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var t,e=this.renderableComponents.length;for(t=0;t.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},AudioElement.prototype.show=function(){},AudioElement.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},AudioElement.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},AudioElement.prototype.resume=function(){this._canPlay=!0},AudioElement.prototype.setRate=function(t){this.audio.rate(t)},AudioElement.prototype.volume=function(t){this._volumeMultiplier=t,this._previousVolume=t*this._volume,this.audio.volume(this._previousVolume)},AudioElement.prototype.getBaseElement=function(){return null},AudioElement.prototype.destroy=function(){},AudioElement.prototype.sourceRectAtTime=function(){},AudioElement.prototype.initExpressions=function(){},BaseRenderer.prototype.checkLayers=function(t){var e,i,r=this.layers.length;for(this.completeLayers=!0,e=r-1;e>=0;e-=1)this.elements[e]||(i=this.layers[e]).ip-i.st<=t-this.layers[e].st&&i.op-i.st>t-this.layers[e].st&&this.buildItem(e),this.completeLayers=!!this.elements[e]&&this.completeLayers;this.checkPendingElements()},BaseRenderer.prototype.createItem=function(t){switch(t.ty){case 2:return this.createImage(t);case 0:return this.createComp(t);case 1:return this.createSolid(t);case 3:default:return this.createNull(t);case 4:return this.createShape(t);case 5:return this.createText(t);case 6:return this.createAudio(t);case 13:return this.createCamera(t);case 15:return this.createFootage(t)}},BaseRenderer.prototype.createCamera=function(){throw new Error("You're using a 3d camera. Try the html renderer.")},BaseRenderer.prototype.createAudio=function(t){return new AudioElement(t,this.globalData,this)},BaseRenderer.prototype.createFootage=function(t){return new FootageElement(t,this.globalData,this)},BaseRenderer.prototype.buildAllItems=function(){var t,e=this.layers.length;for(t=0;t0&&(this.maskElement.setAttribute("id",y),this.element.maskedElement.setAttribute(v,"url("+getLocationHref()+"#"+y+")"),a.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}TransformElement.prototype={initTransform:function(){var t=new Matrix;this.finalTransform={mProp:this.data.ks?TransformPropertyFactory.getTransformProperty(this,this.data.ks,this):{o:0},_matMdf:!1,_localMatMdf:!1,_opMdf:!1,mat:t,localMat:t,localOpacity:1},this.data.ao&&(this.finalTransform.mProp.autoOriented=!0),this.data.ty},renderTransform:function(){if(this.finalTransform._opMdf=this.finalTransform.mProp.o._mdf||this._isFirstFrame,this.finalTransform._matMdf=this.finalTransform.mProp._mdf||this._isFirstFrame,this.hierarchy){var t,e=this.finalTransform.mat,i=0,r=this.hierarchy.length;if(!this.finalTransform._matMdf)for(;i1&&(a+=" C"+e.o[r-1][0]+","+e.o[r-1][1]+" "+e.i[0][0]+","+e.i[0][1]+" "+e.v[0][0]+","+e.v[0][1]),i.lastPath!==a){var n="";i.elem&&(e.c&&(n=t.inv?this.solidPath+a:a),i.elem.setAttribute("d",n)),i.lastPath=a}},MaskElement.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var filtersFactory=function(){var t={createFilter:function(t,e){var i=createNS("filter");return i.setAttribute("id",t),!0!==e&&(i.setAttribute("filterUnits","objectBoundingBox"),i.setAttribute("x","0%"),i.setAttribute("y","0%"),i.setAttribute("width","100%"),i.setAttribute("height","100%")),i},createAlphaToLuminanceFilter:function(){var t=createNS("feColorMatrix");return t.setAttribute("type","matrix"),t.setAttribute("color-interpolation-filters","sRGB"),t.setAttribute("values","0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1"),t}};return t}(),featureSupport=function(){var t={maskType:!0,svgLumaHidden:!0,offscreenCanvas:"undefined"!=typeof OffscreenCanvas};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(t.maskType=!1),/firefox/i.test(navigator.userAgent)&&(t.svgLumaHidden=!1),t}(),registeredEffects$1={},idPrefix="filter_result_";function SVGEffects(t){var e,i,r="SourceGraphic",s=t.data.ef?t.data.ef.length:0,a=createElementID(),n=filtersFactory.createFilter(a,!0),o=0;for(this.filters=[],e=0;e=0&&!this.shapeModifiers[t].processShapes(this._isFirstFrame);t-=1);}},searchProcessedElement:function(t){for(var e=this.processedElements,i=0,r=e.length;i.01)return!1;i+=1}return!0},GradientProperty.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var t=0,e=this.data.k.k.length;t0;)h=r.transformers[d].mProps._mdf||h,m-=1,d-=1;if(h)for(m=y-r.styles[p].lvl,d=r.transformers.length-1;m>0;)c.multiply(r.transformers[d].mProps.v),m-=1,d-=1}else c=t;if(n=(f=r.sh.paths)._length,h){for(o="",a=0;a=1?v=.99:v<=-1&&(v=-.99);var b=o*v,E=Math.cos(g+e.a.v)*b+p[0],x=Math.sin(g+e.a.v)*b+p[1];h.setAttribute("fx",E),h.setAttribute("fy",x),l&&!e.g._collapsable&&(e.of.setAttribute("fx",E),e.of.setAttribute("fy",x))}}function h(t,e,i){var r=e.style,s=e.d;s&&(s._mdf||i)&&s.dashStr&&(r.pElem.setAttribute("stroke-dasharray",s.dashStr),r.pElem.setAttribute("stroke-dashoffset",s.dashoffset[0])),e.c&&(e.c._mdf||i)&&r.pElem.setAttribute("stroke","rgb("+bmFloor(e.c.v[0])+","+bmFloor(e.c.v[1])+","+bmFloor(e.c.v[2])+")"),(e.o._mdf||i)&&r.pElem.setAttribute("stroke-opacity",e.o.v),(e.w._mdf||i)&&(r.pElem.setAttribute("stroke-width",e.w.v),r.msElem&&r.msElem.setAttribute("stroke-width",e.w.v))}return{createRenderFunction:function(t){switch(t.ty){case"fl":return a;case"gf":return o;case"gs":return n;case"st":return h;case"sh":case"el":case"rc":case"sr":return s;case"tr":return i;case"no":return r;default:return null}}}}();function SVGShapeElement(t,e,i){this.shapes=[],this.shapesData=t.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(t,e,i),this.prevViewData=[]}function LetterProps(t,e,i,r,s,a){this.o=t,this.sw=e,this.sc=i,this.fc=r,this.m=s,this.p=a,this._mdf={o:!0,sw:!!e,sc:!!i,fc:!!r,m:!0,p:!0}}function TextProperty(t,e){this._frameId=initialDefaultFrame,this.pv="",this.v="",this.kf=!1,this._isFirstFrame=!0,this._mdf=!1,e.d&&e.d.sid&&(e.d=t.globalData.slotManager.getProp(e.d)),this.data=e,this.elem=t,this.comp=this.elem.comp,this.keysIndex=0,this.canResize=!1,this.minimumFontSize=1,this.effectsSequence=[],this.currentData={ascent:0,boxWidth:this.defaultBoxWidth,f:"",fStyle:"",fWeight:"",fc:"",j:"",justifyOffset:"",l:[],lh:0,lineWidths:[],ls:"",of:"",s:"",sc:"",sw:0,t:0,tr:0,sz:0,ps:null,fillColorAnim:!1,strokeColorAnim:!1,strokeWidthAnim:!1,yOffset:0,finalSize:0,finalText:[],finalLineHeight:0,__complete:!1},this.copyData(this.currentData,this.data.d.k[0].s),this.searchProperty()||this.completeTextData(this.currentData)}extendPrototype([BaseElement,TransformElement,SVGBaseElement,IShapeElement,HierarchyElement,FrameElement,RenderableDOMElement],SVGShapeElement),SVGShapeElement.prototype.initSecondaryElement=function(){},SVGShapeElement.prototype.identityMatrix=new Matrix,SVGShapeElement.prototype.buildExpressionInterface=function(){},SVGShapeElement.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},SVGShapeElement.prototype.filterUniqueShapes=function(){var t,e,i,r,s=this.shapes.length,a=this.stylesList.length,n=[],o=!1;for(i=0;i1&&o&&this.setShapesAsAnimated(n)}},SVGShapeElement.prototype.setShapesAsAnimated=function(t){var e,i=t.length;for(e=0;e=0;o-=1){if((c=this.searchProcessedElement(t[o]))?e[o]=i[c-1]:t[o]._render=n,"fl"===t[o].ty||"st"===t[o].ty||"gf"===t[o].ty||"gs"===t[o].ty||"no"===t[o].ty)c?e[o].style.closed=!1:e[o]=this.createStyleElement(t[o],s),t[o]._render&&e[o].style.pElem.parentNode!==r&&r.appendChild(e[o].style.pElem),u.push(e[o].style);else if("gr"===t[o].ty){if(c)for(l=e[o].it.length,h=0;h1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},TextProperty.prototype.addEffect=function(t){this.effectsSequence.push(t),this.elem.addDynamicProperty(this)},TextProperty.prototype.getValue=function(t){if(this.elem.globalData.frameId!==this.frameId&&this.effectsSequence.length||t){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var e=this.currentData,i=this.keysIndex;if(this.lock)this.setCurrentData(this.currentData);else{var r;this.lock=!0,this._mdf=!1;var s=this.effectsSequence.length,a=t||this.data.d.k[this.keysIndex].s;for(r=0;re);)i+=1;return this.keysIndex!==i&&(this.keysIndex=i),this.data.d.k[this.keysIndex].s},TextProperty.prototype.buildFinalText=function(t){for(var e,i,r=[],s=0,a=t.length,n=!1,o=!1,h="";s=55296&&e<=56319?FontManager.isRegionalFlag(t,s)?h=t.substr(s,14):(i=t.charCodeAt(s+1))>=56320&&i<=57343&&(FontManager.isModifier(e,i)?(h=t.substr(s,2),n=!0):h=FontManager.isFlagEmoji(t.substr(s,4))?t.substr(s,4):t.substr(s,2)):e>56319?(i=t.charCodeAt(s+1),FontManager.isVariationSelector(e)&&(n=!0)):FontManager.isZeroWidthJoiner(e)&&(n=!0,o=!0),n?(r[r.length-1]+=h,n=!1):r.push(h),s+=h.length;return r},TextProperty.prototype.completeTextData=function(t){t.__complete=!0;var e,i,r,s,a,n,o,h=this.elem.globalData.fontManager,l=this.data,p=[],f=0,c=l.m.g,m=0,d=0,u=0,y=[],g=0,v=0,b=h.getFontByName(t.f),E=0,x=getFontProperties(b);t.fWeight=x.weight,t.fStyle=x.style,t.finalSize=t.s,t.finalText=this.buildFinalText(t.t),i=t.finalText.length,t.finalLineHeight=t.lh;var P,S=t.tr/1e3*t.finalSize;if(t.sz)for(var _,C,T=!0,A=t.sz[0],k=t.sz[1];T;){_=0,g=0,i=(C=this.buildFinalText(t.t)).length,S=t.tr/1e3*t.finalSize;var M=-1;for(e=0;eA&&" "!==C[e]?(-1===M?i+=1:e=M,_+=t.finalLineHeight||1.2*t.finalSize,C.splice(e,M===e?1:0,"\r"),M=-1,g=0):(g+=E,g+=S);_+=b.ascent*t.finalSize/100,this.canResize&&t.finalSize>this.minimumFontSize&&k<_?(t.finalSize-=1,t.finalLineHeight=t.finalSize*t.lh/t.s):(t.finalText=C,i=t.finalText.length,T=!1)}g=-S,E=0;var D,F=0;for(e=0;ev?g:v,g=-2*S,s="",r=!0,u+=1):s=D,h.chars?(o=h.getCharData(D,b.fStyle,h.getFontByName(t.f).fFamily),E=r?0:o.w*t.finalSize/100):E=h.measureText(s,t.f,t.finalSize)," "===D?F+=E+S:(g+=E+S+F,F=0),p.push({l:E,an:E,add:m,n:r,anIndexes:[],val:s,line:u,animatorJustifyOffset:0}),2==c){if(m+=E,""===s||" "===s||e===i-1){for(""!==s&&" "!==s||(m-=E);d<=e;)p[d].an=m,p[d].ind=f,p[d].extra=E,d+=1;f+=1,m=0}}else if(3==c){if(m+=E,""===s||e===i-1){for(""===s&&(m-=E);d<=e;)p[d].an=m,p[d].ind=f,p[d].extra=E,d+=1;m=0,f+=1}}else p[f].ind=f,p[f].extra=0,f+=1;if(t.l=p,v=g>v?g:v,y.push(g),t.sz)t.boxWidth=t.sz[0],t.justifyOffset=0;else switch(t.boxWidth=v,t.j){case 1:t.justifyOffset=-t.boxWidth;break;case 2:t.justifyOffset=-t.boxWidth/2;break;default:t.justifyOffset=0}t.lineWidths=y;var w,I,L,B,V=l.a;n=V.length;var R=[];for(a=0;a0?s=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?n=1-this.xe.v/100:o=1+this.xe.v/100;var h=BezierFactory.getBezierEasing(s,a,n,o).get,l=0,p=this.finalS,f=this.finalE,c=this.data.sh;if(2===c)l=h(l=f===p?r>=f?1:0:t(0,e(.5/(f-p)+(r-p)/(f-p),1)));else if(3===c)l=h(l=f===p?r>=f?0:1:1-t(0,e(.5/(f-p)+(r-p)/(f-p),1)));else if(4===c)f===p?l=0:(l=t(0,e(.5/(f-p)+(r-p)/(f-p),1)))<.5?l*=2:l=1-2*(l-.5),l=h(l);else if(5===c){if(f===p)l=0;else{var m=f-p,d=-m/2+(r=e(t(0,r+.5-p),f-p)),u=m/2;l=Math.sqrt(1-d*d/(u*u))}l=h(l)}else 6===c?(f===p?l=0:(r=e(t(0,r+.5-p),f-p),l=(1+Math.cos(Math.PI+2*Math.PI*r/(f-p)))/2),l=h(l)):(r>=i(p)&&(l=t(0,e(r-p<0?e(f,1)-(p-r):f-r,1))),l=h(l));if(100!==this.sm.v){var y=.01*this.sm.v;0===y&&(y=1e-8);var g=.5-.5*y;l1&&(l=1)}return l*this.a.v},getValue:function(t){this.iterateDynamicProperties(),this._mdf=t||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,t&&2===this.data.r&&(this.e.v=this._currentTextLength);var e=2===this.data.r?1:100/this.data.totalChars,i=this.o.v/e,r=this.s.v/e+i,s=this.e.v/e+i;if(r>s){var a=r;r=s,s=a}this.finalS=r,this.finalE=s}},extendPrototype([DynamicPropertyContainer],r),{getTextSelectorProp:function(t,e,i){return new r(t,e)}}}();function TextAnimatorDataProperty(t,e,i){var r={propType:!1},s=PropertyFactory.getProp,a=e.a;this.a={r:a.r?s(t,a.r,0,degToRads,i):r,rx:a.rx?s(t,a.rx,0,degToRads,i):r,ry:a.ry?s(t,a.ry,0,degToRads,i):r,sk:a.sk?s(t,a.sk,0,degToRads,i):r,sa:a.sa?s(t,a.sa,0,degToRads,i):r,s:a.s?s(t,a.s,1,.01,i):r,a:a.a?s(t,a.a,1,0,i):r,o:a.o?s(t,a.o,0,.01,i):r,p:a.p?s(t,a.p,1,0,i):r,sw:a.sw?s(t,a.sw,0,0,i):r,sc:a.sc?s(t,a.sc,1,0,i):r,fc:a.fc?s(t,a.fc,1,0,i):r,fh:a.fh?s(t,a.fh,0,0,i):r,fs:a.fs?s(t,a.fs,0,.01,i):r,fb:a.fb?s(t,a.fb,0,.01,i):r,t:a.t?s(t,a.t,0,0,i):r},this.s=TextSelectorProp.getTextSelectorProp(t,e.s,i),this.s.t=e.s.t}function TextAnimatorProperty(t,e,i){this._isFirstFrame=!0,this._hasMaskedPath=!1,this._frameId=-1,this._textData=t,this._renderType=e,this._elem=i,this._animatorsData=createSizedArray(this._textData.a.length),this._pathData={},this._moreOptions={alignment:{}},this.renderedLetters=[],this.lettersChangedFlag=!1,this.initDynamicPropertyContainer(i)}function ITextElement(){}TextAnimatorProperty.prototype.searchProperties=function(){var t,e,i=this._textData.a.length,r=PropertyFactory.getProp;for(t=0;t=o+ot||!d?(v=(o+ot-l)/h.partialLength,G=m.point[0]+(h.point[0]-m.point[0])*v,z=m.point[1]+(h.point[1]-m.point[1])*v,_.translate(-x[0]*A[s].an*.005,-x[1]*B*.01),p=!1):d&&(l+=h.partialLength,(f+=1)>=d.length&&(f=0,u[c+=1]?d=u[c].points:E.v.c?(f=0,d=u[c=0].points):(l-=h.partialLength,d=null)),d&&(m=h,y=(h=d[f]).partialLength));R=A[s].an/2-A[s].add,_.translate(-R,0,0)}else R=A[s].an/2-A[s].add,_.translate(-R,0,0),_.translate(-x[0]*A[s].an*.005,-x[1]*B*.01,0);for(F=0;Ft?this.textSpans[t].span:createNS(h?"g":"text"),y<=t){if(n.setAttribute("stroke-linecap","butt"),n.setAttribute("stroke-linejoin","round"),n.setAttribute("stroke-miterlimit","4"),this.textSpans[t].span=n,h){var g=createNS("g");n.appendChild(g),this.textSpans[t].childSpan=g}this.textSpans[t].span=n,this.layerElement.appendChild(n)}n.style.display="inherit"}if(l.reset(),p&&(o[t].n&&(f=-d,c+=i.yOffset,c+=m?1:0,m=!1),this.applyTextPropertiesToMatrix(i,l,o[t].line,f,c),f+=o[t].l||0,f+=d),h){var v;if(1===(u=this.globalData.fontManager.getCharData(i.finalText[t],r.fStyle,this.globalData.fontManager.getFontByName(i.f).fFamily)).t)v=new SVGCompElement(u.data,this.globalData,this);else{var b=emptyShapeData;u.data&&u.data.shapes&&(b=this.buildShapeData(u.data,i.finalSize)),v=new SVGShapeElement(b,this.globalData,this)}if(this.textSpans[t].glyph){var E=this.textSpans[t].glyph;this.textSpans[t].childSpan.removeChild(E.layerElement),E.destroy()}this.textSpans[t].glyph=v,v._debug=!0,v.prepareFrame(0),v.renderFrame(),this.textSpans[t].childSpan.appendChild(v.layerElement),1===u.t&&this.textSpans[t].childSpan.setAttribute("transform","scale("+i.finalSize/100+","+i.finalSize/100+")")}else p&&n.setAttribute("transform","translate("+l.props[12]+","+l.props[13]+")"),n.textContent=o[t].val,n.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve")}p&&n&&n.setAttribute("d","")}else{var x=this.textContainer,P="start";switch(i.j){case 1:P="end";break;case 2:P="middle";break;default:P="start"}x.setAttribute("text-anchor",P),x.setAttribute("letter-spacing",d);var S=this.buildTextContents(i.finalText);for(e=S.length,c=i.ps?i.ps[1]+i.ascent:0,t=0;t=0;e-=1)(this.completeLayers||this.elements[e])&&this.elements[e].prepareFrame(t-this.layers[e].st);if(this.globalData._mdf)for(e=0;e=0;i-=1)(this.completeLayers||this.elements[i])&&(this.elements[i].prepareFrame(this.renderedFrame-this.layers[i].st),this.elements[i]._mdf&&(this._mdf=!0))}},ICompElement.prototype.renderInnerContent=function(){var t,e=this.layers.length;for(t=0;t=0;i-=1)t.finalTransform.multiply(t.transforms[i].transform.mProps.v);t._mdf=s},processSequences:function(t){var e,i=this.sequenceList.length;for(e=0;e=1){this.buffers=[];var t=this.globalData.canvasContext,e=assetLoader.createCanvas(t.canvas.width,t.canvas.height);this.buffers.push(e);var i=assetLoader.createCanvas(t.canvas.width,t.canvas.height);this.buffers.push(i),this.data.tt>=3&&!document._isProxy&&assetLoader.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new CVEffects(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var t=this.globalData;if(t.blendMode!==this.data.bm){t.blendMode=this.data.bm;var e=getBlendMode(this.data.bm);t.canvasContext.globalCompositeOperation=e}},createRenderableComponents:function(){this.maskManager=new CVMaskElement(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(effectTypes.TRANSFORM_EFFECT)},hideElement:function(){this.hidden||this.isInRange&&!this.isTransparent||(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(t){t.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var t=this.buffers[0].getContext("2d");this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var t=this.buffers[1],e=t.getContext("2d");if(this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById("tp"in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var i=assetLoader.getLumaCanvas(this.canvasContext.canvas);i.getContext("2d").drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(i,0,0)}this.canvasContext.globalCompositeOperation=operationsMap[this.data.tt],this.canvasContext.drawImage(t,0,0),this.canvasContext.globalCompositeOperation="destination-over",this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation="source-over"}},renderFrame:function(t){if(!this.hidden&&!this.data.hd&&(1!==this.data.td||t)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var e=0===this.data.ty;this.prepareLayer(),this.globalData.renderer.save(e),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(e),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&(this._isFirstFrame=!1)}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new Matrix},CVBaseElement.prototype.hide=CVBaseElement.prototype.hideElement,CVBaseElement.prototype.show=CVBaseElement.prototype.showElement,CVShapeData.prototype.setAsAnimated=SVGShapeData.prototype.setAsAnimated,extendPrototype([BaseElement,TransformElement,CVBaseElement,IShapeElement,HierarchyElement,FrameElement,RenderableElement],CVShapeElement),CVShapeElement.prototype.initElement=RenderableDOMElement.prototype.initElement,CVShapeElement.prototype.transformHelper={opacity:1,_opMdf:!1},CVShapeElement.prototype.dashResetter=[],CVShapeElement.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,!0,[])},CVShapeElement.prototype.createStyleElement=function(t,e){var i={data:t,type:t.ty,preTransforms:this.transformsManager.addTransformSequence(e),transforms:[],elements:[],closed:!0===t.hd},r={};if("fl"===t.ty||"st"===t.ty?(r.c=PropertyFactory.getProp(this,t.c,1,255,this),r.c.k||(i.co="rgb("+bmFloor(r.c.v[0])+","+bmFloor(r.c.v[1])+","+bmFloor(r.c.v[2])+")")):"gf"!==t.ty&&"gs"!==t.ty||(r.s=PropertyFactory.getProp(this,t.s,1,null,this),r.e=PropertyFactory.getProp(this,t.e,1,null,this),r.h=PropertyFactory.getProp(this,t.h||{k:0},0,.01,this),r.a=PropertyFactory.getProp(this,t.a||{k:0},0,degToRads,this),r.g=new GradientProperty(this,t.g,this)),r.o=PropertyFactory.getProp(this,t.o,0,.01,this),"st"===t.ty||"gs"===t.ty){if(i.lc=lineCapEnum[t.lc||2],i.lj=lineJoinEnum[t.lj||2],1==t.lj&&(i.ml=t.ml),r.w=PropertyFactory.getProp(this,t.w,0,null,this),r.w.k||(i.wi=r.w.v),t.d){var s=new DashProperty(this,t.d,"canvas",this);r.d=s,r.d.k||(i.da=r.d.dashArray,i.do=r.d.dashoffset[0])}}else i.r=2===t.r?"evenodd":"nonzero";return this.stylesList.push(i),r.style=i,r},CVShapeElement.prototype.createGroupElement=function(){return{it:[],prevViewData:[]}},CVShapeElement.prototype.createTransformElement=function(t){return{transform:{opacity:1,_opMdf:!1,key:this.transformsManager.getNewKey(),op:PropertyFactory.getProp(this,t.o,0,.01,this),mProps:TransformPropertyFactory.getTransformProperty(this,t,this)}}},CVShapeElement.prototype.createShapeElement=function(t){var e=new CVShapeData(this,t,this.stylesList,this.transformsManager);return this.shapes.push(e),this.addShapeToModifiers(e),e},CVShapeElement.prototype.reloadShapes=function(){var t;this._isFirstFrame=!0;var e=this.itemsData.length;for(t=0;t=0;a-=1){if((h=this.searchProcessedElement(t[a]))?e[a]=i[h-1]:t[a]._shouldRender=r,"fl"===t[a].ty||"st"===t[a].ty||"gf"===t[a].ty||"gs"===t[a].ty)h?e[a].style.closed=!1:e[a]=this.createStyleElement(t[a],d),c.push(e[a].style);else if("gr"===t[a].ty){if(h)for(o=e[a].it.length,n=0;n=0;s-=1)"tr"===e[s].ty?(a=i[s].transform,this.renderShapeTransform(t,a)):"sh"===e[s].ty||"el"===e[s].ty||"rc"===e[s].ty||"sr"===e[s].ty?this.renderPath(e[s],i[s]):"fl"===e[s].ty?this.renderFill(e[s],i[s],a):"st"===e[s].ty?this.renderStroke(e[s],i[s],a):"gf"===e[s].ty||"gs"===e[s].ty?this.renderGradientFill(e[s],i[s],a):"gr"===e[s].ty?this.renderShape(a,e[s].it,i[s].it):e[s].ty;r&&this.drawLayer()},CVShapeElement.prototype.renderStyledShape=function(t,e){if(this._isFirstFrame||e._mdf||t.transforms._mdf){var i,r,s,a=t.trNodes,n=e.paths,o=n._length;a.length=0;var h=t.transforms.finalTransform;for(s=0;s=1?f=.99:f<=-1&&(f=-.99);var c=l*f,m=Math.cos(p+e.a.v)*c+o[0],d=Math.sin(p+e.a.v)*c+o[1];r=n.createRadialGradient(m,d,0,o[0],o[1],l)}var u=t.g.p,y=e.g.c,g=1;for(a=0;ao&&"xMidYMid slice"===h||ns&&"meet"===o||as&&"slice"===o)?(i-this.transformCanvas.w*(r/this.transformCanvas.h))/2*this.renderConfig.dpr:"xMax"===l&&(as&&"slice"===o)?(i-this.transformCanvas.w*(r/this.transformCanvas.h))*this.renderConfig.dpr:0,this.transformCanvas.ty="YMid"===p&&(a>s&&"meet"===o||as&&"meet"===o||a=0;t-=1)this.elements[t]&&this.elements[t].destroy&&this.elements[t].destroy();this.elements.length=0,this.globalData.canvasContext=null,this.animationItem.container=null,this.destroyed=!0},CanvasRendererBase.prototype.renderFrame=function(t,e){if((this.renderedFrame!==t||!0!==this.renderConfig.clearCanvas||e)&&!this.destroyed&&-1!==t){var i;this.renderedFrame=t,this.globalData.frameNum=t-this.animationItem._isFirstFrame,this.globalData.frameId+=1,this.globalData._mdf=!this.renderConfig.clearCanvas||e,this.globalData.projectInterface.currentFrame=t;var r=this.layers.length;for(this.completeLayers||this.checkLayers(t),i=r-1;i>=0;i-=1)(this.completeLayers||this.elements[i])&&this.elements[i].prepareFrame(t-this.layers[i].st);if(this.globalData._mdf){for(!0===this.renderConfig.clearCanvas?this.canvasContext.clearRect(0,0,this.transformCanvas.w,this.transformCanvas.h):this.save(),i=r-1;i>=0;i-=1)(this.completeLayers||this.elements[i])&&this.elements[i].renderFrame();!0!==this.renderConfig.clearCanvas&&this.restore()}}},CanvasRendererBase.prototype.buildItem=function(t){var e=this.elements;if(!e[t]&&99!==this.layers[t].ty){var i=this.createItem(this.layers[t],this,this.globalData);e[t]=i,i.initExpressions()}},CanvasRendererBase.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},CanvasRendererBase.prototype.hide=function(){this.animationItem.container.style.display="none"},CanvasRendererBase.prototype.show=function(){this.animationItem.container.style.display="block"},CVContextData.prototype.duplicate=function(){var t=2*this._length,e=0;for(e=this._length;e=0;t-=1)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},CVCompElement.prototype.destroy=function(){var t;for(t=this.layers.length-1;t>=0;t-=1)this.elements[t]&&this.elements[t].destroy();this.layers=null,this.elements=null},CVCompElement.prototype.createComp=function(t){return new CVCompElement(t,this.globalData,this)},extendPrototype([CanvasRendererBase],CanvasRenderer),CanvasRenderer.prototype.createComp=function(t){return new CVCompElement(t,this.globalData,this)},HBaseElement.prototype={checkBlendMode:function(){},initRendererElement:function(){this.baseElement=createTag(this.data.tg||"div"),this.data.hasMask?(this.svgElement=createNS("svg"),this.layerElement=createNS("g"),this.maskedElement=this.layerElement,this.svgElement.appendChild(this.layerElement),this.baseElement.appendChild(this.svgElement)):this.layerElement=this.baseElement,styleDiv(this.baseElement)},createContainerElements:function(){this.renderableEffectsManager=new CVEffects(this),this.transformedElement=this.baseElement,this.maskedElement=this.layerElement,this.data.ln&&this.layerElement.setAttribute("id",this.data.ln),this.data.cl&&this.layerElement.setAttribute("class",this.data.cl),0!==this.data.bm&&this.setBlendMode()},renderElement:function(){var t=this.transformedElement?this.transformedElement.style:{};if(this.finalTransform._matMdf){var e=this.finalTransform.mat.toCSS();t.transform=e,t.webkitTransform=e}this.finalTransform._opMdf&&(t.opacity=this.finalTransform.mProp.o.v)},renderFrame:function(){this.data.hd||this.hidden||(this.renderTransform(),this.renderRenderable(),this.renderElement(),this.renderInnerContent(),this._isFirstFrame&&(this._isFirstFrame=!1))},destroy:function(){this.layerElement=null,this.transformedElement=null,this.matteElement&&(this.matteElement=null),this.maskManager&&(this.maskManager.destroy(),this.maskManager=null)},createRenderableComponents:function(){this.maskManager=new MaskElement(this.data,this,this.globalData)},addEffects:function(){},setMatte:function(){}},HBaseElement.prototype.getBaseElement=SVGBaseElement.prototype.getBaseElement,HBaseElement.prototype.destroyBaseElement=HBaseElement.prototype.destroy,HBaseElement.prototype.buildElementParenting=BaseRenderer.prototype.buildElementParenting,extendPrototype([BaseElement,TransformElement,HBaseElement,HierarchyElement,FrameElement,RenderableDOMElement],HSolidElement),HSolidElement.prototype.createContent=function(){var t;this.data.hasMask?((t=createNS("rect")).setAttribute("width",this.data.sw),t.setAttribute("height",this.data.sh),t.setAttribute("fill",this.data.sc),this.svgElement.setAttribute("width",this.data.sw),this.svgElement.setAttribute("height",this.data.sh)):((t=createTag("div")).style.width=this.data.sw+"px",t.style.height=this.data.sh+"px",t.style.backgroundColor=this.data.sc),this.layerElement.appendChild(t)},extendPrototype([BaseElement,TransformElement,HSolidElement,SVGShapeElement,HBaseElement,HierarchyElement,FrameElement,RenderableElement],HShapeElement),HShapeElement.prototype._renderShapeFrame=HShapeElement.prototype.renderInnerContent,HShapeElement.prototype.createContent=function(){var t;if(this.baseElement.style.fontSize=0,this.data.hasMask)this.layerElement.appendChild(this.shapesContainer),t=this.svgElement;else{t=createNS("svg");var e=this.comp.data?this.comp.data:this.globalData.compSize;t.setAttribute("width",e.w),t.setAttribute("height",e.h),t.appendChild(this.shapesContainer),this.layerElement.appendChild(t)}this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.shapesContainer,0,[],!0),this.filterUniqueShapes(),this.shapeCont=t},HShapeElement.prototype.getTransformedPoint=function(t,e){var i,r=t.length;for(i=0;i0&&o<1&&f[c].push(this.calculateF(o,t,e,i,r,c)):(h=a*a-4*n*s)>=0&&((l=(-a+bmSqrt(h))/(2*s))>0&&l<1&&f[c].push(this.calculateF(l,t,e,i,r,c)),(p=(-a-bmSqrt(h))/(2*s))>0&&p<1&&f[c].push(this.calculateF(p,t,e,i,r,c))));this.shapeBoundingBox.left=bmMin.apply(null,f[0]),this.shapeBoundingBox.top=bmMin.apply(null,f[1]),this.shapeBoundingBox.right=bmMax.apply(null,f[0]),this.shapeBoundingBox.bottom=bmMax.apply(null,f[1])},HShapeElement.prototype.calculateF=function(t,e,i,r,s,a){return bmPow(1-t,3)*e[a]+3*bmPow(1-t,2)*t*i[a]+3*(1-t)*bmPow(t,2)*r[a]+bmPow(t,3)*s[a]},HShapeElement.prototype.calculateBoundingBox=function(t,e){var i,r=t.length;for(i=0;ii&&(i=s)}i*=t.mult}else i=t.v*t.mult;e.x-=i,e.xMax+=i,e.y-=i,e.yMax+=i},HShapeElement.prototype.currentBoxContains=function(t){return this.currentBBox.x<=t.x&&this.currentBBox.y<=t.y&&this.currentBBox.width+this.currentBBox.x>=t.x+t.width&&this.currentBBox.height+this.currentBBox.y>=t.y+t.height},HShapeElement.prototype.renderInnerContent=function(){if(this._renderShapeFrame(),!this.hidden&&(this._isFirstFrame||this._mdf)){var t=this.tempBoundingBox,e=999999;if(t.x=e,t.xMax=-e,t.y=e,t.yMax=-e,this.calculateBoundingBox(this.itemsData,t),t.width=t.xMax=0;t-=1){var r=this.hierarchy[t].finalTransform.mProp;this.mat.translate(-r.p.v[0],-r.p.v[1],r.p.v[2]),this.mat.rotateX(-r.or.v[0]).rotateY(-r.or.v[1]).rotateZ(r.or.v[2]),this.mat.rotateX(-r.rx.v).rotateY(-r.ry.v).rotateZ(r.rz.v),this.mat.scale(1/r.s.v[0],1/r.s.v[1],1/r.s.v[2]),this.mat.translate(r.a.v[0],r.a.v[1],r.a.v[2])}if(this.p?this.mat.translate(-this.p.v[0],-this.p.v[1],this.p.v[2]):this.mat.translate(-this.px.v,-this.py.v,this.pz.v),this.a){var s;s=this.p?[this.p.v[0]-this.a.v[0],this.p.v[1]-this.a.v[1],this.p.v[2]-this.a.v[2]]:[this.px.v-this.a.v[0],this.py.v-this.a.v[1],this.pz.v-this.a.v[2]];var a=Math.sqrt(Math.pow(s[0],2)+Math.pow(s[1],2)+Math.pow(s[2],2)),n=[s[0]/a,s[1]/a,s[2]/a],o=Math.sqrt(n[2]*n[2]+n[0]*n[0]),h=Math.atan2(n[1],o),l=Math.atan2(n[0],-n[2]);this.mat.rotateY(l).rotateX(-h)}this.mat.rotateX(-this.rx.v).rotateY(-this.ry.v).rotateZ(this.rz.v),this.mat.rotateX(-this.or.v[0]).rotateY(-this.or.v[1]).rotateZ(this.or.v[2]),this.mat.translate(this.globalData.compSize.w/2,this.globalData.compSize.h/2,0),this.mat.translate(0,0,this.pe.v);var p=!this._prevMat.equals(this.mat);if((p||this.pe._mdf)&&this.comp.threeDElements){var f,c,m;for(e=this.comp.threeDElements.length,t=0;t=t)return this.threeDElements[e].perspectiveElem;e+=1}return null},HybridRendererBase.prototype.createThreeDContainer=function(t,e){var i,r,s=createTag("div");styleDiv(s);var a=createTag("div");if(styleDiv(a),"3d"===e){(i=s.style).width=this.globalData.compSize.w+"px",i.height=this.globalData.compSize.h+"px";var n="50% 50%";i.webkitTransformOrigin=n,i.mozTransformOrigin=n,i.transformOrigin=n;var o="matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)";(r=a.style).transform=o,r.webkitTransform=o}s.appendChild(a);var h={container:a,perspectiveElem:s,startPos:t,endPos:t,type:e};return this.threeDElements.push(h),h},HybridRendererBase.prototype.build3dContainers=function(){var t,e,i=this.layers.length,r="";for(t=0;t=0;t-=1)this.resizerElem.appendChild(this.threeDElements[t].perspectiveElem)},HybridRendererBase.prototype.addTo3dContainer=function(t,e){for(var i=0,r=this.threeDElements.length;in?(t=s/this.globalData.compSize.w,e=s/this.globalData.compSize.w,i=0,r=(a-this.globalData.compSize.h*(s/this.globalData.compSize.w))/2):(t=a/this.globalData.compSize.h,e=a/this.globalData.compSize.h,i=(s-this.globalData.compSize.w*(a/this.globalData.compSize.h))/2,r=0);var o=this.resizerElem.style;o.webkitTransform="matrix3d("+t+",0,0,0,0,"+e+",0,0,0,0,1,0,"+i+","+r+",0,1)",o.transform=o.webkitTransform},HybridRendererBase.prototype.renderFrame=SVGRenderer.prototype.renderFrame,HybridRendererBase.prototype.hide=function(){this.resizerElem.style.display="none"},HybridRendererBase.prototype.show=function(){this.resizerElem.style.display="block"},HybridRendererBase.prototype.initItems=function(){if(this.buildAllItems(),this.camera)this.camera.setup();else{var t,e=this.globalData.compSize.w,i=this.globalData.compSize.h,r=this.threeDElements.length;for(t=0;t=o;)t/=2,e/=2,i>>>=1;return(t+i)/e};return b.int32=function(){return 0|v.g(4)},b.quick=function(){return v.g(4)/4294967296},b.double=b,c(m(v.S),t),(d.pass||u||function(t,i,r,a){return a&&(a.S&&p(a,v),t.state=function(){return p(v,{})}),r?(e[s]=t,i):t})(b,g,"global"in d?d.global:this==e,d.state)},c(e.random(),t)}function initialize$2(t){seedRandom([],t)}var propTypes={SHAPE:"shape"};function _typeof$1(t){return _typeof$1="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof$1(t)}var ExpressionManager=function(){var ob={},Math=BMMath,window=null,document=null,XMLHttpRequest=null,fetch=null,frames=null,_lottieGlobal={};function resetFrame(){_lottieGlobal={}}function $bm_isInstanceOfArray(t){return t.constructor===Array||t.constructor===Float32Array}function isNumerable(t,e){return"number"===t||e instanceof Number||"boolean"===t||"string"===t}function $bm_neg(t){var e=_typeof$1(t);if("number"===e||t instanceof Number||"boolean"===e)return-t;if($bm_isInstanceOfArray(t)){var i,r=t.length,s=[];for(i=0;ii){var r=i;i=e,e=r}return Math.min(Math.max(t,e),i)}function radiansToDegrees(t){return t/degToRads}var radians_to_degrees=radiansToDegrees;function degreesToRadians(t){return t*degToRads}var degrees_to_radians=radiansToDegrees,helperLengthArray=[0,0,0,0,0,0];function length(t,e){if("number"==typeof t||t instanceof Number)return e=e||0,Math.abs(t-e);var i;e||(e=helperLengthArray);var r=Math.min(t.length,e.length),s=0;for(i=0;i.5?l/(2-n-o):l/(n+o),n){case r:e=(s-a)/l+(s1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}function hslToRgb(t){var e,i,r,s=t[0],a=t[1],n=t[2];if(0===a)e=n,r=n,i=n;else{var o=n<.5?n*(1+a):n+a-n*a,h=2*n-o;e=hue2rgb(h,o,s+1/3),i=hue2rgb(h,o,s),r=hue2rgb(h,o,s-1/3)}return[e,i,r,t[3]]}function linear(t,e,i,r,s){if(void 0!==r&&void 0!==s||(r=e,s=i,e=0,i=1),i=i)return s;var n,o=i===e?0:(t-e)/(i-e);if(!r.length)return r+(s-r)*o;var h=r.length,l=createTypedArray("float32",h);for(n=0;n1){for(r=0;r1?e=1:e<0&&(e=0);var n=t(e);if($bm_isInstanceOfArray(s)){var o,h=s.length,l=createTypedArray("float32",h);for(o=0;odata.k[e].t&&tdata.k[e+1].t-t?(i=e+2,r=data.k[e+1].t):(i=e+1,r=data.k[e].t);break}}-1===i&&(i=e+1,r=data.k[e].t)}else i=0,r=0;var a={};return a.index=i,a.time=r/elem.comp.globalData.frameRate,a}function key(t){var e,i,r;if(!data.k.length||"number"==typeof data.k[0])throw new Error("The property has no keyframe at index "+t);t-=1,e={time:data.k[t].t/elem.comp.globalData.frameRate,value:[]};var s=Object.prototype.hasOwnProperty.call(data.k[t],"s")?data.k[t].s:data.k[t-1].e;for(r=s.length,i=0;il.length-1)&&(e=l.length-1),r=p-(s=l[l.length-1-e].t)),"pingpong"===t){if(Math.floor((h-s)/r)%2!=0)return this.getValueAtTime((r-(h-s)%r+s)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var f=this.getValueAtTime(s/this.comp.globalData.frameRate,0),c=this.getValueAtTime(p/this.comp.globalData.frameRate,0),m=this.getValueAtTime(((h-s)%r+s)/this.comp.globalData.frameRate,0),d=Math.floor((h-s)/r);if(this.pv.length){for(n=(o=new Array(f.length)).length,a=0;a=p)return this.pv;if(i?s=p+(r=e?Math.abs(this.elem.comp.globalData.frameRate*e):Math.max(0,this.elem.data.op-p)):((!e||e>l.length-1)&&(e=l.length-1),r=(s=l[e].t)-p),"pingpong"===t){if(Math.floor((p-h)/r)%2==0)return this.getValueAtTime(((p-h)%r+p)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var f=this.getValueAtTime(p/this.comp.globalData.frameRate,0),c=this.getValueAtTime(s/this.comp.globalData.frameRate,0),m=this.getValueAtTime((r-(p-h)%r+p)/this.comp.globalData.frameRate,0),d=Math.floor((p-h)/r)+1;if(this.pv.length){for(n=(o=new Array(f.length)).length,a=0;a1?(s+t-a)/(e-1):1,o=0,h=0;for(i=this.pv.length?createTypedArray("float32",this.pv.length):0;on){var p=o,f=i.c&&o===h-1?0:o+1,c=(n-l)/a[o].addedLength;r=bez.getPointInSegment(i.v[p],i.v[f],i.o[p],i.i[f],c,a[o]);break}l+=a[o].addedLength,o+=1}return r||(r=i.c?[i.v[0][0],i.v[0][1]]:[i.v[i._length-1][0],i.v[i._length-1][1]]),r},vectorOnPath:function(t,e,i){1==t?t=this.v.c:0==t&&(t=.999);var r=this.pointOnPath(t,e),s=this.pointOnPath(t+.001,e),a=s[0]-r[0],n=s[1]-r[1],o=Math.sqrt(Math.pow(a,2)+Math.pow(n,2));return 0===o?[0,0]:"tangent"===i?[a/o,n/o]:[-n/o,a/o]},tangentOnPath:function(t,e){return this.vectorOnPath(t,e,"tangent")},normalOnPath:function(t,e){return this.vectorOnPath(t,e,"normal")},setGroupProperty:expressionHelpers.setGroupProperty,getValueAtTime:expressionHelpers.getStaticValueAtTime},extendPrototype([l],o),extendPrototype([l],h),h.prototype.getValueAtTime=function(t){return this._cachingAtTime||(this._cachingAtTime={shapeValue:shapePool.clone(this.pv),lastIndex:0,lastTime:initialDefaultFrame}),t*=this.elem.globalData.frameRate,(t-=this.offsetTime)!==this._cachingAtTime.lastTime&&(this._cachingAtTime.lastIndex=this._cachingAtTime.lastTime=l?m<0?r:s:r+c*Math.pow((a-t)/m,1/i),p[f]=n,f+=1,o+=256/255;return p.join(" ")},SVGProLevelsFilter.prototype.renderFrame=function(t){if(t||this.filterManager._mdf){var e,i=this.filterManager.effectElements;this.feFuncRComposed&&(t||i[3].p._mdf||i[4].p._mdf||i[5].p._mdf||i[6].p._mdf||i[7].p._mdf)&&(e=this.getTableValue(i[3].p.v,i[4].p.v,i[5].p.v,i[6].p.v,i[7].p.v),this.feFuncRComposed.setAttribute("tableValues",e),this.feFuncGComposed.setAttribute("tableValues",e),this.feFuncBComposed.setAttribute("tableValues",e)),this.feFuncR&&(t||i[10].p._mdf||i[11].p._mdf||i[12].p._mdf||i[13].p._mdf||i[14].p._mdf)&&(e=this.getTableValue(i[10].p.v,i[11].p.v,i[12].p.v,i[13].p.v,i[14].p.v),this.feFuncR.setAttribute("tableValues",e)),this.feFuncG&&(t||i[17].p._mdf||i[18].p._mdf||i[19].p._mdf||i[20].p._mdf||i[21].p._mdf)&&(e=this.getTableValue(i[17].p.v,i[18].p.v,i[19].p.v,i[20].p.v,i[21].p.v),this.feFuncG.setAttribute("tableValues",e)),this.feFuncB&&(t||i[24].p._mdf||i[25].p._mdf||i[26].p._mdf||i[27].p._mdf||i[28].p._mdf)&&(e=this.getTableValue(i[24].p.v,i[25].p.v,i[26].p.v,i[27].p.v,i[28].p.v),this.feFuncB.setAttribute("tableValues",e)),this.feFuncA&&(t||i[31].p._mdf||i[32].p._mdf||i[33].p._mdf||i[34].p._mdf||i[35].p._mdf)&&(e=this.getTableValue(i[31].p.v,i[32].p.v,i[33].p.v,i[34].p.v,i[35].p.v),this.feFuncA.setAttribute("tableValues",e))}},extendPrototype([SVGComposableEffect],SVGDropShadowEffect),SVGDropShadowEffect.prototype.renderFrame=function(t){if(t||this.filterManager._mdf){if((t||this.filterManager.effectElements[4].p._mdf)&&this.feGaussianBlur.setAttribute("stdDeviation",this.filterManager.effectElements[4].p.v/4),t||this.filterManager.effectElements[0].p._mdf){var e=this.filterManager.effectElements[0].p.v;this.feFlood.setAttribute("flood-color",rgbToHex(Math.round(255*e[0]),Math.round(255*e[1]),Math.round(255*e[2])))}if((t||this.filterManager.effectElements[1].p._mdf)&&this.feFlood.setAttribute("flood-opacity",this.filterManager.effectElements[1].p.v/255),t||this.filterManager.effectElements[2].p._mdf||this.filterManager.effectElements[3].p._mdf){var i=this.filterManager.effectElements[3].p.v,r=(this.filterManager.effectElements[2].p.v-90)*degToRads,s=i*Math.cos(r),a=i*Math.sin(r);this.feOffset.setAttribute("dx",s),this.feOffset.setAttribute("dy",a)}}};var _svgMatteSymbols=[];function SVGMatte3Effect(t,e,i){this.initialized=!1,this.filterManager=e,this.filterElem=t,this.elem=i,i.matteElement=createNS("g"),i.matteElement.appendChild(i.layerElement),i.matteElement.appendChild(i.transformedElement),i.baseElement=i.matteElement}function SVGGaussianBlurEffect(t,e,i,r){t.setAttribute("x","-100%"),t.setAttribute("y","-100%"),t.setAttribute("width","300%"),t.setAttribute("height","300%"),this.filterManager=e;var s=createNS("feGaussianBlur");s.setAttribute("result",r),t.appendChild(s),this.feGaussianBlur=s}function TransformEffect(){}function SVGTransformEffect(t,e){this.init(e)}function CVTransformEffect(t){this.init(t)}return SVGMatte3Effect.prototype.findSymbol=function(t){for(var e=0,i=_svgMatteSymbols.length;e1?t-1:0),r=1;r0?" Args: "+n.join(", "):""))}var j=function(){function e(e){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=e}var t=e.prototype;return t.indexOfGroup=function(e){for(var t=0,n=0;n=this.groupSizes.length){for(var n=this.groupSizes,r=n.length,o=r;e>=o;)(o<<=1)<0&&k(16,""+e);this.groupSizes=new Uint32Array(o),this.groupSizes.set(n),this.length=o;for(var i=r;i=this.length||0===this.groupSizes[e])return t;for(var n=this.groupSizes[e],r=this.indexOfGroup(e),o=r+n,i=r;i=R&&(R=t+1),C.set(e,t),_.set(t,e)},L="style["+x+'][data-styled-version="5.3.11"]',M=new RegExp("^"+x+'\\.g(\\d+)\\[id="([\\w\\d-]+)"\\].*?"([^"]*)'),N=function(e,t,n){for(var r,o=n.split(","),i=0,a=o.length;i=0;n--){var r=t[n];if(r&&1===r.nodeType&&r.hasAttribute(x))return r}}(n),i=void 0!==o?o.nextSibling:null;r.setAttribute(x,"active"),r.setAttribute("data-styled-version","5.3.11");var a=D();return a&&r.setAttribute("nonce",a),n.insertBefore(r,i),r},z=function(){function e(e){var t=this.element=F(e);t.appendChild(document.createTextNode("")),this.sheet=function(e){if(e.sheet)return e.sheet;for(var t=document.styleSheets,n=0,r=t.length;n=0){var n=document.createTextNode(t),r=this.nodes[e];return this.element.insertBefore(n,r||null),this.length++,!0}return!1},t.deleteRule=function(e){this.element.removeChild(this.nodes[e]),this.length--},t.getRule=function(e){return e0&&(l+=e+",")})),r+=""+s+u+'{content:"'+l+'"}/*!sc*/\n'}}}return r}(this)},e}(),q=/(a)(d)/gi,$=function(e){return String.fromCharCode(e+(e>25?39:97))};function K(e){var t,n="";for(t=Math.abs(e);t>52;t=t/52|0)n=$(t%52)+n;return($(t%52)+n).replace(q,"$1-$2")}var Q=function(e,t){for(var n=t.length;n;)e=33*e^t.charCodeAt(--n);return e},Y=function(e){return Q(5381,e)};function G(e){for(var t=0;t>>0);if(!t.hasNameForId(r,a)){var s=n(i,"."+a,void 0,r);t.insertRules(r,a,s)}o.push(a),this.staticRulesId=a}else{for(var u=this.rules.length,l=Q(this.baseHash,n.hash),c="",f=0;f>>0);if(!t.hasNameForId(r,m)){var v=n(c,"."+m,void 0,r);t.insertRules(r,m,v)}o.push(m)}}return o.join(" ")},e}(),Z=/^\s*\/\/.*$/gm,ee=[":","[",".","#"];function te(e){var t,n,r,o,i=void 0===e?y:e,a=i.options,s=void 0===a?y:a,l=i.plugins,c=void 0===l?v:l,f=new u.a(s),d=[],p=function(e){function t(t){if(t)try{e(t+"}")}catch(e){}}return function(n,r,o,i,a,s,u,l,c,f){switch(n){case 1:if(0===c&&64===r.charCodeAt(0))return e(r+";"),"";break;case 2:if(0===l)return r+"/*|*/";break;case 3:switch(l){case 102:case 112:return e(o[0]+r),"";default:return r+(0===f?"/*|*/":"")}case-2:r.split("/*|*/}").forEach(t)}}}((function(e){d.push(e)})),h=function(e,r,i){return 0===r&&-1!==ee.indexOf(i[n.length])||i.match(o)?e:"."+t};function m(e,i,a,s){void 0===s&&(s="&");var u=e.replace(Z,""),l=i&&a?a+" "+i+" { "+u+" }":u;return t=s,n=i,r=new RegExp("\\"+n+"\\b","g"),o=new RegExp("(\\"+n+"\\b){2,}"),f(a||!i?"":i,l)}return f.use([].concat(c,[function(e,t,o){2===e&&o.length&&o[0].lastIndexOf(n)>0&&(o[0]=o[0].replace(r,h))},p,function(e){if(-2===e){var t=d;return d=[],t}}])),m.hash=c.length?c.reduce((function(e,t){return t.name||k(15),Q(e,t.name)}),5381).toString():"",m}var ne=i.a.createContext(),re=(ne.Consumer,i.a.createContext()),oe=(re.Consumer,new V),ie=te();function ae(){return Object(o.useContext)(ne)||oe}function se(){return Object(o.useContext)(re)||ie}function ue(e){var t=Object(o.useState)(e.stylisPlugins),n=t[0],r=t[1],a=ae(),u=Object(o.useMemo)((function(){var t=a;return e.sheet?t=e.sheet:e.target&&(t=t.reconstructWithOptions({target:e.target},!1)),e.disableCSSOMInjection&&(t=t.reconstructWithOptions({useCSSOMInjection:!1})),t}),[e.disableCSSOMInjection,e.sheet,e.target]),l=Object(o.useMemo)((function(){return te({options:{prefix:!e.disableVendorPrefixes},plugins:n})}),[e.disableVendorPrefixes,n]);return Object(o.useEffect)((function(){s()(n,e.stylisPlugins)||r(e.stylisPlugins)}),[e.stylisPlugins]),i.a.createElement(ne.Provider,{value:u},i.a.createElement(re.Provider,{value:l},e.children))}var le=function(){function e(e,t){var n=this;this.inject=function(e,t){void 0===t&&(t=ie);var r=n.name+t.hash;e.hasNameForId(n.id,r)||e.insertRules(n.id,r,t(n.rules,r,"@keyframes"))},this.toString=function(){return k(12,String(n.name))},this.name=e,this.id="sc-keyframes-"+e,this.rules=t}return e.prototype.getName=function(e){return void 0===e&&(e=ie),this.name+e.hash},e}(),ce=/([A-Z])/,fe=/([A-Z])/g,de=/^ms-/,pe=function(e){return"-"+e.toLowerCase()};function he(e){return ce.test(e)?e.replace(fe,pe).replace(de,"-ms-"):e}var me=function(e){return null==e||!1===e||""===e};function ve(e,t,n,r){if(Array.isArray(e)){for(var o,i=[],a=0,s=e.length;a1?t-1:0),r=1;r?@[\\\]^`{|}~-]+/g,xe=/(^-|-$)/g;function Oe(e){return e.replace(we,"-").replace(xe,"")}var Se=function(e){return K(Y(e)>>>0)};function Ee(e){return"string"==typeof e&&!0}var ke=function(e){return"function"==typeof e||"object"==typeof e&&null!==e&&!Array.isArray(e)},je=function(e){return"__proto__"!==e&&"constructor"!==e&&"prototype"!==e};function Ce(e,t,n){var r=e[n];ke(t)&&ke(r)?_e(r,t):e[n]=t}function _e(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r=0||(o[n]=e[n]);return o}(t,["componentId"]),i=r&&r+"-"+(Ee(e)?e:Oe(b(e)));return Ae(e,p({},o,{attrs:O,componentId:i}),n)},Object.defineProperty(E,"defaultProps",{get:function(){return this._foldedDefaultProps},set:function(t){this._foldedDefaultProps=r?_e({},e.defaultProps,t):t}}),Object.defineProperty(E,"toString",{value:function(){return"."+E.styledComponentId}}),a&&d()(E,e,{attrs:!0,componentStyle:!0,displayName:!0,foldedComponentIds:!0,shouldForwardProp:!0,styledComponentId:!0,target:!0,withComponent:!0}),E}var Le=function(e){return function e(t,n,o){if(void 0===o&&(o=y),!Object(r.isValidElementType)(n))return k(1,String(n));var i=function(){return t(n,o,ge.apply(void 0,arguments))};return i.withConfig=function(r){return e(t,n,p({},o,{},r))},i.attrs=function(r){return e(t,n,p({},o,{attrs:Array.prototype.concat(o.attrs,r).filter(Boolean)}))},i}(Ae,e)};["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","marquee","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","marker","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","textPath","tspan"].forEach((function(e){Le[e]=Le(e)}));var Me=function(){function e(e,t){this.rules=e,this.componentId=t,this.isStatic=G(e),V.registerId(this.componentId+1)}var t=e.prototype;return t.createStyles=function(e,t,n,r){var o=r(ve(this.rules,t,n,r).join(""),""),i=this.componentId+e;n.insertRules(i,i,o)},t.removeStyles=function(e,t){t.clearRules(this.componentId+e)},t.renderStyles=function(e,t,n,r){e>2&&V.registerId(this.componentId+e),this.removeStyles(e,n),this.createStyles(e,t,n,r)},e}();function Ne(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r"+t+""},this.getStyleTags=function(){return e.sealed?k(2):e._emitSheetCSS()},this.getStyleElement=function(){var t;if(e.sealed)return k(2);var n=((t={})[x]="",t["data-styled-version"]="5.3.11",t.dangerouslySetInnerHTML={__html:e.instance.toString()},t),r=D();return r&&(n.nonce=r),[i.a.createElement("style",p({},n,{key:"sc-0-0"}))]},this.seal=function(){e.sealed=!0},this.instance=new V({isServer:!0}),this.sealed=!1}var t=e.prototype;t.collectStyles=function(e){return this.sealed?k(2):i.a.createElement(ue,{sheet:this.instance},e)},t.interleaveWithNodeStream=function(e){return k(3)}}();t.d=Le}).call(this,n(60))},function(e,t,n){"use strict";function r(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";(function(e,r,o){var i=n(114);const{toString:a}=Object.prototype,{getPrototypeOf:s}=Object,{iterator:u,toStringTag:l}=Symbol,c=(f=Object.create(null),e=>{const t=a.call(e);return f[t]||(f[t]=t.slice(8,-1).toLowerCase())});var f;const d=e=>(e=e.toLowerCase(),t=>c(t)===e),p=e=>t=>typeof t===e,{isArray:h}=Array,m=p("undefined");function v(e){return null!==e&&!m(e)&&null!==e.constructor&&!m(e.constructor)&&b(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const y=d("ArrayBuffer");const g=p("string"),b=p("function"),w=p("number"),x=e=>null!==e&&"object"===typeof e,O=e=>{if("object"!==c(e))return!1;const t=s(e);return(null===t||t===Object.prototype||null===Object.getPrototypeOf(t))&&!(l in e)&&!(u in e)},S=d("Date"),E=d("File"),k=d("Blob"),j=d("FileList"),C=d("URLSearchParams"),[_,R,P,T]=["ReadableStream","Request","Response","Headers"].map(d);function A(e,t){let n,r,{allOwnKeys:o=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(null!==e&&"undefined"!==typeof e)if("object"!==typeof e&&(e=[e]),h(e))for(n=0,r=e.length;n0;)if(r=n[o],t===r.toLowerCase())return r;return null}const M="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:"undefined"!==typeof window?window:e,N=e=>!m(e)&&e!==M;const I=(D="undefined"!==typeof Uint8Array&&s(Uint8Array),e=>D&&e instanceof D);var D;const F=d("HTMLFormElement"),z=(e=>{let{hasOwnProperty:t}=e;return(e,n)=>t.call(e,n)})(Object.prototype),B=d("RegExp"),U=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};A(n,((n,o)=>{let i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)};const H=d("AsyncFunction"),W=((e,t)=>{return e?r:t?(n="axios@".concat(Math.random()),o=[],M.addEventListener("message",(e=>{let{source:t,data:r}=e;t===M&&r===n&&o.length&&o.shift()()}),!1),e=>{o.push(e),M.postMessage(n,"*")}):e=>setTimeout(e);var n,o})("function"===typeof r,b(M.postMessage)),V="undefined"!==typeof queueMicrotask?queueMicrotask.bind(M):"undefined"!==typeof o&&o.nextTick||W;t.a={isArray:h,isArrayBuffer:y,isBuffer:v,isFormData:e=>{let t;return e&&("function"===typeof FormData&&e instanceof FormData||b(e.append)&&("formdata"===(t=c(e))||"object"===t&&b(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){let t;return t="undefined"!==typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&y(e.buffer),t},isString:g,isNumber:w,isBoolean:e=>!0===e||!1===e,isObject:x,isPlainObject:O,isEmptyObject:e=>{if(!x(e)||v(e))return!1;try{return 0===Object.keys(e).length&&Object.getPrototypeOf(e)===Object.prototype}catch(t){return!1}},isReadableStream:_,isRequest:R,isResponse:P,isHeaders:T,isUndefined:m,isDate:S,isFile:E,isBlob:k,isRegExp:B,isFunction:b,isStream:e=>x(e)&&b(e.pipe),isURLSearchParams:C,isTypedArray:I,isFileList:j,forEach:A,merge:function e(){const{caseless:t,skipUndefined:n}=N(this)&&this||{},r={},o=(o,i)=>{if("__proto__"===i||"constructor"===i||"prototype"===i)return;const a=t&&L(r,i)||i;O(r[a])&&O(o)?r[a]=e(r[a],o):O(o)?r[a]=e({},o):h(o)?r[a]=o.slice():n&&m(o)||(r[a]=o)};for(let i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{};return A(t,((t,r)=>{n&&b(t)?Object.defineProperty(e,r,{value:Object(i.a)(t,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,r,{value:t,writable:!0,enumerable:!0,configurable:!0})}),{allOwnKeys:r}),e},trim:e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,""),stripBOM:e=>(65279===e.charCodeAt(0)&&(e=e.slice(1)),e),inherits:(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:(e,t,n,r)=>{let o,i,a;const u={};if(t=t||{},null==e)return t;do{for(o=Object.getOwnPropertyNames(e),i=o.length;i-- >0;)a=o[i],r&&!r(a,e,t)||u[a]||(t[a]=e[a],u[a]=!0);e=!1!==n&&s(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:c,kindOfTest:d,endsWith:(e,t,n)=>{e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return-1!==r&&r===n},toArray:e=>{if(!e)return null;if(h(e))return e;let t=e.length;if(!w(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},forEachEntry:(e,t)=>{const n=(e&&e[u]).call(e);let r;for(;(r=n.next())&&!r.done;){const n=r.value;t.call(e,n[0],n[1])}},matchAll:(e,t)=>{let n;const r=[];for(;null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:F,hasOwnProperty:z,hasOwnProp:z,reduceDescriptors:U,freezeMethods:e=>{U(e,((t,n)=>{if(b(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;const r=e[n];b(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:(e,t)=>{const n={},r=e=>{e.forEach((e=>{n[e]=!0}))};return h(e)?r(e):r(String(e).split(t)),n},toCamelCase:e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n})),noop:()=>{},toFiniteNumber:(e,t)=>null!=e&&Number.isFinite(e=+e)?e:t,findKey:L,global:M,isContextDefined:N,isSpecCompliantForm:function(e){return!!(e&&b(e.append)&&"FormData"===e[l]&&e[u])},toJSONObject:e=>{const t=new Array(10),n=(e,r)=>{if(x(e)){if(t.indexOf(e)>=0)return;if(v(e))return e;if(!("toJSON"in e)){t[r]=e;const o=h(e)?[]:{};return A(e,((e,t)=>{const i=n(e,r+1);!m(i)&&(o[t]=i)})),t[r]=void 0,o}}return e};return n(e,0)},isAsyncFn:H,isThenable:e=>e&&(x(e)||b(e))&&b(e.then)&&b(e.catch),setImmediate:W,asap:V,isIterable:e=>null!=e&&b(e[u])}}).call(this,n(34),n(160).setImmediate,n(60))},function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));var r=n(57);function o(e,t){if(null==e)return{};var n,o,i=Object(r.a)(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o1&&void 0!==arguments[1]?arguments[1]:{};return function(n){var i=t.defaultTheme,s=t.withTheme,d=void 0!==s&&s,p=t.name,h=Object(o.a)(t,["defaultTheme","withTheme","name"]);var m=p,v=Object(l.a)(e,Object(r.a)({defaultTheme:i,Component:n,name:p||n.displayName,classNamePrefix:m},h)),y=a.a.forwardRef((function(e,t){e.classes;var s,u=e.innerRef,l=Object(o.a)(e,["classes","innerRef"]),h=v(Object(r.a)({},n.defaultProps,e)),m=l;return("string"===typeof p||d)&&(s=Object(f.a)()||i,p&&(m=Object(c.a)({theme:s,name:p,props:l})),d&&!m.theme&&(m.theme=s)),a.a.createElement(n,Object(r.a)({ref:u||t,classes:h},m))}));return u()(y,n),y}},p=n(69);t.a=function(e,t){return d(e,Object(r.a)({defaultTheme:p.a},t))}},function(e,t,n){"use strict";n.d(t,"f",(function(){return o})),n.d(t,"j",(function(){return i})),n.d(t,"b",(function(){return a})),n.d(t,"g",(function(){return s})),n.d(t,"a",(function(){return u})),n.d(t,"r",(function(){return l})),n.d(t,"l",(function(){return c})),n.d(t,"k",(function(){return f})),n.d(t,"i",(function(){return d})),n.d(t,"h",(function(){return p})),n.d(t,"e",(function(){return h})),n.d(t,"d",(function(){return m})),n.d(t,"m",(function(){return v})),n.d(t,"n",(function(){return g})),n.d(t,"p",(function(){return b})),n.d(t,"q",(function(){return S})),n.d(t,"o",(function(){return E})),n.d(t,"c",(function(){return k}));var r=n(2),o="undefined"===typeof window;function i(){}function a(e,t){return"function"===typeof e?e(t):e}function s(e){return"number"===typeof e&&e>=0&&e!==1/0}function u(e){return Array.isArray(e)?e:[e]}function l(e,t){return Math.max(e+(t||0)-Date.now(),0)}function c(e,t,n){return O(e)?"function"===typeof t?Object(r.a)({},n,{queryKey:e,queryFn:t}):Object(r.a)({},t,{queryKey:e}):e}function f(e,t,n){return O(e)?[Object(r.a)({},t,{queryKey:e}),n]:[e||{},t]}function d(e,t){var n=e.active,r=e.exact,o=e.fetching,i=e.inactive,a=e.predicate,s=e.queryKey,u=e.stale;if(O(s))if(r){if(t.queryHash!==h(s,t.options))return!1}else if(!v(t.queryKey,s))return!1;var l=function(e,t){return!0===e&&!0===t||null==e&&null==t?"all":!1===e&&!1===t?"none":(null!=e?e:!t)?"active":"inactive"}(n,i);if("none"===l)return!1;if("all"!==l){var c=t.isActive();if("active"===l&&!c)return!1;if("inactive"===l&&c)return!1}return("boolean"!==typeof u||t.isStale()===u)&&(("boolean"!==typeof o||t.isFetching()===o)&&!(a&&!a(t)))}function p(e,t){var n=e.exact,r=e.fetching,o=e.predicate,i=e.mutationKey;if(O(i)){if(!t.options.mutationKey)return!1;if(n){if(m(t.options.mutationKey)!==m(i))return!1}else if(!v(t.options.mutationKey,i))return!1}return("boolean"!==typeof r||"loading"===t.state.status===r)&&!(o&&!o(t))}function h(e,t){return((null==t?void 0:t.queryKeyHashFn)||m)(e)}function m(e){var t,n=u(e);return t=n,JSON.stringify(t,(function(e,t){return w(t)?Object.keys(t).sort().reduce((function(e,n){return e[n]=t[n],e}),{}):t}))}function v(e,t){return y(u(e),u(t))}function y(e,t){return e===t||typeof e===typeof t&&(!(!e||!t||"object"!==typeof e||"object"!==typeof t)&&!Object.keys(t).some((function(n){return!y(e[n],t[n])})))}function g(e,t){if(e===t)return e;var n=Array.isArray(e)&&Array.isArray(t);if(n||w(e)&&w(t)){for(var r=n?e.length:Object.keys(e).length,o=n?t:Object.keys(t),i=o.length,a=n?[]:{},s=0,u=0;u1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;return Math.min(Math.max(t,e),n)}function i(e){if(e.type)return e;if("#"===e.charAt(0))return i(function(e){e=e.substr(1);var t=new RegExp(".{1,".concat(e.length>=6?2:1,"}"),"g"),n=e.match(t);return n&&1===n[0].length&&(n=n.map((function(e){return e+e}))),n?"rgb".concat(4===n.length?"a":"","(").concat(n.map((function(e,t){return t<3?parseInt(e,16):Math.round(parseInt(e,16)/255*1e3)/1e3})).join(", "),")"):""}(e));var t=e.indexOf("("),n=e.substring(0,t);if(-1===["rgb","rgba","hsl","hsla"].indexOf(n))throw new Error(Object(r.a)(3,e));var o=e.substring(t+1,e.length-1).split(",");return{type:n,values:o=o.map((function(e){return parseFloat(e)}))}}function a(e){var t=e.type,n=e.values;return-1!==t.indexOf("rgb")?n=n.map((function(e,t){return t<3?parseInt(e,10):e})):-1!==t.indexOf("hsl")&&(n[1]="".concat(n[1],"%"),n[2]="".concat(n[2],"%")),"".concat(t,"(").concat(n.join(", "),")")}function s(e,t){var n=u(e),r=u(t);return(Math.max(n,r)+.05)/(Math.min(n,r)+.05)}function u(e){var t="hsl"===(e=i(e)).type?i(function(e){var t=(e=i(e)).values,n=t[0],r=t[1]/100,o=t[2]/100,s=r*Math.min(o,1-o),u=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:(e+n/30)%12;return o-s*Math.max(Math.min(t-3,9-t,1),-1)},l="rgb",c=[Math.round(255*u(0)),Math.round(255*u(8)),Math.round(255*u(4))];return"hsla"===e.type&&(l+="a",c.push(t[3])),a({type:l,values:c})}(e)).values:e.values;return t=t.map((function(e){return(e/=255)<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4)})),Number((.2126*t[0]+.7152*t[1]+.0722*t[2]).toFixed(3))}function l(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:.15;return u(e)>.5?f(e,t):d(e,t)}function c(e,t){return e=i(e),t=o(t),"rgb"!==e.type&&"hsl"!==e.type||(e.type+="a"),e.values[3]=t,a(e)}function f(e,t){if(e=i(e),t=o(t),-1!==e.type.indexOf("hsl"))e.values[2]*=1-t;else if(-1!==e.type.indexOf("rgb"))for(var n=0;n<3;n+=1)e.values[n]*=1-t;return a(e)}function d(e,t){if(e=i(e),t=o(t),-1!==e.type.indexOf("hsl"))e.values[2]+=(100-e.values[2])*t;else if(-1!==e.type.indexOf("rgb"))for(var n=0;n<3;n+=1)e.values[n]+=(255-e.values[n])*t;return a(e)}},,function(e,t,n){"use strict";!function e(){if("undefined"!==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}(),e.exports=n(268)},function(e,t,n){"use strict";n.d(t,"a",(function(){return i}));var r=n(13);function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;ti.addHandler(e,t,n),t.parse=e=>i.parse(e),t.Parser=r},function(e,t){e.exports=function(e){return e&&e.__esModule?e:{default:e}},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){var r=n(129).default;function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(o=function(e){return e?n:t})(e)}e.exports=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=r(e)&&"function"!=typeof e)return{default:e};var n=o(t);if(n&&n.has(e))return n.get(e);var i={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&{}.hasOwnProperty.call(e,s)){var u=a?Object.getOwnPropertyDescriptor(e,s):null;u&&(u.get||u.set)?Object.defineProperty(i,s,u):i[s]=e[s]}return i.default=e,n&&n.set(e,i),i},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return r.createSvgIcon}});var r=n(281)},function(e,t,n){"use strict";n.d(t,"a",(function(){return D}));var r=n(2),o=n(38),i=n(31),a=n(77),s=n(66);function u(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(u=function(){return!!e})()}function l(e){var t="function"==typeof Map?new Map:void 0;return l=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf("[native code]")}catch(t){return"function"==typeof e}}(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return function(e,t,n){if(u())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,t);var o=new(e.bind.apply(e,r));return n&&Object(s.a)(o,n.prototype),o}(e,arguments,Object(a.a)(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),Object(s.a)(n,e)},l(e)}var c=function(e){function t(t){var n;return n=e.call(this,"An error occurred. See https://github.com/styled-components/polished/blob/main/src/internalHelpers/errors.md#"+t+" for more information.")||this,Object(o.a)(n)}return Object(i.a)(t,e),t}(l(Error));function f(e,t){return e.substr(-t.length)===t}var d=/^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/;function p(e){return"string"!==typeof e?e:e.match(d)?parseFloat(e):e}var h=function(e){return function(t,n){void 0===n&&(n="16px");var r=t,o=n;if("string"===typeof t){if(!f(t,"px"))throw new c(69,e,t);r=p(t)}if("string"===typeof n){if(!f(n,"px"))throw new c(70,e,n);o=p(n)}if("string"===typeof r)throw new c(71,t,e);if("string"===typeof o)throw new c(72,n,e);return""+r/o+e}};h("em");h("rem");function m(e){return Math.round(255*e)}function v(e,t,n){return m(e)+","+m(t)+","+m(n)}function y(e,t,n,r){if(void 0===r&&(r=v),0===t)return r(n,n,n);var o=(e%360+360)%360/60,i=(1-Math.abs(2*n-1))*t,a=i*(1-Math.abs(o%2-1)),s=0,u=0,l=0;o>=0&&o<1?(s=i,u=a):o>=1&&o<2?(s=a,u=i):o>=2&&o<3?(u=i,l=a):o>=3&&o<4?(u=a,l=i):o>=4&&o<5?(s=a,l=i):o>=5&&o<6&&(s=i,l=a);var c=n-i/2;return r(s+c,u+c,l+c)}var g={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"639",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"};var b=/^#[a-fA-F0-9]{6}$/,w=/^#[a-fA-F0-9]{8}$/,x=/^#[a-fA-F0-9]{3}$/,O=/^#[a-fA-F0-9]{4}$/,S=/^rgb\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*\)$/i,E=/^rgb(?:a)?\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i,k=/^hsl\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*\)$/i,j=/^hsl(?:a)?\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i;function C(e){if("string"!==typeof e)throw new c(3);var t=function(e){if("string"!==typeof e)return e;var t=e.toLowerCase();return g[t]?"#"+g[t]:e}(e);if(t.match(b))return{red:parseInt(""+t[1]+t[2],16),green:parseInt(""+t[3]+t[4],16),blue:parseInt(""+t[5]+t[6],16)};if(t.match(w)){var n=parseFloat((parseInt(""+t[7]+t[8],16)/255).toFixed(2));return{red:parseInt(""+t[1]+t[2],16),green:parseInt(""+t[3]+t[4],16),blue:parseInt(""+t[5]+t[6],16),alpha:n}}if(t.match(x))return{red:parseInt(""+t[1]+t[1],16),green:parseInt(""+t[2]+t[2],16),blue:parseInt(""+t[3]+t[3],16)};if(t.match(O)){var r=parseFloat((parseInt(""+t[4]+t[4],16)/255).toFixed(2));return{red:parseInt(""+t[1]+t[1],16),green:parseInt(""+t[2]+t[2],16),blue:parseInt(""+t[3]+t[3],16),alpha:r}}var o=S.exec(t);if(o)return{red:parseInt(""+o[1],10),green:parseInt(""+o[2],10),blue:parseInt(""+o[3],10)};var i=E.exec(t.substring(0,50));if(i)return{red:parseInt(""+i[1],10),green:parseInt(""+i[2],10),blue:parseInt(""+i[3],10),alpha:parseFloat(""+i[4])>1?parseFloat(""+i[4])/100:parseFloat(""+i[4])};var a=k.exec(t);if(a){var s="rgb("+y(parseInt(""+a[1],10),parseInt(""+a[2],10)/100,parseInt(""+a[3],10)/100)+")",u=S.exec(s);if(!u)throw new c(4,t,s);return{red:parseInt(""+u[1],10),green:parseInt(""+u[2],10),blue:parseInt(""+u[3],10)}}var l=j.exec(t.substring(0,50));if(l){var f="rgb("+y(parseInt(""+l[1],10),parseInt(""+l[2],10)/100,parseInt(""+l[3],10)/100)+")",d=S.exec(f);if(!d)throw new c(4,t,f);return{red:parseInt(""+d[1],10),green:parseInt(""+d[2],10),blue:parseInt(""+d[3],10),alpha:parseFloat(""+l[4])>1?parseFloat(""+l[4])/100:parseFloat(""+l[4])}}throw new c(5)}function _(e){return function(e){var t,n=e.red/255,r=e.green/255,o=e.blue/255,i=Math.max(n,r,o),a=Math.min(n,r,o),s=(i+a)/2;if(i===a)return void 0!==e.alpha?{hue:0,saturation:0,lightness:s,alpha:e.alpha}:{hue:0,saturation:0,lightness:s};var u=i-a,l=s>.5?u/(2-i-a):u/(i+a);switch(i){case n:t=(r-o)/u+(r=1?L(e,t,n):"rgba("+y(e,t,n)+","+r+")";if("object"===typeof e&&void 0===t&&void 0===n&&void 0===r)return e.alpha>=1?L(e.hue,e.saturation,e.lightness):"rgba("+y(e.hue,e.saturation,e.lightness)+","+e.alpha+")";throw new c(2)}function I(e,t,n){if("number"===typeof e&&"number"===typeof t&&"number"===typeof n)return R("#"+P(e)+P(t)+P(n));if("object"===typeof e&&void 0===t&&void 0===n)return R("#"+P(e.red)+P(e.green)+P(e.blue));throw new c(6)}function D(e,t,n,r){if("string"===typeof e&&"number"===typeof t){var o=C(e);return"rgba("+o.red+","+o.green+","+o.blue+","+t+")"}if("number"===typeof e&&"number"===typeof t&&"number"===typeof n&&"number"===typeof r)return r>=1?I(e,t,n):"rgba("+e+","+t+","+n+","+r+")";if("object"===typeof e&&void 0===t&&void 0===n&&void 0===r)return e.alpha>=1?I(e.red,e.green,e.blue):"rgba("+e.red+","+e.green+","+e.blue+","+e.alpha+")";throw new c(7)}function F(e){if("object"!==typeof e)throw new c(8);if(function(e){return"number"===typeof e.red&&"number"===typeof e.green&&"number"===typeof e.blue&&"number"===typeof e.alpha}(e))return D(e);if(function(e){return"number"===typeof e.red&&"number"===typeof e.green&&"number"===typeof e.blue&&("number"!==typeof e.alpha||"undefined"===typeof e.alpha)}(e))return I(e);if(function(e){return"number"===typeof e.hue&&"number"===typeof e.saturation&&"number"===typeof e.lightness&&"number"===typeof e.alpha}(e))return N(e);if(function(e){return"number"===typeof e.hue&&"number"===typeof e.saturation&&"number"===typeof e.lightness&&("number"!==typeof e.alpha||"undefined"===typeof e.alpha)}(e))return M(e);throw new c(8)}function z(e,t,n){return function(){var r=n.concat(Array.prototype.slice.call(arguments));return r.length>=t?e.apply(this,r):z(e,t,r)}}function B(e){return z(e,e.length,[])}B((function(e,t){if("transparent"===t)return t;var n=_(t);return F(Object(r.a)({},n,{hue:n.hue+parseFloat(e)}))}));function U(e,t,n){return Math.max(e,Math.min(t,n))}B((function(e,t){if("transparent"===t)return t;var n=_(t);return F(Object(r.a)({},n,{lightness:U(0,1,n.lightness-parseFloat(e))}))}));B((function(e,t){if("transparent"===t)return t;var n=_(t);return F(Object(r.a)({},n,{saturation:U(0,1,n.saturation-parseFloat(e))}))}));B((function(e,t){if("transparent"===t)return t;var n=_(t);return F(Object(r.a)({},n,{lightness:U(0,1,n.lightness+parseFloat(e))}))}));var H=B((function(e,t,n){if("transparent"===t)return n;if("transparent"===n)return t;if(0===e)return n;var o=C(t),i=Object(r.a)({},o,{alpha:"number"===typeof o.alpha?o.alpha:1}),a=C(n),s=Object(r.a)({},a,{alpha:"number"===typeof a.alpha?a.alpha:1}),u=i.alpha-s.alpha,l=2*parseFloat(e)-1,c=((l*u===-1?l:l+u)/(1+l*u)+1)/2,f=1-c;return D({red:Math.floor(i.red*c+s.red*f),green:Math.floor(i.green*c+s.green*f),blue:Math.floor(i.blue*c+s.blue*f),alpha:i.alpha*parseFloat(e)+s.alpha*(1-parseFloat(e))})}));B((function(e,t){if("transparent"===t)return t;var n=C(t),o="number"===typeof n.alpha?n.alpha:1;return D(Object(r.a)({},n,{alpha:U(0,1,(100*o+100*parseFloat(e))/100)}))}));B((function(e,t){if("transparent"===t)return t;var n=_(t);return F(Object(r.a)({},n,{saturation:U(0,1,n.saturation+parseFloat(e))}))}));B((function(e,t){return"transparent"===t?t:F(Object(r.a)({},_(t),{hue:parseFloat(e)}))}));B((function(e,t){return"transparent"===t?t:F(Object(r.a)({},_(t),{lightness:parseFloat(e)}))}));B((function(e,t){return"transparent"===t?t:F(Object(r.a)({},_(t),{saturation:parseFloat(e)}))}));B((function(e,t){return"transparent"===t?t:H(parseFloat(e),"rgb(0, 0, 0)",t)}));B((function(e,t){return"transparent"===t?t:H(parseFloat(e),"rgb(255, 255, 255)",t)}));B((function(e,t){if("transparent"===t)return t;var n=C(t),o="number"===typeof n.alpha?n.alpha:1;return D(Object(r.a)({},n,{alpha:U(0,1,+(100*o-100*parseFloat(e)).toFixed(2)/100)}))}))},function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));var r=n(66);function o(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,Object(r.a)(e,t)}},function(e,t,n){"use strict";function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";n.d(t,"a",(function(){return i}));var r=n(123);function o(e,t){for(var n=0;n=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function h(e,t){if(u.isBuffer(e))return e.length;if("undefined"!==typeof ArrayBuffer&&"function"===typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!==typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return U(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return H(e).length;default:if(r)return U(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return P(this,t,n);case"utf8":case"utf-8":return j(this,t,n);case"ascii":return _(this,t,n);case"latin1":case"binary":return R(this,t,n);case"base64":return k(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function v(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"===typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"===typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:g(e,t,n,r,o);if("number"===typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"===typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):g(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function g(e,t,n,r,o){var i,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function l(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var c=-1;for(i=n;is&&(n=s-u),i=n;i>=0;i--){for(var f=!0,d=0;do&&(r=o):r=o;var i=t.length;if(i%2!==0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function k(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function j(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:l>223?3:l>191?2:1;if(o+f<=n)switch(f){case 1:l<128&&(c=l);break;case 2:128===(192&(i=e[o+1]))&&(u=(31&l)<<6|63&i)>127&&(c=u);break;case 3:i=e[o+1],a=e[o+2],128===(192&i)&&128===(192&a)&&(u=(15&l)<<12|(63&i)<<6|63&a)>2047&&(u<55296||u>57343)&&(c=u);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128===(192&i)&&128===(192&a)&&128===(192&s)&&(u=(15&l)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(c=u)}null===c?(c=65533,f=1):c>65535&&(c-=65536,r.push(c>>>10&1023|55296),c=56320|1023&c),r.push(c),o+=f}return function(e){var t=e.length;if(t<=C)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),l=this.slice(r,o),c=e.slice(t,n),f=0;fo)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":return x(this,e,t,n);case"latin1":case"binary":return O(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return E(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var C=4096;function _(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function L(e,t,n,r,o,i){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function M(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function N(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function I(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function D(e,t,n,r,i){return i||I(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,i){return i||I(e,0,n,8),o.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},u.prototype.readUInt8=function(e,t){return t||A(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||A(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||A(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||A(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||A(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||A(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||A(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},u.prototype.readInt8=function(e,t){return t||A(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||A(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||A(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||A(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||A(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||A(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||A(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||A(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||A(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||L(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):N(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);L(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a|0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):N(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||L(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return D(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return D(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"===typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function H(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(z,"")).length<2)return"";for(;e.length%4!==0;)e+="=";return e}(e))}function W(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(34))},function(e,t,n){"use strict";n.d(t,"b",(function(){return s})),n.d(t,"c",(function(){return l})),n.d(t,"a",(function(){return c}));var r=n(55),o=n(67),i=n(10);function a(e){return Math.min(1e3*Math.pow(2,e),3e4)}function s(e){return"function"===typeof(null==e?void 0:e.cancel)}var u=function(e){this.revert=null==e?void 0:e.revert,this.silent=null==e?void 0:e.silent};function l(e){return e instanceof u}var c=function(e){var t,n,l,c,f=this,d=!1;this.abort=e.abort,this.cancel=function(e){return null==t?void 0:t(e)},this.cancelRetry=function(){d=!0},this.continueRetry=function(){d=!1},this.continue=function(){return null==n?void 0:n()},this.failureCount=0,this.isPaused=!1,this.isResolved=!1,this.isTransportCancelable=!1,this.promise=new Promise((function(e,t){l=e,c=t}));var p=function(t){f.isResolved||(f.isResolved=!0,null==e.onSuccess||e.onSuccess(t),null==n||n(),l(t))},h=function(t){f.isResolved||(f.isResolved=!0,null==e.onError||e.onError(t),null==n||n(),c(t))};!function l(){if(!f.isResolved){var c;try{c=e.fn()}catch(m){c=Promise.reject(m)}t=function(e){if(!f.isResolved&&(h(new u(e)),null==f.abort||f.abort(),s(c)))try{c.cancel()}catch(t){}},f.isTransportCancelable=s(c),Promise.resolve(c).then(p).catch((function(t){var s,u;if(!f.isResolved){var c=null!=(s=e.retry)?s:3,p=null!=(u=e.retryDelay)?u:a,m="function"===typeof p?p(f.failureCount,t):p,v=!0===c||"number"===typeof c&&f.failureCount0&&void 0!==arguments[0]?arguments[0]:["all"],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.duration,s=void 0===n?i.standard:n,u=t.easing,l=void 0===u?o.easeInOut:u,c=t.delay,f=void 0===c?0:c;Object(r.a)(t,["duration","easing","delay"]);return(Array.isArray(e)?e:[e]).map((function(e){return"".concat(e," ").concat("string"===typeof s?s:a(s)," ").concat(l," ").concat("string"===typeof f?f:a(f))})).join(",")},getAutoHeightDuration:function(e){if(!e)return 0;var t=e/36;return Math.round(10*(4+15*Math.pow(t,.25)+t/5))}}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";function r(e){var t,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:166;function r(){for(var r=arguments.length,o=new Array(r),i=0;i0},t.onSubscribe=function(){},t.onUnsubscribe=function(){},e}()},function(e,t,n){"use strict";function r(e,t){if(null==e)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.includes(r))continue;n[r]=e[r]}return n}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";var r=n(1),o=r.createContext({});t.a=o},function(e,t,n){"use strict";n.d(t,"b",(function(){return i}));var r=n(1),o=r.createContext();function i(){return r.useContext(o)}t.a=o},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"===typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"===typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var u,l=[],c=!1,f=-1;function d(){c&&u&&(c=!1,u.length?l=u.concat(l):f=-1,l.length&&p())}function p(){if(!c){var e=s(d);c=!0;for(var t=l.length;t;){for(u=l,l=[];++f1)for(var n=1;n1&&void 0!==arguments[1]&&arguments[1];return e&&(r(e.value)&&""!==e.value||t&&r(e.defaultValue)&&""!==e.defaultValue)}function i(e){return e.startAdornment}n.d(t,"b",(function(){return o})),n.d(t,"a",(function(){return i}))},function(e,t,n){"use strict";function r(e){return r=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},r(e)}n.d(t,"a",(function(){return r}))},,,,function(e,t,n){var r=n(297),o=n(302);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){"use strict";var r=n(106),o=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=f;var i=Object.create(n(91));i.inherits=n(74);var a=n(175),s=n(179);i.inherits(f,a);for(var u=o(s.prototype),l=0;l{p("number"===typeof e.length,"info.files[0].length"),p(e["path.utf-8"]||e.path,"info.files[0].path")})):p("number"===typeof e.info.length,"info.length");const n={info:e.info,infoBuffer:r.encode(e.info),name:(e.info["name.utf-8"]||e.info.name).toString(),announce:[]};n.infoHash=l.sync(n.infoBuffer),n.infoHashBuffer=t.from(n.infoHash,"hex"),void 0!==e.info.private&&(n.private=!!e.info.private);e["creation date"]&&(n.created=new Date(1e3*e["creation date"]));e["created by"]&&(n.createdBy=e["created by"].toString());t.isBuffer(e.comment)&&(n.comment=e.comment.toString());Array.isArray(e["announce-list"])&&e["announce-list"].length>0?e["announce-list"].forEach((e=>{e.forEach((e=>{n.announce.push(e.toString())}))})):e.announce&&n.announce.push(e.announce.toString());t.isBuffer(e["url-list"])&&(e["url-list"]=e["url-list"].length>0?[e["url-list"]]:[]);n.urlList=(e["url-list"]||[]).map((e=>e.toString())),n.announce=Array.from(new Set(n.announce)),n.urlList=Array.from(new Set(n.urlList));const o=e.info.files||[e.info];n.files=o.map(((e,t)=>{const r=[].concat(n.name,e["path.utf-8"]||e.path||[]).map((e=>e.toString()));return{path:u.join.apply(null,[u.sep].concat(r)).slice(1),name:r[r.length-1],length:e.length,offset:o.slice(0,t).reduce(d,0)}})),n.length=o.reduce(d,0);const i=n.files[n.files.length-1];return n.pieceLength=e.info["piece length"],n.lastPieceLength=(i.offset+i.length)%n.pieceLength||n.pieceLength,n.pieces=function(e){const t=[];for(let n=0;n{r(null,s)})):(u=t,"undefined"!==typeof Blob&&u instanceof Blob?o(t,((e,t)=>{if(e)return r(new Error("Error converting Blob: ".concat(e.message)));l(t)})):"function"===typeof a&&/^https?:/.test(t)?(n=Object.assign({url:t,timeout:3e4,headers:{"user-agent":"WebTorrent (https://webtorrent.io)"}},n),a.concat(n,((e,t,n)=>{if(e)return r(new Error("Error downloading torrent: ".concat(e.message)));l(n)}))):"function"===typeof i.readFile&&"string"===typeof t?i.readFile(t,((e,t)=>{if(e)return r(new Error("Invalid torrent identifier"));l(t)})):c((()=>{r(new Error("Invalid torrent identifier"))})));var u;function l(e){try{s=f(e)}catch(d){return r(d)}s&&s.infoHash?r(null,s):r(new Error("Invalid torrent identifier"))}},e.exports.toMagnetURI=s.encode,e.exports.toTorrentFile=function(e){const n={info:e.info};n["announce-list"]=(e.announce||[]).map((e=>(n.announce||(n.announce=e),[e=t.from(e,"utf8")]))),n["url-list"]=e.urlList||[],void 0!==e.private&&(n.private=Number(e.private));e.created&&(n["creation date"]=e.created.getTime()/1e3|0);e.createdBy&&(n["created by"]=e.createdBy);e.comment&&(n.comment=e.comment);return r.encode(n)},t.alloc(0)}).call(this,n(35).Buffer)},function(e,t,n){"use strict";var r=n(1),o=n.n(r);t.a=o.a.createContext(null)},,,function(e,t,n){"use strict";var r=n(197);n.d(t,"QueryClient",(function(){return r.a}));var o=n(155);n.o(o,"QueryClientProvider")&&n.d(t,"QueryClientProvider",(function(){return o.QueryClientProvider})),n.o(o,"useQuery")&&n.d(t,"useQuery",(function(){return o.useQuery}))},function(e,t,n){var r=n(133),o=n(298),i=n(299),a=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":a&&a in Object(e)?o(e):i(e)}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t,n){function r(e){return Object.prototype.toString.call(e)}t.isArray=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===r(e)},t.isBoolean=function(e){return"boolean"===typeof e},t.isNull=function(e){return null===e},t.isNullOrUndefined=function(e){return null==e},t.isNumber=function(e){return"number"===typeof e},t.isString=function(e){return"string"===typeof e},t.isSymbol=function(e){return"symbol"===typeof e},t.isUndefined=function(e){return void 0===e},t.isRegExp=function(e){return"[object RegExp]"===r(e)},t.isObject=function(e){return"object"===typeof e&&null!==e},t.isDate=function(e){return"[object Date]"===r(e)},t.isError=function(e){return"[object Error]"===r(e)||e instanceof Error},t.isFunction=function(e){return"function"===typeof e},t.isPrimitive=function(e){return null===e||"boolean"===typeof e||"number"===typeof e||"string"===typeof e||"symbol"===typeof e||"undefined"===typeof e},t.isBuffer=n(35).Buffer.isBuffer},function(e,t,n){"use strict";n.d(t,"b",(function(){return u})),n.d(t,"a",(function(){return l}));var r=n(1),o=n.n(r),i=o.a.createContext(void 0),a=o.a.createContext(!1);function s(e){return e&&"undefined"!==typeof window?(window.ReactQueryClientContext||(window.ReactQueryClientContext=i),window.ReactQueryClientContext):i}var u=function(){var e=o.a.useContext(s(o.a.useContext(a)));if(!e)throw new Error("No QueryClient set, use QueryClientProvider to set one");return e},l=function(e){var t=e.client,n=e.contextSharing,r=void 0!==n&&n,i=e.children;o.a.useEffect((function(){return t.mount(),function(){t.unmount()}}),[t]);var u=s(r);return o.a.createElement(a.Provider,{value:r},o.a.createElement(u.Provider,{value:t},i))}},function(e,t,n){"use strict";n.d(t,"a",(function(){return s}));var r=n(1),o=n.n(r);function i(){var e=!1;return{clearReset:function(){e=!1},reset:function(){e=!0},isReset:function(){return e}}}var a=o.a.createContext(i()),s=function(){return o.a.useContext(a)}},function(e,t,n){"use strict";function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"\xa9","©":"\xa9","®":"\xae","®":"\xae","…":"\u2026","…":"\u2026","/":"/","/":"/"},d=function(e){return f[e]};function p(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function h(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};v=h(h({},v),e)}(e.options.react),function(e){m=e}(e)}}},,,function(e,t,n){var r=n(287),o=n(288),i=n(289),a=n(290),s=n(291);function u(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1e3&&e<1e3||O.call(/e/,t))return t;var n=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"===typeof e){var r=e<0?-j(-e):j(e);if(r!==e){var o=String(r),i=g.call(t,o.length+1);return b.call(o,n,"$&_")+"."+b.call(b.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return b.call(t,n,"$&_")}var N=n(388),I=N.custom,D=V(I)?I:null,F={__proto__:null,double:'"',single:"'"},z={__proto__:null,double:/(["\\])/g,single:/(['\\])/g};function B(e,t,n){var r=n.quoteStyle||t,o=F[r];return o+e+o}function U(e){return b.call(String(e),/"/g,""")}function H(e){return"[object Array]"===K(e)&&(!T||!("object"===typeof e&&T in e))}function W(e){return"[object RegExp]"===K(e)&&(!T||!("object"===typeof e&&T in e))}function V(e){if(P)return e&&"object"===typeof e&&e instanceof Symbol;if("symbol"===typeof e)return!0;if(!e||"object"!==typeof e||!R)return!1;try{return R.call(e),!0}catch(t){}return!1}e.exports=function e(n,r,o,s){var u=r||{};if($(u,"quoteStyle")&&!$(F,u.quoteStyle))throw new TypeError('option "quoteStyle" must be "single" or "double"');if($(u,"maxStringLength")&&("number"===typeof u.maxStringLength?u.maxStringLength<0&&u.maxStringLength!==1/0:null!==u.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var m=!$(u,"customInspect")||u.customInspect;if("boolean"!==typeof m&&"symbol"!==m)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if($(u,"indent")&&null!==u.indent&&"\t"!==u.indent&&!(parseInt(u.indent,10)===u.indent&&u.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if($(u,"numericSeparator")&&"boolean"!==typeof u.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var w=u.numericSeparator;if("undefined"===typeof n)return"undefined";if(null===n)return"null";if("boolean"===typeof n)return n?"true":"false";if("string"===typeof n)return Y(n,u);if("number"===typeof n){if(0===n)return 1/0/n>0?"0":"-0";var O=String(n);return w?M(n,O):O}if("bigint"===typeof n){var j=String(n)+"n";return w?M(n,j):j}var _="undefined"===typeof u.depth?5:u.depth;if("undefined"===typeof o&&(o=0),o>=_&&_>0&&"object"===typeof n)return H(n)?"[Array]":"[Object]";var I=function(e,t){var n;if("\t"===e.indent)n="\t";else{if(!("number"===typeof e.indent&&e.indent>0))return null;n=E.call(Array(e.indent+1)," ")}return{base:n,prev:E.call(Array(t+1),n)}}(u,o);if("undefined"===typeof s)s=[];else if(Q(s,n)>=0)return"[Circular]";function z(t,n,r){if(n&&(s=k.call(s)).push(n),r){var i={depth:u.depth};return $(u,"quoteStyle")&&(i.quoteStyle=u.quoteStyle),e(t,i,o+1,s)}return e(t,u,o+1,s)}if("function"===typeof n&&!W(n)){var q=function(e){if(e.name)return e.name;var t=y.call(v.call(e),/^function\s*([\w$]+)/);if(t)return t[1];return null}(n),G=te(n,z);return"[Function"+(q?": "+q:" (anonymous)")+"]"+(G.length>0?" { "+E.call(G,", ")+" }":"")}if(V(n)){var ne=P?b.call(String(n),/^(Symbol\(.*\))_[^)]*$/,"$1"):R.call(n);return"object"!==typeof n||P?ne:X(ne)}if(function(e){if(!e||"object"!==typeof e)return!1;if("undefined"!==typeof HTMLElement&&e instanceof HTMLElement)return!0;return"string"===typeof e.nodeName&&"function"===typeof e.getAttribute}(n)){for(var re="<"+x.call(String(n.nodeName)),oe=n.attributes||[],ie=0;ie"}if(H(n)){if(0===n.length)return"[]";var ae=te(n,z);return I&&!function(e){for(var t=0;t=0)return!1;return!0}(ae)?"["+ee(ae,I)+"]":"[ "+E.call(ae,", ")+" ]"}if(function(e){return"[object Error]"===K(e)&&(!T||!("object"===typeof e&&T in e))}(n)){var se=te(n,z);return"cause"in Error.prototype||!("cause"in n)||A.call(n,"cause")?0===se.length?"["+String(n)+"]":"{ ["+String(n)+"] "+E.call(se,", ")+" }":"{ ["+String(n)+"] "+E.call(S.call("[cause]: "+z(n.cause),se),", ")+" }"}if("object"===typeof n&&m){if(D&&"function"===typeof n[D]&&N)return N(n,{depth:_-o});if("symbol"!==m&&"function"===typeof n.inspect)return n.inspect()}if(function(e){if(!i||!e||"object"!==typeof e)return!1;try{i.call(e);try{l.call(e)}catch(re){return!0}return e instanceof Map}catch(t){}return!1}(n)){var ue=[];return a&&a.call(n,(function(e,t){ue.push(z(t,n,!0)+" => "+z(e,n))})),Z("Map",i.call(n),ue,I)}if(function(e){if(!l||!e||"object"!==typeof e)return!1;try{l.call(e);try{i.call(e)}catch(t){return!0}return e instanceof Set}catch(n){}return!1}(n)){var le=[];return c&&c.call(n,(function(e){le.push(z(e,n))})),Z("Set",l.call(n),le,I)}if(function(e){if(!f||!e||"object"!==typeof e)return!1;try{f.call(e,f);try{d.call(e,d)}catch(re){return!0}return e instanceof WeakMap}catch(t){}return!1}(n))return J("WeakMap");if(function(e){if(!d||!e||"object"!==typeof e)return!1;try{d.call(e,d);try{f.call(e,f)}catch(re){return!0}return e instanceof WeakSet}catch(t){}return!1}(n))return J("WeakSet");if(function(e){if(!p||!e||"object"!==typeof e)return!1;try{return p.call(e),!0}catch(t){}return!1}(n))return J("WeakRef");if(function(e){return"[object Number]"===K(e)&&(!T||!("object"===typeof e&&T in e))}(n))return X(z(Number(n)));if(function(e){if(!e||"object"!==typeof e||!C)return!1;try{return C.call(e),!0}catch(t){}return!1}(n))return X(z(C.call(n)));if(function(e){return"[object Boolean]"===K(e)&&(!T||!("object"===typeof e&&T in e))}(n))return X(h.call(n));if(function(e){return"[object String]"===K(e)&&(!T||!("object"===typeof e&&T in e))}(n))return X(z(String(n)));if("undefined"!==typeof window&&n===window)return"{ [object Window] }";if("undefined"!==typeof globalThis&&n===globalThis||"undefined"!==typeof t&&n===t)return"{ [object globalThis] }";if(!function(e){return"[object Date]"===K(e)&&(!T||!("object"===typeof e&&T in e))}(n)&&!W(n)){var ce=te(n,z),fe=L?L(n)===Object.prototype:n instanceof Object||n.constructor===Object,de=n instanceof Object?"":"null prototype",pe=!fe&&T&&Object(n)===n&&T in n?g.call(K(n),8,-1):de?"Object":"",he=(fe||"function"!==typeof n.constructor?"":n.constructor.name?n.constructor.name+" ":"")+(pe||de?"["+E.call(S.call([],pe||[],de||[]),": ")+"] ":"");return 0===ce.length?he+"{}":I?he+"{"+ee(ce,I)+"}":he+"{ "+E.call(ce,", ")+" }"}return String(n)};var q=Object.prototype.hasOwnProperty||function(e){return e in this};function $(e,t){return q.call(e,t)}function K(e){return m.call(e)}function Q(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,r=e.length;nt.maxStringLength){var n=e.length-t.maxStringLength,r="... "+n+" more character"+(n>1?"s":"");return Y(g.call(e,0,t.maxStringLength),t)+r}var o=z[t.quoteStyle||"single"];return o.lastIndex=0,B(b.call(b.call(e,o,"\\$1"),/[\x00-\x1f]/g,G),"single",t)}function G(e){var t=e.charCodeAt(0),n={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return n?"\\"+n:"\\x"+(t<16?"0":"")+w.call(t.toString(16))}function X(e){return"Object("+e+")"}function J(e){return e+" { ? }"}function Z(e,t,n,r){return e+" ("+t+") {"+(r?ee(n,r):E.call(n,", "))+"}"}function ee(e,t){if(0===e.length)return"";var n="\n"+t.prev+t.base;return n+E.call(e,","+n)+"\n"+t.prev}function te(e,t){var n=H(e),r=[];if(n){r.length=e.length;for(var o=0;o0?this.queries.filter((function(e){return Object(o.i)(n,e)})):this.queries},n.notify=function(e){var t=this;a.a.batch((function(){t.listeners.forEach((function(t){t(e)}))}))},n.onFocus=function(){var e=this;a.a.batch((function(){e.queries.forEach((function(e){e.onFocus()}))}))},n.onOnline=function(){var e=this;a.a.batch((function(){e.queries.forEach((function(e){e.onOnline()}))}))},t}(n(56).a)},function(e,t,n){"use strict";n.d(t,"a",(function(){return c}));var r=n(31),o=n(22),i=n(2),a=n(61),s=n(36),u=n(10),l=function(){function e(e){this.options=Object(i.a)({},e.defaultOptions,e.options),this.mutationId=e.mutationId,this.mutationCache=e.mutationCache,this.observers=[],this.state=e.state||{context:void 0,data:void 0,error:null,failureCount:0,isPaused:!1,status:"idle",variables:void 0},this.meta=e.meta}var t=e.prototype;return t.setState=function(e){this.dispatch({type:"setState",state:e})},t.addObserver=function(e){-1===this.observers.indexOf(e)&&this.observers.push(e)},t.removeObserver=function(e){this.observers=this.observers.filter((function(t){return t!==e}))},t.cancel=function(){return this.retryer?(this.retryer.cancel(),this.retryer.promise.then(u.j).catch(u.j)):Promise.resolve()},t.continue=function(){return this.retryer?(this.retryer.continue(),this.retryer.promise):this.execute()},t.execute=function(){var e,t=this,n="loading"===this.state.status,r=Promise.resolve();return n||(this.dispatch({type:"loading",variables:this.options.variables}),r=r.then((function(){null==t.mutationCache.config.onMutate||t.mutationCache.config.onMutate(t.state.variables,t)})).then((function(){return null==t.options.onMutate?void 0:t.options.onMutate(t.state.variables)})).then((function(e){e!==t.state.context&&t.dispatch({type:"loading",context:e,variables:t.state.variables})}))),r.then((function(){return t.executeMutation()})).then((function(n){e=n,null==t.mutationCache.config.onSuccess||t.mutationCache.config.onSuccess(e,t.state.variables,t.state.context,t)})).then((function(){return null==t.options.onSuccess?void 0:t.options.onSuccess(e,t.state.variables,t.state.context)})).then((function(){return null==t.options.onSettled?void 0:t.options.onSettled(e,null,t.state.variables,t.state.context)})).then((function(){return t.dispatch({type:"success",data:e}),e})).catch((function(e){return null==t.mutationCache.config.onError||t.mutationCache.config.onError(e,t.state.variables,t.state.context,t),Object(a.a)().error(e),Promise.resolve().then((function(){return null==t.options.onError?void 0:t.options.onError(e,t.state.variables,t.state.context)})).then((function(){return null==t.options.onSettled?void 0:t.options.onSettled(void 0,e,t.state.variables,t.state.context)})).then((function(){throw t.dispatch({type:"error",error:e}),e}))}))},t.executeMutation=function(){var e,t=this;return this.retryer=new s.a({fn:function(){return t.options.mutationFn?t.options.mutationFn(t.state.variables):Promise.reject("No mutationFn found")},onFail:function(){t.dispatch({type:"failed"})},onPause:function(){t.dispatch({type:"pause"})},onContinue:function(){t.dispatch({type:"continue"})},retry:null!=(e=this.options.retry)?e:0,retryDelay:this.options.retryDelay}),this.retryer.promise},t.dispatch=function(e){var t=this;this.state=function(e,t){switch(t.type){case"failed":return Object(i.a)({},e,{failureCount:e.failureCount+1});case"pause":return Object(i.a)({},e,{isPaused:!0});case"continue":return Object(i.a)({},e,{isPaused:!1});case"loading":return Object(i.a)({},e,{context:t.context,data:void 0,error:null,isPaused:!1,status:"loading",variables:t.variables});case"success":return Object(i.a)({},e,{data:t.data,error:null,status:"success",isPaused:!1});case"error":return Object(i.a)({},e,{data:void 0,error:t.error,failureCount:e.failureCount+1,isPaused:!1,status:"error"});case"setState":return Object(i.a)({},e,t.state);default:return e}}(this.state,e),o.a.batch((function(){t.observers.forEach((function(t){t.onMutationUpdate(e)})),t.mutationCache.notify(t)}))},e}();var c=function(e){function t(t){var n;return(n=e.call(this)||this).config=t||{},n.mutations=[],n.mutationId=0,n}Object(r.a)(t,e);var n=t.prototype;return n.build=function(e,t,n){var r=new l({mutationCache:this,mutationId:++this.mutationId,options:e.defaultMutationOptions(t),state:n,defaultOptions:t.mutationKey?e.getMutationDefaults(t.mutationKey):void 0,meta:t.meta});return this.add(r),r},n.add=function(e){this.mutations.push(e),this.notify(e)},n.remove=function(e){this.mutations=this.mutations.filter((function(t){return t!==e})),e.cancel(),this.notify(e)},n.clear=function(){var e=this;o.a.batch((function(){e.mutations.forEach((function(t){e.remove(t)}))}))},n.getAll=function(){return this.mutations},n.find=function(e){return"undefined"===typeof e.exact&&(e.exact=!0),this.mutations.find((function(t){return Object(u.h)(e,t)}))},n.findAll=function(e){return this.mutations.filter((function(t){return Object(u.h)(e,t)}))},n.notify=function(e){var t=this;o.a.batch((function(){t.listeners.forEach((function(t){t(e)}))}))},n.onFocus=function(){this.resumePausedMutations()},n.onOnline=function(){this.resumePausedMutations()},n.resumePausedMutations=function(){var e=this.mutations.filter((function(e){return e.state.isPaused}));return o.a.batch((function(){return e.reduce((function(e,t){return e.then((function(){return t.continue().catch(u.j)}))}),Promise.resolve())}))},t}(n(56).a)},function(e,t,n){var r=n(159);e.exports=function(e,t,n){return(t=r(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){"use strict";function r(e,t){return function(){return e.apply(t,arguments)}}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";t.a=null},function(e,t,n){"use strict";var r=n(1),o=n.n(r).a.createContext(null);t.a=o},function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));var r=n(94);function o(e,t){if(e){if("string"==typeof e)return Object(r.a)(e,t);var n={}.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Object(r.a)(e,t):void 0}}},function(e,t,n){"use strict";n.d(t,"a",(function(){return a}));n(49),n(2);var r=n(32),o=(n(63),{xs:0,sm:600,md:960,lg:1280,xl:1920}),i={keys:["xs","sm","md","lg","xl"],up:function(e){return"@media (min-width:".concat(o[e],"px)")}};function a(e,t,n){if(Array.isArray(t)){var o=e.theme.breakpoints||i;return t.reduce((function(e,r,i){return e[o.up(o.keys[i])]=n(t[i]),e}),{})}if("object"===Object(r.a)(t)){var a=e.theme.breakpoints||i;return Object.keys(t).reduce((function(e,r){return e[a.up(r)]=n(t[r]),e}),{})}return n(t)}},function(e,t,n){"use strict";t.a={mobileStepper:1e3,speedDial:1050,appBar:1100,drawer:1200,modal:1300,snackbar:1400,tooltip:1500}},function(e,t,n){"use strict";var r="function"===typeof Symbol&&Symbol.for;t.a=r?Symbol.for("mui.nested"):"__THEME_NESTED__"},function(e,t,n){"use strict";function r(){var e=document.createElement("div");e.style.width="99px",e.style.height="99px",e.style.position="absolute",e.style.top="-9999px",e.style.overflow="scroll",document.body.appendChild(e);var t=e.offsetWidth-e.clientWidth;return document.body.removeChild(e),t}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";var r=n(2),o=n(40),i=n(6),a=n(1),s=n(7),u=n(54),l=n(45),c=n(9),f=n(454),d=a.forwardRef((function(e,t){var n=e.autoFocus,c=e.checked,d=e.checkedIcon,p=e.classes,h=e.className,m=e.defaultChecked,v=e.disabled,y=e.icon,g=e.id,b=e.inputProps,w=e.inputRef,x=e.name,O=e.onBlur,S=e.onChange,E=e.onFocus,k=e.readOnly,j=e.required,C=e.tabIndex,_=e.type,R=e.value,P=Object(i.a)(e,["autoFocus","checked","checkedIcon","classes","className","defaultChecked","disabled","icon","id","inputProps","inputRef","name","onBlur","onChange","onFocus","readOnly","required","tabIndex","type","value"]),T=Object(u.a)({controlled:c,default:Boolean(m),name:"SwitchBase",state:"checked"}),A=Object(o.a)(T,2),L=A[0],M=A[1],N=Object(l.a)(),I=v;N&&"undefined"===typeof I&&(I=N.disabled);var D="checkbox"===_||"radio"===_;return a.createElement(f.a,Object(r.a)({component:"span",className:Object(s.a)(p.root,h,L&&p.checked,I&&p.disabled),disabled:I,tabIndex:null,role:void 0,onFocus:function(e){E&&E(e),N&&N.onFocus&&N.onFocus(e)},onBlur:function(e){O&&O(e),N&&N.onBlur&&N.onBlur(e)},ref:t},P),a.createElement("input",Object(r.a)({autoFocus:n,checked:c,defaultChecked:m,className:p.input,disabled:I,id:D&&g,name:x,onChange:function(e){var t=e.target.checked;M(t),S&&S(e,t)},readOnly:k,ref:w,required:j,tabIndex:C,type:_,value:R},b)),L?d:y)}));t.a=Object(c.a)({root:{padding:9},checked:{},disabled:{},input:{cursor:"inherit",position:"absolute",opacity:0,width:"100%",height:"100%",top:0,left:0,margin:0,padding:0,zIndex:1}},{name:"PrivateSwitchBase"})(d)},function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));var r=n(32);function o(e){var t=function(e,t){if("object"!=Object(r.a)(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var o=n.call(e,t||"default");if("object"!=Object(r.a)(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==Object(r.a)(t)?t:t+""}},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M8 5v14l11-7z"}),"PlayArrow");t.default=a},function(e,t,n){"use strict";var r=n(2),o=n(6),i=n(1),a=n.n(i),s=n(19),u=n(7),l=n(12),c=n(21),f=n(9),d=n(53),p=n(49),h=n(57),m=n(38),v=n(31),y=n(85);function g(e,t){var n=Object.create(null);return e&&i.Children.map(e,(function(e){return e})).forEach((function(e){n[e.key]=function(e){return t&&Object(i.isValidElement)(e)?t(e):e}(e)})),n}function b(e,t,n){return null!=n[t]?n[t]:e.props[t]}function w(e,t,n){var r=g(e.children),o=function(e,t){function n(n){return n in t?t[n]:e[n]}e=e||{},t=t||{};var r,o=Object.create(null),i=[];for(var a in e)a in t?i.length&&(o[a]=i,i=[]):i.push(a);var s={};for(var u in t){if(o[u])for(r=0;r0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0,r=t.pulsate,o=void 0!==r&&r,i=t.center,s=void 0===i?a||t.pulsate:i,u=t.fakeElement,l=void 0!==u&&u;if("mousedown"===e.type&&y.current)y.current=!1;else{"touchstart"===e.type&&(y.current=!0);var c,f,d,p=l?null:w.current,h=p?p.getBoundingClientRect():{width:0,height:0,left:0,top:0};if(s||0===e.clientX&&0===e.clientY||!e.clientX&&!e.touches)c=Math.round(h.width/2),f=Math.round(h.height/2);else{var m=e.touches?e.touches[0]:e,v=m.clientX,O=m.clientY;c=Math.round(v-h.left),f=Math.round(O-h.top)}if(s)(d=Math.sqrt((2*Math.pow(h.width,2)+Math.pow(h.height,2))/3))%2===0&&(d+=1);else{var S=2*Math.max(Math.abs((p?p.clientWidth:0)-c),c)+2,E=2*Math.max(Math.abs((p?p.clientHeight:0)-f),f)+2;d=Math.sqrt(Math.pow(S,2)+Math.pow(E,2))}e.touches?null===b.current&&(b.current=function(){x({pulsate:o,rippleX:c,rippleY:f,rippleSize:d,cb:n})},g.current=setTimeout((function(){b.current&&(b.current(),b.current=null)}),80)):x({pulsate:o,rippleX:c,rippleY:f,rippleSize:d,cb:n})}}),[a,x]),E=i.useCallback((function(){O({},{pulsate:!0})}),[O]),j=i.useCallback((function(e,t){if(clearTimeout(g.current),"touchend"===e.type&&b.current)return e.persist(),b.current(),b.current=null,void(g.current=setTimeout((function(){j(e,t)})));b.current=null,h((function(e){return e.length>0?e.slice(1):e})),v.current=t}),[]);return i.useImperativeHandle(t,(function(){return{pulsate:E,start:O,stop:j}}),[E,O,j]),i.createElement("span",Object(r.a)({className:Object(u.a)(s.root,l),ref:w},c),i.createElement(S,{component:null,exit:!0},d))})),C=Object(f.a)((function(e){return{root:{overflow:"hidden",pointerEvents:"none",position:"absolute",zIndex:0,top:0,right:0,bottom:0,left:0,borderRadius:"inherit"},ripple:{opacity:0,position:"absolute"},rippleVisible:{opacity:.3,transform:"scale(1)",animation:"$enter ".concat(550,"ms ").concat(e.transitions.easing.easeInOut)},ripplePulsate:{animationDuration:"".concat(e.transitions.duration.shorter,"ms")},child:{opacity:1,display:"block",width:"100%",height:"100%",borderRadius:"50%",backgroundColor:"currentColor"},childLeaving:{opacity:0,animation:"$exit ".concat(550,"ms ").concat(e.transitions.easing.easeInOut)},childPulsate:{position:"absolute",left:0,top:0,animation:"$pulsate 2500ms ".concat(e.transitions.easing.easeInOut," 200ms infinite")},"@keyframes enter":{"0%":{transform:"scale(0)",opacity:.1},"100%":{transform:"scale(1)",opacity:.3}},"@keyframes exit":{"0%":{opacity:1},"100%":{opacity:0}},"@keyframes pulsate":{"0%":{transform:"scale(1)"},"50%":{transform:"scale(0.92)"},"100%":{transform:"scale(1)"}}}}),{flip:!1,name:"MuiTouchRipple"})(i.memo(j)),_=i.forwardRef((function(e,t){var n=e.action,a=e.buttonRef,f=e.centerRipple,p=void 0!==f&&f,h=e.children,m=e.classes,v=e.className,y=e.component,g=void 0===y?"button":y,b=e.disabled,w=void 0!==b&&b,x=e.disableRipple,O=void 0!==x&&x,S=e.disableTouchRipple,E=void 0!==S&&S,k=e.focusRipple,j=void 0!==k&&k,_=e.focusVisibleClassName,R=e.onBlur,P=e.onClick,T=e.onFocus,A=e.onFocusVisible,L=e.onKeyDown,M=e.onKeyUp,N=e.onMouseDown,I=e.onMouseLeave,D=e.onMouseUp,F=e.onTouchEnd,z=e.onTouchMove,B=e.onTouchStart,U=e.onDragLeave,H=e.tabIndex,W=void 0===H?0:H,V=e.TouchRippleProps,q=e.type,$=void 0===q?"button":q,K=Object(o.a)(e,["action","buttonRef","centerRipple","children","classes","className","component","disabled","disableRipple","disableTouchRipple","focusRipple","focusVisibleClassName","onBlur","onClick","onFocus","onFocusVisible","onKeyDown","onKeyUp","onMouseDown","onMouseLeave","onMouseUp","onTouchEnd","onTouchMove","onTouchStart","onDragLeave","tabIndex","TouchRippleProps","type"]),Q=i.useRef(null);var Y=i.useRef(null),G=i.useState(!1),X=G[0],J=G[1];w&&X&&J(!1);var Z=Object(d.a)(),ee=Z.isFocusVisible,te=Z.onBlurVisible,ne=Z.ref;function re(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:E;return Object(c.a)((function(r){return t&&t(r),!n&&Y.current&&Y.current[e](r),!0}))}i.useImperativeHandle(n,(function(){return{focusVisible:function(){J(!0),Q.current.focus()}}}),[]),i.useEffect((function(){X&&j&&!O&&Y.current.pulsate()}),[O,j,X]);var oe=re("start",N),ie=re("stop",U),ae=re("stop",D),se=re("stop",(function(e){X&&e.preventDefault(),I&&I(e)})),ue=re("start",B),le=re("stop",F),ce=re("stop",z),fe=re("stop",(function(e){X&&(te(e),J(!1)),R&&R(e)}),!1),de=Object(c.a)((function(e){Q.current||(Q.current=e.currentTarget),ee(e)&&(J(!0),A&&A(e)),T&&T(e)})),pe=function(){var e=s.findDOMNode(Q.current);return g&&"button"!==g&&!("A"===e.tagName&&e.href)},he=i.useRef(!1),me=Object(c.a)((function(e){j&&!he.current&&X&&Y.current&&" "===e.key&&(he.current=!0,e.persist(),Y.current.stop(e,(function(){Y.current.start(e)}))),e.target===e.currentTarget&&pe()&&" "===e.key&&e.preventDefault(),L&&L(e),e.target===e.currentTarget&&pe()&&"Enter"===e.key&&!w&&(e.preventDefault(),P&&P(e))})),ve=Object(c.a)((function(e){j&&" "===e.key&&Y.current&&X&&!e.defaultPrevented&&(he.current=!1,e.persist(),Y.current.stop(e,(function(){Y.current.pulsate(e)}))),M&&M(e),P&&e.target===e.currentTarget&&pe()&&" "===e.key&&!e.defaultPrevented&&P(e)})),ye=g;"button"===ye&&K.href&&(ye="a");var ge={};"button"===ye?(ge.type=$,ge.disabled=w):("a"===ye&&K.href||(ge.role="button"),ge["aria-disabled"]=w);var be=Object(l.a)(a,t),we=Object(l.a)(ne,Q),xe=Object(l.a)(be,we),Oe=i.useState(!1),Se=Oe[0],Ee=Oe[1];i.useEffect((function(){Ee(!0)}),[]);var ke=Se&&!O&&!w;return i.createElement(ye,Object(r.a)({className:Object(u.a)(m.root,v,X&&[m.focusVisible,_],w&&m.disabled),onBlur:fe,onClick:P,onFocus:de,onKeyDown:me,onKeyUp:ve,onMouseDown:oe,onMouseLeave:se,onMouseUp:ae,onDragLeave:ie,onTouchEnd:le,onTouchMove:ce,onTouchStart:ue,ref:xe,tabIndex:w?-1:W},ge,K),h,ke?i.createElement(C,Object(r.a)({ref:Y,center:p},V)):null)}));t.a=Object(f.a)({root:{display:"inline-flex",alignItems:"center",justifyContent:"center",position:"relative",WebkitTapHighlightColor:"transparent",backgroundColor:"transparent",outline:0,border:0,margin:0,borderRadius:0,padding:0,cursor:"pointer",userSelect:"none",verticalAlign:"middle","-moz-appearance":"none","-webkit-appearance":"none",textDecoration:"none",color:"inherit","&::-moz-focus-inner":{borderStyle:"none"},"&$disabled":{pointerEvents:"none",cursor:"default"},"@media print":{colorAdjust:"exact"}},disabled:{},focusVisible:{}},{name:"MuiButtonBase"})(_)},,,function(e,t,n){"use strict";var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(o){return!1}}()?Object.assign:function(e,t){for(var n,a,s=function(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}(e),u=1;u",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(u),c=["%","/","?",";","#"].concat(l),f=["/","?","#"],d=/^[+a-z0-9A-Z_-]{0,63}$/,p=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,h={javascript:!0,"javascript:":!0},m={javascript:!0,"javascript:":!0},v={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},y=n(386);function g(e,t,n){if(e&&"object"===typeof e&&e instanceof o)return e;var r=new o;return r.parse(e,t,n),r}o.prototype.parse=function(e,t,n){if("string"!==typeof e)throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),a=-1!==o&&o127?A+="x":A+=T[L];if(!A.match(d)){var N=R.slice(0,j),I=R.slice(j+1),D=T.match(p);D&&(N.push(D[1]),I.unshift(D[2])),I.length&&(g="/"+I.join(".")+g),this.hostname=N.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),_||(this.hostname=r.toASCII(this.hostname));var F=this.port?":"+this.port:"",z=this.hostname||"";this.host=z+F,this.href+=this.host,_&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==g[0]&&(g="/"+g))}if(!h[x])for(j=0,P=l.length;j0)&&n.host.split("@"))&&(n.auth=_.shift(),n.hostname=_.shift(),n.host=n.hostname);return n.search=e.search,n.query=e.query,null===n.pathname&&null===n.search||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!O.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var E=O.slice(-1)[0],k=(n.host||e.host||O.length>1)&&("."===E||".."===E)||""===E,j=0,C=O.length;C>=0;C--)"."===(E=O[C])?O.splice(C,1):".."===E?(O.splice(C,1),j++):j&&(O.splice(C,1),j--);if(!w&&!x)for(;j--;j)O.unshift("..");!w||""===O[0]||O[0]&&"/"===O[0].charAt(0)||O.unshift(""),k&&"/"!==O.join("/").substr(-1)&&O.push("");var _,R=""===O[0]||O[0]&&"/"===O[0].charAt(0);S&&(n.hostname=R?"":O.length?O.shift():"",n.host=n.hostname,(_=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=_.shift(),n.hostname=_.shift(),n.host=n.hostname));return(w=w||n.host&&O.length)&&!R&&O.unshift(""),O.length>0?n.pathname=O.join("/"):(n.pathname=null,n.path=null),null===n.pathname&&null===n.search||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=a.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)},t.parse=g,t.resolve=function(e,t){return g(e,!1,!0).resolve(t)},t.resolveObject=function(e,t){return e?g(e,!1,!0).resolveObject(t):t},t.format=function(e){return"string"===typeof e&&(e=g(e)),e instanceof o?e.format():o.prototype.format.call(e)},t.Url=o},function(e,t,n){"use strict";var r,o=n(184),i=n(390),a=n(391),s=n(392),u=n(393),l=n(394),c=n(83),f=n(395),d=n(396),p=n(397),h=n(398),m=n(399),v=n(400),y=n(401),g=n(402),b=Function,w=function(e){try{return b('"use strict"; return ('+e+").constructor;")()}catch(t){}},x=n(185),O=n(405),S=function(){throw new c},E=x?function(){try{return S}catch(e){try{return x(arguments,"callee").get}catch(t){return S}}}():S,k=n(406)(),j=n(408),C=n(187),_=n(186),R=n(189),P=n(139),T={},A="undefined"!==typeof Uint8Array&&j?j(Uint8Array):r,L={__proto__:null,"%AggregateError%":"undefined"===typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"===typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":k&&j?j([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":T,"%AsyncGenerator%":T,"%AsyncGeneratorFunction%":T,"%AsyncIteratorPrototype%":T,"%Atomics%":"undefined"===typeof Atomics?r:Atomics,"%BigInt%":"undefined"===typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"===typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"===typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"===typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":i,"%eval%":eval,"%EvalError%":a,"%Float32Array%":"undefined"===typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"===typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"===typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":b,"%GeneratorFunction%":T,"%Int8Array%":"undefined"===typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"===typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"===typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":k&&j?j(j([][Symbol.iterator]())):r,"%JSON%":"object"===typeof JSON?JSON:r,"%Map%":"undefined"===typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!==typeof Map&&k&&j?j((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":o,"%Object.getOwnPropertyDescriptor%":x,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"===typeof Promise?r:Promise,"%Proxy%":"undefined"===typeof Proxy?r:Proxy,"%RangeError%":s,"%ReferenceError%":u,"%Reflect%":"undefined"===typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"===typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!==typeof Set&&k&&j?j((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"===typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":k&&j?j(""[Symbol.iterator]()):r,"%Symbol%":k?Symbol:r,"%SyntaxError%":l,"%ThrowTypeError%":E,"%TypedArray%":A,"%TypeError%":c,"%Uint8Array%":"undefined"===typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"===typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"===typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"===typeof Uint32Array?r:Uint32Array,"%URIError%":f,"%WeakMap%":"undefined"===typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"===typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"===typeof WeakSet?r:WeakSet,"%Function.prototype.call%":P,"%Function.prototype.apply%":R,"%Object.defineProperty%":O,"%Object.getPrototypeOf%":C,"%Math.abs%":d,"%Math.floor%":p,"%Math.max%":h,"%Math.min%":m,"%Math.pow%":v,"%Math.round%":y,"%Math.sign%":g,"%Reflect.getPrototypeOf%":_};if(j)try{null.error}catch(K){var M=j(j(K));L["%Error.prototype%"]=M}var N=function e(t){var n;if("%AsyncFunction%"===t)n=w("async function () {}");else if("%GeneratorFunction%"===t)n=w("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=w("async function* () {}");else if("%AsyncGenerator%"===t){var r=e("%AsyncGeneratorFunction%");r&&(n=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var o=e("%AsyncGenerator%");o&&j&&(n=j(o.prototype))}return L[t]=n,n},I={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},D=n(108),F=n(413),z=D.call(P,Array.prototype.concat),B=D.call(R,Array.prototype.splice),U=D.call(P,String.prototype.replace),H=D.call(P,String.prototype.slice),W=D.call(P,RegExp.prototype.exec),V=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,q=/\\(\\)?/g,$=function(e,t){var n,r=e;if(F(I,r)&&(r="%"+(n=I[r])[0]+"%"),F(L,r)){var o=L[r];if(o===T&&(o=N(r)),"undefined"===typeof o&&!t)throw new c("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:r,value:o}}throw new l("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!==typeof e||0===e.length)throw new c("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!==typeof t)throw new c('"allowMissing" argument must be a boolean');if(null===W(/^%?[^%]*%?$/,e))throw new l("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=function(e){var t=H(e,0,1),n=H(e,-1);if("%"===t&&"%"!==n)throw new l("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new l("invalid intrinsic syntax, expected opening `%`");var r=[];return U(e,V,(function(e,t,n,o){r[r.length]=n?U(o,q,"$1"):t||e})),r}(e),r=n.length>0?n[0]:"",o=$("%"+r+"%",t),i=o.name,a=o.value,s=!1,u=o.alias;u&&(r=u[0],B(n,z([0,1],u)));for(var f=1,d=!0;f=n.length){var v=x(a,p);a=(d=!!v)&&"get"in v&&!("originalValue"in v.get)?v.get:a[p]}else d=F(a,p),a=a[p];d&&!s&&(L[i]=a)}}return a}},function(e,t,n){"use strict";e.exports=Function.prototype.call},function(e,t,n){"use strict";var r=String.prototype.replace,o=/%20/g,i="RFC1738",a="RFC3986";e.exports={default:a,formatters:{RFC1738:function(e){return r.call(e,o,"+")},RFC3986:function(e){return String(e)}},RFC1738:i,RFC3986:a}},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"}),"Settings");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"}),"Info");t.default=a},function(e,t,n){"use strict";var r=n(32),o=n(13);function i(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{};Object(a.a)(this,e),this.init(t,n)}return Object(s.a)(e,[{key:"init",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.prefix=t.prefix||"i18next:",this.logger=e||p,this.options=t,this.debug=t.debug}},{key:"setDebug",value:function(e){this.debug=e}},{key:"log",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n1?t-1:0),r=1;r-1?e.replace(/###/g,"."):e}function o(){return!e||"string"===typeof e}for(var i="string"!==typeof t?[].concat(t):t.split(".");i.length>1;){if(o())return{};var a=r(i.shift());!e[a]&&n&&(e[a]=new n),e=Object.prototype.hasOwnProperty.call(e,a)?e[a]:{}}return o()?{}:{obj:e,k:r(i.shift())}}function w(e,t,n){var r=b(e,t,Object);r.obj[r.k]=n}function x(e,t){var n=b(e,t),r=n.obj,o=n.k;if(r)return r[o]}function O(e,t,n){var r=x(e,n);return void 0!==r?r:x(t,n)}function S(e,t,n){for(var r in t)"__proto__"!==r&&"constructor"!==r&&(r in e?"string"===typeof e[r]||e[r]instanceof String||"string"===typeof t[r]||t[r]instanceof String?n&&(e[r]=t[r]):S(e[r],t[r],n):e[r]=t[r]);return e}function E(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var k={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};function j(e){return"string"===typeof e?e.replace(/[&<>"'\/]/g,(function(e){return k[e]})):e}var C="undefined"!==typeof window&&window.navigator&&window.navigator.userAgent&&window.navigator.userAgent.indexOf("MSIE")>-1;function _(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:".";if(e){if(e[t])return e[t];for(var r=t.split(n),o=e,i=0;ii+a;)a++,u=o[s=r.slice(i,i+a).join(n)];if(void 0===u)return;if("string"===typeof u)return u;if(s&&"string"===typeof u[s])return u[s];var l=r.slice(i+a).join(n);return l?_(u,l,n):void 0}o=o[r[i]]}return o}}var R=function(e){function t(e){var n,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{ns:["translation"],defaultNS:"translation"};return Object(a.a)(this,t),n=l(this,Object(c.a)(t).call(this)),C&&v.call(Object(u.a)(n)),n.data=e||{},n.options=r,void 0===n.options.keySeparator&&(n.options.keySeparator="."),void 0===n.options.ignoreJSONStructure&&(n.options.ignoreJSONStructure=!0),n}return d(t,e),Object(s.a)(t,[{key:"addNamespaces",value:function(e){this.options.ns.indexOf(e)<0&&this.options.ns.push(e)}},{key:"removeNamespaces",value:function(e){var t=this.options.ns.indexOf(e);t>-1&&this.options.ns.splice(t,1)}},{key:"getResource",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=void 0!==r.keySeparator?r.keySeparator:this.options.keySeparator,i=void 0!==r.ignoreJSONStructure?r.ignoreJSONStructure:this.options.ignoreJSONStructure,a=[e,t];n&&"string"!==typeof n&&(a=a.concat(n)),n&&"string"===typeof n&&(a=a.concat(o?n.split(o):n)),e.indexOf(".")>-1&&(a=e.split("."));var s=x(this.data,a);return s||!i||"string"!==typeof n?s:_(this.data&&this.data[e]&&this.data[e][t],n,o)}},{key:"addResource",value:function(e,t,n,r){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{silent:!1},i=this.options.keySeparator;void 0===i&&(i=".");var a=[e,t];n&&(a=a.concat(i?n.split(i):n)),e.indexOf(".")>-1&&(r=t,t=(a=e.split("."))[1]),this.addNamespaces(t),w(this.data,a,r),o.silent||this.emit("added",e,t,n,r)}},{key:"addResources",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{silent:!1};for(var o in n)"string"!==typeof n[o]&&"[object Array]"!==Object.prototype.toString.apply(n[o])||this.addResource(e,t,o,n[o],{silent:!0});r.silent||this.emit("added",e,t,n)}},{key:"addResourceBundle",value:function(e,t,n,r,o){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{silent:!1},s=[e,t];e.indexOf(".")>-1&&(r=n,n=t,t=(s=e.split("."))[1]),this.addNamespaces(t);var u=x(this.data,s)||{};r?S(u,n,o):u=i({},u,n),w(this.data,s,u),a.silent||this.emit("added",e,t,n)}},{key:"removeResourceBundle",value:function(e,t){this.hasResourceBundle(e,t)&&delete this.data[e][t],this.removeNamespaces(t),this.emit("removed",e,t)}},{key:"hasResourceBundle",value:function(e,t){return void 0!==this.getResource(e,t)}},{key:"getResourceBundle",value:function(e,t){return t||(t=this.options.defaultNS),"v1"===this.options.compatibilityAPI?i({},{},this.getResource(e,t)):this.getResource(e,t)}},{key:"getDataByLanguage",value:function(e){return this.data[e]}},{key:"toJSON",value:function(){return this.data}}]),t}(v),P={processors:{},addPostProcessor:function(e){this.processors[e.name]=e},handle:function(e,t,n,r,o){var i=this;return e.forEach((function(e){i.processors[e]&&(t=i.processors[e].process(t,n,r,o))})),t}},T={},A=function(e){function t(e){var n,r,o,i,s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this,t),n=l(this,Object(c.a)(t).call(this)),C&&v.call(Object(u.a)(n)),r=["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],o=e,i=Object(u.a)(n),r.forEach((function(e){o[e]&&(i[e]=o[e])})),n.options=s,void 0===n.options.keySeparator&&(n.options.keySeparator="."),n.logger=m.create("translator"),n}return d(t,e),Object(s.a)(t,[{key:"changeLanguage",value:function(e){e&&(this.language=e)}},{key:"exists",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}};if(void 0===e||null===e)return!1;var n=this.resolve(e,t);return n&&void 0!==n.res}},{key:"extractFromKey",value:function(e,t){var n=void 0!==t.nsSeparator?t.nsSeparator:this.options.nsSeparator;void 0===n&&(n=":");var r=void 0!==t.keySeparator?t.keySeparator:this.options.keySeparator,o=t.ns||this.options.defaultNS;if(n&&e.indexOf(n)>-1){var i=e.match(this.interpolator.nestingRegexp);if(i&&i.length>0)return{key:e,namespaces:o};var a=e.split(n);(n!==r||n===r&&this.options.ns.indexOf(a[0])>-1)&&(o=a.shift()),e=a.join(r)}return"string"===typeof o&&(o=[o]),{key:e,namespaces:o}}},{key:"translate",value:function(e,n,o){var a=this;if("object"!==Object(r.a)(n)&&this.options.overloadTranslationOptionHandler&&(n=this.options.overloadTranslationOptionHandler(arguments)),n||(n={}),void 0===e||null===e)return"";Array.isArray(e)||(e=[String(e)]);var s=void 0!==n.keySeparator?n.keySeparator:this.options.keySeparator,u=this.extractFromKey(e[e.length-1],n),l=u.key,c=u.namespaces,f=c[c.length-1],d=n.lng||this.language,p=n.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(d&&"cimode"===d.toLowerCase()){if(p){var h=n.nsSeparator||this.options.nsSeparator;return f+h+l}return l}var m=this.resolve(e,n),v=m&&m.res,y=m&&m.usedKey||l,g=m&&m.exactUsedKey||l,b=Object.prototype.toString.apply(v),w=void 0!==n.joinArrays?n.joinArrays:this.options.joinArrays,x=!this.i18nFormat||this.i18nFormat.handleAsObject;if(x&&v&&("string"!==typeof v&&"boolean"!==typeof v&&"number"!==typeof v)&&["[object Number]","[object Function]","[object RegExp]"].indexOf(b)<0&&("string"!==typeof w||"[object Array]"!==b)){if(!n.returnObjects&&!this.options.returnObjects)return this.options.returnedObjectHandler||this.logger.warn("accessing an object - but returnObjects options is not enabled!"),this.options.returnedObjectHandler?this.options.returnedObjectHandler(y,v,i({},n,{ns:c})):"key '".concat(l," (").concat(this.language,")' returned an object instead of string.");if(s){var O="[object Array]"===b,S=O?[]:{},E=O?g:y;for(var k in v)if(Object.prototype.hasOwnProperty.call(v,k)){var j="".concat(E).concat(s).concat(k);S[k]=this.translate(j,i({},n,{joinArrays:!1,ns:c})),S[k]===j&&(S[k]=v[k])}v=S}}else if(x&&"string"===typeof w&&"[object Array]"===b)(v=v.join(w))&&(v=this.extendTranslation(v,e,n,o));else{var C=!1,_=!1,R=void 0!==n.count&&"string"!==typeof n.count,P=t.hasDefaultValue(n),T=R?this.pluralResolver.getSuffix(d,n.count):"",A=n["defaultValue".concat(T)]||n.defaultValue;!this.isValidLookup(v)&&P&&(C=!0,v=A),this.isValidLookup(v)||(_=!0,v=l);var L=(n.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey)&&_?void 0:v,M=P&&A!==v&&this.options.updateMissing;if(_||C||M){if(this.logger.log(M?"updateKey":"missingKey",d,f,l,M?A:v),s){var N=this.resolve(l,i({},n,{keySeparator:!1}));N&&N.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}var I=[],D=this.languageUtils.getFallbackCodes(this.options.fallbackLng,n.lng||this.language);if("fallback"===this.options.saveMissingTo&&D&&D[0])for(var F=0;F1&&void 0!==arguments[1]?arguments[1]:{};return"string"===typeof e&&(e=[e]),e.forEach((function(e){if(!a.isValidLookup(t)){var u=a.extractFromKey(e,s),l=u.key;n=l;var c=u.namespaces;a.options.fallbackNS&&(c=c.concat(a.options.fallbackNS));var f=void 0!==s.count&&"string"!==typeof s.count,d=void 0!==s.context&&("string"===typeof s.context||"number"===typeof s.context)&&""!==s.context,p=s.lngs?s.lngs:a.languageUtils.toResolveHierarchy(s.lng||a.language,s.fallbackLng);c.forEach((function(e){a.isValidLookup(t)||(i=e,!T["".concat(p[0],"-").concat(e)]&&a.utils&&a.utils.hasLoadedNamespace&&!a.utils.hasLoadedNamespace(i)&&(T["".concat(p[0],"-").concat(e)]=!0,a.logger.warn('key "'.concat(n,'" for languages "').concat(p.join(", "),'" won\'t get resolved as namespace "').concat(i,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),p.forEach((function(n){if(!a.isValidLookup(t)){o=n;var i,u,c=l,p=[c];if(a.i18nFormat&&a.i18nFormat.addLookupKeys)a.i18nFormat.addLookupKeys(p,l,n,e,s);else f&&(i=a.pluralResolver.getSuffix(n,s.count)),f&&d&&p.push(c+i),d&&p.push(c+="".concat(a.options.contextSeparator).concat(s.context)),f&&p.push(c+=i);for(;u=p.pop();)a.isValidLookup(t)||(r=u,t=a.getResource(n,e,u,s))}})))}))}})),{res:t,usedKey:n,exactUsedKey:r,usedLng:o,usedNS:i}}},{key:"isValidLookup",value:function(e){return void 0!==e&&!(!this.options.returnNull&&null===e)&&!(!this.options.returnEmptyString&&""===e)}},{key:"getResource",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return this.i18nFormat&&this.i18nFormat.getResource?this.i18nFormat.getResource(e,t,n,r):this.resourceStore.getResource(e,t,n,r)}}],[{key:"hasDefaultValue",value:function(e){var t="defaultValue";for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)&&t===n.substring(0,12)&&void 0!==e[n])return!0;return!1}}]),t}(v);function L(e){return e.charAt(0).toUpperCase()+e.slice(1)}var M=function(){function e(t){Object(a.a)(this,e),this.options=t,this.whitelist=this.options.supportedLngs||!1,this.supportedLngs=this.options.supportedLngs||!1,this.logger=m.create("languageUtils")}return Object(s.a)(e,[{key:"getScriptPartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return null;var t=e.split("-");return 2===t.length?null:(t.pop(),"x"===t[t.length-1].toLowerCase()?null:this.formatLanguageCode(t.join("-")))}},{key:"getLanguagePartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return e;var t=e.split("-");return this.formatLanguageCode(t[0])}},{key:"formatLanguageCode",value:function(e){if("string"===typeof e&&e.indexOf("-")>-1){var t=["hans","hant","latn","cyrl","cans","mong","arab"],n=e.split("-");return this.options.lowerCaseLng?n=n.map((function(e){return e.toLowerCase()})):2===n.length?(n[0]=n[0].toLowerCase(),n[1]=n[1].toUpperCase(),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=L(n[1].toLowerCase()))):3===n.length&&(n[0]=n[0].toLowerCase(),2===n[1].length&&(n[1]=n[1].toUpperCase()),"sgn"!==n[0]&&2===n[2].length&&(n[2]=n[2].toUpperCase()),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=L(n[1].toLowerCase())),t.indexOf(n[2].toLowerCase())>-1&&(n[2]=L(n[2].toLowerCase()))),n.join("-")}return this.options.cleanCode||this.options.lowerCaseLng?e.toLowerCase():e}},{key:"isWhitelisted",value:function(e){return this.logger.deprecate("languageUtils.isWhitelisted",'function "isWhitelisted" will be renamed to "isSupportedCode" in the next major - please make sure to rename it\'s usage asap.'),this.isSupportedCode(e)}},{key:"isSupportedCode",value:function(e){return("languageOnly"===this.options.load||this.options.nonExplicitSupportedLngs)&&(e=this.getLanguagePartFromCode(e)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(e)>-1}},{key:"getBestMatchFromCodes",value:function(e){var t,n=this;return e?(e.forEach((function(e){if(!t){var r=n.formatLanguageCode(e);n.options.supportedLngs&&!n.isSupportedCode(r)||(t=r)}})),!t&&this.options.supportedLngs&&e.forEach((function(e){if(!t){var r=n.getLanguagePartFromCode(e);if(n.isSupportedCode(r))return t=r;t=n.options.supportedLngs.find((function(e){if(0===e.indexOf(r))return e}))}})),t||(t=this.getFallbackCodes(this.options.fallbackLng)[0]),t):null}},{key:"getFallbackCodes",value:function(e,t){if(!e)return[];if("function"===typeof e&&(e=e(t)),"string"===typeof e&&(e=[e]),"[object Array]"===Object.prototype.toString.apply(e))return e;if(!t)return e.default||[];var n=e[t];return n||(n=e[this.getScriptPartFromCode(t)]),n||(n=e[this.formatLanguageCode(t)]),n||(n=e[this.getLanguagePartFromCode(t)]),n||(n=e.default),n||[]}},{key:"toResolveHierarchy",value:function(e,t){var n=this,r=this.getFallbackCodes(t||this.options.fallbackLng||[],e),o=[],i=function(e){e&&(n.isSupportedCode(e)?o.push(e):n.logger.warn("rejecting language code not found in supportedLngs: ".concat(e)))};return"string"===typeof e&&e.indexOf("-")>-1?("languageOnly"!==this.options.load&&i(this.formatLanguageCode(e)),"languageOnly"!==this.options.load&&"currentOnly"!==this.options.load&&i(this.getScriptPartFromCode(e)),"currentOnly"!==this.options.load&&i(this.getLanguagePartFromCode(e))):"string"===typeof e&&i(this.formatLanguageCode(e)),r.forEach((function(e){o.indexOf(e)<0&&i(n.formatLanguageCode(e))})),o}}]),e}(),N=[{lngs:["ach","ak","am","arn","br","fil","gun","ln","mfe","mg","mi","oc","pt","pt-BR","tg","tl","ti","tr","uz","wa"],nr:[1,2],fc:1},{lngs:["af","an","ast","az","bg","bn","ca","da","de","dev","el","en","eo","es","et","eu","fi","fo","fur","fy","gl","gu","ha","hi","hu","hy","ia","it","kk","kn","ku","lb","mai","ml","mn","mr","nah","nap","nb","ne","nl","nn","no","nso","pa","pap","pms","ps","pt-PT","rm","sco","se","si","so","son","sq","sv","sw","ta","te","tk","ur","yo"],nr:[1,2],fc:2},{lngs:["ay","bo","cgg","fa","ht","id","ja","jbo","ka","km","ko","ky","lo","ms","sah","su","th","tt","ug","vi","wo","zh"],nr:[1],fc:3},{lngs:["be","bs","cnr","dz","hr","ru","sr","uk"],nr:[1,2,5],fc:4},{lngs:["ar"],nr:[0,1,2,3,11,100],fc:5},{lngs:["cs","sk"],nr:[1,2,5],fc:6},{lngs:["csb","pl"],nr:[1,2,5],fc:7},{lngs:["cy"],nr:[1,2,3,8],fc:8},{lngs:["fr"],nr:[1,2],fc:9},{lngs:["ga"],nr:[1,2,3,7,11],fc:10},{lngs:["gd"],nr:[1,2,3,20],fc:11},{lngs:["is"],nr:[1,2],fc:12},{lngs:["jv"],nr:[0,1],fc:13},{lngs:["kw"],nr:[1,2,3,4],fc:14},{lngs:["lt"],nr:[1,2,10],fc:15},{lngs:["lv"],nr:[1,2,0],fc:16},{lngs:["mk"],nr:[1,2],fc:17},{lngs:["mnk"],nr:[0,1,2],fc:18},{lngs:["mt"],nr:[1,2,11,20],fc:19},{lngs:["or"],nr:[2,1],fc:2},{lngs:["ro"],nr:[1,2,20],fc:20},{lngs:["sl"],nr:[5,1,2,3],fc:21},{lngs:["he","iw"],nr:[1,2,20,21],fc:22}],I={1:function(e){return Number(e>1)},2:function(e){return Number(1!=e)},3:function(e){return 0},4:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},5:function(e){return Number(0==e?0:1==e?1:2==e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5)},6:function(e){return Number(1==e?0:e>=2&&e<=4?1:2)},7:function(e){return Number(1==e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},8:function(e){return Number(1==e?0:2==e?1:8!=e&&11!=e?2:3)},9:function(e){return Number(e>=2)},10:function(e){return Number(1==e?0:2==e?1:e<7?2:e<11?3:4)},11:function(e){return Number(1==e||11==e?0:2==e||12==e?1:e>2&&e<20?2:3)},12:function(e){return Number(e%10!=1||e%100==11)},13:function(e){return Number(0!==e)},14:function(e){return Number(1==e?0:2==e?1:3==e?2:3)},15:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2)},16:function(e){return Number(e%10==1&&e%100!=11?0:0!==e?1:2)},17:function(e){return Number(1==e||e%10==1&&e%100!=11?0:1)},18:function(e){return Number(0==e?0:1==e?1:2)},19:function(e){return Number(1==e?0:0==e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3)},20:function(e){return Number(1==e?0:0==e||e%100>0&&e%100<20?1:2)},21:function(e){return Number(e%100==1?1:e%100==2?2:e%100==3||e%100==4?3:0)},22:function(e){return Number(1==e?0:2==e?1:(e<0||e>10)&&e%10==0?2:3)}};var D=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};Object(a.a)(this,e),this.languageUtils=t,this.options=n,this.logger=m.create("pluralResolver"),this.rules=function(){var e={};return N.forEach((function(t){t.lngs.forEach((function(n){e[n]={numbers:t.nr,plurals:I[t.fc]}}))})),e}()}return Object(s.a)(e,[{key:"addRule",value:function(e,t){this.rules[e]=t}},{key:"getRule",value:function(e){return this.rules[e]||this.rules[this.languageUtils.getLanguagePartFromCode(e)]}},{key:"needsPlural",value:function(e){var t=this.getRule(e);return t&&t.numbers.length>1}},{key:"getPluralFormsOfKey",value:function(e,t){return this.getSuffixes(e).map((function(e){return t+e}))}},{key:"getSuffixes",value:function(e){var t=this,n=this.getRule(e);return n?n.numbers.map((function(n){return t.getSuffix(e,n)})):[]}},{key:"getSuffix",value:function(e,t){var n=this,r=this.getRule(e);if(r){var o=r.noAbs?r.plurals(t):r.plurals(Math.abs(t)),i=r.numbers[o];this.options.simplifyPluralSuffix&&2===r.numbers.length&&1===r.numbers[0]&&(2===i?i="plural":1===i&&(i=""));var a=function(){return n.options.prepend&&i.toString()?n.options.prepend+i.toString():i.toString()};return"v1"===this.options.compatibilityJSON?1===i?"":"number"===typeof i?"_plural_".concat(i.toString()):a():"v2"===this.options.compatibilityJSON||this.options.simplifyPluralSuffix&&2===r.numbers.length&&1===r.numbers[0]?a():this.options.prepend&&o.toString()?this.options.prepend+o.toString():o.toString()}return this.logger.warn("no plural rule found for: ".concat(e)),""}}]),e}(),F=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};Object(a.a)(this,e),this.logger=m.create("interpolator"),this.options=t,this.format=t.interpolation&&t.interpolation.format||function(e){return e},this.init(t)}return Object(s.a)(e,[{key:"init",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};e.interpolation||(e.interpolation={escapeValue:!0});var t=e.interpolation;this.escape=void 0!==t.escape?t.escape:j,this.escapeValue=void 0===t.escapeValue||t.escapeValue,this.useRawValueToEscape=void 0!==t.useRawValueToEscape&&t.useRawValueToEscape,this.prefix=t.prefix?E(t.prefix):t.prefixEscaped||"{{",this.suffix=t.suffix?E(t.suffix):t.suffixEscaped||"}}",this.formatSeparator=t.formatSeparator?t.formatSeparator:t.formatSeparator||",",this.unescapePrefix=t.unescapeSuffix?"":t.unescapePrefix||"-",this.unescapeSuffix=this.unescapePrefix?"":t.unescapeSuffix||"",this.nestingPrefix=t.nestingPrefix?E(t.nestingPrefix):t.nestingPrefixEscaped||E("$t("),this.nestingSuffix=t.nestingSuffix?E(t.nestingSuffix):t.nestingSuffixEscaped||E(")"),this.nestingOptionsSeparator=t.nestingOptionsSeparator?t.nestingOptionsSeparator:t.nestingOptionsSeparator||",",this.maxReplaces=t.maxReplaces?t.maxReplaces:1e3,this.alwaysFormat=void 0!==t.alwaysFormat&&t.alwaysFormat,this.resetRegExp()}},{key:"reset",value:function(){this.options&&this.init(this.options)}},{key:"resetRegExp",value:function(){var e="".concat(this.prefix,"(.+?)").concat(this.suffix);this.regexp=new RegExp(e,"g");var t="".concat(this.prefix).concat(this.unescapePrefix,"(.+?)").concat(this.unescapeSuffix).concat(this.suffix);this.regexpUnescape=new RegExp(t,"g");var n="".concat(this.nestingPrefix,"(.+?)").concat(this.nestingSuffix);this.nestingRegexp=new RegExp(n,"g")}},{key:"interpolate",value:function(e,t,n,r){var o,a,s,u=this,l=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{};function c(e){return e.replace(/\$/g,"$$$$")}var f=function(e){if(e.indexOf(u.formatSeparator)<0){var o=O(t,l,e);return u.alwaysFormat?u.format(o,void 0,n,i({},r,t,{interpolationkey:e})):o}var a=e.split(u.formatSeparator),s=a.shift().trim(),c=a.join(u.formatSeparator).trim();return u.format(O(t,l,s),c,n,i({},r,t,{interpolationkey:s}))};this.resetRegExp();var d=r&&r.missingInterpolationHandler||this.options.missingInterpolationHandler,p=r&&r.interpolation&&r.interpolation.skipOnVariables||this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:function(e){return c(e)}},{regex:this.regexp,safeValue:function(e){return u.escapeValue?c(u.escape(e)):c(e)}}].forEach((function(t){for(s=0;o=t.regex.exec(e);){if(void 0===(a=f(o[1].trim())))if("function"===typeof d){var n=d(e,o,r);a="string"===typeof n?n:""}else{if(p){a=o[0];continue}u.logger.warn("missed to pass in variable ".concat(o[1]," for interpolating ").concat(e)),a=""}else"string"===typeof a||u.useRawValueToEscape||(a=g(a));var i=t.safeValue(a);if(e=e.replace(o[0],i),p?(t.regex.lastIndex+=i.length,t.regex.lastIndex-=o[0].length):t.regex.lastIndex=0,++s>=u.maxReplaces)break}})),e}},{key:"nest",value:function(e,t){var n,r,o=this,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},s=i({},a);function u(e,t){var n=this.nestingOptionsSeparator;if(e.indexOf(n)<0)return e;var r=e.split(new RegExp("".concat(n,"[ ]*{"))),o="{".concat(r[1]);e=r[0],o=(o=this.interpolate(o,s)).replace(/'/g,'"');try{s=JSON.parse(o),t&&(s=i({},t,s))}catch(a){return this.logger.warn("failed parsing options string in nesting for key ".concat(e),a),"".concat(e).concat(n).concat(o)}return delete s.defaultValue,e}for(s.applyPostProcessor=!1,delete s.defaultValue;n=this.nestingRegexp.exec(e);){var l=[],c=!1;if(-1!==n[0].indexOf(this.formatSeparator)&&!/{.*}/.test(n[1])){var f=n[1].split(this.formatSeparator).map((function(e){return e.trim()}));n[1]=f.shift(),l=f,c=!0}if((r=t(u.call(this,n[1].trim(),s),s))&&n[0]===e&&"string"!==typeof r)return r;"string"!==typeof r&&(r=g(r)),r||(this.logger.warn("missed to resolve ".concat(n[1]," for nesting ").concat(e)),r=""),c&&(r=l.reduce((function(e,t){return o.format(e,t,a.lng,i({},a,{interpolationkey:n[1].trim()}))}),r.trim())),e=e.replace(n[0],r),this.regexp.lastIndex=0}return e}}]),e}();var z=function(e){function t(e,n,r){var o,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return Object(a.a)(this,t),o=l(this,Object(c.a)(t).call(this)),C&&v.call(Object(u.a)(o)),o.backend=e,o.store=n,o.services=r,o.languageUtils=r.languageUtils,o.options=i,o.logger=m.create("backendConnector"),o.state={},o.queue=[],o.backend&&o.backend.init&&o.backend.init(r,i.backend,i),o}return d(t,e),Object(s.a)(t,[{key:"queueLoad",value:function(e,t,n,r){var o=this,i=[],a=[],s=[],u=[];return e.forEach((function(e){var r=!0;t.forEach((function(t){var s="".concat(e,"|").concat(t);!n.reload&&o.store.hasResourceBundle(e,t)?o.state[s]=2:o.state[s]<0||(1===o.state[s]?a.indexOf(s)<0&&a.push(s):(o.state[s]=1,r=!1,a.indexOf(s)<0&&a.push(s),i.indexOf(s)<0&&i.push(s),u.indexOf(t)<0&&u.push(t)))})),r||s.push(e)})),(i.length||a.length)&&this.queue.push({pending:a,loaded:{},errors:[],callback:r}),{toLoad:i,pending:a,toLoadLanguages:s,toLoadNamespaces:u}}},{key:"loaded",value:function(e,t,n){var r=e.split("|"),o=r[0],i=r[1];t&&this.emit("failedLoading",o,i,t),n&&this.store.addResourceBundle(o,i,n),this.state[e]=t?-1:2;var a={};this.queue.forEach((function(n){!function(e,t,n,r){var o=b(e,t,Object),i=o.obj,a=o.k;i[a]=i[a]||[],r&&(i[a]=i[a].concat(n)),r||i[a].push(n)}(n.loaded,[o],i),function(e,t){for(var n=e.indexOf(t);-1!==n;)e.splice(n,1),n=e.indexOf(t)}(n.pending,e),t&&n.errors.push(t),0!==n.pending.length||n.done||(Object.keys(n.loaded).forEach((function(e){a[e]||(a[e]=[]),n.loaded[e].length&&n.loaded[e].forEach((function(t){a[e].indexOf(t)<0&&a[e].push(t)}))})),n.done=!0,n.errors.length?n.callback(n.errors):n.callback())})),this.emit("loaded",a),this.queue=this.queue.filter((function(e){return!e.done}))}},{key:"read",value:function(e,t,n){var r=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:350,a=arguments.length>5?arguments[5]:void 0;return e.length?this.backend[n](e,t,(function(s,u){s&&u&&o<5?setTimeout((function(){r.read.call(r,e,t,n,o+1,2*i,a)}),i):a(s,u)})):a(null,{})}},{key:"prepareLoading",value:function(e,t){var n=this,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=arguments.length>3?arguments[3]:void 0;if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),o&&o();"string"===typeof e&&(e=this.languageUtils.toResolveHierarchy(e)),"string"===typeof t&&(t=[t]);var i=this.queueLoad(e,t,r,o);if(!i.toLoad.length)return i.pending.length||o(),null;i.toLoad.forEach((function(e){n.loadOne(e)}))}},{key:"load",value:function(e,t,n){this.prepareLoading(e,t,{},n)}},{key:"reload",value:function(e,t,n){this.prepareLoading(e,t,{reload:!0},n)}},{key:"loadOne",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=e.split("|"),o=r[0],i=r[1];this.read(o,i,"read",void 0,void 0,(function(r,a){r&&t.logger.warn("".concat(n,"loading namespace ").concat(i," for language ").concat(o," failed"),r),!r&&a&&t.logger.log("".concat(n,"loaded namespace ").concat(i," for language ").concat(o),a),t.loaded(e,r,a)}))}},{key:"saveMissing",value:function(e,t,n,r,o){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{};this.services.utils&&this.services.utils.hasLoadedNamespace&&!this.services.utils.hasLoadedNamespace(t)?this.logger.warn('did not save key "'.concat(n,'" as the namespace "').concat(t,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!"):void 0!==n&&null!==n&&""!==n&&(this.backend&&this.backend.create&&this.backend.create(e,t,n,r,null,i({},a,{isUpdate:o})),e&&e[0]&&this.store.addResource(e[0],t,n,r))}}]),t}(v);function B(e){return"string"===typeof e.ns&&(e.ns=[e.ns]),"string"===typeof e.fallbackLng&&(e.fallbackLng=[e.fallbackLng]),"string"===typeof e.fallbackNS&&(e.fallbackNS=[e.fallbackNS]),e.whitelist&&(e.whitelist&&e.whitelist.indexOf("cimode")<0&&(e.whitelist=e.whitelist.concat(["cimode"])),e.supportedLngs=e.whitelist),e.nonExplicitWhitelist&&(e.nonExplicitSupportedLngs=e.nonExplicitWhitelist),e.supportedLngs&&e.supportedLngs.indexOf("cimode")<0&&(e.supportedLngs=e.supportedLngs.concat(["cimode"])),e}function U(){}var H=function(e){function t(){var e,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=arguments.length>1?arguments[1]:void 0;if(Object(a.a)(this,t),e=l(this,Object(c.a)(t).call(this)),C&&v.call(Object(u.a)(e)),e.options=B(n),e.services={},e.logger=m,e.modules={external:[]},r&&!e.isInitialized&&!n.isClone){if(!e.options.initImmediate)return e.init(n,r),l(e,Object(u.a)(e));setTimeout((function(){e.init(n,r)}),0)}return e}return d(t,e),Object(s.a)(t,[{key:"init",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1?arguments[1]:void 0;function o(e){return e?"function"===typeof e?new e:e:null}if("function"===typeof t&&(n=t,t={}),t.whitelist&&!t.supportedLngs&&this.logger.deprecate("whitelist",'option "whitelist" will be renamed to "supportedLngs" in the next major - please make sure to rename this option asap.'),t.nonExplicitWhitelist&&!t.nonExplicitSupportedLngs&&this.logger.deprecate("whitelist",'options "nonExplicitWhitelist" will be renamed to "nonExplicitSupportedLngs" in the next major - please make sure to rename this option asap.'),this.options=i({},{debug:!1,initImmediate:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,whitelist:!1,nonExplicitWhitelist:!1,supportedLngs:!1,nonExplicitSupportedLngs:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",partialBundledLanguages:!1,saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,postProcessPassResolved:!1,returnNull:!0,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:!1,parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:function(e){var t={};if("object"===Object(r.a)(e[1])&&(t=e[1]),"string"===typeof e[1]&&(t.defaultValue=e[1]),"string"===typeof e[2]&&(t.tDescription=e[2]),"object"===Object(r.a)(e[2])||"object"===Object(r.a)(e[3])){var n=e[3]||e[2];Object.keys(n).forEach((function(e){t[e]=n[e]}))}return t},interpolation:{escapeValue:!0,format:function(e,t,n,r){return e},prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",nestingOptionsSeparator:",",maxReplaces:1e3,skipOnVariables:!1}},this.options,B(t)),this.format=this.options.interpolation.format,n||(n=U),!this.options.isClone){this.modules.logger?m.init(o(this.modules.logger),this.options):m.init(null,this.options);var a=new M(this.options);this.store=new R(this.options.resources,this.options);var s=this.services;s.logger=m,s.resourceStore=this.store,s.languageUtils=a,s.pluralResolver=new D(a,{prepend:this.options.pluralSeparator,compatibilityJSON:this.options.compatibilityJSON,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),s.interpolator=new F(this.options),s.utils={hasLoadedNamespace:this.hasLoadedNamespace.bind(this)},s.backendConnector=new z(o(this.modules.backend),s.resourceStore,s,this.options),s.backendConnector.on("*",(function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o1?n-1:0),o=1;o0&&"dev"!==u[0]&&(this.options.lng=u[0])}this.services.languageDetector||this.options.lng||this.logger.warn("init: no languageDetector is used and no lng is defined");["getResource","hasResourceBundle","getResourceBundle","getDataByLanguage"].forEach((function(t){e[t]=function(){var n;return(n=e.store)[t].apply(n,arguments)}}));["addResource","addResources","addResourceBundle","removeResourceBundle"].forEach((function(t){e[t]=function(){var n;return(n=e.store)[t].apply(n,arguments),e}}));var l=y(),c=function(){var t=function(t,r){e.isInitialized&&!e.initializedStoreOnce&&e.logger.warn("init: i18next is already initialized. You should call init just once!"),e.isInitialized=!0,e.options.isClone||e.logger.log("initialized",e.options),e.emit("initialized",e.options),l.resolve(r),n(t,r)};if(e.languages&&"v1"!==e.options.compatibilityAPI&&!e.isInitialized)return t(null,e.t.bind(e));e.changeLanguage(e.options.lng,t)};return this.options.resources||!this.options.initImmediate?c():setTimeout(c,0),l}},{key:"loadResources",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:U,r="string"===typeof e?e:this.language;if("function"===typeof e&&(n=e),!this.options.resources||this.options.partialBundledLanguages){if(r&&"cimode"===r.toLowerCase())return n();var o=[],i=function(e){e&&t.services.languageUtils.toResolveHierarchy(e).forEach((function(e){o.indexOf(e)<0&&o.push(e)}))};if(r)i(r);else this.services.languageUtils.getFallbackCodes(this.options.fallbackLng).forEach((function(e){return i(e)}));this.options.preload&&this.options.preload.forEach((function(e){return i(e)})),this.services.backendConnector.load(o,this.options.ns,n)}else n(null)}},{key:"reloadResources",value:function(e,t,n){var r=y();return e||(e=this.languages),t||(t=this.options.ns),n||(n=U),this.services.backendConnector.reload(e,t,(function(e){r.resolve(),n(e)})),r}},{key:"use",value:function(e){if(!e)throw new Error("You are passing an undefined module! Please check the object you are passing to i18next.use()");if(!e.type)throw new Error("You are passing a wrong module! Please check the object you are passing to i18next.use()");return"backend"===e.type&&(this.modules.backend=e),("logger"===e.type||e.log&&e.warn&&e.error)&&(this.modules.logger=e),"languageDetector"===e.type&&(this.modules.languageDetector=e),"i18nFormat"===e.type&&(this.modules.i18nFormat=e),"postProcessor"===e.type&&P.addPostProcessor(e),"3rdParty"===e.type&&this.modules.external.push(e),this}},{key:"changeLanguage",value:function(e,t){var n=this;this.isLanguageChangingTo=e;var r=y();this.emit("languageChanging",e);var o=function(o){e||o||!n.services.languageDetector||(o=[]);var i="string"===typeof o?o:n.services.languageUtils.getBestMatchFromCodes(o);i&&(n.language||(n.language=i,n.languages=n.services.languageUtils.toResolveHierarchy(i)),n.translator.language||n.translator.changeLanguage(i),n.services.languageDetector&&n.services.languageDetector.cacheUserLanguage(i)),n.loadResources(i,(function(e){!function(e,o){o?(n.language=o,n.languages=n.services.languageUtils.toResolveHierarchy(o),n.translator.changeLanguage(o),n.isLanguageChangingTo=void 0,n.emit("languageChanged",o),n.logger.log("languageChanged",o)):n.isLanguageChangingTo=void 0,r.resolve((function(){return n.t.apply(n,arguments)})),t&&t(e,(function(){return n.t.apply(n,arguments)}))}(e,i)}))};return e||!this.services.languageDetector||this.services.languageDetector.async?!e&&this.services.languageDetector&&this.services.languageDetector.async?this.services.languageDetector.detect(o):o(e):o(this.services.languageDetector.detect()),r}},{key:"getFixedT",value:function(e,t,n){var o=this,a=function e(t,a){var s;if("object"!==Object(r.a)(a)){for(var u=arguments.length,l=new Array(u>2?u-2:0),c=2;c1&&void 0!==arguments[1]?arguments[1]:{};if(!this.isInitialized)return this.logger.warn("hasLoadedNamespace: i18next was not initialized",this.languages),!1;if(!this.languages||!this.languages.length)return this.logger.warn("hasLoadedNamespace: i18n.languages were undefined or empty",this.languages),!1;var r=this.languages[0],o=!!this.options&&this.options.fallbackLng,i=this.languages[this.languages.length-1];if("cimode"===r.toLowerCase())return!0;var a=function(e,n){var r=t.services.backendConnector.state["".concat(e,"|").concat(n)];return-1===r||2===r};if(n.precheck){var s=n.precheck(this,a);if(void 0!==s)return s}return!!this.hasResourceBundle(r,e)||(!this.services.backendConnector.backend||!(!a(r,e)||o&&!a(i,e)))}},{key:"loadNamespaces",value:function(e,t){var n=this,r=y();return this.options.ns?("string"===typeof e&&(e=[e]),e.forEach((function(e){n.options.ns.indexOf(e)<0&&n.options.ns.push(e)})),this.loadResources((function(e){r.resolve(),t&&t(e)})),r):(t&&t(),Promise.resolve())}},{key:"loadLanguages",value:function(e,t){var n=y();"string"===typeof e&&(e=[e]);var r=this.options.preload||[],o=e.filter((function(e){return r.indexOf(e)<0}));return o.length?(this.options.preload=r.concat(o),this.loadResources((function(e){n.resolve(),t&&t(e)})),n):(t&&t(),Promise.resolve())}},{key:"dir",value:function(e){if(e||(e=this.languages&&this.languages.length>0?this.languages[0]:this.language),!e)return"rtl";return["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ug","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam"].indexOf(this.services.languageUtils.getLanguagePartFromCode(e))>=0?"rtl":"ltr"}},{key:"createInstance",value:function(){return new t(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},arguments.length>1?arguments[1]:void 0)}},{key:"cloneInstance",value:function(){var e=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:U,o=i({},this.options,n,{isClone:!0}),a=new t(o);return["store","services","language"].forEach((function(t){a[t]=e[t]})),a.services=i({},this.services),a.services.utils={hasLoadedNamespace:a.hasLoadedNamespace.bind(a)},a.translator=new A(a.services,a.options),a.translator.on("*",(function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r0||v.errorUpdateCount>0,isFetchedAfterMount:v.dataUpdateCount>f.dataUpdateCount||v.errorUpdateCount>f.errorUpdateCount,isFetching:w,isRefetching:w&&"loading"!==x,isLoadingError:"error"===x&&0===v.dataUpdatedAt,isPlaceholderData:S,isPreviousData:O,isRefetchError:"error"===x&&0!==v.dataUpdatedAt,isStale:m(e,t),refetch:this.refetch,remove:this.remove}},n.shouldNotifyListeners=function(e,t){if(!t)return!0;var n=this.options,r=n.notifyOnChangeProps,o=n.notifyOnChangePropsExclusions;if(!r&&!o)return!0;if("tracked"===r&&!this.trackedProps.length)return!0;var i="tracked"===r?this.trackedProps:r;return Object.keys(e).some((function(n){var r=n,a=e[r]!==t[r],s=null==i?void 0:i.some((function(e){return e===n})),u=null==o?void 0:o.some((function(e){return e===n}));return a&&!u&&(!i||s)}))},n.updateResult=function(e){var t=this.currentResult;if(this.currentResult=this.createResult(this.currentQuery,this.options),this.currentResultState=this.currentQuery.state,this.currentResultOptions=this.options,!Object(i.p)(this.currentResult,t)){var n={cache:!0};!1!==(null==e?void 0:e.listeners)&&this.shouldNotifyListeners(this.currentResult,t)&&(n.listeners=!0),this.notify(Object(r.a)({},n,e))}},n.updateQuery=function(){var e=this.client.getQueryCache().build(this.client,this.options);if(e!==this.currentQuery){var t=this.currentQuery;this.currentQuery=e,this.currentQueryInitialState=e.state,this.previousQueryResult=this.currentResult,this.hasListeners()&&(null==t||t.removeObserver(this),e.addObserver(this))}},n.onQueryUpdate=function(e){var t={};"success"===e.type?t.onSuccess=!0:"error"!==e.type||Object(c.c)(e.error)||(t.onError=!0),this.updateResult(t),this.hasListeners()&&this.updateTimers()},n.notify=function(e){var t=this;a.a.batch((function(){e.onSuccess?(null==t.options.onSuccess||t.options.onSuccess(t.currentResult.data),null==t.options.onSettled||t.options.onSettled(t.currentResult.data,null)):e.onError&&(null==t.options.onError||t.options.onError(t.currentResult.error),null==t.options.onSettled||t.options.onSettled(void 0,t.currentResult.error)),e.listeners&&t.listeners.forEach((function(e){e(t.currentResult)})),e.cache&&t.client.getQueryCache().notify({query:t.currentQuery,type:"observerResultsUpdated"})}))},t}(u.a);function d(e,t){return function(e,t){return!1!==t.enabled&&!e.state.dataUpdatedAt&&!("error"===e.state.status&&!1===t.retryOnMount)}(e,t)||e.state.dataUpdatedAt>0&&p(e,t,t.refetchOnMount)}function p(e,t,n){if(!1!==t.enabled){var r="function"===typeof n?n(e):n;return"always"===r||!1!==r&&m(e,t)}return!1}function h(e,t,n,r){return!1!==n.enabled&&(e!==t||!1===r.enabled)&&(!n.suspense||"error"!==e.state.status)&&m(e,n)}function m(e,t){return e.isStaleByTime(t.staleTime)}},function(e,t){},function(e,t,n){var r=n(129).default,o=n(271);e.exports=function(e){var t=o(e,"string");return"symbol"==r(t)?t:t+""},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){(function(e){var r="undefined"!==typeof e&&e||"undefined"!==typeof self&&self||window,o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(272),t.setImmediate="undefined"!==typeof self&&self.setImmediate||"undefined"!==typeof e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!==typeof self&&self.clearImmediate||"undefined"!==typeof e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(34))},function(e,t){e.exports=function(e,t){return e===t||e!==e&&t!==t}},function(e,t,n){var r=n(89),o=n(103);e.exports=function(e){if(!o(e))return!1;var t=r(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(34))},function(e,t){var n=Function.prototype.toString;e.exports=function(e){if(null!=e){try{return n.call(e)}catch(t){}try{return e+""}catch(t){}}return""}},function(e,t,n){var r=n(303),o=n(310),i=n(312),a=n(313),s=n(314);function u(e){var t=-1,n=null==e?0:e.length;for(this.clear();++tc))return!1;var d=u.get(e),p=u.get(t);if(d&&p)return d==t&&p==e;var h=-1,m=!0,v=2&n?new r:void 0;for(u.set(e,t),u.set(t,e);++h-1&&e%1==0&&e<=9007199254740991}},function(e,t,n){(function(t){const n=e.exports;n.digitCount=function(e){const t=e<0?1:0;return e=Math.abs(Number(e||1)),Math.floor(Math.log10(e))+1+t},n.getType=function(e){return t.isBuffer(e)?"buffer":ArrayBuffer.isView(e)?"arraybufferview":Array.isArray(e)?"array":e instanceof Number?"number":e instanceof Boolean?"boolean":e instanceof Set?"set":e instanceof Map?"map":e instanceof String?"string":e instanceof ArrayBuffer?"arraybuffer":typeof e}}).call(this,n(35).Buffer)},function(e,t,n){(function(e){var r=n(374),o=n(173),i=n(383),a=n(384),s=n(137),u=t;u.request=function(t,n){t="string"===typeof t?s.parse(t):i(t);var o=-1===e.location.protocol.search(/^https?:$/)?"http:":"",a=t.protocol||o,u=t.hostname||t.host,l=t.port,c=t.path||"/";u&&-1!==u.indexOf(":")&&(u="["+u+"]"),t.url=(u?a+"//"+u:"")+(l?":"+l:"")+c,t.method=(t.method||"GET").toUpperCase(),t.headers=t.headers||{};var f=new r(t);return n&&f.on("response",n),f},u.get=function(e,t){var n=u.request(e,t);return n.end(),n},u.ClientRequest=r,u.IncomingMessage=o.IncomingMessage,u.Agent=function(){},u.Agent.defaultMaxSockets=4,u.globalAgent=new u.Agent,u.STATUS_CODES=a,u.METHODS=["CHECKOUT","CONNECT","COPY","DELETE","GET","HEAD","LOCK","M-SEARCH","MERGE","MKACTIVITY","MKCOL","MOVE","NOTIFY","OPTIONS","PATCH","POST","PROPFIND","PROPPATCH","PURGE","PUT","REPORT","SEARCH","SUBSCRIBE","TRACE","UNLOCK","UNSUBSCRIBE"]}).call(this,n(34))},function(e,t,n){(function(e){t.fetch=s(e.fetch)&&s(e.ReadableStream),t.writableStream=s(e.WritableStream),t.abortController=s(e.AbortController),t.blobConstructor=!1;try{new Blob([new ArrayBuffer(1)]),t.blobConstructor=!0}catch(u){}var n;function r(){if(void 0!==n)return n;if(e.XMLHttpRequest){n=new e.XMLHttpRequest;try{n.open("GET",e.XDomainRequest?"/":"https://example.com")}catch(u){n=null}}else n=null;return n}function o(e){var t=r();if(!t)return!1;try{return t.responseType=e,t.responseType===e}catch(u){}return!1}var i="undefined"!==typeof e.ArrayBuffer,a=i&&s(e.ArrayBuffer.prototype.slice);function s(e){return"function"===typeof e}t.arraybuffer=t.fetch||i&&o("arraybuffer"),t.msstream=!t.fetch&&a&&o("ms-stream"),t.mozchunkedarraybuffer=!t.fetch&&i&&o("moz-chunked-arraybuffer"),t.overrideMimeType=t.fetch||!!r()&&s(r().overrideMimeType),t.vbArray=s(e.VBArray),n=null}).call(this,n(34))},function(e,t,n){(function(e,r,o){var i=n(172),a=n(74),s=n(174),u=t.readyStates={UNSENT:0,OPENED:1,HEADERS_RECEIVED:2,LOADING:3,DONE:4},l=t.IncomingMessage=function(t,n,a,u){var l=this;if(s.Readable.call(l),l._mode=a,l.headers={},l.rawHeaders=[],l.trailers={},l.rawTrailers=[],l.on("end",(function(){e.nextTick((function(){l.emit("close")}))})),"fetch"===a){if(l._fetchResponse=n,l.url=n.url,l.statusCode=n.status,l.statusMessage=n.statusText,n.headers.forEach((function(e,t){l.headers[t.toLowerCase()]=e,l.rawHeaders.push(t,e)})),i.writableStream){var c=new WritableStream({write:function(e){return new Promise((function(t,n){l._destroyed?n():l.push(new r(e))?t():l._resumeFetch=t}))},close:function(){o.clearTimeout(u),l._destroyed||l.push(null)},abort:function(e){l._destroyed||l.emit("error",e)}});try{return void n.body.pipeTo(c).catch((function(e){o.clearTimeout(u),l._destroyed||l.emit("error",e)}))}catch(h){}}var f=n.body.getReader();!function e(){f.read().then((function(t){if(!l._destroyed){if(t.done)return o.clearTimeout(u),void l.push(null);l.push(new r(t.value)),e()}})).catch((function(e){o.clearTimeout(u),l._destroyed||l.emit("error",e)}))}()}else{if(l._xhr=t,l._pos=0,l.url=t.responseURL,l.statusCode=t.status,l.statusMessage=t.statusText,t.getAllResponseHeaders().split(/\r?\n/).forEach((function(e){var t=e.match(/^([^:]+):\s*(.*)/);if(t){var n=t[1].toLowerCase();"set-cookie"===n?(void 0===l.headers[n]&&(l.headers[n]=[]),l.headers[n].push(t[2])):void 0!==l.headers[n]?l.headers[n]+=", "+t[2]:l.headers[n]=t[2],l.rawHeaders.push(t[1],t[2])}})),l._charset="x-user-defined",!i.overrideMimeType){var d=l.rawHeaders["mime-type"];if(d){var p=d.match(/;\s*charset=([^;])(;|$)/);p&&(l._charset=p[1].toLowerCase())}l._charset||(l._charset="utf-8")}}};a(l,s.Readable),l.prototype._read=function(){var e=this._resumeFetch;e&&(this._resumeFetch=null,e())},l.prototype._onXHRProgress=function(){var e=this,t=e._xhr,n=null;switch(e._mode){case"text:vbarray":if(t.readyState!==u.DONE)break;try{n=new o.VBArray(t.responseBody).toArray()}catch(c){}if(null!==n){e.push(new r(n));break}case"text":try{n=t.responseText}catch(c){e._mode="text:vbarray";break}if(n.length>e._pos){var i=n.substr(e._pos);if("x-user-defined"===e._charset){for(var a=new r(i.length),s=0;se._pos&&(e.push(new r(new Uint8Array(l.result.slice(e._pos)))),e._pos=l.result.byteLength)},l.onload=function(){e.push(null)},l.readAsArrayBuffer(n)}e._xhr.readyState===u.DONE&&"ms-stream"!==e._mode&&e.push(null)}}).call(this,n(60),n(35).Buffer,n(34))},function(e,t,n){(t=e.exports=n(175)).Stream=t,t.Readable=t,t.Writable=n(179),t.Duplex=n(82),t.Transform=n(181),t.PassThrough=n(381)},function(e,t,n){"use strict";(function(t,r){var o=n(106);e.exports=b;var i,a=n(375);b.ReadableState=g;n(176).EventEmitter;var s=function(e,t){return e.listeners(t).length},u=n(177),l=n(136).Buffer,c=("undefined"!==typeof t?t:"undefined"!==typeof window?window:"undefined"!==typeof self?self:{}).Uint8Array||function(){};var f=Object.create(n(91));f.inherits=n(74);var d=n(376),p=void 0;p=d&&d.debuglog?d.debuglog("stream"):function(){};var h,m=n(377),v=n(178);f.inherits(b,u);var y=["error","close","destroy","pause","resume"];function g(e,t){e=e||{};var r=t instanceof(i=i||n(82));this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.readableObjectMode);var o=e.highWaterMark,a=e.readableHighWaterMark,s=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(a||0===a)?a:s,this.highWaterMark=Math.floor(this.highWaterMark),this.buffer=new m,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.destroyed=!1,this.defaultEncoding=e.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(h||(h=n(180).StringDecoder),this.decoder=new h(e.encoding),this.encoding=e.encoding)}function b(e){if(i=i||n(82),!(this instanceof b))return new b(e);this._readableState=new g(e,this),this.readable=!0,e&&("function"===typeof e.read&&(this._read=e.read),"function"===typeof e.destroy&&(this._destroy=e.destroy)),u.call(this)}function w(e,t,n,r,o){var i,a=e._readableState;null===t?(a.reading=!1,function(e,t){if(t.ended)return;if(t.decoder){var n=t.decoder.end();n&&n.length&&(t.buffer.push(n),t.length+=t.objectMode?1:n.length)}t.ended=!0,E(e)}(e,a)):(o||(i=function(e,t){var n;r=t,l.isBuffer(r)||r instanceof c||"string"===typeof t||void 0===t||e.objectMode||(n=new TypeError("Invalid non-string/buffer chunk"));var r;return n}(a,t)),i?e.emit("error",i):a.objectMode||t&&t.length>0?("string"===typeof t||a.objectMode||Object.getPrototypeOf(t)===l.prototype||(t=function(e){return l.from(e)}(t)),r?a.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):x(e,a,t,!0):a.ended?e.emit("error",new Error("stream.push() after EOF")):(a.reading=!1,a.decoder&&!n?(t=a.decoder.write(t),a.objectMode||0!==t.length?x(e,a,t,!1):j(e,a)):x(e,a,t,!1))):r||(a.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function(e){return e>=O?e=O:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function E(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(p("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?o.nextTick(k,e):k(e))}function k(e){p("emit readable"),e.emit("readable"),P(e)}function j(e,t){t.readingMore||(t.readingMore=!0,o.nextTick(C,e,t))}function C(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;ei.length?i.length:e;if(a===i.length?o+=i:o+=i.slice(0,e),0===(e-=a)){a===i.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=i.slice(a));break}++r}return t.length-=r,o}(e,t):function(e,t){var n=l.allocUnsafe(e),r=t.head,o=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var i=r.data,a=e>i.length?i.length:e;if(i.copy(n,n.length-e,0,a),0===(e-=a)){a===i.length?(++o,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=i.slice(a));break}++o}return t.length-=o,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function A(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,o.nextTick(L,t,e))}function L(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function M(e,t){for(var n=0,r=e.length;n=t.highWaterMark||t.ended))return p("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?A(this):E(this),null;if(0===(e=S(e,t))&&t.ended)return 0===t.length&&A(this),null;var r,o=t.needReadable;return p("need readable",o),(0===t.length||t.length-e0?T(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&A(this)),null!==r&&this.emit("data",r),r},b.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},b.prototype.pipe=function(e,t){var n=this,i=this._readableState;switch(i.pipesCount){case 0:i.pipes=e;break;case 1:i.pipes=[i.pipes,e];break;default:i.pipes.push(e)}i.pipesCount+=1,p("pipe count=%d opts=%j",i.pipesCount,t);var u=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?c:b;function l(t,r){p("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,p("cleanup"),e.removeListener("close",y),e.removeListener("finish",g),e.removeListener("drain",f),e.removeListener("error",v),e.removeListener("unpipe",l),n.removeListener("end",c),n.removeListener("end",b),n.removeListener("data",m),d=!0,!i.awaitDrain||e._writableState&&!e._writableState.needDrain||f())}function c(){p("onend"),e.end()}i.endEmitted?o.nextTick(u):n.once("end",u),e.on("unpipe",l);var f=function(e){return function(){var t=e._readableState;p("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&s(e,"data")&&(t.flowing=!0,P(e))}}(n);e.on("drain",f);var d=!1;var h=!1;function m(t){p("ondata"),h=!1,!1!==e.write(t)||h||((1===i.pipesCount&&i.pipes===e||i.pipesCount>1&&-1!==M(i.pipes,e))&&!d&&(p("false write response, pause",i.awaitDrain),i.awaitDrain++,h=!0),n.pause())}function v(t){p("onerror",t),b(),e.removeListener("error",v),0===s(e,"error")&&e.emit("error",t)}function y(){e.removeListener("finish",g),b()}function g(){p("onfinish"),e.removeListener("close",y),b()}function b(){p("unpipe"),n.unpipe(e)}return n.on("data",m),function(e,t,n){if("function"===typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?a(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",v),e.once("close",y),e.once("finish",g),e.emit("pipe",n),i.flowing||(p("pipe resume"),n.resume()),e},b.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes||(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n)),this;if(!e){var r=t.pipes,o=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var i=0;i0&&a.length>o&&!a.warned){a.warned=!0;var u=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");u.name="MaxListenersExceededWarning",u.emitter=e,u.type=t,u.count=a.length,s=u,console&&console.warn&&console.warn(s)}return e}function d(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function p(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=d.bind(r);return o.listener=n,r.wrapFn=o,o}function h(e,t,n){var r=e._events;if(void 0===r)return[];var o=r[t];return void 0===o?[]:"function"===typeof o?n?[o.listener||o]:[o]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var u=o[e];if(void 0===u)return!1;if("function"===typeof u)i(u,this,t);else{var l=u.length,c=v(u,l);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){a=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return h(this,e,!0)},s.prototype.rawListeners=function(e){return h(this,e,!1)},s.listenerCount=function(e,t){return"function"===typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},s.prototype.listenerCount=m,s.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){e.exports=n(176).EventEmitter},function(e,t,n){"use strict";var r=n(106);function o(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,i=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return i||a?(t?t(e):e&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,r.nextTick(o,this,e)):r.nextTick(o,this,e)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,(function(e){!t&&e?n._writableState?n._writableState.errorEmitted||(n._writableState.errorEmitted=!0,r.nextTick(o,n,e)):r.nextTick(o,n,e):t&&t(e)})),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";(function(t,r,o){var i=n(106);function a(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var o=r.callback;t.pendingcb--,o(n),r=r.next}t.corkedRequestsFree.next=e}(t,e)}}e.exports=g;var s,u=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:i.nextTick;g.WritableState=y;var l=Object.create(n(91));l.inherits=n(74);var c={deprecate:n(379)},f=n(177),d=n(136).Buffer,p=("undefined"!==typeof o?o:"undefined"!==typeof window?window:"undefined"!==typeof self?self:{}).Uint8Array||function(){};var h,m=n(178);function v(){}function y(e,t){s=s||n(82),e=e||{};var r=t instanceof s;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var o=e.highWaterMark,l=e.writableHighWaterMark,c=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(l||0===l)?l:c,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var f=!1===e.decodeStrings;this.decodeStrings=!f,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,o=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,o){--t.pendingcb,n?(i.nextTick(o,r),i.nextTick(E,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(o(r),e._writableState.errorEmitted=!0,e.emit("error",r),E(e,t))}(e,n,r,t,o);else{var a=O(n);a||n.corked||n.bufferProcessing||!n.bufferedRequest||x(e,n),r?u(w,e,n,a,o):w(e,n,a,o)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new a(this)}function g(e){if(s=s||n(82),!h.call(g,this)&&!(this instanceof s))return new g(e);this._writableState=new y(e,this),this.writable=!0,e&&("function"===typeof e.write&&(this._write=e.write),"function"===typeof e.writev&&(this._writev=e.writev),"function"===typeof e.destroy&&(this._destroy=e.destroy),"function"===typeof e.final&&(this._final=e.final)),f.call(this)}function b(e,t,n,r,o,i,a){t.writelen=r,t.writecb=a,t.writing=!0,t.sync=!0,n?e._writev(o,t.onwrite):e._write(o,i,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),E(e,t)}function x(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,o=new Array(r),i=t.corkedRequestsFree;i.entry=n;for(var s=0,u=!0;n;)o[s]=n,n.isBuf||(u=!1),n=n.next,s+=1;o.allBuffers=u,b(e,t,!0,t.length,o,"",i.finish),t.pendingcb++,t.lastBufferedRequest=null,i.next?(t.corkedRequestsFree=i.next,i.next=null):t.corkedRequestsFree=new a(t),t.bufferedRequestCount=0}else{for(;n;){var l=n.chunk,c=n.encoding,f=n.callback;if(b(e,t,!1,t.objectMode?1:l.length,l,c,f),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function O(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function S(e,t){e._final((function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),E(e,t)}))}function E(e,t){var n=O(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"===typeof e._final?(t.pendingcb++,t.finalCalled=!0,i.nextTick(S,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}l.inherits(g,f),y.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(y.prototype,"buffer",{get:c.deprecate((function(){return this.getBuffer()}),"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"===typeof Symbol&&Symbol.hasInstance&&"function"===typeof Function.prototype[Symbol.hasInstance]?(h=Function.prototype[Symbol.hasInstance],Object.defineProperty(g,Symbol.hasInstance,{value:function(e){return!!h.call(this,e)||this===g&&(e&&e._writableState instanceof y)}})):h=function(e){return e instanceof this},g.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},g.prototype.write=function(e,t,n){var r,o=this._writableState,a=!1,s=!o.objectMode&&(r=e,d.isBuffer(r)||r instanceof p);return s&&!d.isBuffer(e)&&(e=function(e){return d.from(e)}(e)),"function"===typeof t&&(n=t,t=null),s?t="buffer":t||(t=o.defaultEncoding),"function"!==typeof n&&(n=v),o.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),i.nextTick(t,n)}(this,n):(s||function(e,t,n,r){var o=!0,a=!1;return null===n?a=new TypeError("May not write null values to stream"):"string"===typeof n||void 0===n||t.objectMode||(a=new TypeError("Invalid non-string/buffer chunk")),a&&(e.emit("error",a),i.nextTick(r,a),o=!1),o}(this,o,e,n))&&(o.pendingcb++,a=function(e,t,n,r,o,i){if(!n){var a=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!==typeof t||(t=d.from(t,n));return t}(t,r,o);r!==a&&(n=!0,o="buffer",r=a)}var s=t.objectMode?1:r.length;t.length+=s;var u=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(g.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),g.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},g.prototype._writev=null,g.prototype.end=function(e,t,n){var r=this._writableState;"function"===typeof e?(n=e,e=null,t=null):"function"===typeof t&&(n=t,t=null),null!==e&&void 0!==e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||function(e,t,n){t.ending=!0,E(e,t),n&&(t.finished?i.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(g.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),g.prototype.destroy=m.destroy,g.prototype._undestroy=m.undestroy,g.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(60),n(160).setImmediate,n(34))},function(e,t,n){"use strict";var r=n(380).Buffer,o=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function i(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!==typeof t&&(r.isEncoding===o||!o(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=u,this.end=l,t=4;break;case"utf8":this.fillLast=s,t=4;break;case"base64":this.text=c,this.end=f,t=3;break;default:return this.write=d,void(this.end=p)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function a(e){return e<=127?0:e>>5===6?2:e>>4===14?3:e>>3===30?4:e>>6===2?-1:-2}function s(e){var t=this.lastTotal-this.lastNeed,n=function(e,t){if(128!==(192&t[0]))return e.lastNeed=0,"\ufffd";if(e.lastNeed>1&&t.length>1){if(128!==(192&t[1]))return e.lastNeed=1,"\ufffd";if(e.lastNeed>2&&t.length>2&&128!==(192&t[2]))return e.lastNeed=2,"\ufffd"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function u(e,t){if((e.length-t)%2===0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function l(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function c(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function f(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function d(e){return e.toString(this.encoding)}function p(e){return e&&e.length?this.write(e):""}t.StringDecoder=i,i.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n=0)return o>0&&(e.lastNeed=o-1),o;if(--r=0)return o>0&&(e.lastNeed=o-2),o;if(--r=0)return o>0&&(2===o?o=0:e.lastNeed=o-3),o;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},i.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";e.exports=a;var r=n(82),o=Object.create(n(91));function i(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var o=this._readableState;o.reading=!1,(o.needReadable||o.length-1?o([n]):n}},function(e,t,n){"use strict";var r=n(140),o=n(182),i=Object.prototype.hasOwnProperty,a=Array.isArray,s=o(),u=function(e,t){return s.set(e,t),e},l=function(e){return s.has(e)},c=function(e){return s.get(e)},f=function(e,t){s.set(e,t)},d=function(){for(var e=[],t=0;t<256;++t)e[e.length]="%"+((t<16?"0":"")+t.toString(16)).toUpperCase();return e}(),p=function(e,t){for(var n=t&&t.plainObjects?{__proto__:null}:{},r=0;rn?u(p(i,{plainObjects:r}),i.length-1):i},compact:function(e){for(var t=[{obj:{o:e},prop:"o"}],n=[],r=0;r1;){var t=e.pop(),n=t.obj[t.prop];if(a(n)){for(var r=[],o=0;o=h?a.slice(u,u+h):a,c=[],f=0;f=48&&p<=57||p>=65&&p<=90||p>=97&&p<=122||i===r.RFC1738&&(40===p||41===p)?c[c.length]=l.charAt(f):p<128?c[c.length]=d[p]:p<2048?c[c.length]=d[192|p>>6]+d[128|63&p]:p<55296||p>=57344?c[c.length]=d[224|p>>12]+d[128|p>>6&63]+d[128|63&p]:(f+=1,p=65536+((1023&p)<<10|1023&l.charCodeAt(f)),c[c.length]=d[240|p>>18]+d[128|p>>12&63]+d[128|p>>6&63]+d[128|63&p])}s+=c.join("")}return s},isBuffer:function(e){return!(!e||"object"!==typeof e)&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isOverflow:l,isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},markOverflow:u,maybeMap:function(e,t){if(a(e)){for(var n=[],r=0;rr.arrayLimit)return u(p(t.concat(n),r),o);t[o]=n}else{if(!t||"object"!==typeof t)return[t,n];if(l(t)){var s=c(t)+1;t[s]=n,f(t,s)}else(r&&(r.plainObjects||r.allowPrototypes)||!i.call(Object.prototype,n))&&(t[n]=!0)}return t}if(!t||"object"!==typeof t){if(l(n)){for(var d=Object.keys(n),h=r&&r.plainObjects?{__proto__:null,0:t}:{0:t},m=0;mr.arrayLimit?u(p(v,r),v.length-1):v}var y=t;return a(t)&&!a(n)&&(y=p(t,r)),a(t)&&a(n)?(n.forEach((function(n,o){if(i.call(t,o)){var a=t[o];a&&"object"===typeof a&&n&&"object"===typeof n?t[o]=e(a,n,r):t[t.length]=n}else t[o]=n})),t):Object.keys(n).reduce((function(t,o){var a=n[o];if(i.call(t,o)?t[o]=e(t[o],a,r):t[o]=a,l(n)&&!l(t)&&u(t,c(n)),l(t)){var s=parseInt(o,10);String(s)===o&&s>=0&&s>c(t)&&f(t,s)}return t}),y)}}},function(e,t,n){var r;"undefined"!==typeof self&&self,r=function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t,n){var r=n(5),o=n(1),i=o.toHex,a=o.ceilHeapSize,s=n(6),u=function(e){for(e+=9;e%64>0;e+=1);return e},l=function(e,t){var n=new Int32Array(e,t+320,5),r=new Int32Array(5),o=new DataView(r.buffer);return o.setInt32(0,n[0],!1),o.setInt32(4,n[1],!1),o.setInt32(8,n[2],!1),o.setInt32(12,n[3],!1),o.setInt32(16,n[4],!1),r},c=function(){function e(t){if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),(t=t||65536)%64>0)throw new Error("Chunk size must be a multiple of 128 bit");this._offset=0,this._maxChunkLen=t,this._padMaxChunkLen=u(t),this._heap=new ArrayBuffer(a(this._padMaxChunkLen+320+20)),this._h32=new Int32Array(this._heap),this._h8=new Int8Array(this._heap),this._core=new r({Int32Array:Int32Array},{},this._heap)}return e.prototype._initState=function(e,t){this._offset=0;var n=new Int32Array(e,t+320,5);n[0]=1732584193,n[1]=-271733879,n[2]=-1732584194,n[3]=271733878,n[4]=-1009589776},e.prototype._padChunk=function(e,t){var n=u(e),r=new Int32Array(this._heap,0,n>>2);return function(e,t){var n=new Uint8Array(e.buffer),r=t%4,o=t-r;switch(r){case 0:n[o+3]=0;case 1:n[o+2]=0;case 2:n[o+1]=0;case 3:n[o+0]=0}for(var i=1+(t>>2);i>2]|=128<<24-(t%4<<3),e[14+(2+(t>>2)&-16)]=n/(1<<29)|0,e[15+(2+(t>>2)&-16)]=n<<3}(r,e,t),n},e.prototype._write=function(e,t,n,r){s(e,this._h8,this._h32,t,n,r||0)},e.prototype._coreCall=function(e,t,n,r,o){var i=n;this._write(e,t,n),o&&(i=this._padChunk(n,r)),this._core.hash(i,this._padMaxChunkLen)},e.prototype.rawDigest=function(e){var t=e.byteLength||e.length||e.size||0;this._initState(this._heap,this._padMaxChunkLen);var n=0,r=this._maxChunkLen;for(n=0;t>n+r;n+=r)this._coreCall(e,n,r,t,!1);return this._coreCall(e,n,t-n,t,!0),l(this._heap,this._padMaxChunkLen)},e.prototype.digest=function(e){return i(this.rawDigest(e).buffer)},e.prototype.digestFromString=function(e){return this.digest(e)},e.prototype.digestFromBuffer=function(e){return this.digest(e)},e.prototype.digestFromArrayBuffer=function(e){return this.digest(e)},e.prototype.resetState=function(){return this._initState(this._heap,this._padMaxChunkLen),this},e.prototype.append=function(e){var t=0,n=e.byteLength||e.length||e.size||0,r=this._offset%this._maxChunkLen,o=void 0;for(this._offset+=n;t0}),!1)}e.exports=function(e,t){t=t||{};var o={main:n.m},i=t.all?{main:Object.keys(o)}:function(e,t){for(var n={main:[t]},r={main:[]},o={main:{}};u(n);)for(var i=Object.keys(n),a=0;a>2]|0;s=r[t+324>>2]|0;l=r[t+328>>2]|0;f=r[t+332>>2]|0;p=r[t+336>>2]|0;for(n=0;(n|0)<(e|0);n=n+64|0){a=i;u=s;c=l;d=f;h=p;for(o=0;(o|0)<64;o=o+4|0){v=r[n+o>>2]|0;m=((i<<5|i>>>27)+(s&l|~s&f)|0)+((v+p|0)+1518500249|0)|0;p=f;f=l;l=s<<30|s>>>2;s=i;i=m;r[e+o>>2]=v}for(o=e+64|0;(o|0)<(e+80|0);o=o+4|0){v=(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])<<1|(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])>>>31;m=((i<<5|i>>>27)+(s&l|~s&f)|0)+((v+p|0)+1518500249|0)|0;p=f;f=l;l=s<<30|s>>>2;s=i;i=m;r[o>>2]=v}for(o=e+80|0;(o|0)<(e+160|0);o=o+4|0){v=(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])<<1|(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])>>>31;m=((i<<5|i>>>27)+(s^l^f)|0)+((v+p|0)+1859775393|0)|0;p=f;f=l;l=s<<30|s>>>2;s=i;i=m;r[o>>2]=v}for(o=e+160|0;(o|0)<(e+240|0);o=o+4|0){v=(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])<<1|(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])>>>31;m=((i<<5|i>>>27)+(s&l|s&f|l&f)|0)+((v+p|0)-1894007588|0)|0;p=f;f=l;l=s<<30|s>>>2;s=i;i=m;r[o>>2]=v}for(o=e+240|0;(o|0)<(e+320|0);o=o+4|0){v=(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])<<1|(r[o-12>>2]^r[o-32>>2]^r[o-56>>2]^r[o-64>>2])>>>31;m=((i<<5|i>>>27)+(s^l^f)|0)+((v+p|0)-899497514|0)|0;p=f;f=l;l=s<<30|s>>>2;s=i;i=m;r[o>>2]=v}i=i+a|0;s=s+u|0;l=l+c|0;f=f+d|0;p=p+h|0}r[t+320>>2]=i;r[t+324>>2]=s;r[t+328>>2]=l;r[t+332>>2]=f;r[t+336>>2]=p}return{hash:o}}},function(e,t){var n=this,r=void 0;"undefined"!==typeof self&&"undefined"!==typeof self.FileReaderSync&&(r=new self.FileReaderSync);var o=function(e,t,n,r,o,i){var a=void 0,s=i%4,u=(o+s)%4,l=o-u;switch(s){case 0:t[i]=e[r+3];case 1:t[i+1-(s<<1)|0]=e[r+2];case 2:t[i+2-(s<<1)|0]=e[r+1];case 3:t[i+3-(s<<1)|0]=e[r]}if(!(o>2]=e[r+a]<<24|e[r+a+1]<<16|e[r+a+2]<<8|e[r+a+3];switch(u){case 3:t[i+l+1|0]=e[r+l+2];case 2:t[i+l+2|0]=e[r+l+1];case 1:t[i+l+3|0]=e[r+l]}}};e.exports=function(e,t,i,a,s,u){if("string"===typeof e)return function(e,t,n,r,o,i){var a=void 0,s=i%4,u=(o+s)%4,l=o-u;switch(s){case 0:t[i]=e.charCodeAt(r+3);case 1:t[i+1-(s<<1)|0]=e.charCodeAt(r+2);case 2:t[i+2-(s<<1)|0]=e.charCodeAt(r+1);case 3:t[i+3-(s<<1)|0]=e.charCodeAt(r)}if(!(o>2]=e.charCodeAt(r+a)<<24|e.charCodeAt(r+a+1)<<16|e.charCodeAt(r+a+2)<<8|e.charCodeAt(r+a+3);switch(u){case 3:t[i+l+1|0]=e.charCodeAt(r+l+2);case 2:t[i+l+2|0]=e.charCodeAt(r+l+1);case 1:t[i+l+3|0]=e.charCodeAt(r+l)}}}(e,t,i,a,s,u);if(e instanceof Array)return o(e,t,i,a,s,u);if(n&&n.Buffer&&n.Buffer.isBuffer(e))return o(e,t,i,a,s,u);if(e instanceof ArrayBuffer)return o(new Uint8Array(e),t,i,a,s,u);if(e.buffer instanceof ArrayBuffer)return o(new Uint8Array(e.buffer,e.byteOffset,e.byteLength),t,i,a,s,u);if(e instanceof Blob)return function(e,t,n,o,i,a){var s=void 0,u=a%4,l=(i+u)%4,c=i-l,f=new Uint8Array(r.readAsArrayBuffer(e.slice(o,o+i)));switch(u){case 0:t[a]=f[3];case 1:t[a+1-(u<<1)|0]=f[2];case 2:t[a+2-(u<<1)|0]=f[1];case 3:t[a+3-(u<<1)|0]=f[0]}if(!(i>2]=f[s]<<24|f[s+1]<<16|f[s+2]<<8|f[s+3];switch(l){case 3:t[a+c+1|0]=f[c+2];case 2:t[a+c+2|0]=f[c+1];case 1:t[a+c+3|0]=f[c]}}}(e,t,i,a,s,u);throw new Error("Unsupported data type.")}},function(e,t,n){var r=function(){function e(e,t){for(var n=0;n overrides the height property of the style prop"));var d=i(i({},n),{height:f?f+"px":"100vh"});return o.a.createElement("div",i({ref:t,style:d},u))}));function l(){var e;return c()?(null===(e=document.documentElement)||void 0===e?void 0:e.clientHeight)||window.innerHeight:null}function c(){return"undefined"!==typeof window&&"undefined"!==typeof document}u.displayName="Div100vh",t.a=u},function(e,t,n){"use strict";n.d(t,"a",(function(){return S}));var r=n(42),o=n(33),i=[],a=i.forEach,s=i.slice;var u=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/,l=function(e,t,n,r){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{path:"/",sameSite:"strict"};n&&(o.expires=new Date,o.expires.setTime(o.expires.getTime()+60*n*1e3)),r&&(o.domain=r),document.cookie=function(e,t,n){var r=n||{};r.path=r.path||"/";var o=encodeURIComponent(t),i="".concat(e,"=").concat(o);if(r.maxAge>0){var a=r.maxAge-0;if(Number.isNaN(a))throw new Error("maxAge should be a Number");i+="; Max-Age=".concat(Math.floor(a))}if(r.domain){if(!u.test(r.domain))throw new TypeError("option domain is invalid");i+="; Domain=".concat(r.domain)}if(r.path){if(!u.test(r.path))throw new TypeError("option path is invalid");i+="; Path=".concat(r.path)}if(r.expires){if("function"!==typeof r.expires.toUTCString)throw new TypeError("option expires is invalid");i+="; Expires=".concat(r.expires.toUTCString())}if(r.httpOnly&&(i+="; HttpOnly"),r.secure&&(i+="; Secure"),r.sameSite)switch("string"===typeof r.sameSite?r.sameSite.toLowerCase():r.sameSite){case!0:i+="; SameSite=Strict";break;case"lax":i+="; SameSite=Lax";break;case"strict":i+="; SameSite=Strict";break;case"none":i+="; SameSite=None";break;default:throw new TypeError("option sameSite is invalid")}return i}(e,encodeURIComponent(t),o)},c=function(e){for(var t="".concat(e,"="),n=document.cookie.split(";"),r=0;r-1&&(n=window.location.hash.substring(window.location.hash.indexOf("?")));for(var r=n.substring(1).split("&"),o=0;o0)r[o].substring(0,i)===e.lookupQuerystring&&(t=r[o].substring(i+1))}}return t}},p=null,h=function(){if(null!==p)return p;try{p="undefined"!==window&&null!==window.localStorage;var e="i18next.translate.boo";window.localStorage.setItem(e,"foo"),window.localStorage.removeItem(e)}catch(t){p=!1}return p},m={name:"localStorage",lookup:function(e){var t;if(e.lookupLocalStorage&&h()){var n=window.localStorage.getItem(e.lookupLocalStorage);n&&(t=n)}return t},cacheUserLanguage:function(e,t){t.lookupLocalStorage&&h()&&window.localStorage.setItem(t.lookupLocalStorage,e)}},v=null,y=function(){if(null!==v)return v;try{v="undefined"!==window&&null!==window.sessionStorage;var e="i18next.translate.boo";window.sessionStorage.setItem(e,"foo"),window.sessionStorage.removeItem(e)}catch(t){v=!1}return v},g={name:"sessionStorage",lookup:function(e){var t;if(e.lookupSessionStorage&&y()){var n=window.sessionStorage.getItem(e.lookupSessionStorage);n&&(t=n)}return t},cacheUserLanguage:function(e,t){t.lookupSessionStorage&&y()&&window.sessionStorage.setItem(t.lookupSessionStorage,e)}},b={name:"navigator",lookup:function(e){var t=[];if("undefined"!==typeof navigator){if(navigator.languages)for(var n=0;n0?t:void 0}},w={name:"htmlTag",lookup:function(e){var t,n=e.htmlTag||("undefined"!==typeof document?document.documentElement:null);return n&&"function"===typeof n.getAttribute&&(t=n.getAttribute("lang")),t}},x={name:"path",lookup:function(e){var t;if("undefined"!==typeof window){var n=window.location.pathname.match(/\/([a-zA-Z-]*)/g);if(n instanceof Array)if("number"===typeof e.lookupFromPathIndex){if("string"!==typeof n[e.lookupFromPathIndex])return;t=n[e.lookupFromPathIndex].replace("/","")}else t=n[0].replace("/","")}return t}},O={name:"subdomain",lookup:function(e){var t="number"===typeof e.lookupFromSubdomainIndex?e.lookupFromSubdomainIndex+1:1,n="undefined"!==typeof window&&window.location&&window.location.hostname&&window.location.hostname.match(/^(\w{2,5})\.(([a-z0-9-]{1,63}\.[a-z]{2,6})|localhost)/i);if(n)return n[t]}};var S=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};Object(r.a)(this,e),this.type="languageDetector",this.detectors={},this.init(t,n)}return Object(o.a)(e,[{key:"init",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.services=e,this.options=function(e){return a.call(s.call(arguments,1),(function(t){if(t)for(var n in t)void 0===e[n]&&(e[n]=t[n])})),e}(t,this.options||{},{order:["querystring","cookie","localStorage","sessionStorage","navigator","htmlTag"],lookupQuerystring:"lng",lookupCookie:"i18next",lookupLocalStorage:"i18nextLng",lookupSessionStorage:"i18nextLng",caches:["localStorage"],excludeCacheFor:["cimode"]}),this.options.lookupFromUrlIndex&&(this.options.lookupFromPathIndex=this.options.lookupFromUrlIndex),this.i18nOptions=n,this.addDetector(f),this.addDetector(d),this.addDetector(m),this.addDetector(g),this.addDetector(b),this.addDetector(w),this.addDetector(x),this.addDetector(O)}},{key:"addDetector",value:function(e){this.detectors[e.name]=e}},{key:"detect",value:function(e){var t=this;e||(e=this.options.order);var n=[];return e.forEach((function(e){if(t.detectors[e]){var r=t.detectors[e].lookup(t.options);r&&"string"===typeof r&&(r=[r]),r&&(n=n.concat(r))}})),this.services.languageUtils.getBestMatchFromCodes?n:n.length>0?n[0]:null}},{key:"cacheUserLanguage",value:function(e,t){var n=this;t||(t=this.options.caches),t&&(this.options.excludeCacheFor&&this.options.excludeCacheFor.indexOf(e)>-1||t.forEach((function(t){n.detectors[t]&&n.detectors[t].cacheUserLanguage(e,n.options)})))}}]),e}();S.type="languageDetector"},,,,,,,,function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,n){var r=n(159);function o(e,t){for(var n=0;n-1&&(n.client={top:e.clientTop,left:e.clientLeft,width:e.clientWidth,height:e.clientHeight}),t.indexOf("offset")>-1&&(n.offset={top:e.offsetTop,left:e.offsetLeft,width:e.offsetWidth,height:e.offsetHeight}),t.indexOf("scroll")>-1&&(n.scroll={top:e.scrollTop,left:e.scrollLeft,width:e.scrollWidth,height:e.scrollHeight}),t.indexOf("bounds")>-1){var r=e.getBoundingClientRect();n.bounds={top:r.top,right:r.right,bottom:r.bottom,left:r.left,width:r.width,height:r.height}}if(t.indexOf("margin")>-1){var o=getComputedStyle(e);n.margin={top:o?parseInt(o.marginTop):0,right:o?parseInt(o.marginRight):0,bottom:o?parseInt(o.marginBottom):0,left:o?parseInt(o.marginLeft):0}}return n}var p=function(e){return function(t){var n,s;return s=n=function(n){function s(){for(var t,r=arguments.length,o=new Array(r),i=0;i0},e.prototype.connect_=function(){r&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),s?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){r&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t;a.some((function(e){return!!~n.indexOf(e)}))&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),l=function(e,t){for(var n=0,r=Object.keys(t);n0},e}(),x="undefined"!==typeof WeakMap?new WeakMap:new n,O=function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=u.getInstance(),r=new w(t,n,this);x.set(this,r)};["observe","unobserve","disconnect"].forEach((function(e){O.prototype[e]=function(){var t;return(t=x.get(this))[e].apply(t,arguments)}}));var S="undefined"!==typeof o.ResizeObserver?o.ResizeObserver:O;t.a=S}).call(this,n(34))},function(e,t){e.exports=function(e,t,n,r){var o=n?n.call(r,e,t):void 0;if(void 0!==o)return!!o;if(e===t)return!0;if("object"!==typeof e||!e||"object"!==typeof t||!t)return!1;var i=Object.keys(e),a=Object.keys(t);if(i.length!==a.length)return!1;for(var s=Object.prototype.hasOwnProperty.bind(t),u=0;up)&&(z=(H=H.replace(" ",":")).length),0r&&(r=(t=t.trim()).charCodeAt(0)),r){case 38:return t.replace(v,"$1"+e.trim());case 58:return e.trim()+t.replace(v,"$1"+e.trim());default:if(0<1*n&&0u.charCodeAt(8))break;case 115:a=a.replace(u,"-webkit-"+u)+";"+a;break;case 207:case 102:a=a.replace(u,"-webkit-"+(102r.charCodeAt(0)&&(r=r.trim()),r=[r],0=0)return 1;return 0}();var o=n&&window.Promise?function(e){var t=!1;return function(){t||(t=!0,window.Promise.resolve().then((function(){t=!1,e()})))}}:function(e){var t=!1;return function(){t||(t=!0,setTimeout((function(){t=!1,e()}),r))}};function i(e){return e&&"[object Function]"==={}.toString.call(e)}function a(e,t){if(1!==e.nodeType)return[];var n=e.ownerDocument.defaultView.getComputedStyle(e,null);return t?n[t]:n}function s(e){return"HTML"===e.nodeName?e:e.parentNode||e.host}function u(e){if(!e)return document.body;switch(e.nodeName){case"HTML":case"BODY":return e.ownerDocument.body;case"#document":return e.body}var t=a(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/(auto|scroll|overlay)/.test(n+o+r)?e:u(s(e))}function l(e){return e&&e.referenceNode?e.referenceNode:e}var c=n&&!(!window.MSInputMethodContext||!document.documentMode),f=n&&/MSIE 10/.test(navigator.userAgent);function d(e){return 11===e?c:10===e?f:c||f}function p(e){if(!e)return document.documentElement;for(var t=d(10)?document.body:null,n=e.offsetParent||null;n===t&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var r=n&&n.nodeName;return r&&"BODY"!==r&&"HTML"!==r?-1!==["TH","TD","TABLE"].indexOf(n.nodeName)&&"static"===a(n,"position")?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function h(e){return null!==e.parentNode?h(e.parentNode):e}function m(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var n=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,r=n?e:t,o=n?t:e,i=document.createRange();i.setStart(r,0),i.setEnd(o,0);var a=i.commonAncestorContainer;if(e!==a&&t!==a||r.contains(o))return function(e){var t=e.nodeName;return"BODY"!==t&&("HTML"===t||p(e.firstElementChild)===e)}(a)?a:p(a);var s=h(e);return s.host?m(s.host,t):m(e,h(t).host)}function v(e){var t="top"===(arguments.length>1&&void 0!==arguments[1]?arguments[1]:"top")?"scrollTop":"scrollLeft",n=e.nodeName;if("BODY"===n||"HTML"===n){var r=e.ownerDocument.documentElement;return(e.ownerDocument.scrollingElement||r)[t]}return e[t]}function y(e,t){var n="x"===t?"Left":"Top",r="Left"===n?"Right":"Bottom";return parseFloat(e["border"+n+"Width"])+parseFloat(e["border"+r+"Width"])}function g(e,t,n,r){return Math.max(t["offset"+e],t["scroll"+e],n["client"+e],n["offset"+e],n["scroll"+e],d(10)?parseInt(n["offset"+e])+parseInt(r["margin"+("Height"===e?"Top":"Left")])+parseInt(r["margin"+("Height"===e?"Bottom":"Right")]):0)}function b(e){var t=e.body,n=e.documentElement,r=d(10)&&getComputedStyle(n);return{height:g("Height",t,n,r),width:g("Width",t,n,r)}}var w=function(){function e(e,t){for(var n=0;n2&&void 0!==arguments[2]&&arguments[2],r=d(10),o="HTML"===t.nodeName,i=E(e),s=E(t),l=u(e),c=a(t),f=parseFloat(c.borderTopWidth),p=parseFloat(c.borderLeftWidth);n&&o&&(s.top=Math.max(s.top,0),s.left=Math.max(s.left,0));var h=S({top:i.top-s.top-f,left:i.left-s.left-p,width:i.width,height:i.height});if(h.marginTop=0,h.marginLeft=0,!r&&o){var m=parseFloat(c.marginTop),y=parseFloat(c.marginLeft);h.top-=f-m,h.bottom-=f-m,h.left-=p-y,h.right-=p-y,h.marginTop=m,h.marginLeft=y}return(r&&!n?t.contains(l):t===l&&"BODY"!==l.nodeName)&&(h=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=v(t,"top"),o=v(t,"left"),i=n?-1:1;return e.top+=r*i,e.bottom+=r*i,e.left+=o*i,e.right+=o*i,e}(h,t)),h}function j(e){var t=e.nodeName;if("BODY"===t||"HTML"===t)return!1;if("fixed"===a(e,"position"))return!0;var n=s(e);return!!n&&j(n)}function C(e){if(!e||!e.parentElement||d())return document.documentElement;for(var t=e.parentElement;t&&"none"===a(t,"transform");)t=t.parentElement;return t||document.documentElement}function _(e,t,n,r){var o=arguments.length>4&&void 0!==arguments[4]&&arguments[4],i={top:0,left:0},a=o?C(e):m(e,l(t));if("viewport"===r)i=function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=e.ownerDocument.documentElement,r=k(e,n),o=Math.max(n.clientWidth,window.innerWidth||0),i=Math.max(n.clientHeight,window.innerHeight||0),a=t?0:v(n),s=t?0:v(n,"left");return S({top:a-r.top+r.marginTop,left:s-r.left+r.marginLeft,width:o,height:i})}(a,o);else{var c=void 0;"scrollParent"===r?"BODY"===(c=u(s(t))).nodeName&&(c=e.ownerDocument.documentElement):c="window"===r?e.ownerDocument.documentElement:r;var f=k(c,a,o);if("HTML"!==c.nodeName||j(a))i=f;else{var d=b(e.ownerDocument),p=d.height,h=d.width;i.top+=f.top-f.marginTop,i.bottom=p+f.top,i.left+=f.left-f.marginLeft,i.right=h+f.left}}var y="number"===typeof(n=n||0);return i.left+=y?n:n.left||0,i.top+=y?n:n.top||0,i.right-=y?n:n.right||0,i.bottom-=y?n:n.bottom||0,i}function R(e,t,n,r,o){var i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:0;if(-1===e.indexOf("auto"))return e;var a=_(n,r,i,o),s={top:{width:a.width,height:t.top-a.top},right:{width:a.right-t.right,height:a.height},bottom:{width:a.width,height:a.bottom-t.bottom},left:{width:t.left-a.left,height:a.height}},u=Object.keys(s).map((function(e){return O({key:e},s[e],{area:(t=s[e],t.width*t.height)});var t})).sort((function(e,t){return t.area-e.area})),l=u.filter((function(e){var t=e.width,r=e.height;return t>=n.clientWidth&&r>=n.clientHeight})),c=l.length>0?l[0].key:u[0].key,f=e.split("-")[1];return c+(f?"-"+f:"")}function P(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return k(n,r?C(t):m(t,l(n)),r)}function T(e){var t=e.ownerDocument.defaultView.getComputedStyle(e),n=parseFloat(t.marginTop||0)+parseFloat(t.marginBottom||0),r=parseFloat(t.marginLeft||0)+parseFloat(t.marginRight||0);return{width:e.offsetWidth+r,height:e.offsetHeight+n}}function A(e){var t={left:"right",right:"left",bottom:"top",top:"bottom"};return e.replace(/left|right|bottom|top/g,(function(e){return t[e]}))}function L(e,t,n){n=n.split("-")[0];var r=T(e),o={width:r.width,height:r.height},i=-1!==["right","left"].indexOf(n),a=i?"top":"left",s=i?"left":"top",u=i?"height":"width",l=i?"width":"height";return o[a]=t[a]+t[u]/2-r[u]/2,o[s]=n===s?t[s]-r[l]:t[A(s)],o}function M(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function N(e,t,n){return(void 0===n?e:e.slice(0,function(e,t,n){if(Array.prototype.findIndex)return e.findIndex((function(e){return e[t]===n}));var r=M(e,(function(e){return e[t]===n}));return e.indexOf(r)}(e,"name",n))).forEach((function(e){e.function&&console.warn("`modifier.function` is deprecated, use `modifier.fn`!");var n=e.function||e.fn;e.enabled&&i(n)&&(t.offsets.popper=S(t.offsets.popper),t.offsets.reference=S(t.offsets.reference),t=n(t,e))})),t}function I(){if(!this.state.isDestroyed){var e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=P(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=R(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=L(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?"fixed":"absolute",e=N(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}}function D(e,t){return e.some((function(e){var n=e.name;return e.enabled&&n===t}))}function F(e){for(var t=[!1,"ms","Webkit","Moz","O"],n=e.charAt(0).toUpperCase()+e.slice(1),r=0;r1&&void 0!==arguments[1]&&arguments[1],n=G.indexOf(e),r=G.slice(n+1).concat(G.slice(0,n));return t?r.reverse():r}var J="flip",Z="clockwise",ee="counterclockwise";function te(e,t,n,r){var o=[0,0],i=-1!==["right","left"].indexOf(r),a=e.split(/(\+|\-)/).map((function(e){return e.trim()})),s=a.indexOf(M(a,(function(e){return-1!==e.search(/,|\s/)})));a[s]&&-1===a[s].indexOf(",")&&console.warn("Offsets separated by white space(s) are deprecated, use a comma (,) instead.");var u=/\s*,\s*|\s+/,l=-1!==s?[a.slice(0,s).concat([a[s].split(u)[0]]),[a[s].split(u)[1]].concat(a.slice(s+1))]:[a];return l=l.map((function(e,r){var o=(1===r?!i:i)?"height":"width",a=!1;return e.reduce((function(e,t){return""===e[e.length-1]&&-1!==["+","-"].indexOf(t)?(e[e.length-1]=t,a=!0,e):a?(e[e.length-1]+=t,a=!1,e):e.concat(t)}),[]).map((function(e){return function(e,t,n,r){var o=e.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),i=+o[1],a=o[2];if(!i)return e;if(0===a.indexOf("%")){return S("%p"===a?n:r)[t]/100*i}if("vh"===a||"vw"===a)return("vh"===a?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0))/100*i;return i}(e,o,t,n)}))})),l.forEach((function(e,t){e.forEach((function(n,r){q(n)&&(o[t]+=n*("-"===e[r-1]?-1:1))}))})),o}var ne={shift:{order:100,enabled:!0,fn:function(e){var t=e.placement,n=t.split("-")[0],r=t.split("-")[1];if(r){var o=e.offsets,i=o.reference,a=o.popper,s=-1!==["bottom","top"].indexOf(n),u=s?"left":"top",l=s?"width":"height",c={start:x({},u,i[u]),end:x({},u,i[u]+i[l]-a[l])};e.offsets.popper=O({},a,c[r])}return e}},offset:{order:200,enabled:!0,fn:function(e,t){var n=t.offset,r=e.placement,o=e.offsets,i=o.popper,a=o.reference,s=r.split("-")[0],u=void 0;return u=q(+n)?[+n,0]:te(n,i,a,s),"left"===s?(i.top+=u[0],i.left-=u[1]):"right"===s?(i.top+=u[0],i.left+=u[1]):"top"===s?(i.left+=u[0],i.top-=u[1]):"bottom"===s&&(i.left+=u[0],i.top+=u[1]),e.popper=i,e},offset:0},preventOverflow:{order:300,enabled:!0,fn:function(e,t){var n=t.boundariesElement||p(e.instance.popper);e.instance.reference===n&&(n=p(n));var r=F("transform"),o=e.instance.popper.style,i=o.top,a=o.left,s=o[r];o.top="",o.left="",o[r]="";var u=_(e.instance.popper,e.instance.reference,t.padding,n,e.positionFixed);o.top=i,o.left=a,o[r]=s,t.boundaries=u;var l=t.priority,c=e.offsets.popper,f={primary:function(e){var n=c[e];return c[e]u[e]&&!t.escapeWithReference&&(r=Math.min(c[n],u[e]-("right"===e?c.width:c.height))),x({},n,r)}};return l.forEach((function(e){var t=-1!==["left","top"].indexOf(e)?"primary":"secondary";c=O({},c,f[t](e))})),e.offsets.popper=c,e},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,n=t.popper,r=t.reference,o=e.placement.split("-")[0],i=Math.floor,a=-1!==["top","bottom"].indexOf(o),s=a?"right":"bottom",u=a?"left":"top",l=a?"width":"height";return n[s]i(r[s])&&(e.offsets.popper[u]=i(r[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,t){var n;if(!Q(e.instance.modifiers,"arrow","keepTogether"))return e;var r=t.element;if("string"===typeof r){if(!(r=e.instance.popper.querySelector(r)))return e}else if(!e.instance.popper.contains(r))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),e;var o=e.placement.split("-")[0],i=e.offsets,s=i.popper,u=i.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",f=l?"Top":"Left",d=f.toLowerCase(),p=l?"left":"top",h=l?"bottom":"right",m=T(r)[c];u[h]-ms[h]&&(e.offsets.popper[d]+=u[d]+m-s[h]),e.offsets.popper=S(e.offsets.popper);var v=u[d]+u[c]/2-m/2,y=a(e.instance.popper),g=parseFloat(y["margin"+f]),b=parseFloat(y["border"+f+"Width"]),w=v-e.offsets.popper[d]-g-b;return w=Math.max(Math.min(s[c]-m,w),0),e.arrowElement=r,e.offsets.arrow=(x(n={},d,Math.round(w)),x(n,p,""),n),e},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(e,t){if(D(e.instance.modifiers,"inner"))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var n=_(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),r=e.placement.split("-")[0],o=A(r),i=e.placement.split("-")[1]||"",a=[];switch(t.behavior){case J:a=[r,o];break;case Z:a=X(r);break;case ee:a=X(r,!0);break;default:a=t.behavior}return a.forEach((function(s,u){if(r!==s||a.length===u+1)return e;r=e.placement.split("-")[0],o=A(r);var l=e.offsets.popper,c=e.offsets.reference,f=Math.floor,d="left"===r&&f(l.right)>f(c.left)||"right"===r&&f(l.left)f(c.top)||"bottom"===r&&f(l.top)f(n.right),m=f(l.top)f(n.bottom),y="left"===r&&p||"right"===r&&h||"top"===r&&m||"bottom"===r&&v,g=-1!==["top","bottom"].indexOf(r),b=!!t.flipVariations&&(g&&"start"===i&&p||g&&"end"===i&&h||!g&&"start"===i&&m||!g&&"end"===i&&v),w=!!t.flipVariationsByContent&&(g&&"start"===i&&h||g&&"end"===i&&p||!g&&"start"===i&&v||!g&&"end"===i&&m),x=b||w;(d||y||x)&&(e.flipped=!0,(d||y)&&(r=a[u+1]),x&&(i=function(e){return"end"===e?"start":"start"===e?"end":e}(i)),e.placement=r+(i?"-"+i:""),e.offsets.popper=O({},e.offsets.popper,L(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,"flip"))})),e},behavior:"flip",padding:5,boundariesElement:"viewport",flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,n=t.split("-")[0],r=e.offsets,o=r.popper,i=r.reference,a=-1!==["left","right"].indexOf(n),s=-1===["top","left"].indexOf(n);return o[a?"left":"top"]=i[n]-(s?o[a?"width":"height"]:0),e.placement=A(t),e.offsets.popper=S(o),e}},hide:{order:800,enabled:!0,fn:function(e){if(!Q(e.instance.modifiers,"hide","preventOverflow"))return e;var t=e.offsets.reference,n=M(e.instance.modifiers,(function(e){return"preventOverflow"===e.name})).boundaries;if(t.bottomn.right||t.top>n.bottom||t.right2&&void 0!==arguments[2]?arguments[2]:{};!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.scheduleUpdate=function(){return requestAnimationFrame(r.update)},this.update=o(this.update.bind(this)),this.options=O({},e.Defaults,a),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=t&&t.jquery?t[0]:t,this.popper=n&&n.jquery?n[0]:n,this.options.modifiers={},Object.keys(O({},e.Defaults.modifiers,a.modifiers)).forEach((function(t){r.options.modifiers[t]=O({},e.Defaults.modifiers[t]||{},a.modifiers?a.modifiers[t]:{})})),this.modifiers=Object.keys(this.options.modifiers).map((function(e){return O({name:e},r.options.modifiers[e])})).sort((function(e,t){return e.order-t.order})),this.modifiers.forEach((function(e){e.enabled&&i(e.onLoad)&&e.onLoad(r.reference,r.popper,r.options,e,r.state)})),this.update();var s=this.options.eventsEnabled;s&&this.enableEventListeners(),this.state.eventsEnabled=s}return w(e,[{key:"update",value:function(){return I.call(this)}},{key:"destroy",value:function(){return z.call(this)}},{key:"enableEventListeners",value:function(){return W.call(this)}},{key:"disableEventListeners",value:function(){return V.call(this)}}]),e}();oe.Utils=("undefined"!==typeof window?window:e).PopperUtils,oe.placements=Y,oe.Defaults=re,t.a=oe}).call(this,n(34))},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"}),"Pause");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement(i.Fragment,null,i.createElement("path",{d:"M11.99 5V1l-5 5 5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6h-2c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"}),i.createElement("path",{d:"M10.89 16h-.85v-3.26l-1.01.31v-.69l1.77-.63h.09V16zM15.17 14.24c0 .32-.03.6-.1.82s-.17.42-.29.57-.28.26-.45.33-.37.1-.59.1-.41-.03-.59-.1-.33-.18-.46-.33-.23-.34-.3-.57-.11-.5-.11-.82v-.74c0-.32.03-.6.1-.82s.17-.42.29-.57.28-.26.45-.33.37-.1.59-.1.41.03.59.1.33.18.46.33.23.34.3.57.11.5.11.82v.74zm-.85-.86c0-.19-.01-.35-.04-.48s-.07-.23-.12-.31-.11-.14-.19-.17-.16-.05-.25-.05-.18.02-.25.05-.14.09-.19.17-.09.18-.12.31-.04.29-.04.48v.97c0 .19.01.35.04.48s.07.24.12.32.11.14.19.17.16.05.25.05.18-.02.25-.05.14-.09.19-.17.09-.19.11-.32.04-.29.04-.48v-.97z"})),"Replay10");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement(i.Fragment,null,i.createElement("path",{d:"M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"}),i.createElement("path",{d:"M10.86 15.94v-4.27h-.09L9 12.3v.69l1.01-.31v3.26zM12.25 13.44v.74c0 1.9 1.31 1.82 1.44 1.82.14 0 1.44.09 1.44-1.82v-.74c0-1.9-1.31-1.82-1.44-1.82-.14 0-1.44-.09-1.44 1.82zm2.04-.12v.97c0 .77-.21 1.03-.59 1.03s-.6-.26-.6-1.03v-.97c0-.75.22-1.01.59-1.01.38-.01.6.26.6 1.01z"})),"Forward10");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"}),"VolumeOff");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"}),"VolumeUp");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M20.38 8.57l-1.23 1.85a8 8 0 01-.22 7.58H5.07A8 8 0 0115.58 6.85l1.85-1.23A10 10 0 003.35 19a2 2 0 001.72 1h13.85a2 2 0 001.74-1 10 10 0 00-.27-10.44zm-9.79 6.84a2 2 0 002.83 0l5.66-8.49-8.49 5.66a2 2 0 000 2.83z"}),"Speed");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"}),"PictureInPicture");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"}),"GetApp");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"}),"FullscreenExit");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"}),"Fullscreen");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4h-4z"}),"MovieCreation");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M21 6h-7.59l3.29-3.29L16 2l-4 4-4-4-.71.71L10.59 6H3c-1.1 0-2 .89-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.11-.9-2-2-2zm0 14H3V8h18v12zM9 10v8l7-4z"}),"LiveTv");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"}),"MusicNote");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"}),"MoreHoriz");t.default=a},function(e,t,n){var r=n(103),o=n(360),i=n(361),a=Math.max,s=Math.min;e.exports=function(e,t,n){var u,l,c,f,d,p,h=0,m=!1,v=!1,y=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function g(t){var n=u,r=l;return u=l=void 0,h=t,f=e.apply(r,n)}function b(e){var n=e-p;return void 0===p||n>=t||n<0||v&&e-h>=c}function w(){var e=o();if(b(e))return x(e);d=setTimeout(w,function(e){var n=t-(e-p);return v?s(n,c-(e-h)):n}(e))}function x(e){return d=void 0,y&&u?g(e):(u=l=void 0,f)}function O(){var e=o(),n=b(e);if(u=arguments,l=this,p=e,n){if(void 0===d)return function(e){return h=e,d=setTimeout(w,t),m?g(e):f}(p);if(v)return clearTimeout(d),d=setTimeout(w,t),g(p)}return void 0===d&&(d=setTimeout(w,t)),f}return t=i(t)||0,r(n)&&(m=!!n.leading,c=(v="maxWait"in n)?a(i(n.maxWait)||0,t):c,y="trailing"in n?!!n.trailing:y),O.cancel=function(){void 0!==d&&clearTimeout(d),h=0,u=p=l=d=void 0},O.flush=function(){return void 0===d?f:x(o())},O}},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){if(e&&t){var n=Array.isArray(t)?t:t.split(",");if(0===n.length)return!0;var r=e.name||"",o=(e.type||"").toLowerCase(),i=o.replace(/\/.*$/,"");return n.some((function(e){var t=e.trim().toLowerCase();return"."===t.charAt(0)?r.toLowerCase().endsWith(t):t.endsWith("/*")?i===t.replace(/\/.*$/,""):o===t}))}return!0}},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"}),"CreditCard");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z"}),"LibraryAdd");t.default=a},function(e,t,n){"use strict";var r=n(193);Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return o.default}}),Object.defineProperty(t,"SwipeableViewsContext",{enumerable:!0,get:function(){return o.SwipeableViewsContext}});var o=r(n(430))},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"}),"Search");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"}),"Check");t.default=a},function(e,t,n){"use strict";var r=n(27),o=n(28);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=o(n(1)),a=(0,r(n(29)).default)(i.createElement("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"}),"Clear");t.default=a},function(e,t,n){"use strict";var r=n(13),o=n(6),i=n(451),a=n(2),s=["xs","sm","md","lg","xl"];function u(e,t,n){var o;return Object(a.a)({gutters:function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return console.warn(["Material-UI: theme.mixins.gutters() is deprecated.","You can use the source of the mixin directly:","\n paddingLeft: theme.spacing(2),\n paddingRight: theme.spacing(2),\n [theme.breakpoints.up('sm')]: {\n paddingLeft: theme.spacing(3),\n paddingRight: theme.spacing(3),\n },\n "].join("\n")),Object(a.a)({paddingLeft:t(2),paddingRight:t(2)},n,Object(r.a)({},e.up("sm"),Object(a.a)({paddingLeft:t(3),paddingRight:t(3)},n[e.up("sm")])))},toolbar:(o={minHeight:56},Object(r.a)(o,"".concat(e.up("xs")," and (orientation: landscape)"),{minHeight:48}),Object(r.a)(o,e.up("sm"),{minHeight:64}),o)},n)}var l=n(245),c={black:"#000",white:"#fff"},f={50:"#fafafa",100:"#f5f5f5",200:"#eeeeee",300:"#e0e0e0",400:"#bdbdbd",500:"#9e9e9e",600:"#757575",700:"#616161",800:"#424242",900:"#212121",A100:"#d5d5d5",A200:"#aaaaaa",A400:"#303030",A700:"#616161"},d={50:"#e8eaf6",100:"#c5cae9",200:"#9fa8da",300:"#7986cb",400:"#5c6bc0",500:"#3f51b5",600:"#3949ab",700:"#303f9f",800:"#283593",900:"#1a237e",A100:"#8c9eff",A200:"#536dfe",A400:"#3d5afe",A700:"#304ffe"},p={50:"#fce4ec",100:"#f8bbd0",200:"#f48fb1",300:"#f06292",400:"#ec407a",500:"#e91e63",600:"#d81b60",700:"#c2185b",800:"#ad1457",900:"#880e4f",A100:"#ff80ab",A200:"#ff4081",A400:"#f50057",A700:"#c51162"},h={50:"#ffebee",100:"#ffcdd2",200:"#ef9a9a",300:"#e57373",400:"#ef5350",500:"#f44336",600:"#e53935",700:"#d32f2f",800:"#c62828",900:"#b71c1c",A100:"#ff8a80",A200:"#ff5252",A400:"#ff1744",A700:"#d50000"},m={50:"#fff3e0",100:"#ffe0b2",200:"#ffcc80",300:"#ffb74d",400:"#ffa726",500:"#ff9800",600:"#fb8c00",700:"#f57c00",800:"#ef6c00",900:"#e65100",A100:"#ffd180",A200:"#ffab40",A400:"#ff9100",A700:"#ff6d00"},v={50:"#e3f2fd",100:"#bbdefb",200:"#90caf9",300:"#64b5f6",400:"#42a5f5",500:"#2196f3",600:"#1e88e5",700:"#1976d2",800:"#1565c0",900:"#0d47a1",A100:"#82b1ff",A200:"#448aff",A400:"#2979ff",A700:"#2962ff"},y={50:"#e8f5e9",100:"#c8e6c9",200:"#a5d6a7",300:"#81c784",400:"#66bb6a",500:"#4caf50",600:"#43a047",700:"#388e3c",800:"#2e7d32",900:"#1b5e20",A100:"#b9f6ca",A200:"#69f0ae",A400:"#00e676",A700:"#00c853"},g=n(17),b={text:{primary:"rgba(0, 0, 0, 0.87)",secondary:"rgba(0, 0, 0, 0.54)",disabled:"rgba(0, 0, 0, 0.38)",hint:"rgba(0, 0, 0, 0.38)"},divider:"rgba(0, 0, 0, 0.12)",background:{paper:c.white,default:f[50]},action:{active:"rgba(0, 0, 0, 0.54)",hover:"rgba(0, 0, 0, 0.04)",hoverOpacity:.04,selected:"rgba(0, 0, 0, 0.08)",selectedOpacity:.08,disabled:"rgba(0, 0, 0, 0.26)",disabledBackground:"rgba(0, 0, 0, 0.12)",disabledOpacity:.38,focus:"rgba(0, 0, 0, 0.12)",focusOpacity:.12,activatedOpacity:.12}},w={text:{primary:c.white,secondary:"rgba(255, 255, 255, 0.7)",disabled:"rgba(255, 255, 255, 0.5)",hint:"rgba(255, 255, 255, 0.5)",icon:"rgba(255, 255, 255, 0.5)"},divider:"rgba(255, 255, 255, 0.12)",background:{paper:f[800],default:"#303030"},action:{active:c.white,hover:"rgba(255, 255, 255, 0.08)",hoverOpacity:.08,selected:"rgba(255, 255, 255, 0.16)",selectedOpacity:.16,disabled:"rgba(255, 255, 255, 0.3)",disabledBackground:"rgba(255, 255, 255, 0.12)",disabledOpacity:.38,focus:"rgba(255, 255, 255, 0.12)",focusOpacity:.12,activatedOpacity:.24}};function x(e,t,n,r){var o=r.light||r,i=r.dark||1.5*r;e[t]||(e.hasOwnProperty(n)?e[t]=e[n]:"light"===t?e.light=Object(g.e)(e.main,o):"dark"===t&&(e.dark=Object(g.b)(e.main,i)))}function O(e){return Math.round(1e5*e)/1e5}function S(e){return O(e)}var E={textTransform:"uppercase"},k='"Roboto", "Helvetica", "Arial", sans-serif';function j(e,t){var n="function"===typeof t?t(e):t,r=n.fontFamily,s=void 0===r?k:r,u=n.fontSize,l=void 0===u?14:u,c=n.fontWeightLight,f=void 0===c?300:c,d=n.fontWeightRegular,p=void 0===d?400:d,h=n.fontWeightMedium,m=void 0===h?500:h,v=n.fontWeightBold,y=void 0===v?700:v,g=n.htmlFontSize,b=void 0===g?16:g,w=n.allVariants,x=n.pxToRem,j=Object(o.a)(n,["fontFamily","fontSize","fontWeightLight","fontWeightRegular","fontWeightMedium","fontWeightBold","htmlFontSize","allVariants","pxToRem"]);var C=l/14,_=x||function(e){return"".concat(e/b*C,"rem")},R=function(e,t,n,r,o){return Object(a.a)({fontFamily:s,fontWeight:e,fontSize:_(t),lineHeight:n},s===k?{letterSpacing:"".concat(O(r/t),"em")}:{},o,w)},P={h1:R(f,96,1.167,-1.5),h2:R(f,60,1.2,-.5),h3:R(p,48,1.167,0),h4:R(p,34,1.235,.25),h5:R(p,24,1.334,0),h6:R(m,20,1.6,.15),subtitle1:R(p,16,1.75,.15),subtitle2:R(m,14,1.57,.1),body1:R(p,16,1.5,.15),body2:R(p,14,1.43,.15),button:R(m,14,1.75,.4,E),caption:R(p,12,1.66,.4),overline:R(p,12,2.66,1,E)};return Object(i.a)(Object(a.a)({htmlFontSize:b,pxToRem:_,round:S,fontFamily:s,fontSize:l,fontWeightLight:f,fontWeightRegular:p,fontWeightMedium:m,fontWeightBold:y},P),j,{clone:!1})}function C(){return["".concat(arguments.length<=0?void 0:arguments[0],"px ").concat(arguments.length<=1?void 0:arguments[1],"px ").concat(arguments.length<=2?void 0:arguments[2],"px ").concat(arguments.length<=3?void 0:arguments[3],"px rgba(0,0,0,").concat(.2,")"),"".concat(arguments.length<=4?void 0:arguments[4],"px ").concat(arguments.length<=5?void 0:arguments[5],"px ").concat(arguments.length<=6?void 0:arguments[6],"px ").concat(arguments.length<=7?void 0:arguments[7],"px rgba(0,0,0,").concat(.14,")"),"".concat(arguments.length<=8?void 0:arguments[8],"px ").concat(arguments.length<=9?void 0:arguments[9],"px ").concat(arguments.length<=10?void 0:arguments[10],"px ").concat(arguments.length<=11?void 0:arguments[11],"px rgba(0,0,0,").concat(.12,")")].join(",")}var _=["none",C(0,2,1,-1,0,1,1,0,0,1,3,0),C(0,3,1,-2,0,2,2,0,0,1,5,0),C(0,3,3,-2,0,3,4,0,0,1,8,0),C(0,2,4,-1,0,4,5,0,0,1,10,0),C(0,3,5,-1,0,5,8,0,0,1,14,0),C(0,3,5,-1,0,6,10,0,0,1,18,0),C(0,4,5,-2,0,7,10,1,0,2,16,1),C(0,5,5,-3,0,8,10,1,0,3,14,2),C(0,5,6,-3,0,9,12,1,0,3,16,2),C(0,6,6,-3,0,10,14,1,0,4,18,3),C(0,6,7,-4,0,11,15,1,0,4,20,3),C(0,7,8,-4,0,12,17,2,0,5,22,4),C(0,7,8,-4,0,13,19,2,0,5,24,4),C(0,7,9,-4,0,14,21,2,0,5,26,4),C(0,8,9,-5,0,15,22,2,0,6,28,5),C(0,8,10,-5,0,16,24,2,0,6,30,5),C(0,8,11,-5,0,17,26,2,0,6,32,5),C(0,9,11,-5,0,18,28,2,0,7,34,6),C(0,9,12,-6,0,19,29,2,0,7,36,6),C(0,10,13,-6,0,20,31,3,0,8,38,7),C(0,10,13,-6,0,21,33,3,0,8,40,7),C(0,10,14,-6,0,22,35,3,0,8,42,7),C(0,11,14,-7,0,23,36,3,0,9,44,8),C(0,11,15,-7,0,24,38,3,0,9,46,8)],R={borderRadius:4},P=n(563);var T=n(41),A=n(119);function L(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.breakpoints,n=void 0===t?{}:t,r=e.mixins,O=void 0===r?{}:r,S=e.palette,E=void 0===S?{}:S,k=e.spacing,C=e.typography,L=void 0===C?{}:C,M=Object(o.a)(e,["breakpoints","mixins","palette","spacing","typography"]),N=function(e){var t=e.primary,n=void 0===t?{light:d[300],main:d[500],dark:d[700]}:t,r=e.secondary,s=void 0===r?{light:p.A200,main:p.A400,dark:p.A700}:r,u=e.error,O=void 0===u?{light:h[300],main:h[500],dark:h[700]}:u,S=e.warning,E=void 0===S?{light:m[300],main:m[500],dark:m[700]}:S,k=e.info,j=void 0===k?{light:v[300],main:v[500],dark:v[700]}:k,C=e.success,_=void 0===C?{light:y[300],main:y[500],dark:y[700]}:C,R=e.type,P=void 0===R?"light":R,T=e.contrastThreshold,A=void 0===T?3:T,L=e.tonalOffset,M=void 0===L?.2:L,N=Object(o.a)(e,["primary","secondary","error","warning","info","success","type","contrastThreshold","tonalOffset"]);function I(e){return Object(g.d)(e,w.text.primary)>=A?w.text.primary:b.text.primary}var D=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:500,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:300,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:700;if(!(e=Object(a.a)({},e)).main&&e[t]&&(e.main=e[t]),!e.main)throw new Error(Object(l.a)(4,t));if("string"!==typeof e.main)throw new Error(Object(l.a)(5,JSON.stringify(e.main)));return x(e,"light",n,M),x(e,"dark",r,M),e.contrastText||(e.contrastText=I(e.main)),e},F={dark:w,light:b};return Object(i.a)(Object(a.a)({common:c,type:P,primary:D(n),secondary:D(s,"A400","A200","A700"),error:D(O),warning:D(E),info:D(j),success:D(_),grey:f,contrastThreshold:A,getContrastText:I,augmentColor:D,tonalOffset:M},F[P]),N)}(E),I=function(e){var t=e.values,n=void 0===t?{xs:0,sm:600,md:960,lg:1280,xl:1920}:t,r=e.unit,i=void 0===r?"px":r,u=e.step,l=void 0===u?5:u,c=Object(o.a)(e,["values","unit","step"]);function f(e){var t="number"===typeof n[e]?n[e]:e;return"@media (min-width:".concat(t).concat(i,")")}function d(e,t){var r=s.indexOf(t);return r===s.length-1?f(e):"@media (min-width:".concat("number"===typeof n[e]?n[e]:e).concat(i,") and ")+"(max-width:".concat((-1!==r&&"number"===typeof n[s[r+1]]?n[s[r+1]]:t)-l/100).concat(i,")")}return Object(a.a)({keys:s,values:n,up:f,down:function(e){var t=s.indexOf(e)+1,r=n[s[t]];return t===s.length?f("xs"):"@media (max-width:".concat(("number"===typeof r&&t>0?r:e)-l/100).concat(i,")")},between:d,only:function(e){return d(e,e)},width:function(e){return n[e]}},c)}(n),D=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:8;if(e.mui)return e;var t=Object(P.a)({spacing:e}),n=function(){for(var e=arguments.length,n=new Array(e),r=0;r1?z-1:0),U=1;U0&&o[o.length-1])&&(6===s[0]||2===s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0)&&!(r=i.next()).done;)a.push(r.value)}catch(s){o={error:s}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return a}function c(){for(var e=[],t=0;t0?r:e.name,writable:!1,configurable:!1,enumerable:!0})}return n}var p=[".DS_Store","Thumbs.db"];function h(e){return"object"===typeof e&&null!==e}function m(e){return b(e.target.files).map((function(e){return d(e)}))}function v(e){return s(this,void 0,void 0,(function(){return u(this,(function(t){switch(t.label){case 0:return[4,Promise.all(e.map((function(e){return e.getFile()})))];case 1:return[2,t.sent().map((function(e){return d(e)}))]}}))}))}function y(e,t){return s(this,void 0,void 0,(function(){var n;return u(this,(function(r){switch(r.label){case 0:return null===e?[2,[]]:e.items?(n=b(e.items).filter((function(e){return"file"===e.kind})),"drop"!==t?[2,n]:[4,Promise.all(n.map(w))]):[3,2];case 1:return[2,g(x(r.sent()))];case 2:return[2,g(b(e.files).map((function(e){return d(e)})))]}}))}))}function g(e){return e.filter((function(e){return-1===p.indexOf(e.name)}))}function b(e){if(null===e)return[];for(var t=[],n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nn)return[!1,F(n)];if(e.sizen)return[!1,F(n)]}return[!0,null]}function W(e){return void 0!==e&&null!==e}function V(e){return"function"===typeof e.isPropagationStopped?e.isPropagationStopped():"undefined"!==typeof e.cancelBubble&&e.cancelBubble}function q(e){return e.dataTransfer?Array.prototype.some.call(e.dataTransfer.types,(function(e){return"Files"===e||"application/x-moz-file"===e})):!!e.target&&!!e.target.files}function $(e){e.preventDefault()}function K(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),o=1;oe.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var ue=Object(r.forwardRef)((function(e,t){var n=e.children,i=fe(se(e,G)),a=i.open,s=se(i,X);return Object(r.useImperativeHandle)(t,(function(){return{open:a}}),[a]),o.a.createElement(r.Fragment,null,n(ie(ie({},s),{},{open:a})))}));ue.displayName="Dropzone";var le={disabled:!1,getFilesFromEvent:function(e){return s(this,void 0,void 0,(function(){return u(this,(function(t){return h(e)&&h(e.dataTransfer)?[2,y(e.dataTransfer,e.type)]:function(e){return h(e)&&h(e.target)}(e)?[2,m(e)]:Array.isArray(e)&&e.every((function(e){return"getFile"in e&&"function"===typeof e.getFile}))?[2,v(e)]:[2,[]]}))}))},maxSize:1/0,minSize:0,multiple:!0,maxFiles:0,preventDropOnDocument:!0,noClick:!1,noKeyboard:!1,noDrag:!1,noDragEventsBubbling:!1,validator:null,useFsAccessApi:!1};ue.defaultProps=le,ue.propTypes={children:a.a.func,accept:a.a.oneOfType([a.a.string,a.a.arrayOf(a.a.string)]),multiple:a.a.bool,preventDropOnDocument:a.a.bool,noClick:a.a.bool,noKeyboard:a.a.bool,noDrag:a.a.bool,noDragEventsBubbling:a.a.bool,minSize:a.a.number,maxSize:a.a.number,maxFiles:a.a.number,disabled:a.a.bool,getFilesFromEvent:a.a.func,onFileDialogCancel:a.a.func,onFileDialogOpen:a.a.func,useFsAccessApi:a.a.bool,onDragEnter:a.a.func,onDragLeave:a.a.func,onDragOver:a.a.func,onDrop:a.a.func,onDropAccepted:a.a.func,onDropRejected:a.a.func,validator:a.a.func};var ce={isFocused:!1,isFileDialogActive:!1,isDragActive:!1,isDragAccept:!1,isDragReject:!1,draggedFiles:[],acceptedFiles:[],fileRejections:[]};function fe(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=ie(ie({},le),e),n=t.accept,o=t.disabled,i=t.getFilesFromEvent,a=t.maxSize,s=t.minSize,u=t.multiple,l=t.maxFiles,c=t.onDragEnter,f=t.onDragLeave,d=t.onDragOver,p=t.onDrop,h=t.onDropAccepted,m=t.onDropRejected,v=t.onFileDialogCancel,y=t.onFileDialogOpen,g=t.useFsAccessApi,b=t.preventDropOnDocument,w=t.noClick,x=t.noKeyboard,O=t.noDrag,S=t.noDragEventsBubbling,E=t.validator,k=Object(r.useMemo)((function(){return"function"===typeof y?y:pe}),[y]),j=Object(r.useMemo)((function(){return"function"===typeof v?v:pe}),[v]),C=Object(r.useRef)(null),_=Object(r.useRef)(null),R=te(Object(r.useReducer)(de,ce),2),P=R[0],A=R[1],L=P.isFocused,M=P.isFileDialogActive,N=P.draggedFiles,I=function(){M&&setTimeout((function(){_.current&&(_.current.files.length||(A({type:"closeDialog"}),j()))}),300)};Object(r.useEffect)((function(){return g&&Q()?function(){}:(window.addEventListener("focus",I,!1),function(){window.removeEventListener("focus",I,!1)})}),[_,M,j,g]);var D=Object(r.useRef)([]),F=function(e){C.current&&C.current.contains(e.target)||(e.preventDefault(),D.current=[])};Object(r.useEffect)((function(){return b&&(document.addEventListener("dragover",$,!1),document.addEventListener("drop",F,!1)),function(){b&&(document.removeEventListener("dragover",$),document.removeEventListener("drop",F))}}),[C,b]);var z=Object(r.useCallback)((function(e){e.preventDefault(),e.persist(),ge(e),D.current=[].concat(ee(D.current),[e.target]),q(e)&&Promise.resolve(i(e)).then((function(t){V(e)&&!S||(A({draggedFiles:t,isDragActive:!0,type:"setDraggedFiles"}),c&&c(e))}))}),[i,c,S]),W=Object(r.useCallback)((function(e){e.preventDefault(),e.persist(),ge(e);var t=q(e);if(t&&e.dataTransfer)try{e.dataTransfer.dropEffect="copy"}catch(n){}return t&&d&&d(e),!1}),[d,S]),G=Object(r.useCallback)((function(e){e.preventDefault(),e.persist(),ge(e);var t=D.current.filter((function(e){return C.current&&C.current.contains(e)})),n=t.indexOf(e.target);-1!==n&&t.splice(n,1),D.current=t,t.length>0||(A({isDragActive:!1,type:"setDraggedFiles",draggedFiles:[]}),q(e)&&f&&f(e))}),[C,f,S]),X=Object(r.useCallback)((function(e,t){var r=[],o=[];e.forEach((function(e){var t=te(U(e,n),2),i=t[0],u=t[1],l=te(H(e,s,a),2),c=l[0],f=l[1],d=E?E(e):null;if(i&&c&&!d)r.push(e);else{var p=[u,f];d&&(p=p.concat(d)),o.push({file:e,errors:p.filter((function(e){return e}))})}})),(!u&&r.length>1||u&&l>=1&&r.length>l)&&(r.forEach((function(e){o.push({file:e,errors:[B]})})),r.splice(0)),A({acceptedFiles:r,fileRejections:o,type:"setFiles"}),p&&p(r,o,t),o.length>0&&m&&m(o,t),r.length>0&&h&&h(r,t)}),[A,u,n,s,a,l,p,h,m,E]),ne=Object(r.useCallback)((function(e){e.preventDefault(),e.persist(),ge(e),D.current=[],q(e)&&Promise.resolve(i(e)).then((function(t){V(e)&&!S||X(t,e)})),A({type:"reset"})}),[i,X,S]),re=Object(r.useCallback)((function(){if(g&&Q()){A({type:"openDialog"}),k();var e={multiple:u,types:Y(n)};window.showOpenFilePicker(e).then((function(e){return i(e)})).then((function(e){return X(e,null)})).catch((function(e){return j(e)})).finally((function(){return A({type:"closeDialog"})}))}else _.current&&(A({type:"openDialog"}),k(),_.current.value=null,_.current.click())}),[A,k,j,g,X,n,u]),oe=Object(r.useCallback)((function(e){C.current&&C.current.isEqualNode(e.target)&&(32!==e.keyCode&&13!==e.keyCode||(e.preventDefault(),re()))}),[C,_,re]),ue=Object(r.useCallback)((function(){A({type:"focus"})}),[]),fe=Object(r.useCallback)((function(){A({type:"blur"})}),[]),he=Object(r.useCallback)((function(){w||(!function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:window.navigator.userAgent;return function(e){return-1!==e.indexOf("MSIE")||-1!==e.indexOf("Trident/")}(e)||function(e){return-1!==e.indexOf("Edge/")}(e)}()?re():setTimeout(re,0))}),[_,w,re]),me=function(e){return o?null:e},ve=function(e){return x?null:me(e)},ye=function(e){return O?null:me(e)},ge=function(e){S&&e.stopPropagation()},be=Object(r.useMemo)((function(){return function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.refKey,n=void 0===t?"ref":t,r=e.role,i=e.onKeyDown,a=e.onFocus,s=e.onBlur,u=e.onClick,l=e.onDragEnter,c=e.onDragOver,f=e.onDragLeave,d=e.onDrop,p=se(e,J);return ie(ie(ae({onKeyDown:ve(K(i,oe)),onFocus:ve(K(a,ue)),onBlur:ve(K(s,fe)),onClick:me(K(u,he)),onDragEnter:ye(K(l,z)),onDragOver:ye(K(c,W)),onDragLeave:ye(K(f,G)),onDrop:ye(K(d,ne)),role:"string"===typeof r&&""!==r?r:"button"},n,C),o||x?{}:{tabIndex:0}),p)}}),[C,oe,ue,fe,he,z,W,G,ne,x,O,o]),we=Object(r.useCallback)((function(e){e.stopPropagation()}),[]),xe=Object(r.useMemo)((function(){return function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.refKey,r=void 0===t?"ref":t,o=e.onChange,i=e.onClick,a=se(e,Z);return ie(ie({},ae({accept:n,multiple:u,type:"file",style:{display:"none"},onChange:me(K(o,ne)),onClick:me(K(i,we)),autoComplete:"off",tabIndex:-1},r,_)),a)}}),[_,n,u,ne,o]),Oe=N.length,Se=Oe>0&&function(e){var t=e.files,n=e.accept,r=e.minSize,o=e.maxSize,i=e.multiple,a=e.maxFiles;return!(!i&&t.length>1||i&&a>=1&&t.length>a)&&t.every((function(e){var t=T(U(e,n),1)[0],i=T(H(e,r,o),1)[0];return t&&i}))}({files:N,accept:n,minSize:s,maxSize:a,multiple:u,maxFiles:l}),Ee=Oe>0&&!Se;return ie(ie({},P),{},{isDragAccept:Se,isDragReject:Ee,isFocused:L&&!o,getRootProps:be,getInputProps:xe,rootRef:C,inputRef:_,open:me(re)})}function de(e,t){switch(t.type){case"focus":return ie(ie({},e),{},{isFocused:!0});case"blur":return ie(ie({},e),{},{isFocused:!1});case"openDialog":return ie(ie({},ce),{},{isFileDialogActive:!0});case"closeDialog":return ie(ie({},e),{},{isFileDialogActive:!1});case"setDraggedFiles":var n=t.isDragActive,r=t.draggedFiles;return ie(ie({},e),{},{draggedFiles:r,isDragActive:n});case"setFiles":return ie(ie({},e),{},{acceptedFiles:t.acceptedFiles,fileRejections:t.fileRejections});case"reset":return ie({},ce);default:return e}}function pe(){}},function(e,t,n){"use strict";var r=n(2),o=n(6),i=n(1),a=(n(64),n(7)),s=n(9),u=n(19),l=n(43),c=n(24),f=n(65),d=n(44),p=n(556),h=n(508),m=n(452);function v(e,t){var n=0;return"number"===typeof t?n=t:"center"===t?n=e.height/2:"bottom"===t&&(n=e.height),n}function y(e,t){var n=0;return"number"===typeof t?n=t:"center"===t?n=e.width/2:"right"===t&&(n=e.width),n}function g(e){return[e.horizontal,e.vertical].map((function(e){return"number"===typeof e?"".concat(e,"px"):e})).join(" ")}function b(e){return"function"===typeof e?e():e}var w=i.forwardRef((function(e,t){var n=e.action,s=e.anchorEl,w=e.anchorOrigin,x=void 0===w?{vertical:"top",horizontal:"left"}:w,O=e.anchorPosition,S=e.anchorReference,E=void 0===S?"anchorEl":S,k=e.children,j=e.classes,C=e.className,_=e.container,R=e.elevation,P=void 0===R?8:R,T=e.getContentAnchorEl,A=e.marginThreshold,L=void 0===A?16:A,M=e.onEnter,N=e.onEntered,I=e.onEntering,D=e.onExit,F=e.onExited,z=e.onExiting,B=e.open,U=e.PaperProps,H=void 0===U?{}:U,W=e.transformOrigin,V=void 0===W?{vertical:"top",horizontal:"left"}:W,q=e.TransitionComponent,$=void 0===q?h.a:q,K=e.transitionDuration,Q=void 0===K?"auto":K,Y=e.TransitionProps,G=void 0===Y?{}:Y,X=Object(o.a)(e,["action","anchorEl","anchorOrigin","anchorPosition","anchorReference","children","classes","className","container","elevation","getContentAnchorEl","marginThreshold","onEnter","onEntered","onEntering","onExit","onExited","onExiting","open","PaperProps","transformOrigin","TransitionComponent","transitionDuration","TransitionProps"]),J=i.useRef(),Z=i.useCallback((function(e){if("anchorPosition"===E)return O;var t=b(s),n=(t&&1===t.nodeType?t:Object(c.a)(J.current).body).getBoundingClientRect(),r=0===e?x.vertical:"center";return{top:n.top+v(n,r),left:n.left+y(n,x.horizontal)}}),[s,x.horizontal,x.vertical,O,E]),ee=i.useCallback((function(e){var t=0;if(T&&"anchorEl"===E){var n=T(e);if(n&&e.contains(n)){var r=function(e,t){for(var n=t,r=0;n&&n!==e;)r+=(n=n.parentElement).scrollTop;return r}(e,n);t=n.offsetTop+n.clientHeight/2-r||0}0}return t}),[x.vertical,E,T]),te=i.useCallback((function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return{vertical:v(e,V.vertical)+t,horizontal:y(e,V.horizontal)}}),[V.horizontal,V.vertical]),ne=i.useCallback((function(e){var t=ee(e),n={width:e.offsetWidth,height:e.offsetHeight},r=te(n,t);if("none"===E)return{top:null,left:null,transformOrigin:g(r)};var o=Z(t),i=o.top-r.vertical,a=o.left-r.horizontal,u=i+n.height,l=a+n.width,c=Object(f.a)(b(s)),d=c.innerHeight-L,p=c.innerWidth-L;if(id){var m=u-d;i-=m,r.vertical+=m}if(ap){var y=l-p;a-=y,r.horizontal+=y}return{top:"".concat(Math.round(i),"px"),left:"".concat(Math.round(a),"px"),transformOrigin:g(r)}}),[s,E,Z,ee,te,L]),re=i.useCallback((function(){var e=J.current;if(e){var t=ne(e);null!==t.top&&(e.style.top=t.top),null!==t.left&&(e.style.left=t.left),e.style.transformOrigin=t.transformOrigin}}),[ne]),oe=i.useCallback((function(e){J.current=u.findDOMNode(e)}),[]);i.useEffect((function(){B&&re()})),i.useImperativeHandle(n,(function(){return B?{updatePosition:function(){re()}}:null}),[B,re]),i.useEffect((function(){if(B){var e=Object(l.a)((function(){re()}));return window.addEventListener("resize",e),function(){e.clear(),window.removeEventListener("resize",e)}}}),[B,re]);var ie=Q;"auto"!==Q||$.muiSupportAuto||(ie=void 0);var ae=_||(s?Object(c.a)(b(s)).body:void 0);return i.createElement(p.a,Object(r.a)({container:ae,open:B,ref:t,BackdropProps:{invisible:!0},className:Object(a.a)(j.root,C)},X),i.createElement($,Object(r.a)({appear:!0,in:B,onEnter:M,onEntered:N,onExit:D,onExited:F,onExiting:z,timeout:ie},G,{onEntering:Object(d.a)((function(e,t){I&&I(e,t),re()}),G.onEntering)}),i.createElement(m.a,Object(r.a)({elevation:P,ref:oe},H,{className:Object(a.a)(j.paper,H.className)}),k)))})),x=Object(s.a)({root:{},paper:{position:"absolute",overflowY:"auto",overflowX:"hidden",minWidth:16,minHeight:16,maxWidth:"calc(100% - 32px)",maxHeight:"calc(100% - 32px)",outline:0}},{name:"MuiPopover"})(w),O=n(509),S=n(121),E=n(12);function k(e,t,n){return e===t?e.firstChild:t&&t.nextElementSibling?t.nextElementSibling:n?null:e.firstChild}function j(e,t,n){return e===t?n?e.firstChild:e.lastChild:t&&t.previousElementSibling?t.previousElementSibling:n?null:e.lastChild}function C(e,t){if(void 0===t)return!0;var n=e.innerText;return void 0===n&&(n=e.textContent),0!==(n=n.trim().toLowerCase()).length&&(t.repeating?n[0]===t.keys[0]:0===n.indexOf(t.keys.join("")))}function _(e,t,n,r,o,i){for(var a=!1,s=o(e,t,!!t&&n);s;){if(s===e.firstChild){if(a)return;a=!0}var u=!r&&(s.disabled||"true"===s.getAttribute("aria-disabled"));if(s.hasAttribute("tabindex")&&C(s,i)&&!u)return void s.focus();s=o(e,s,n)}}var R="undefined"===typeof window?i.useEffect:i.useLayoutEffect,P=i.forwardRef((function(e,t){var n=e.actions,a=e.autoFocus,s=void 0!==a&&a,l=e.autoFocusItem,f=void 0!==l&&l,d=e.children,p=e.className,h=e.disabledItemsFocusable,m=void 0!==h&&h,v=e.disableListWrap,y=void 0!==v&&v,g=e.onKeyDown,b=e.variant,w=void 0===b?"selectedMenu":b,x=Object(o.a)(e,["actions","autoFocus","autoFocusItem","children","className","disabledItemsFocusable","disableListWrap","onKeyDown","variant"]),P=i.useRef(null),T=i.useRef({keys:[],repeating:!0,previousKeyMatched:!0,lastTime:null});R((function(){s&&P.current.focus()}),[s]),i.useImperativeHandle(n,(function(){return{adjustStyleForScrollbar:function(e,t){var n=!P.current.style.width;if(e.clientHeight0&&(a-o.lastTime>500?(o.keys=[],o.repeating=!0,o.previousKeyMatched=!0):o.repeating&&i!==o.keys[0]&&(o.repeating=!1)),o.lastTime=a,o.keys.push(i);var s=r&&!o.repeating&&C(r,o);o.previousKeyMatched&&(s||_(t,r,!1,m,k,o))?e.preventDefault():o.previousKeyMatched=!1}g&&g(e)},tabIndex:s?0:-1},x),N)})),T=n(39),A=n(37),L={vertical:"top",horizontal:"right"},M={vertical:"top",horizontal:"left"},N=i.forwardRef((function(e,t){var n=e.autoFocus,s=void 0===n||n,l=e.children,c=e.classes,f=e.disableAutoFocusItem,d=void 0!==f&&f,p=e.MenuListProps,h=void 0===p?{}:p,m=e.onClose,v=e.onEntering,y=e.open,g=e.PaperProps,b=void 0===g?{}:g,w=e.PopoverClasses,O=e.transitionDuration,S=void 0===O?"auto":O,E=e.TransitionProps,k=(E=void 0===E?{}:E).onEntering,j=Object(o.a)(E,["onEntering"]),C=e.variant,_=void 0===C?"selectedMenu":C,R=Object(o.a)(e,["autoFocus","children","classes","disableAutoFocusItem","MenuListProps","onClose","onEntering","open","PaperProps","PopoverClasses","transitionDuration","TransitionProps","variant"]),N=Object(A.a)(),I=s&&!d&&y,D=i.useRef(null),F=i.useRef(null),z=-1;i.Children.map(l,(function(e,t){i.isValidElement(e)&&(e.props.disabled||("menu"!==_&&e.props.selected||-1===z)&&(z=t))}));var B=i.Children.map(l,(function(e,t){return t===z?i.cloneElement(e,{ref:function(t){F.current=u.findDOMNode(t),Object(T.a)(e.ref,t)}}):e}));return i.createElement(x,Object(r.a)({getContentAnchorEl:function(){return F.current},classes:w,onClose:m,TransitionProps:Object(r.a)({onEntering:function(e,t){D.current&&D.current.adjustStyleForScrollbar(e,N),v&&v(e,t),k&&k(e,t)}},j),anchorOrigin:"rtl"===N.direction?L:M,transformOrigin:"rtl"===N.direction?L:M,PaperProps:Object(r.a)({},b,{classes:Object(r.a)({},b.classes,{root:c.paper})}),open:y,ref:t,transitionDuration:S},R),i.createElement(P,Object(r.a)({onKeyDown:function(e){"Tab"===e.key&&(e.preventDefault(),m&&m(e,"tabKeyDown"))},actions:D,autoFocus:s&&(-1===z||d),autoFocusItem:I,variant:_},h,{className:Object(a.a)(c.list,h.className)}),B))}));t.a=Object(s.a)({paper:{maxHeight:"calc(100% - 96px)",WebkitOverflowScrolling:"touch"},list:{outline:0}},{name:"MuiMenu"})(N)},function(e,t,n){"use strict";var r=n(2),o=n(6),i=n(1),a=n.n(i),s=n(7),u=n(75),l=n.n(u),c=n(461);function f(e){return function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.name,u=Object(o.a)(n,["name"]);var f,d=i,p="function"===typeof t?function(e){return{root:function(n){return t(Object(r.a)({theme:e},n))}}}:{root:t},h=Object(c.a)(p,Object(r.a)({Component:e,name:i||e.displayName,classNamePrefix:d},u));t.filterProps&&(f=t.filterProps,delete t.filterProps),t.propTypes&&(t.propTypes,delete t.propTypes);var m=a.a.forwardRef((function(t,n){var i=t.children,u=t.className,l=t.clone,c=t.component,d=Object(o.a)(t,["children","className","clone","component"]),p=h(t),m=Object(s.a)(p.root,u),v=d;if(f&&(v=function(e,t){var n={};return Object.keys(e).forEach((function(r){-1===t.indexOf(r)&&(n[r]=e[r])})),n}(v,f)),l)return a.a.cloneElement(i,Object(r.a)({className:Object(s.a)(i.props.className,m)},v));if("function"===typeof i)return i(Object(r.a)({className:m},v));var y=c||e;return a.a.createElement(y,Object(r.a)({ref:n,className:m},v),i)}));return l()(m,e),m}}var d=n(69);t.a=function(e){var t=f(e);return function(e,n){return t(e,Object(r.a)({defaultTheme:d.a},n))}}},function(e,t,n){"use strict";function r(e){for(var t="https://mui.com/production-error/?code="+e,n=1;n0&&Math.abs((e.outerHeightStyle||0)-c)>1||e.overflow!==f)?(j.current+=1,{overflow:f,outerHeightStyle:c}):e}))}),[w,x,e.placeholder]);a.useEffect((function(){var e=Object(p.a)((function(){j.current=0,P()}));return window.addEventListener("resize",e),function(){e.clear(),window.removeEventListener("resize",e)}}),[P]),m((function(){P()})),a.useEffect((function(){j.current=0}),[g]);return a.createElement(a.Fragment,null,a.createElement("textarea",Object(o.a)({value:g,onChange:function(e){j.current=0,O||P(),n&&n(e)},ref:E,rows:x,style:Object(o.a)({height:_.outerHeightStyle,overflow:_.overflow?"hidden":null},y)},b)),a.createElement("textarea",{"aria-hidden":!0,className:e.className,readOnly:!0,ref:k,tabIndex:-1,style:Object(o.a)({},v,y)}))})),g=n(76),b="undefined"===typeof window?a.useEffect:a.useLayoutEffect,w=a.forwardRef((function(e,t){var n=e["aria-describedby"],c=e.autoComplete,p=e.autoFocus,h=e.classes,m=e.className,v=(e.color,e.defaultValue),w=e.disabled,x=e.endAdornment,O=(e.error,e.fullWidth),S=void 0!==O&&O,E=e.id,k=e.inputComponent,j=void 0===k?"input":k,C=e.inputProps,_=void 0===C?{}:C,R=e.inputRef,P=(e.margin,e.multiline),T=void 0!==P&&P,A=e.name,L=e.onBlur,M=e.onChange,N=e.onClick,I=e.onFocus,D=e.onKeyDown,F=e.onKeyUp,z=e.placeholder,B=e.readOnly,U=e.renderSuffix,H=e.rows,W=e.rowsMax,V=e.rowsMin,q=e.maxRows,$=e.minRows,K=e.startAdornment,Q=e.type,Y=void 0===Q?"text":Q,G=e.value,X=Object(r.a)(e,["aria-describedby","autoComplete","autoFocus","classes","className","color","defaultValue","disabled","endAdornment","error","fullWidth","id","inputComponent","inputProps","inputRef","margin","multiline","name","onBlur","onChange","onClick","onFocus","onKeyDown","onKeyUp","placeholder","readOnly","renderSuffix","rows","rowsMax","rowsMin","maxRows","minRows","startAdornment","type","value"]),J=null!=_.value?_.value:G,Z=a.useRef(null!=J).current,ee=a.useRef(),te=a.useCallback((function(e){0}),[]),ne=Object(d.a)(_.ref,te),re=Object(d.a)(R,ne),oe=Object(d.a)(ee,re),ie=a.useState(!1),ae=ie[0],se=ie[1],ue=Object(l.b)();var le=Object(u.a)({props:e,muiFormControl:ue,states:["color","disabled","error","hiddenLabel","margin","required","filled"]});le.focused=ue?ue.focused:ae,a.useEffect((function(){!ue&&w&&ae&&(se(!1),L&&L())}),[ue,w,ae,L]);var ce=ue&&ue.onFilled,fe=ue&&ue.onEmpty,de=a.useCallback((function(e){Object(g.b)(e)?ce&&ce():fe&&fe()}),[ce,fe]);b((function(){Z&&de({value:J})}),[J,de,Z]);a.useEffect((function(){de(ee.current)}),[]);var pe=j,he=Object(o.a)({},_,{ref:oe});"string"!==typeof pe?he=Object(o.a)({inputRef:oe,type:Y},he,{ref:null}):T?!H||q||$||W||V?(he=Object(o.a)({minRows:H||$,rowsMax:W,maxRows:q},he),pe=y):pe="textarea":he=Object(o.a)({type:Y},he);return a.useEffect((function(){ue&&ue.setAdornedStart(Boolean(K))}),[ue,K]),a.createElement("div",Object(o.a)({className:Object(s.a)(h.root,h["color".concat(Object(f.a)(le.color||"primary"))],m,le.disabled&&h.disabled,le.error&&h.error,S&&h.fullWidth,le.focused&&h.focused,ue&&h.formControl,T&&h.multiline,K&&h.adornedStart,x&&h.adornedEnd,"dense"===le.margin&&h.marginDense),onClick:function(e){ee.current&&e.currentTarget===e.target&&ee.current.focus(),N&&N(e)},ref:t},X),K,a.createElement(l.a.Provider,{value:null},a.createElement(pe,Object(o.a)({"aria-invalid":le.error,"aria-describedby":n,autoComplete:c,autoFocus:p,defaultValue:v,disabled:le.disabled,id:E,onAnimationStart:function(e){de("mui-auto-fill-cancel"===e.animationName?ee.current:{value:"x"})},name:A,placeholder:z,readOnly:B,required:le.required,rows:H,value:J,onKeyDown:D,onKeyUp:F},he,{className:Object(s.a)(h.input,_.className,le.disabled&&h.disabled,T&&h.inputMultiline,le.hiddenLabel&&h.inputHiddenLabel,K&&h.inputAdornedStart,x&&h.inputAdornedEnd,"search"===Y&&h.inputTypeSearch,"dense"===le.margin&&h.inputMarginDense),onBlur:function(e){L&&L(e),_.onBlur&&_.onBlur(e),ue&&ue.onBlur?ue.onBlur(e):se(!1)},onChange:function(e){if(!Z){var t=e.target||ee.current;if(null==t)throw new Error(Object(i.a)(1));de({value:t.value})}for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o