├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature-request.yaml └── workflows │ ├── Check_patch_notes.yml │ ├── ci.yml │ ├── docker-image.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── appimage ├── AppImageBuilder.yml └── pickaxe.png ├── build.bat ├── build.spec ├── cache.py ├── channel.py ├── constants.py ├── docker_entrypoint.sh ├── exceptions.py ├── gui.py ├── healthcheck.sh ├── inventory.py ├── lang ├── Dansk.json ├── Deutsch.json ├── English.json ├── Español.json ├── Français.json ├── Indonesian.json ├── Italiano.json ├── Nederlandse.json ├── Polski.json ├── Português.json ├── Türkçe.json ├── Čeština.json ├── Русский.json ├── Українська.json ├── العربية.json ├── 简体中文.json └── 繁體中文.json ├── main.py ├── manual.txt ├── pack.bat ├── patch_notes.txt ├── pickaxe.ico ├── registry.py ├── requirements.txt ├── run_dev.bat ├── settings.py ├── setup_env.bat ├── translate.py ├── twitch.py ├── utils.py ├── version.py └── websocket.py /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.idea 3 | /.vscode 4 | /.mypy_cache 5 | /__pycache__ 6 | /build 7 | /dist 8 | /env 9 | /venv 10 | *.exe 11 | *.zip 12 | *.tmp 13 | 14 | /cache 15 | 16 | log.txt 17 | lock.file 18 | cookies.jar 19 | settings.json 20 | healthcheck.timestamp 21 | 22 | Dockerfile 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: DevilXD 2 | custom: "https://www.buymeacoffee.com/DevilXD" 3 | github: DevilXD 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report. 3 | labels: ["Bug"] 4 | body: 5 | 6 | 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Before submitting an issue, look at the [project goals](https://github.com/Windows200000/TwitchDropsMiner-updated#project-goals)! 11 | 12 | ### Bad Title: 13 | - Need help 14 | - Cookie Jar 15 | - Received error (working fine prior to reboot) 16 | - I am getting an error when trying to run 17 | 18 | ### Good Title: 19 | - Not finding any Campaigns 20 | - Random crash with GQL error "PersistedQueryNotFound" 21 | - TypeError: 'NoneType' object is not subscriptable 22 | - Drops not being claimed in the intended way, causing some to be missed and no notifications 23 | 24 | - type: textarea 25 | attributes: 26 | label: Description 27 | description: A clear and concise description of what the bug is. 28 | placeholder: TDM crashes when I open the `Inventory` tab. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | attributes: 34 | label: To Reproduce 35 | description: Steps to reproduce the behavior. Being able to reliably cause a bug can often result in a quick fix. 36 | placeholder: | 37 | 1. Add Rust Twitch Drops 38 | 2. Reload 39 | 3. Go to Inventory tab 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Expected behavior 46 | description: What should have happened? 47 | placeholder: The `Inventory` tab shows. 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | attributes: 53 | label: Observed behavior 54 | description: What actually happened? 55 | placeholder: TDM stopped responding. 56 | validations: 57 | required: true 58 | 59 | - type: textarea 60 | attributes: 61 | label: Screenshots 62 | description: Add any relevant screenshots. 63 | validations: 64 | required: false 65 | 66 | - type: textarea 67 | attributes: 68 | label: Logs 69 | description: | 70 | If you have them, provide any logs. They can help, even if no error is shown. 71 | Please run TDM with `-vvv --log`. This can be done via cmd. 72 | placeholder: | 73 | ``` 74 | 11:35:35: No available channels to watch. Waiting for an ONLINE channel... 75 | 12:35:39: No available channels to watch. Waiting for an ONLINE channel... 76 | 13:35:44: No available channels to watch. Waiting for an ONLINE channel... 77 | 14:35:48: No available channels to watch. Waiting for an ONLINE channel... 78 | 15:33:21: ibai goes ONLINE, switching... 79 | 15:33:21: Watching: ibai 80 | ``` 81 | validations: 82 | required: false 83 | 84 | - type: input 85 | attributes: 86 | label: OS 87 | description: What Operating System are you using? 88 | placeholder: Windows 11 | Linux Mint 89 | validations: 90 | required: true 91 | 92 | - type: input 93 | attributes: 94 | label: Build 95 | description: How are you running TDM? 96 | placeholder: from source | .exe | AppImage 97 | validations: 98 | required: true 99 | 100 | - type: input 101 | attributes: 102 | label: Version/Commit 103 | description: What version are you using? If you are NOT using an official release, enter the commit. 104 | placeholder: v1.5.0 | 54ef767 105 | validations: 106 | required: true 107 | 108 | - type: textarea 109 | attributes: 110 | label: Additional context 111 | description: Add any other context about the problem. 112 | placeholder: You got to the end, thanks for reporting! ;) 113 | validations: 114 | required: false 115 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for TDM. 3 | labels: ["Enhancement"] 4 | body: 5 | 6 | 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Before submitting an issue, look at the [project goals](https://github.com/Windows200000/TwitchDropsMiner-updated#project-goals)! 11 | If your issue is specific to one Operating System, consider whether it should be a bug report instead. 12 | Create only **one issue per feature**. This ensures no feature is forgotten. 13 | 14 | ### Bad Title: 15 | - 32-bit | What do you actually want? 16 | - Status Icon in the tray | What should it do? 17 | - Received error (working fine prior to reboot) 18 | - I am getting an error when trying to run 19 | 20 | ### Good Title: 21 | - Create a build for 32-bit Windows 22 | - Exclude impossible campaigns 23 | - TypeError: 'NoneType' object is not subscriptable 24 | - Drops not being claimed in the intended way, causing some to be missed and no notifications 25 | 26 | - type: textarea 27 | attributes: 28 | label: Problem 29 | description: Is your feature request related to a problem? If so, provide a clear and concise description of what the problem is. 30 | placeholder: When I have many Windows open, it can take a while to find TDM. 31 | validations: 32 | required: false 33 | 34 | - type: textarea 35 | attributes: 36 | label: Suggestion(s) 37 | description: What do you want to happen/change. 38 | placeholder: Don't hide tray icon when TDM window is open. 39 | validations: 40 | required: true 41 | 42 | - type: textarea 43 | attributes: 44 | label: Additional notes 45 | description: Add any other context, ideas or screenshots about the feature request here. 46 | placeholder: There should be an option to allways hide/show it. 47 | validations: 48 | required: false 49 | -------------------------------------------------------------------------------- /.github/workflows/Check_patch_notes.yml: -------------------------------------------------------------------------------- 1 | name: Check patch notes 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - in-dev 7 | workflow_dispatch: 8 | 9 | jobs: 10 | check_patch_notes: 11 | runs-on: ubuntu-latest # windows-latest | macos-latest 12 | name: Test changed-files 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 2 # "0" OR "2" -> To retrieve the preceding commit. 17 | 18 | - name: Get changed files 19 | id: changed-files 20 | uses: tj-actions/changed-files@v9.2 21 | 22 | - name: Run when patch noted don't change 23 | if: ${{ !contains(steps.changed-files.outputs.modified_files, 'patch_notes.txt') }} 24 | run: exit 1 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build dev version 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'in-dev' 8 | pull_request: 9 | branches: 10 | - 'in-dev' 11 | workflow_dispatch: 12 | 13 | env: 14 | PYTHON_VERSION: '3.10' 15 | 16 | jobs: 17 | windows: 18 | name: Windows 19 | runs-on: windows-latest 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up variables 26 | id: vars 27 | run: | 28 | Add-Content $env:GITHUB_OUTPUT "sha_short=$(git rev-parse --short HEAD)" 29 | 30 | - name: Append git revision to project version 31 | run: | 32 | (Get-Content version.py) ` 33 | -Replace '^__version__\s*=\s*"[^"]+', "`$0.${{steps.vars.outputs.sha_short}}" |` 34 | Out-File version.py 35 | 36 | # Ensure Python version 37 | - name: Set up Python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: ${{env.PYTHON_VERSION}} 41 | 42 | - name: Install project dependencies 43 | run: | 44 | python3 -m pip install wheel 45 | python3 -m pip install -r requirements.txt 46 | 47 | - name: Install UPX 48 | run: | 49 | Invoke-WebRequest -Uri https://github.com/upx/upx/releases/download/v4.0.2/upx-4.0.2-win64.zip -OutFile (Join-Path $env:Temp upx.zip) 50 | Expand-Archive -LiteralPath (Join-Path $env:Temp upx.zip) -DestinationPath $env:Temp 51 | Move-Item -Path (Join-Path $env:Temp upx-*) -Destination (Join-Path $env:Temp upx) 52 | Add-Content $env:GITHUB_PATH (Join-Path $env:Temp upx) 53 | 54 | - name: Install PyInstaller 55 | run: | 56 | python3 -m pip install pyinstaller 57 | 58 | - name: Create portable executable 59 | run: | 60 | pyinstaller build.spec 61 | 62 | - name: Create release folder 63 | run: | 64 | $FolderName = 'Twitch Drops Miner' 65 | New-Item $FolderName -ItemType Directory 66 | Copy-Item dist\*.exe $FolderName 67 | Copy-Item manual.txt $FolderName 68 | Compress-Archive -Path $FolderName -DestinationPath Twitch.Drops.Miner.Windows.zip 69 | 70 | - name: Upload build artifact 71 | uses: actions/upload-artifact@v4 72 | with: 73 | if-no-files-found: error 74 | name: Twitch.Drops.Miner.Windows 75 | path: Twitch.Drops.Miner.Windows.zip 76 | 77 | linux-pyinstaller: 78 | name: Linux (PyInstaller) 79 | runs-on: ubuntu-20.04 80 | 81 | steps: 82 | - name: Checkout code 83 | uses: actions/checkout@v4 84 | 85 | - name: Set up variables 86 | id: vars 87 | run: | 88 | echo "sha_short=$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}" 89 | 90 | - name: Append git revision to project version 91 | run: | 92 | sed -ri "s/^__version__\s*=\s*\"[^\"]+/\0.${{ steps.vars.outputs.sha_short }}/" version.py 93 | 94 | # NOTE: We're only use a custom version of Python here because truststore requires at least Python 3.10, but Ubuntu 20.04 has Python 3.8. 95 | - name: Set up Python 96 | uses: actions/setup-python@v5 97 | with: 98 | python-version: ${{env.PYTHON_VERSION}} 99 | 100 | - name: Install system dependencies 101 | run: | 102 | sudo apt update 103 | sudo apt install gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-tk 104 | 105 | - name: Install project dependencies 106 | run: | 107 | python3 -m pip install wheel 108 | python3 -m pip install -r requirements.txt 109 | 110 | - name: Install PyInstaller 111 | run: | 112 | python3 -m pip install pyinstaller 113 | 114 | # NOTE: Remove this step if/once libxft gets updated to 2.3.5 or newer on Ubuntu 20.04, which currently has 2.3.3. 115 | - name: Build a recent version of libXft 116 | run: | 117 | mkdir -p /tmp/libXft 118 | cd /tmp/libXft 119 | curl -L https://xorg.freedesktop.org/releases/individual/lib/libXft-2.3.8.tar.xz -o libXft.tar.xz 120 | tar xvf libXft.tar.xz 121 | cd libXft-* 122 | ./configure --prefix=/tmp/libXft --sysconfdir=/etc --disable-static 123 | make 124 | make install-strip 125 | 126 | - name: Create portable executable 127 | run: | 128 | LD_LIBRARY_PATH=/tmp/libXft/lib xvfb-run --auto-servernum pyinstaller build.spec 129 | 130 | - name: Show PyInstaller warnings 131 | run: | 132 | cat build/build/warn-build.txt || true 133 | 134 | - name: Create release folder 135 | run: | 136 | folder='Twitch Drops Miner' 137 | mkdir "${folder}" 138 | cp manual.txt dist/* "${folder}" 139 | 7z a Twitch.Drops.Miner.Linux.PyInstaller.zip "${folder}" 140 | 141 | - name: Upload build artifact 142 | uses: actions/upload-artifact@v4 143 | with: 144 | if-no-files-found: error 145 | name: Twitch.Drops.Miner.Linux.PyInstaller 146 | path: Twitch.Drops.Miner.Linux.PyInstaller.zip 147 | 148 | linux-appimage: 149 | name: Linux (AppImage) 150 | runs-on: ubuntu-22.04 151 | 152 | steps: 153 | - name: Checkout code 154 | uses: actions/checkout@v4 155 | 156 | - name: Set up variables 157 | id: vars 158 | run: | 159 | echo "app_version=$(python3 -c 'from version import __version__ as v; print(v)')" >> "${GITHUB_OUTPUT}" 160 | echo "sha_short=$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}" 161 | 162 | - name: Append git revision to project version 163 | run: | 164 | sed -ri "s/^__version__\s*=\s*\"[^\"]+/\0.${{steps.vars.outputs.sha_short}}/" version.py 165 | 166 | - name: Install system dependencies 167 | run: | 168 | sudo apt update 169 | sudo apt install libgirepository1.0-dev python3-testresources 170 | 171 | - name: Download AppImage Builder 172 | run: | 173 | curl -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage -o appimage-builder 174 | chmod +x appimage-builder 175 | 176 | - name: Create AppImage 177 | env: 178 | APPIMAGE_EXTRACT_AND_RUN: 1 179 | APP_VERSION: ${{steps.vars.outputs.app_version}}.${{steps.vars.outputs.sha_short}} 180 | PYTHON_VERSION: ${{env.PYTHON_VERSION}} 181 | run: | 182 | ./appimage-builder --recipe appimage/AppImageBuilder.yml 183 | 184 | - name: Create release folder 185 | run: | 186 | folder='Twitch Drops Miner' 187 | mkdir "${folder}" 188 | cp *.AppImage manual.txt "${folder}" 189 | 7z a Twitch.Drops.Miner.Linux.AppImage.zip "${folder}" 190 | 191 | - name: Upload build artifact 192 | uses: actions/upload-artifact@v4 193 | with: 194 | if-no-files-found: error 195 | name: Twitch.Drops.Miner.Linux.AppImage 196 | path: Twitch.Drops.Miner.Linux.AppImage.zip 197 | 198 | update_releases_page: 199 | #NOTEif: github.event.pull_request.merged == true || github.event_name == 'push' 200 | name: Upload builds to Releases 201 | needs: 202 | - windows 203 | - linux-pyinstaller 204 | - linux-appimage 205 | runs-on: ubuntu-latest 206 | permissions: 207 | contents: write 208 | 209 | steps: 210 | - name: Set up variables 211 | id: vars 212 | run: | 213 | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" 214 | 215 | - name: Download build artifacts from previous jobs 216 | uses: actions/download-artifact@v4 217 | with: 218 | path: artifacts 219 | 220 | - name: Upload builds to Releases 221 | uses: ncipollo/release-action@v1 222 | with: 223 | allowUpdates: true 224 | artifactErrorsFailBuild: true 225 | artifacts: artifacts/*/* 226 | body: | 227 | **This is an automatically generated in-development pre-release version of the application, that includes the latest in-dev branch changes.** 228 | **⚠️ This build is not stable and may end up terminating with a fatal error. ⚠️** 229 | **Use at your own risk.** 230 | 231 | - Last build date: `${{steps.vars.outputs.date_now}}` 232 | - Reference commit: ${{github.sha}} 233 | name: Development build 234 | prerelease: true 235 | removeArtifacts: true 236 | tag: dev-build 237 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build_publish: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | with: 25 | platforms: arm64 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | - name: Login against Container registry 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ${{ env.REGISTRY }} 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Extract metadata (tags, labels) for Docker 38 | id: meta 39 | uses: docker/metadata-action@v4 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | 43 | - name: Build and push Docker image 44 | uses: docker/build-push-action@v4 45 | with: 46 | context: . 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | platforms: linux/amd64, linux/arm64 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build release 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - 'master' 8 | workflow_dispatch: 9 | 10 | env: 11 | PYTHON_VERSION: '3.10' 12 | 13 | jobs: 14 | windows: 15 | name: Windows 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up variables 23 | id: vars 24 | run: | 25 | Add-Content $env:GITHUB_OUTPUT "sha_short=$(git rev-parse --short HEAD)" 26 | 27 | - name: Append git revision to project version 28 | run: | 29 | (Get-Content version.py) ` 30 | -Replace '^__version__\s*=\s*"[^"]+', "`$0.${{steps.vars.outputs.sha_short}}" |` 31 | Out-File version.py 32 | 33 | # Ensure Python version 34 | - name: Set up Python 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{env.PYTHON_VERSION}} 38 | 39 | - name: Install project dependencies 40 | run: | 41 | python3 -m pip install wheel 42 | python3 -m pip install -r requirements.txt 43 | 44 | - name: Install UPX 45 | run: | 46 | Invoke-WebRequest -Uri https://github.com/upx/upx/releases/download/v4.0.2/upx-4.0.2-win64.zip -OutFile (Join-Path $env:Temp upx.zip) 47 | Expand-Archive -LiteralPath (Join-Path $env:Temp upx.zip) -DestinationPath $env:Temp 48 | Move-Item -Path (Join-Path $env:Temp upx-*) -Destination (Join-Path $env:Temp upx) 49 | Add-Content $env:GITHUB_PATH (Join-Path $env:Temp upx) 50 | 51 | - name: Install PyInstaller 52 | run: | 53 | python3 -m pip install pyinstaller 54 | 55 | - name: Create portable executable 56 | run: | 57 | pyinstaller build.spec 58 | 59 | - name: Create release folder 60 | run: | 61 | $FolderName = 'Twitch Drops Miner' 62 | New-Item $FolderName -ItemType Directory 63 | Copy-Item dist\*.exe $FolderName 64 | Copy-Item manual.txt $FolderName 65 | Compress-Archive -Path $FolderName -DestinationPath Twitch.Drops.Miner.Windows.zip 66 | 67 | - name: Upload build artifact 68 | uses: actions/upload-artifact@v4 69 | with: 70 | if-no-files-found: error 71 | name: Twitch.Drops.Miner.Windows 72 | path: Twitch.Drops.Miner.Windows.zip 73 | 74 | linux-pyinstaller: 75 | name: Linux (PyInstaller) 76 | runs-on: ubuntu-20.04 77 | 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v4 81 | 82 | - name: Set up variables 83 | id: vars 84 | run: | 85 | echo "sha_short=$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}" 86 | 87 | - name: Append git revision to project version 88 | run: | 89 | sed -ri "s/^__version__\s*=\s*\"[^\"]+/\0.${{ steps.vars.outputs.sha_short }}/" version.py 90 | 91 | # NOTE: We're only use a custom version of Python here because truststore requires at least Python 3.10, but Ubuntu 20.04 has Python 3.8. 92 | - name: Set up Python 93 | uses: actions/setup-python@v5 94 | with: 95 | python-version: ${{env.PYTHON_VERSION}} 96 | 97 | - name: Install system dependencies 98 | run: | 99 | sudo apt update 100 | sudo apt install gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-tk 101 | 102 | - name: Install project dependencies 103 | run: | 104 | python3 -m pip install wheel 105 | python3 -m pip install -r requirements.txt 106 | 107 | - name: Install PyInstaller 108 | run: | 109 | python3 -m pip install pyinstaller 110 | 111 | # NOTE: Remove this step if/once libxft gets updated to 2.3.5 or newer on Ubuntu 20.04, which currently has 2.3.3. 112 | - name: Build a recent version of libXft 113 | run: | 114 | mkdir -p /tmp/libXft 115 | cd /tmp/libXft 116 | curl -L https://xorg.freedesktop.org/releases/individual/lib/libXft-2.3.8.tar.xz -o libXft.tar.xz 117 | tar xvf libXft.tar.xz 118 | cd libXft-* 119 | ./configure --prefix=/tmp/libXft --sysconfdir=/etc --disable-static 120 | make 121 | make install-strip 122 | 123 | - name: Create portable executable 124 | run: | 125 | LD_LIBRARY_PATH=/tmp/libXft/lib xvfb-run --auto-servernum pyinstaller build.spec 126 | 127 | - name: Show PyInstaller warnings 128 | run: | 129 | cat build/build/warn-build.txt || true 130 | 131 | - name: Create release folder 132 | run: | 133 | folder='Twitch Drops Miner' 134 | mkdir "${folder}" 135 | cp manual.txt dist/* "${folder}" 136 | 7z a Twitch.Drops.Miner.Linux.PyInstaller.zip "${folder}" 137 | 138 | - name: Upload build artifact 139 | uses: actions/upload-artifact@v4 140 | with: 141 | if-no-files-found: error 142 | name: Twitch.Drops.Miner.Linux.PyInstaller 143 | path: Twitch.Drops.Miner.Linux.PyInstaller.zip 144 | 145 | linux-appimage: 146 | name: Linux (AppImage) 147 | runs-on: ubuntu-22.04 148 | 149 | steps: 150 | - name: Checkout code 151 | uses: actions/checkout@v4 152 | 153 | - name: Set up variables 154 | id: vars 155 | run: | 156 | echo "app_version=$(python3 -c 'from version import __version__ as v; print(v)')" >> "${GITHUB_OUTPUT}" 157 | echo "sha_short=$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}" 158 | 159 | - name: Append git revision to project version 160 | run: | 161 | sed -ri "s/^__version__\s*=\s*\"[^\"]+/\0.${{steps.vars.outputs.sha_short}}/" version.py 162 | 163 | - name: Install system dependencies 164 | run: | 165 | sudo apt update 166 | sudo apt install libgirepository1.0-dev python3-testresources 167 | 168 | - name: Download AppImage Builder 169 | run: | 170 | curl -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage -o appimage-builder 171 | chmod +x appimage-builder 172 | 173 | - name: Create AppImage 174 | env: 175 | APPIMAGE_EXTRACT_AND_RUN: 1 176 | APP_VERSION: ${{steps.vars.outputs.app_version}}.${{steps.vars.outputs.sha_short}} 177 | PYTHON_VERSION: ${{env.PYTHON_VERSION}} 178 | run: | 179 | ./appimage-builder --recipe appimage/AppImageBuilder.yml 180 | 181 | - name: Create release folder 182 | run: | 183 | folder='Twitch Drops Miner' 184 | mkdir "${folder}" 185 | cp *.AppImage manual.txt "${folder}" 186 | 7z a Twitch.Drops.Miner.Linux.AppImage.zip "${folder}" 187 | 188 | - name: Upload build artifact 189 | uses: actions/upload-artifact@v4 190 | with: 191 | if-no-files-found: error 192 | name: Twitch.Drops.Miner.Linux.AppImage 193 | path: Twitch.Drops.Miner.Linux.AppImage.zip 194 | 195 | update_releases_page: 196 | name: Upload builds to Releases 197 | #NOTEif: github.event.pull_request.merged == true 198 | needs: 199 | - windows 200 | - linux-pyinstaller 201 | - linux-appimage 202 | runs-on: ubuntu-latest 203 | permissions: 204 | contents: write 205 | 206 | steps: 207 | - name: Checkout repository 208 | uses: actions/checkout@v3 209 | 210 | - name: Get next version 211 | uses: reecetech/version-increment@2024.4.3 212 | id: version 213 | with: 214 | scheme: semver 215 | increment: minor 216 | 217 | - name: Set up variables 218 | id: vars 219 | run: | 220 | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" 221 | echo "sha_short=$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}" 222 | 223 | - name: Download build artifacts from previous jobs 224 | uses: actions/download-artifact@v4 225 | with: 226 | path: artifacts 227 | 228 | - name: Read patch notes from file 229 | id: patch_notes 230 | uses: juliangruber/read-file-action@v1.1.7 231 | with: 232 | path: ./patch_notes.txt 233 | 234 | - name: Upload builds to Releases 235 | uses: ncipollo/release-action@v1 236 | with: 237 | allowUpdates: true 238 | artifactErrorsFailBuild: true 239 | artifacts: artifacts/*/* 240 | body: | 241 | **This is an automatically generated updated version of the application, that includes the latest master branch changes.** 242 | **This build should be stable, but if you encounter anything, please report any issues you find.** 243 | 244 | - Last build date: `${{ steps.vars.outputs.date_now }}` 245 | - Reference commit: ${{ github.sha }} 246 | 247 | *** 248 | ## Patch notes: 249 | ${{ steps.patch_notes.outputs.content }} 250 | 251 | name: ${{ steps.version.outputs.v-version }} - Updated tested build (${{ steps.vars.outputs.sha_short }}) 252 | prerelease: false 253 | removeArtifacts: true 254 | tag: ${{ steps.version.outputs.v-version }} 255 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cache files 2 | __pycache__ 3 | .mypy_cache 4 | # Build and dist files 5 | /build 6 | /dist 7 | /*.exe 8 | /*.zip 9 | /*.tmp 10 | /Twitch Drops Miner 11 | # Dev files 12 | /.idea 13 | /.vscode 14 | /env 15 | /venv 16 | /cache 17 | /*.jar 18 | /*log.txt 19 | /lock.file 20 | settings.json 21 | healthcheck.timestamp 22 | #/lang/English.json 23 | # AppImage 24 | /AppDir 25 | /appimage-builder 26 | /appimage-build 27 | /*.AppImage 28 | /*.AppImage.zsync 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | }, 14 | { 15 | "name": "Run App", 16 | "type": "python", 17 | "request": "launch", 18 | "program": "main.py", 19 | "args": [""], 20 | "console": "integratedTerminal", 21 | "justMyCode": false 22 | }, 23 | { 24 | "name": "Run App (INFO)", 25 | "type": "python", 26 | "request": "launch", 27 | "program": "main.py", 28 | "args": ["-vv"], 29 | "console": "integratedTerminal", 30 | "justMyCode": false 31 | }, 32 | { 33 | "name": "Run App (INFO+Tray)", 34 | "type": "python", 35 | "request": "launch", 36 | "program": "main.py", 37 | "args": ["-vv", "--tray"], 38 | "console": "integratedTerminal", 39 | "justMyCode": false 40 | }, 41 | { 42 | "name": "Run App (CALL)", 43 | "type": "python", 44 | "request": "launch", 45 | "program": "main.py", 46 | "args": ["-vvv"], 47 | "console": "integratedTerminal", 48 | "justMyCode": false 49 | }, 50 | { 51 | "name": "Run App (DEBUG+WS)", 52 | "type": "python", 53 | "request": "launch", 54 | "program": "main.py", 55 | "args": ["-vvv", "--debug-ws"], 56 | "console": "integratedTerminal", 57 | "justMyCode": false 58 | }, 59 | { 60 | "name": "Debug GUI", 61 | "type": "python", 62 | "request": "launch", 63 | "program": "gui.py", 64 | "console": "integratedTerminal", 65 | "justMyCode": false 66 | }, 67 | { 68 | "name": "Run App (DEBUG+GQL)", 69 | "type": "python", 70 | "request": "launch", 71 | "program": "main.py", 72 | "args": ["-vvv", "--debug-gql"], 73 | "console": "integratedTerminal", 74 | "justMyCode": false 75 | }, 76 | { 77 | "name": "Run App (DEBUG+GQL+WS+Log)", 78 | "type": "python", 79 | "request": "launch", 80 | "program": "main.py", 81 | "args": ["-vvv", "--debug-gql", "--debug-ws", "--log"], 82 | "console": "integratedTerminal", 83 | "justMyCode": false 84 | }, 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build App", 8 | "type": "shell", 9 | "command": "build.bat", 10 | "problemMatcher": [], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "presentation": { 16 | "showReuseMessage": false 17 | } 18 | }, 19 | { 20 | "label": "Build and Pack App", 21 | "type": "shell", 22 | "command": "pack.bat", 23 | "problemMatcher": [], 24 | "presentation": { 25 | "showReuseMessage": false 26 | }, 27 | "dependsOn": ["Build App"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | # Install xvfb - a virtual X display server for the GUI to display to 4 | RUN apt-get update && apt-get upgrade -y 5 | RUN apt-get install -y libgirepository1.0-dev xvfb \ 6 | python3-gi gobject-introspection gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1 7 | 8 | COPY . /TwitchDropsMiner/ 9 | WORKDIR /TwitchDropsMiner/ 10 | 11 | RUN pip install --upgrade pip 12 | RUN pip install -r requirements.txt 13 | 14 | RUN chmod +x ./docker_entrypoint.sh 15 | ENTRYPOINT ["./docker_entrypoint.sh"] 16 | 17 | HEALTHCHECK --interval=10s --timeout=5s --start-period=5m --retries=3 CMD ./healthcheck.sh 18 | 19 | ENV UNLINKED_CAMPAIGNS=1 20 | ENV prioritize_by_ending_soonest=0 21 | CMD timeout $(( (60 - $(date +%-M)) * 60 - $(date +%-S) )) python main.py -vvvv 22 | 23 | # Example command to build: 24 | # docker build -t twitch_drops_miner . 25 | 26 | # Suggested command to run: 27 | # docker run -itd --init --pull=always --restart=always -v ./cookies.jar:/TwitchDropsMiner/cookies.jar -v ./settings.json:/TwitchDropsMiner/settings.json:ro -v /etc/localtime:/etc/localtime:ro --name twitch_drops_miner ghcr.io/valentin-metz/twitchdropsminer:master 28 | 29 | # Suggested additional containers for monitoring: 30 | # docker run -d --restart=always --name autoheal -e AUTOHEAL_CONTAINER_LABEL=all -v /var/run/docker.sock:/var/run/docker.sock -v /etc/localtime:/etc/localtime:ro willfarrell/autoheal 31 | # docker run -d --restart=always --name watchtower -v ~/.docker/config.json:/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock -v /etc/localtime:/etc/localtime:ro containrrr/watchtower --cleanup --include-restarting --include-stopped --interval 60 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 DevilXD 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch Drops Miner 2 | 3 | Thanks to @DevilXD and other contributors from the [original repo](https://github.com/DevilXD/TwitchDropsMiner) for the vast majority of the code. 4 | 5 | This application allows you to AFK mine timed Twitch drops, without having to worry about switching channels when the one you were watching goes offline, claiming the drops, or even receiving the stream data itself. This helps both you and Twitch save on bandwidth and hassle. Everyone wins! 6 | 7 | ### How It Works: 8 | 9 | Every ~20 seconds, the application asks Twitch for a URL to the raw stream data of the channel currently being watched. It then fetches the metadata of this data stream - this is enough to advance the drops. Note that this completely bypasses the need to download any actual stream video and sound. To keep the status (ONLINE or OFFLINE) of the channels up-to-date, there's a websocket connection established that receives events about streams going up or down, or updates regarding the current amount of viewers. 10 | 11 | ### Features: 12 | 13 | - Stream-less drop mining - save on bandwidth. 14 | - Game priority and exclusion lists, allowing you to focus on mining what you want, in the order you want, and ignore what you don't want. 15 | - Sharded websocket connection, allowing for tracking up to `8*25-2=199` channels at the same time. 16 | - Automatic drop campaigns discovery based on linked accounts (requires you to do [account linking](https://www.twitch.tv/drops/campaigns) yourself though) 17 | - Stream tags and drop campaign validation, to ensure you won't end up mining a stream that can't earn you the drop. 18 | - Automatic channel stream switching, when the one you were currently watching goes offline, as well as when a channel streaming a higher priority game goes online. 19 | - Login session is saved in a cookies file, so you don't need to login every time. 20 | - Mining is automatically started as new campaigns appear, and stopped when the last available drops have been mined. 21 | 22 | ### Usage: 23 | 24 | - Download and unzip [the latest release](https://github.com/DevilXD/TwitchDropsMiner/releases) - it's recommended to keep it in the folder it comes in. 25 | - Run it and login into your Twitch account using your username and password, and a 2FA key if you have one setup. It's recommended to avoid having to double-take this step, as you can run into CAPTCHA that will prevent you from trying to log in again for the next 12+ hours. You can retry afterwards though. 26 | - After a successful login, the app should fetch a list of all available campaigns and games you can mine drops for - you can then select and add games of choice to the Priority List available on the Settings tab, and then press on the `Reload` button to start processing. It will fetch a list of all applicable streams it can watch, and start mining right away. You can also manually switch to a different channel as needed. 27 | - Make sure to link your Twitch account to game accounts on the [campaigns page](https://www.twitch.tv/drops/campaigns), to enable more games to be mined. 28 | - Persistent cookies will be stored in the `cookies.jar` file, from which the authorization (login) information will be restored on each subsequent run. 29 | 30 | ### Pictures: 31 | 32 | ![Main](https://user-images.githubusercontent.com/4180725/164298155-c0880ad7-6423-4419-8d73-f3c053730a1b.png) 33 | ![Inventory](https://user-images.githubusercontent.com/4180725/164298315-81cae0d2-24a4-4822-a056-154fd763c284.png) 34 | ![Settings](https://user-images.githubusercontent.com/4180725/164298391-b13ad40d-3881-436c-8d4c-34e2bbe33a78.png) 35 | ![Help](https://github.com/Windows200000/TwitchDropsMiner-updated/assets/72623832/ca1c25e1-650f-415b-ab9d-8cfc001fc7e6) 36 | 37 | 38 | ### Notes: 39 | 40 | - Make sure to keep your cookies file safe, as the authorization information it stores can give another person access to your Twitch account. 41 | - Successfully logging into your Twitch account in the application, may cause Twitch to send you a "New Login" notification email. This is normal - you can verify that it comes from your own IP address. The application uses the Twitch's SmartTV account linking process, so the detected browser during the login should signify that as well. 42 | - The time remaining timer always countdowns a single minute and then stops - it is then restarted only after the application redetermines the remaining time. This "redetermination" can happen as early as at 10 seconds in a minute remaining, and as late as 20 seconds after the timer reaches zero (especially when finishing mining a drop), but is generally only an approximation and does not represent nor affect actual mining speed. The time variations are due to Twitch sometimes not reporting drop progress at all, or reporting progress for the wrong drop - these cases have all been accounted for in the application though. 43 | 44 | ### Notes about the Windows build: 45 | 46 | - To achieve a portable-executable format, the application is packaged with PyInstaller into an `EXE`. Some non-mainstream antivirus engines might report the packaged executable as a trojan, because PyInstaller has been used by others to package malicious Python code in the past. These reports can be safely ignored. If you absolutely do not trust the executable, you'll have to install Python yourself and run everything from source. 47 | - The executable uses the `%TEMP%` directory for temporary runtime storage of files, that don't need to be exposed to the user (like compiled code and translation files). For persistent storage, the directory the executable resides in is used instead. 48 | - The autostart feature is implemented as a registry entry to the current user's (`HKCU`) autostart key. It is only altered when toggling the respective option. If you relocate the app to a different directory, the autostart feature will stop working, until you toggle the option off and back on again 49 | 50 | ### Notes about the Linux build: 51 | 52 | - The Linux app is built and distributed using two distinct portable-executable formats: [AppImage](https://appimage.org/) and [PyInstaller](https://pyinstaller.org/). 53 | - There are no major differences between the two formats, but if you're looking for a recommendation, use the AppImage. 54 | - The Linux app should work out of the box on any modern distribution, as long as it has `glibc>=2.31` (PyInstaller package) or `glibc>=2.35` (AppImage package), plus a working display server. 55 | - Every feature of the app is expected to work on Linux just as well as it does on Windows. If you find something that's broken, please [open a new issue](https://github.com/DevilXD/TwitchDropsMiner/issues/new). 56 | - The size of the Linux app is significantly larger than the Windows app due to the inclusion of the `gtk3` library (and its dependencies), which is required for proper system tray/notifications support. 57 | - As an alternative to the native Linux app, you can run the Windows app via [Wine](https://www.winehq.org/) instead. It works really well! 58 | 59 | ### Support 60 | 61 |
62 | 63 | [![Buy me a coffee](https://i.imgur.com/cL95gzE.png)]( 64 | https://www.buymeacoffee.com/DevilXD 65 | ) 66 | [![Support me on Patreon](https://i.imgur.com/Mdkb9jq.png)]( 67 | https://www.patreon.com/bePatron?u=26937862 68 | ) 69 | 70 |
71 | 72 | ### Advanced Usage: 73 | 74 | If you'd be interested in running the latest master from source or building your own executable, see the wiki page explaining how to do so: https://github.com/DevilXD/TwitchDropsMiner/wiki/Setting-up-the-environment,-building-and-running 75 | 76 | ### Project goals: 77 | 78 | Twitch Drops Miner (TDM for short) has been designed with a couple of simple goals in mind. These are, specifically: 79 | 80 | - Twitch Drops oriented - it's in the name. That's what I made it for. 81 | - Easy to use for an average person. Includes a nice looking GUI and is packaged as a ready-to-go executable, without requiring an existing Python installation to work. 82 | - Intended as a helper tool that starts together with your PC, runs in the background through out the day, and then closes together with your PC shutting down at the end of the day. If it can run continuously for 24 hours at minimum, and not run into any errors, I'd call that good enough already. 83 | - Requiring a minimum amount of attention during operation - check it once or twice through out the day to see if everything's fine with it. 84 | - Underlying service friendly - the amount of interactions done with the Twitch site is kept to the minimum required for reliable operation, at a level achievable by a diligent site user. 85 | 86 | TDM is not intended for/as: 87 | 88 | - Mining channel points - again, it's about the drops: only. The current points you're getting are a byproduct of getting the drops, not the main goal of it. 89 | - Mining anything else besides Twitch drops - no, I won't be adding support for a random 3rd party site that also happens to rely on watching Twitch streams. 90 | - Unattended operation: worst case scenario, it'll stop working and you'll hopefully notice that at some point. Hopefully. 91 | - 100% uptime application, due to the underlying nature of it, expect fatal errors to happen every so often. 92 | - Being hosted on a remote server as a 24/7 miner. 93 | - Being used with more than one managed account. 94 | - Mining campaigns the managed account isn't linked to. 95 | 96 | This means that features such as: 97 | 98 | - It being possible to run it without a GUI, or with only a console attached. 99 | - Any form of automatic restart when an error happens. 100 | - Docker or any other form of remote deployment. 101 | - Using it with more than one managed account. 102 | - Making it possible to mine campaigns that the managed account isn't linked to. 103 | - Anything that increases the site processing load caused by the application. 104 | - Any form of additional notifications system (email, webhook, etc.), beyond what's already implemented. 105 | 106 | ..., are most likely not going to be a feature, ever. You're welcome to search through the existing issues to comment on your point of view on the relevant matters, where applicable. Otherwise, most of the new issues that go against these goals will be closed and the user will be pointed to this paragraph. 107 | 108 | For more context about these goals, please check out these issues: [#161](https://github.com/DevilXD/TwitchDropsMiner/issues/161), [#105](https://github.com/DevilXD/TwitchDropsMiner/issues/105), [#84](https://github.com/DevilXD/TwitchDropsMiner/issues/84) 109 | 110 | ### Credits: 111 | 112 | 120 | 121 | @Suz1e - For the entirety of the Chinese (简体中文) translation and revisions. 122 | @wwj010 - For the Chinese (简体中文) translation corrections and revisions. 123 | @nwvh - For the entirety of the Czech (Čeština) translation. 124 | @ThisIsCyreX - For the entirety of the German (Deutsch) translation. 125 | @Shofuu - For the entirety of the Spanish (Español) translation. 126 | @zarigata - For the entirety of the Portuguese (Português) translation. 127 | @alikdb - For the entirety of the Turkish (Türkçe) translation. 128 | @roobini-gamer - For the entirety of the French (Français) translation. 129 | @Sergo1217 - For the entirety of the Russian (Русский) translation. 130 | @Ricky103403 - For the entirety of the Traditional Chinese (繁體中文) translation. 131 | @Patriot99 - For the Polish (Polski) translation (co-authored with @DevilXD). 132 | @Nollasko - For the entirety of the Ukrainian (Українська) translation. 133 | @casungo - For the entirety of the Italian (Italiano) translation. 134 | @Bamboozul - For the entirety of the Arabic (العربية) translation. 135 | @Kjerne - For the entirety of the Danish (Dansk) translation. 136 | 137 | For updating Translations: @Kuddus73, @VSeryi, @Windows200000, @BreakshadowCN, @kilroy98, @zelda0079, @Calvineries, @notNSANE, @ElvisDesigns, @DogancanYr, @Nollasko, @rvpv, @flxderdev, @5wi5wi, @fgr1178707QQ, @Suz1e, @Patriot99, @overkongen, @zizimonzter, @sabala, @hiroweeb, @T-Raptor, @LoLyeah 138 | -------------------------------------------------------------------------------- /appimage/AppImageBuilder.yml: -------------------------------------------------------------------------------- 1 | # Useful links: 2 | # https://appimage-builder.readthedocs.io/en/latest/reference/recipe.html 3 | 4 | version: 1 5 | 6 | script: 7 | # Clean up any previous builds. 8 | - rm -rf "$BUILD_DIR" "$TARGET_APPDIR" || true 9 | - mkdir -p "$BUILD_DIR" "$TARGET_APPDIR" 10 | - cd "$BUILD_DIR" 11 | 12 | # Build a recent version of libXft first. This fixes an issue where the app won't start with libXft 2.3.3 if an emoji font is installed on the system. 13 | - curl -L https://xorg.freedesktop.org/releases/individual/lib/libXft-2.3.8.tar.xz -o libXft.tar.xz 14 | - tar xvf libXft.tar.xz 15 | - cd libXft-2.3.8 16 | - ./configure --prefix="$TARGET_APPDIR/usr" --disable-static 17 | - make -j$(nproc) 18 | - make install-strip 19 | 20 | # Package the app. 21 | - mkdir -p "$TARGET_APPDIR"/usr/{src,share/icons/hicolor/128x128/apps} 22 | - cp -r "$SOURCE_DIR/../lang" "$SOURCE_DIR/../pickaxe.ico" "$SOURCE_DIR"/../*.py "$TARGET_APPDIR/usr/src" 23 | - cp "$SOURCE_DIR/pickaxe.png" "$TARGET_APPDIR/usr/share/icons/hicolor/128x128/apps/io.github.devilxd.twitchdropsminer.png" 24 | 25 | # Create a virtual environment and install up-to-date versions of meson and ninja. 26 | - python3 -m venv env && source ./env/bin/activate && python3 -m pip install meson ninja 27 | # Install requirements. 28 | - python3 -m pip install --ignore-installed --prefix=/usr --root="$TARGET_APPDIR" -r "$SOURCE_DIR/../requirements.txt" certifi 29 | # Generate byte-code files beforehand, for slightly faster app startup. 30 | - python3 -m compileall "$TARGET_APPDIR/usr/src/"*.py 31 | 32 | AppDir: 33 | app_info: 34 | id: io.github.devilxd.twitchdropsminer 35 | name: Twitch Drops Miner 36 | icon: io.github.devilxd.twitchdropsminer 37 | version: '{{APP_VERSION}}' 38 | exec: usr/bin/python3 39 | exec_args: '${APPDIR}/usr/src/main.py $@' 40 | 41 | apt: 42 | arch: amd64 43 | sources: 44 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy main universe 45 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-updates main universe 46 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-backports main universe 47 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-security main universe 48 | key_url: http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C 49 | 50 | include: 51 | - gir1.2-appindicator3-0.1 52 | - python3-tk 53 | 54 | exclude: 55 | - adwaita-icon-theme 56 | - dconf-service 57 | - glib-networking-services 58 | - gsettings-desktop-schemas 59 | - hicolor-icon-theme 60 | - humanity-icon-theme 61 | - libavahi-client3 62 | - libavahi-common3 63 | - libbrotli1 64 | - libcolord2 65 | - libcups2 66 | - libdb5.3 67 | - libdconf1 68 | - libgmp10 69 | - libgnutls30 70 | - libgssapi-krb5-2 71 | - libhogweed5 72 | - libicu66 73 | - libjson-glib-1.0-0 74 | - libk5crypto3 75 | - libkrb5-3 76 | - libkrb5support0 77 | - liblcms2-2 78 | - libncursesw6 79 | - libnettle7 80 | - libp11-kit0 81 | - libpangoxft-1.0-0 82 | - libpsl5 83 | - librest-0.7-0 84 | - libsoup2.4-1 85 | - libsoup-gnome2.4-1 86 | - libsqlite3-0 87 | - libtasn1-6 88 | - libtiff5 89 | - libtinfo6 90 | - libunistring2 91 | - libwebp6 92 | - libxft2 # We'll ship our own, updated version of this library. 93 | - libxml2 94 | - mime-support 95 | - readline-common 96 | - tzdata 97 | - xkb-data 98 | 99 | files: 100 | exclude: 101 | - etc 102 | - usr/bin/normalizer 103 | - usr/bin/pdb3* 104 | - usr/bin/py3* 105 | - usr/bin/pydoc* 106 | - usr/bin/pygettext3* 107 | - usr/include 108 | # The next 2 files come from our own build of libXft, and they can be removed. 109 | - usr/lib/libXft.la 110 | - usr/lib/pkgconfig 111 | - usr/lib/python3.9 112 | - usr/lib/valgrind 113 | - usr/lib/*-linux-gnu/engines-1.1 114 | - usr/lib/*-linux-gnu/glib-2.0 115 | - usr/lib/*-linux-gnu/gtk-3.0 116 | - usr/lib/*-linux-gnu/libgtk-3.0 117 | - usr/share/applications 118 | - usr/share/binfmts 119 | - usr/share/bug 120 | - usr/share/doc 121 | - usr/share/doc-base 122 | - usr/share/glib-2.0 123 | - usr/share/lintian 124 | - usr/share/man 125 | - usr/share/pixmaps 126 | - usr/share/python3 127 | - usr/share/themes 128 | 129 | runtime: 130 | env: 131 | PATH: '${APPDIR}/usr/bin:${PATH}' 132 | PYTHONHOME: '${APPDIR}/usr' 133 | PYTHONPATH: '${APPDIR}/usr/lib/python3.10/tkinter:${APPDIR}/usr/lib/python3.10/site-packages' 134 | APPDIR_LIBRARY_PATH: '${APPDIR}/usr/lib:${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/lib/x86_64-linux-gnu' 135 | TCL_LIBRARY: '${APPDIR}/usr/share/tcltk/tcl8.6' 136 | TK_LIBRARY: '${APPDIR}/usr/lib/tcltk/x86_64-linux-gnu/tk8.6' 137 | TKPATH: '${APPDIR}/usr/lib/tcltk/x86_64-linux-gnu/tk8.6' 138 | # The app seems to have problems running on Wayland at the moment. 139 | # See: https://github.com/DevilXD/TwitchDropsMiner/issues/321 140 | GDK_BACKEND: x11 141 | 142 | AppImage: 143 | arch: x86_64 144 | file_name: Twitch.Drops.Miner-x86_64.AppImage 145 | sign-key: None 146 | update-information: guess 147 | -------------------------------------------------------------------------------- /appimage/pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valentin-Metz/TwitchDropsMiner/4dbd5391f1e4be481709d0f0acda40ab023948e4/appimage/pickaxe.png -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | set dirpath=%~dp0 4 | if "%dirpath:~-1%" == "\" set dirpath=%dirpath:~0,-1% 5 | 6 | if not exist "%dirpath%\env" ( 7 | echo: 8 | echo No virtual environment found! Run setup_env.bat to set it up first. 9 | echo: 10 | pause 11 | exit 12 | ) 13 | 14 | if not exist "%dirpath%\env\scripts\pyinstaller.exe" ( 15 | "%dirpath%\env\scripts\pip" install pyinstaller 16 | "%dirpath%\env\scripts\python" "%dirpath%\env\scripts\pywin32_postinstall.py" -install -silent 17 | ) 18 | "%dirpath%/env/scripts/pyinstaller" "%dirpath%\build.spec" 19 | -------------------------------------------------------------------------------- /build.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from __future__ import annotations 3 | 4 | import sys 5 | from pathlib import Path 6 | from typing import Any, TYPE_CHECKING 7 | 8 | SELF_PATH = str(Path(".").absolute()) 9 | if SELF_PATH not in sys.path: 10 | sys.path.insert(0, SELF_PATH) 11 | 12 | from constants import WORKING_DIR, SITE_PACKAGES_PATH, DEFAULT_LANG 13 | 14 | if TYPE_CHECKING: 15 | from PyInstaller.building.api import PYZ, EXE 16 | from PyInstaller.building.build_main import Analysis 17 | 18 | # (source_path, dest_path, required) 19 | to_add: list[tuple[Path, str, bool]] = [ 20 | (Path("pickaxe.ico"), '.', True), # icon file 21 | # SeleniumWire HTTPS/SSL cert file and key 22 | (Path(SITE_PACKAGES_PATH, "seleniumwire/ca.crt"), "./seleniumwire", False), 23 | (Path(SITE_PACKAGES_PATH, "seleniumwire/ca.key"), "./seleniumwire", False), 24 | ] 25 | for lang_filepath in WORKING_DIR.joinpath("lang").glob("*.json"): 26 | to_add.append((lang_filepath, "lang", True)) 27 | 28 | # ensure the required to-be-added data exists 29 | datas: list[tuple[Path, str]] = [] 30 | for source_path, dest_path, required in to_add: 31 | if source_path.exists(): 32 | datas.append((source_path, dest_path)) 33 | elif required: 34 | raise FileNotFoundError(str(source_path)) 35 | 36 | hooksconfig: dict[str, Any] = {} 37 | binaries: list[tuple[Path, str]] = [] 38 | hiddenimports: list[str] = [ 39 | "PIL._tkinter_finder", 40 | "setuptools._distutils.log", 41 | "setuptools._distutils.dir_util", 42 | "setuptools._distutils.file_util", 43 | "setuptools._distutils.archive_util", 44 | ] 45 | 46 | if sys.platform == "linux": 47 | # Needed files for better system tray support on Linux via pystray (AppIndicator backend). 48 | datas.append((Path("/usr/lib/girepository-1.0/AppIndicator3-0.1.typelib"), "gi_typelibs")) 49 | binaries.append((Path("/lib/x86_64-linux-gnu/libappindicator3.so.1"), ".")) 50 | hiddenimports.extend([ 51 | "gi.repository.Gtk", 52 | "gi.repository.GObject", 53 | ]) 54 | hooksconfig = { 55 | "gi": { 56 | "icons": [], 57 | "themes": [], 58 | "languages": ["en_US"] 59 | } 60 | } 61 | 62 | block_cipher = None 63 | a = Analysis( 64 | ["main.py"], 65 | pathex=[], 66 | datas=datas, 67 | excludes=[], 68 | hookspath=[], 69 | noarchive=False, 70 | runtime_hooks=[], 71 | binaries=binaries, 72 | cipher=block_cipher, 73 | hooksconfig=hooksconfig, 74 | hiddenimports=hiddenimports, 75 | win_private_assemblies=False, 76 | win_no_prefer_redirects=False, 77 | ) 78 | 79 | # Exclude unneeded Linux libraries 80 | excluded_binaries = [ 81 | "libicudata.so.66", 82 | "libicuuc.so.66", 83 | "librsvg-2.so.2" 84 | ] 85 | a.binaries = [b for b in a.binaries if b[0] not in excluded_binaries] 86 | 87 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 88 | exe = EXE( 89 | pyz, 90 | a.scripts, 91 | a.binaries, 92 | a.zipfiles, 93 | a.datas, 94 | [], 95 | upx=True, 96 | debug=False, 97 | strip=False, 98 | console=False, 99 | upx_exclude=[], 100 | target_arch=None, 101 | icon="pickaxe.ico", 102 | runtime_tmpdir=None, 103 | codesign_identity=None, 104 | entitlements_file=None, 105 | bootloader_ignore_signals=False, 106 | disable_windowed_traceback=False, 107 | name="Twitch Drops Miner (by DevilXD)", 108 | ) 109 | -------------------------------------------------------------------------------- /cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import asyncio 3 | from datetime import datetime, timedelta, timezone 4 | 5 | import io 6 | import json 7 | from typing import Dict, TypedDict, NewType, TYPE_CHECKING 8 | 9 | from utils import json_load, json_save 10 | from constants import URLType, CACHE_PATH, CACHE_DB 11 | 12 | from PIL import Image as Image_module 13 | from PIL.ImageTk import PhotoImage 14 | 15 | 16 | if TYPE_CHECKING: 17 | from gui import GUIManager 18 | from PIL.Image import Image 19 | from typing_extensions import TypeAlias 20 | 21 | 22 | ImageHash = NewType("ImageHash", str) 23 | ImageSize: TypeAlias = "tuple[int, int]" 24 | 25 | 26 | class ExpiringHash(TypedDict): 27 | hash: ImageHash 28 | expires: datetime 29 | 30 | 31 | Hashes = Dict[URLType, ExpiringHash] 32 | default_database: Hashes = {} 33 | 34 | 35 | class CurrentSeconds: 36 | LIFETIME = timedelta(days=7) 37 | 38 | def set_current_seconds(value): 39 | global current_seconds 40 | current_seconds = value 41 | 42 | def get_current_seconds(): 43 | return current_seconds 44 | 45 | 46 | class ImageCache: 47 | LIFETIME = timedelta(days=7) 48 | 49 | def __init__(self, manager: GUIManager) -> None: 50 | self._root = manager._root 51 | self._twitch = manager._twitch 52 | cleanup: bool = False 53 | CACHE_PATH.mkdir(parents=True, exist_ok=True) 54 | try: 55 | self._hashes: Hashes = json_load(CACHE_DB, default_database, merge=False) 56 | except json.JSONDecodeError: 57 | # if we can't load the mapping file, delete all existing files, 58 | # then reinitialize the image cache anew 59 | cleanup = True 60 | self._hashes = default_database.copy() 61 | self._images: dict[ImageHash, Image] = {} 62 | self._photos: dict[tuple[ImageHash, ImageSize], PhotoImage] = {} 63 | self._lock = asyncio.Lock() 64 | self._altered: bool = False 65 | # cleanup the URLs 66 | hash_counts: dict[ImageHash, int] = {} 67 | now = datetime.now(timezone.utc) 68 | for url, hash_dict in list(self._hashes.items()): 69 | img_hash = hash_dict["hash"] 70 | if img_hash not in hash_counts: 71 | hash_counts[img_hash] = 0 72 | if now >= hash_dict["expires"]: 73 | del self._hashes[url] 74 | self._altered = True 75 | else: 76 | hash_counts[img_hash] += 1 77 | for img_hash, count in hash_counts.items(): 78 | if count == 0: 79 | # hashes come with an extension already 80 | CACHE_PATH.joinpath(img_hash).unlink(missing_ok=True) 81 | # NOTE: The hashes are deleted from self._hashes above 82 | if cleanup: 83 | # This cleanups the cache folder from unused PNG files 84 | orphans = [ 85 | file.name for file in CACHE_PATH.glob("*.png") if file.name not in hash_counts 86 | ] 87 | for filename in orphans: 88 | CACHE_PATH.joinpath(filename).unlink(missing_ok=True) 89 | 90 | def save(self, *, force: bool = False) -> None: 91 | if self._altered or force: 92 | json_save(CACHE_DB, self._hashes, sort=True) 93 | 94 | def _new_expires(self) -> datetime: 95 | return datetime.now(timezone.utc) + self.LIFETIME 96 | 97 | def _hash(self, image: Image) -> ImageHash: 98 | pixel_data = list(image.resize((10, 10), Image_module.LANCZOS).convert('L').getdata()) 99 | avg_pixel = sum(pixel_data) / len(pixel_data) 100 | bits = ''.join('1' if px >= avg_pixel else '0' for px in pixel_data) 101 | return ImageHash(f"{int(bits, 2):x}.png") 102 | 103 | async def get(self, url: URLType, size: ImageSize | None = None) -> PhotoImage: 104 | async with self._lock: 105 | image: Image | None = None 106 | if url in self._hashes: 107 | img_hash = self._hashes[url]["hash"] 108 | self._hashes[url]["expires"] = self._new_expires() 109 | if img_hash in self._images: 110 | image = self._images[img_hash] 111 | else: 112 | try: 113 | self._images[img_hash] = image = Image_module.open(CACHE_PATH / img_hash) 114 | except FileNotFoundError: 115 | pass 116 | if image is None: 117 | async with self._twitch.request("GET", url) as response: 118 | image = Image_module.open(io.BytesIO(await response.read())) 119 | img_hash = self._hash(image) 120 | self._images[img_hash] = image 121 | image.save(CACHE_PATH / img_hash) 122 | self._hashes[url] = { 123 | "hash": img_hash, 124 | "expires": self._new_expires() 125 | } 126 | # NOTE: If self._hashes ever stops being updated in both above if cases, 127 | # this will need to be moved 128 | self._altered = True 129 | if size is None: 130 | size = image.size 131 | photo_key = (img_hash, size) 132 | if photo_key in self._photos: 133 | return self._photos[photo_key] 134 | if image.size != size: 135 | image = image.resize(size, Image_module.ADAPTIVE) 136 | self._photos[photo_key] = photo = PhotoImage(master=self._root, image=image) 137 | return photo 138 | -------------------------------------------------------------------------------- /docker_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf '%(%Y-%m-%d %H:%M:%S)T: Started new container\n' -1 4 | 5 | # Start X virtual framebuffer 6 | export DISPLAY=:1 7 | rm /tmp/.X1-lock -f 8 | Xvfb :1 -screen 0 640x480x8 -nolisten tcp & 9 | 10 | # Execute CMDs 11 | "$@" 12 | 13 | printf '%(%Y-%m-%d %H:%M:%S)T: Stopped container\n' -1 14 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | class MinerException(Exception): 2 | """ 3 | Base exception class for this application. 4 | """ 5 | def __init__(self, *args: object): 6 | if args: 7 | super().__init__(*args) 8 | else: 9 | super().__init__("Unknown miner error") 10 | 11 | 12 | class ExitRequest(MinerException): 13 | """ 14 | Raised when the application is requested to exit from outside of the main loop. 15 | 16 | Intended for internal use only. 17 | """ 18 | def __init__(self): 19 | super().__init__("Application was requested to exit") 20 | 21 | 22 | class ReloadRequest(MinerException): 23 | """ 24 | Raised when the application is requested to reload entirely, without closing the GUI. 25 | 26 | Intended for internal use only. 27 | """ 28 | def __init__(self): 29 | super().__init__("Application was requested to reload entirely") 30 | 31 | 32 | class RequestInvalid(MinerException): 33 | """ 34 | Raised when a request becomes no longer valid inside its retry loop. 35 | 36 | Intended for internal use only. 37 | """ 38 | def __init__(self): 39 | super().__init__("Request became invalid during its retry loop") 40 | 41 | 42 | class RequestException(MinerException): 43 | """ 44 | Raised for cases where a web request doesn't return what we wanted it to. 45 | """ 46 | def __init__(self, *args: object): 47 | if args: 48 | super().__init__(*args) 49 | else: 50 | super().__init__("Unknown error during request") 51 | 52 | 53 | class WebsocketClosed(RequestException): 54 | """ 55 | Raised when the websocket connection has been closed. 56 | 57 | Attributes: 58 | ----------- 59 | received: bool 60 | `True` if the closing was caused by our side receiving a close frame, `False` otherwise. 61 | """ 62 | def __init__(self, *args: object, received: bool = False): 63 | if args: 64 | super().__init__(*args) 65 | else: 66 | super().__init__("Websocket has been closed") 67 | self.received: bool = received 68 | 69 | 70 | class LoginException(RequestException): 71 | """ 72 | Raised when an exception occurs during login phase. 73 | """ 74 | def __init__(self, *args: object): 75 | if args: 76 | super().__init__(*args) 77 | else: 78 | super().__init__("Unknown error during login") 79 | 80 | 81 | class CaptchaRequired(LoginException): 82 | """ 83 | The most dreaded thing about automated scripts... 84 | """ 85 | def __init__(self): 86 | super().__init__("Captcha is required") 87 | -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | timestamp_file="./healthcheck.timestamp" 4 | maximum_age=300 5 | 6 | # Check if the timestamp file exists 7 | if [[ ! -f "$timestamp_file" ]]; then 8 | echo "Timestamp file does not exist" 9 | exit 1 10 | fi 11 | 12 | # Read the timestamp from the file 13 | last_timestamp=$(cat "$timestamp_file") 14 | 15 | # Get the current Unix timestamp 16 | current_timestamp=$(date +%s) 17 | 18 | # Calculate the difference in seconds 19 | difference=$((current_timestamp - last_timestamp)) 20 | 21 | # Check if the difference is less than the maximum_age 22 | if [[ $difference -lt $maximum_age ]]; then 23 | echo "Timestamp is valid" 24 | exit 0 25 | else 26 | echo "Timestamp is outdated" 27 | exit 1 28 | fi 29 | -------------------------------------------------------------------------------- /lang/Dansk.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Danish", 3 | "status": { 4 | "terminated": "\nProgram Afsluttet.\nLuk vinduet for at afslutte programmet.", 5 | "watching": "Ser på: {channel}", 6 | "goes_online": "{channel} går ONLINE, skifter...", 7 | "goes_offline": "{channel} går OFFLINE, skifter...", 8 | "claimed_drop": "Hentet belønning: {drop}", 9 | "claimed_points": "Hentet ekstra points: {points}", 10 | "earned_points": "Optjente point for at se: {points}, Total: {balance}", 11 | "no_channel": "Ingen tilgængelige kanaler at se. Venter på en ONLINE kanal...", 12 | "no_campaign": "Der er ingen aktive kampagner at hente. Venter på en aktiv kampagne..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Uventet indholdstype returneres, normalt på grund af at blive omdirigeret. Skal du logge ind for internetadgang?", 16 | "chrome": { 17 | "startup": "Åbner Chrome...", 18 | "login_to_complete": "Fuldfør login-proceduren manuelt ved at trykke på Login-knappen igen.", 19 | "no_token": "Der blev ikke fundet noget autorisationstoken.", 20 | "closed_window": "Chrome-vinduet blev lukket, før login-proceduren kunne fuldføres." 21 | }, 22 | "error_code": "Login fejlkode: {error_code}", 23 | "incorrect_login_pass": "Forkert brugernavn eller kodeord.", 24 | "incorrect_email_code": "Forkert email kode.", 25 | "incorrect_twofa_code": "Forkert 2FA-kode.", 26 | "email_code_required": "Email kode påkrævet. Tjek din email.", 27 | "twofa_code_required": "2FA token påkrævet." 28 | }, 29 | "error": { 30 | "captcha": "Dit loginforsøg blev afvist af CAPTCHA.\nPrøv venligst igen om 12+ timer", 31 | "site_down": "Twitch er nede, prøver igen om {seconds} sekunder...", 32 | "no_connection": "Kan ikke oprette forbindelse til Twitch, prøver igen om {seconds} sekunder..." 33 | }, 34 | "gui": { 35 | "output": "Resultat", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Ledig", 39 | "exiting": "Afslutter...", 40 | "terminated": "Afsluttet", 41 | "cleanup": "Oprydning af kanaler...", 42 | "gathering": "Indsamling af kanaler...", 43 | "switching": "Skifter kanal...", 44 | "fetching_inventory": "Henter beholdning...", 45 | "fetching_campaigns": "Henter kampagner...", 46 | "adding_campaigns": "Tilføjer kampagner til beholdning... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Main", 50 | "inventory": "Beholdning", 51 | "settings": "Indstillinger", 52 | "help": "Hjælp" 53 | }, 54 | "tray": { 55 | "notification_title": "Mined Belønning", 56 | "minimize": "Minimer til systembakken", 57 | "show": "Vis", 58 | "quit": "Afslut" 59 | }, 60 | "login": { 61 | "name": "Login formular", 62 | "labels": "Status:\nBruger ID:", 63 | "logged_in": "Logget ind", 64 | "logged_out": "Logget ud", 65 | "logging_in": "Logger ind...", 66 | "required": "Login påkrævet", 67 | "request": "Log venligst ind for at fortsætte.", 68 | "username": "Brugernavn", 69 | "password": "Kodeord", 70 | "twofa_code": "2FA code (valgfrit)", 71 | "button": "Login" 72 | }, 73 | "websocket": { 74 | "name": "Websocket Status", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Initialiserer...", 77 | "connected": "Forbundet", 78 | "disconnected": "Afbrudt", 79 | "connecting": "Tilslutning...", 80 | "disconnecting": "Afbryder forbindelsen...", 81 | "reconnecting": "Genopretter forbindelse..." 82 | }, 83 | "progress": { 84 | "name": "Kampagnefremskridt", 85 | "drop": "Belønning:", 86 | "game": "Spil:", 87 | "campaign": "Kampagne:", 88 | "remaining": "{time} tilbage", 89 | "drop_progress": "Fremskridt:", 90 | "campaign_progress": "Fremskridt:" 91 | }, 92 | "channels": { 93 | "name": "Kanaler", 94 | "switch": "Skift", 95 | "load_points": "Indlæs Points", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Kanal", 101 | "status": "Status", 102 | "game": "Spil", 103 | "viewers": "Seere", 104 | "points": "Points" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filter", 110 | "show": "Vis:", 111 | "not_linked": "Ikke forbundet", 112 | "upcoming": "Kommende", 113 | "expired": "Udløbet", 114 | "excluded": "Udelukket", 115 | "finished": "Færdig", 116 | "refresh": "Opdater" 117 | }, 118 | "status": { 119 | "linked": "Forbundet ✔", 120 | "not_linked": "Ikke forbundet ❌", 121 | "active": "Aktiv ✔", 122 | "upcoming": "Kommende ⏳", 123 | "expired": "Udløbet ❌", 124 | "claimed": "Hentet ✔", 125 | "ready_to_claim": "Klar til at hente ⏳" 126 | }, 127 | "starts": "Starter: {time}", 128 | "ends": "Slutter: {time}", 129 | "allowed_channels": "Tilladte kanaler:", 130 | "all_channels": "Alle", 131 | "and_more": "og {amount} mere...", 132 | "percent_progress": "{percent} af {minutes} minutter", 133 | "minutes_progress": "{minutes} minutter" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "General", 138 | "dark_theme": "Mørk tema:", 139 | "autostart": "Automatisk start: ", 140 | "tray": "Automatisk start i systembakken: ", 141 | "tray_notifications": "Bakkemeddelelser: ", 142 | "priority_only": "Kun prioritet: ", 143 | "prioritize_by_ending_soonest": "Prioriter efter først afsluttet:", 144 | "proxy": "Proxy (kræver genstart):" 145 | }, 146 | "game_name": "Spilnavn", 147 | "priority": "Prioritet", 148 | "exclude": "Udelukke", 149 | "reload": "Genindlæs", 150 | "reload_text": "De fleste ændringer kræver en genindlæsning for at få en øjeblikkelig virkning: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Nyttige Links", 155 | "inventory": "Se Twitch belønninger", 156 | "campaigns": "Se alle kampagner og administrer kontolinks" 157 | }, 158 | "how_it_works": "Hvordan virker det?", 159 | "how_it_works_text": "Omtrent hvert 20. sekund spørger applikationen Twitch efter en URL til den rå datastrøm af den kanal, der i øjeblikket bliver set. Den henter derefter metadataen af denne datastrøm - dette er nok til at fremrykke belønningen. Bemærk, at dette helt omgår behovet for at downloade enhver faktisk stream video og lyd. For at holde status (ONLINE eller OFFLINE) for kanalerne opdateret, er der etableret en websocket-forbindelse, der modtager begivenheder om streams, der går op eller ned, eller opdateringer vedrørende det aktuelle antal seere.", 160 | "getting_started": "Kom igang", 161 | "getting_started_text": "1. Log ind på applikationen.\n2. Sørg for, at din Twitch-konto er linket til alle kampagner, du er interesseret i at mine.\n3. Hvis du bare er interesseret i at mine alt, skal du fjerne markeringen i \"Kun prioritet\" og trykke på \"Genindlæs\".\n4. Hvis du ønsker at mine specifikke spil først, skal du bruge listen \"Prioritet\" til at opsætte en ordnet liste over spil efter eget valg. Spil fra toppen af ​​listen vil blive forsøgt at blive minet først, før dem nederst på listen.\n5. Hold indstillingen \"Kun prioritet\" markeret for at undgå at mine spil, der ikke er på prioritetslisten. Eller ej - det er op til dig.\n6. Brug listen \"Ekskluder\" til at fortælle applikationen, hvilke spil der aldrig bør mines.\n7. Ændring af indholdet af en af ​​listerne eller ændring af tilstanden for \"Kun prioritet\"-indstillingen kræver, at du trykker på \"Genindlæs\" for at ændringerne træder i kraft." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Deutsch.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "German", 3 | "status": { 4 | "terminated": "\nAnwendung gestoppt.\nFenster schließen, um die Anwendung zu beenden", 5 | "watching": "{channel} wird zugesehen", 6 | "goes_online": "{channel} ist ONLINE gegangen, wechseln...", 7 | "goes_offline": "{channel} ist OFFLINE gegangen, wechseln...", 8 | "claimed_drop": "Drop abgeholt: {drop}", 9 | "claimed_points": "Kanal-Punkte verdient: {points}", 10 | "earned_points": "Verdiente Kanal-Punkte fürs zuschauen: {points} | Summe: {balance}", 11 | "no_channel": "Keine teilnehmenden Kanäle online. Warten auf Kanäle...", 12 | "no_campaign": "Keine aktiven Kampagnen verfügbar. Warten auf neue Kampagne..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Unerwarteter Inhaltstyp zurückgegeben, normalerweise aufgrund einer Weiterleitung.\nIst ein Login für den Internetzugang erforderlich?", 16 | "chrome": { 17 | "startup": "Starte Chrome...", 18 | "login_to_complete": "Erneut auf Anmelden drücken, um den Anmeldevorgang manuell abzuschließen.", 19 | "no_token": "Es wurde kein Autorisierungs-Token gefunden.", 20 | "closed_window": "Das Chrome-Fenster wurde geschlossen, bevor der Anmeldevorgang abgeschlossen werden konnte." 21 | }, 22 | "error_code": "Login-Fehlercode: {error_code}", 23 | "incorrect_login_pass": "Falscher Benutzername oder Passwort.", 24 | "incorrect_email_code": "Falscher E-Mail Code.", 25 | "incorrect_twofa_code": "Falscher 2FA Code.", 26 | "email_code_required": "E-Mail Code erforderlich. Bitte E-Mail prüfen.", 27 | "twofa_code_required": "2FA Token erforderlich." 28 | }, 29 | "error": { 30 | "captcha": "Der Anmeldeversuch wurde durch CAPTCHA verweigert.\nBitte versuche es in mindestens 12 Stunden erneut.", 31 | "site_down": "Twitch ist nicht erreichbar. Erneuter Versuch in {seconds} Sekunden...", 32 | "no_connection": "Keine Verbindung zu Twitch möglich. Erneuter Versuch in {seconds} Sekunden..." 33 | }, 34 | "gui": { 35 | "output": "Protokoll", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Im Leerlauf", 39 | "exiting": "Beenden...", 40 | "terminated": "Abgebrochen", 41 | "cleanup": "Kanäle aufräumen..", 42 | "gathering": "Kanäle sammeln...", 43 | "switching": "Wechsel des Kanals...", 44 | "fetching_inventory": "Lade Inventar...", 45 | "fetching_campaigns": "Lade Kampagnen...", 46 | "adding_campaigns": "Kampagnen dem Inventar hinzufügen... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Hauptseite", 50 | "inventory": "Inventar", 51 | "settings": "Einstellungen", 52 | "help": "Hilfe" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop abgeholt", 56 | "minimize": "Minimiere ins System Tray", 57 | "show": "Anzeigen", 58 | "quit": "Beenden" 59 | }, 60 | "login": { 61 | "name": "Login", 62 | "labels": "Status:\nBenutzer ID:", 63 | "logged_in": "Angemeldet", 64 | "logged_out": "Abgemeldet", 65 | "logging_in": "Anmelden...", 66 | "required": "Anmeldung erforderlich", 67 | "request": "Bitte einloggen um fortzufahren.", 68 | "username": "Benutzername", 69 | "password": "Passwort", 70 | "twofa_code": "2FA Code (optional)", 71 | "button": "Anmelden" 72 | }, 73 | "websocket": { 74 | "name": "WebSocket Status", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "Initialisieren...", 77 | "connected": "Verbunden", 78 | "disconnected": "Verbindung verloren", 79 | "connecting": "Verbinden...", 80 | "disconnecting": "Verbindung trennen...", 81 | "reconnecting": "Neu verbinden..." 82 | }, 83 | "progress": { 84 | "name": "Kampagnen-Fortschritt", 85 | "drop": "Drop:", 86 | "game": "Spiel:", 87 | "campaign": "Kampagne:", 88 | "remaining": "{time} verbleibend", 89 | "drop_progress": "Fortschritt:", 90 | "campaign_progress": "Fortschritt:" 91 | }, 92 | "channels": { 93 | "name": "Kanäle", 94 | "switch": "Wechseln", 95 | "load_points": "Lade Punkte", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Kanal", 101 | "status": "Status", 102 | "game": "Spiel", 103 | "viewers": "Zuschauer", 104 | "points": "Punkte" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filter", 110 | "show": "Anzeigen:", 111 | "not_linked": "Nicht verbunden", 112 | "upcoming": "Zukünftig", 113 | "expired": "Abgelaufen", 114 | "excluded": "Ausgeschlossen", 115 | "finished": "Abgeholt", 116 | "refresh": "Aktualisieren" 117 | }, 118 | "status": { 119 | "linked": "Verknüpft ✔", 120 | "not_linked": "Nicht verknüpft ❌", 121 | "active": "Aktiv ✔", 122 | "upcoming": "Zukünftig ⏳", 123 | "expired": "Abgelaufen ❌", 124 | "claimed": "Abgeholt ✔", 125 | "ready_to_claim": "Bereit zum abholen ⏳" 126 | }, 127 | "starts": "Beginnt: {time}", 128 | "ends": "Endet: {time}", 129 | "allowed_channels": "Teilnehmende Kanäle:", 130 | "all_channels": "Alle", 131 | "and_more": "und {amount} weitere...", 132 | "percent_progress": "{percent} von {minutes} Minuten", 133 | "minutes_progress": "{minutes} Minuten" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Allgemein", 138 | "dark_theme": "Dunkler Design: ", 139 | "autostart": "Autostart: ", 140 | "tray": "Autostart ins System Tray: ", 141 | "tray_notifications": "System Tray Benachrichtigungen:", 142 | "priority_only": "Nur Priorität: ", 143 | "prioritize_by_ending_soonest": "Kampagnen nach Ende priorisieren: ", 144 | "proxy": "Proxy (Erfordert Neustart):" 145 | }, 146 | "game_name": "Spiel", 147 | "priority": "Priorität", 148 | "exclude": "Ausschließen", 149 | "reload": "Neu laden", 150 | "reload_text": "Die meisten Änderungen erfordern ein neu laden, um sofort wirksam zu werden: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Hilfreiche Links", 155 | "inventory": "Twitch Inventar ansehen", 156 | "campaigns": "Alle Twitch-Kampagnen ansehen" 157 | }, 158 | "how_it_works": "So funktioniert's", 159 | "how_it_works_text": "Alle ~20 Sekunden fragt die Anwendung Twitch nach einer URL für die Rohdaten des Streams von dem Kanal, dem gerade zugesehen wird. Dann fordert sie die Metadaten dieses Datenstroms an. Dies reicht aus, um den Drop voranzutreiben. Auf diese Weise ist es nicht nötig den Stream herunterzuladen und spart Bandbreite.\nUm den Online- oder Offline-Status der Kanäle aktuell zu halten, wird eine Websocket-Verbindung eingerichtet,\ndie die Kanäle auf ihren Status überprüft.", 160 | "getting_started": "Erste Schritte", 161 | "getting_started_text": "• In der Anwendung anmelden.\n• Stelle sicher, dass das Twitch-Konto mit allen Kampagnen verknüpft ist, an denen Interesse besteht.\n• Sollen alle Drops bearbeitet werden, entferne den Haken bei \"Nur Priorität\" und drücke \"Neu laden\".\n• Sollen nur bestimmte Spiele in betracht gezogen werden, verwende die \"Priorität\" Liste um die Wahl nach Spielen einzugrenzen.\n• Die Liste priorisiert von oben nach unten.\n• Die Option \"Nur Priorität\" verhindert, dass Spiele, die nicht auf der Prioritätenliste stehen, bearbeitet werden.\n• Mit der Liste \"Ausschließen\", ist es möglich Spiele zu filtern, die niemals in betracht gezogen werden sollen.\n• Wenn Listen oder Optionen angepasst wurden, muss \"Neu laden\" gedrückt werden, damit die Änderungen übernommen werden." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/English.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "English", 3 | "status": { 4 | "terminated": "\nApplication Terminated.\nClose the window to exit the application.", 5 | "watching": "Watching: {channel}", 6 | "goes_online": "{channel} goes ONLINE, switching...", 7 | "goes_offline": "{channel} goes OFFLINE, switching...", 8 | "claimed_drop": "Claimed drop: {drop}", 9 | "claimed_points": "Claimed bonus points: {points}", 10 | "earned_points": "Earned points for watching: {points}, total: {balance}", 11 | "no_channel": "No available channels to watch. Waiting for an ONLINE channel...", 12 | "no_campaign": "No active campaigns to mine drops for. Waiting for an active campaign..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Unexpected content type returned, usually due to being redirected. Do you need to login for internet access?", 16 | "chrome": { 17 | "startup": "Opening Chrome...", 18 | "login_to_complete": "Complete the login procedure manually by pressing the Login button again.", 19 | "no_token": "No authorization token could be found.", 20 | "closed_window": "The Chrome window was closed before the login procedure could be completed." 21 | }, 22 | "error_code": "Login error code: {error_code}", 23 | "incorrect_login_pass": "Incorrect username or password.", 24 | "incorrect_email_code": "Incorrect email code.", 25 | "incorrect_twofa_code": "Incorrect 2FA code.", 26 | "email_code_required": "Email code required. Check your email.", 27 | "twofa_code_required": "2FA token required." 28 | }, 29 | "error": { 30 | "captcha": "Your login attempt was denied by CAPTCHA.\nPlease try again in 12+ hours.", 31 | "site_down": "Twitch is down, retrying in {seconds} seconds...", 32 | "no_connection": "Cannot connect to Twitch, retrying in {seconds} seconds..." 33 | }, 34 | "gui": { 35 | "output": "Output", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Idle", 39 | "exiting": "Exiting...", 40 | "terminated": "Terminated", 41 | "cleanup": "Cleaning up channels...", 42 | "gathering": "Gathering channels...", 43 | "switching": "Switching the channel...", 44 | "fetching_inventory": "Fetching inventory...", 45 | "fetching_campaigns": "Fetching campaigns...", 46 | "adding_campaigns": "Adding campaigns to inventory... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Main", 50 | "inventory": "Inventory", 51 | "settings": "Settings", 52 | "help": "Help" 53 | }, 54 | "tray": { 55 | "notification_title": "Mined Drop", 56 | "minimize": "Minimize to Tray", 57 | "show": "Show", 58 | "quit": "Quit" 59 | }, 60 | "login": { 61 | "name": "Login Form", 62 | "labels": "Status:\nUser ID:", 63 | "logged_in": "Logged in", 64 | "logged_out": "Logged out", 65 | "logging_in": "Logging in...", 66 | "required": "Login required", 67 | "request": "Please log in to continue.", 68 | "username": "Username", 69 | "password": "Password", 70 | "twofa_code": "2FA code (optional)", 71 | "button": "Login" 72 | }, 73 | "websocket": { 74 | "name": "Websocket Status", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Initializing...", 77 | "connected": "Connected", 78 | "disconnected": "Disconnected", 79 | "connecting": "Connecting...", 80 | "disconnecting": "Disconnecting...", 81 | "reconnecting": "Reconnecting..." 82 | }, 83 | "progress": { 84 | "name": "Campaign Progress", 85 | "drop": "Drop:", 86 | "game": "Game:", 87 | "campaign": "Campaign:", 88 | "remaining": "{time} remaining", 89 | "drop_progress": "Progress:", 90 | "campaign_progress": "Progress:" 91 | }, 92 | "channels": { 93 | "name": "Channels", 94 | "switch": "Switch", 95 | "load_points": "Load Points", 96 | "online": "ONLINE \u2714", 97 | "pending": "OFFLINE \u23f3", 98 | "offline": "OFFLINE \u274c", 99 | "headings": { 100 | "channel": "Channel", 101 | "status": "Status", 102 | "game": "Game", 103 | "viewers": "Viewers", 104 | "points": "Points" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filter", 110 | "show": "Show:", 111 | "not_linked": "Not linked", 112 | "upcoming": "Upcoming", 113 | "expired": "Expired", 114 | "excluded": "Excluded", 115 | "finished": "Finished", 116 | "refresh": "Refresh" 117 | }, 118 | "status": { 119 | "linked": "Linked \u2714", 120 | "not_linked": "Not Linked \u274c", 121 | "active": "Active \u2714", 122 | "upcoming": "Upcoming \u23f3", 123 | "expired": "Expired \u274c", 124 | "claimed": "Claimed \u2714", 125 | "ready_to_claim": "Ready to claim \u23f3" 126 | }, 127 | "starts": "Starts: {time}", 128 | "ends": "Ends: {time}", 129 | "allowed_channels": "Allowed Channels:", 130 | "all_channels": "All", 131 | "and_more": "and {amount} more...", 132 | "percent_progress": "{percent} of {minutes} minutes", 133 | "minutes_progress": "{minutes} minutes" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "General", 138 | "dark_theme": "Dark theme: ", 139 | "autostart": "Autostart: ", 140 | "tray": "Autostart into tray: ", 141 | "tray_notifications": "Tray notifications: ", 142 | "priority_only": "Priority Only: ", 143 | "prioritize_by_ending_soonest": "Prioritize by ending soonest: ", 144 | "unlinked_campaigns": "Allow Unlinked Campaigns: ", 145 | "proxy": "Proxy (requires restart):" 146 | }, 147 | "game_name": "Game name", 148 | "priority": "Priority", 149 | "exclude": "Exclude", 150 | "reload": "Reload", 151 | "reload_text": "Most changes require a reload to take an immediate effect: " 152 | }, 153 | "help": { 154 | "links": { 155 | "name": "Useful Links", 156 | "inventory": "See Twitch inventory", 157 | "campaigns": "See all campaigns and manage account links" 158 | }, 159 | "how_it_works": "How It Works", 160 | "how_it_works_text": "Every ~20 seconds, the application asks Twitch for a URL to the raw stream data of the channel currently being watched. It then fetches the metadata of this data stream - this is enough to advance the drops. Note that this completely bypasses the need to download any actual stream video and sound. To keep the status (ONLINE or OFFLINE) of the channels up-to-date, there's a websocket connection established that receives events about streams going up or down, or updates regarding the current number of viewers.", 161 | "getting_started": "Getting Started", 162 | "getting_started_text": "1. Log in to the application.\n2. Ensure your Twitch account is linked to all campaigns you're interested in mining.\n3. If you're interested in just mining everything, uncheck \"Priority only\" and press \"Reload\".\n4. If you want to mine specific games first, use the \"Priority\" list to set up an ordered list of games of your choice. Games from the top of the list will be attempted to be mined first, before the ones lower down the list.\n5. Keep the \"Priority only\" option checked to avoid mining games that are not on the priority list. Or not - it's up to you.\n6. Use the \"Exclude\" list to tell the application which games should never be mined.\n7. Changing the contents of either of the lists or changing the state of the \"Priority only\" option, requires you to press \"Reload\" for the changes to take effect." 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lang/Español.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Spanish", 3 | "status": { 4 | "terminated": "\nLa aplicación se ha detenido.\nPor favor, cierre la aplicación.", 5 | "watching": "Viendo el canal: {channel}", 6 | "goes_online": "El canal {channel} se ha conectado, cambiando...", 7 | "goes_offline": "El canal {channel} se ha desconectado, cambiando...", 8 | "claimed_drop": "Drop reclamado: {drop}", 9 | "claimed_points": "Recompensa de puntos reclamados: {points}", 10 | "earned_points": "Puntos obtenidos por ver el stream: {points} | Total: {balance}", 11 | "no_channel": "No se ha encontrado un canal en directo para ver. \nEsperando un canal en directo...", 12 | "no_campaign": "No se ha encontrado una campaña activa. \nEsperando una nueva campaña..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Se produjo un error inesperado, \ngeneralmente debido a una redirección.\n¿Necesitas un VPN o iniciar sesión para acceder a internet?", 16 | "chrome": { 17 | "startup": "Iniciando Chrome...", 18 | "login_to_complete": "Por favor, presione Iniciar sesión nuevamente para \ncompletar el inicio de sesión.", 19 | "no_token": "No se pudo obtener un token de autorización.", 20 | "closed_window": "La ventana de Chrome se cerró antes de que \nse completara el inicio de sesión." 21 | }, 22 | "error_code": "Error de inicio de sesión: {error_code}", 23 | "incorrect_login_pass": "El usuario o contraseña ingresada es incorrecto.", 24 | "incorrect_email_code": "El código de verificación de email es incorrecto.", 25 | "incorrect_twofa_code": "El código 2FA es incorrecto.", 26 | "email_code_required": "Código de verificación de email requerido. \nPor favor, revise su email.", 27 | "twofa_code_required": "Token 2FA requerido." 28 | }, 29 | "error": { 30 | "captcha": "El inicio de sesión fue rechazado por CAPTCHA.\nPor favor, intente nuevamente en aprox. 12 horas.", 31 | "site_down": "Twitch no está disponible. \nIntente de nuevo en {seconds} segundos...", 32 | "no_connection": "No se ha podido conectar a Twitch. \nIntente de nuevo en {seconds} segundos..." 33 | }, 34 | "gui": { 35 | "output": "Registros", 36 | "status": { 37 | "name": "Estado", 38 | "idle": "En espera...", 39 | "exiting": "Saliendo...", 40 | "terminated": "Aplicación suspendida.", 41 | "cleanup": "Limpiando canales...", 42 | "gathering": "Buscando canales en directo...", 43 | "switching": "Cambiando de canal...", 44 | "fetching_inventory": "Obteniendo inventario...", 45 | "fetching_campaigns": "Obteniendo campañas...", 46 | "adding_campaigns": "Agregando lista de campañas al inventario... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "General", 50 | "inventory": "Inventario", 51 | "settings": "Configuración", 52 | "help": "Ayuda" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop minado", 56 | "minimize": "Minimizar a la bandeja", 57 | "show": "Mostrar", 58 | "quit": "Salir" 59 | }, 60 | "login": { 61 | "name": "Inicio de sesión", 62 | "labels": "Estado:\nUsuario:", 63 | "logged_in": "Conectado", 64 | "logged_out": "Desconectado", 65 | "logging_in": "Iniciando sesión...", 66 | "required": "Se requiere inicio de sesión", 67 | "request": "Por favor, inicie sesión para continuar.", 68 | "username": "Usuario", 69 | "password": "Contraseña", 70 | "twofa_code": "Token 2FA (opcional)", 71 | "button": "Iniciar sesión" 72 | }, 73 | "websocket": { 74 | "name": "Estado del Websocket", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Iniciando...", 77 | "connected": "Conectado", 78 | "disconnected": "Desconectado", 79 | "connecting": "Conectando...", 80 | "disconnecting": "Desconectando...", 81 | "reconnecting": "Reconectando..." 82 | }, 83 | "progress": { 84 | "name": "Progreso de la Campaña", 85 | "drop": "Drop:", 86 | "game": "Juego:", 87 | "campaign": "Campaña:", 88 | "remaining": "{time} restante", 89 | "drop_progress": "Progreso:", 90 | "campaign_progress": "Progreso:" 91 | }, 92 | "channels": { 93 | "name": "Canales", 94 | "switch": "Cambiar", 95 | "load_points": "Cargar Puntos", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Canal", 101 | "status": "Estado", 102 | "game": "Juego", 103 | "viewers": "Espectadores", 104 | "points": "Puntos" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtro de campañas", 110 | "show": "Mostrar:", 111 | "not_linked": "No enlazado", 112 | "upcoming": "Próximas", 113 | "expired": "Expiradas", 114 | "excluded": "Excluidas", 115 | "finished": "Completadas", 116 | "refresh": "Actualizar" 117 | }, 118 | "status": { 119 | "linked": "Vinculado ✔", 120 | "not_linked": "Sin vincular ❌", 121 | "active": "Activo ✔", 122 | "upcoming": "Próximamente ⏳", 123 | "expired": "Expirado ❌", 124 | "claimed": "Reclamado ✔", 125 | "ready_to_claim": "Listo para reclamar ⏳" 126 | }, 127 | "starts": "Comienza: {time}", 128 | "ends": "Termina: {time}", 129 | "allowed_channels": "Canales permitidos:", 130 | "all_channels": "Todos", 131 | "and_more": "y {amount} más...", 132 | "percent_progress": "{percent} de {minutes} minutos", 133 | "minutes_progress": "{minutes} minutos" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Ajustes generales", 138 | "dark_theme": "Tema oscuro: ", 139 | "autostart": "Ejecutar al iniciar el sistema: ", 140 | "tray": "Ejecutar en la bandeja del sistema: ", 141 | "tray_notifications": "Mostrar notificaciones: ", 142 | "priority_only": "Minar solo juegos preferidos: ", 143 | "prioritize_by_ending_soonest": "Priorizar campañas por fecha de finalización: ", 144 | "proxy": "Proxy (requiere reinicio):" 145 | }, 146 | "game_name": "Nombre del juego", 147 | "priority": "Lista de juegos preferidos", 148 | "exclude": "Lista de juegos excluidos", 149 | "reload": "Recargar", 150 | "reload_text": "La mayoría de los cambios requieren recargar o reiniciar la aplicación para que sean aplicados: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Enlaces útiles", 155 | "inventory": "Ver inventario de Twitch", 156 | "campaigns": "Ver todas las campañas y administrar cuentas vinculadas" 157 | }, 158 | "how_it_works": "Cómo funciona", 159 | "how_it_works_text": "Aproximadamente cada 20 segundos, la aplicación solicita a Twitch la URL de los datos del canal que estamos viendo. \nDe esta forma, obtenemos sus metadatos, lo que nos permite ahorrar en la descarga de video o audio del stream. \nPara mantener actualizados los estados (ONLINE o OFFLINE) de los canales, así como el número de espectadores, \nse establece una conexión WebSocket mediante de la cual recibimos la información actualizada de los streams.", 160 | "getting_started": "Primeros pasos", 161 | "getting_started_text": "1. Inicie sesión en la aplicación. \n2. Verifique que su cuenta de Twitch esté vinculada a todas las campañas deseadas a minar. \n3. Si desea minar todas las campañas, desmarque la opción \"Minar solo juegos preferidos\" y presione \"Recargar\". \n4. Si desea darle prioridad a una campaña específica, utilice la \"Lista de juegos preferidos\" para crear una lista de prioridad. \nLos juegos en la parte superior de la lista se intentarán minar antes de los que estén más abajo. \n5. Utilice la opción \"Minar solo juegos preferidos\" si desea evitar minar juegos que no estén en la lista de juegos preferidos. \n6. Utilice la \"Lista de juegos excluidos\" si desea evitar minar juegos que no estén en la lista de juegos excluidos. \n7. Al utilizar la opción \"Minar solo juegos preferidos\" o al cambiar el contenido de las listas, \nserá necesario presionar el botón \"Recargar\" para que los cambios sean aplicados." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Français.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "French", 3 | "status": { 4 | "terminated": "\nL'application a été arrêtée.\nVeuillez fermer l'application.", 5 | "watching": "En train de regarder : {channel}", 6 | "goes_online": "{channel} passe EN LIGNE, changement...", 7 | "goes_offline": "{channel} passe HORS LIGNE, changement...", 8 | "claimed_drop": "Drop récupéré : {drop}", 9 | "claimed_points": "Points bonus récupéré : {points}", 10 | "earned_points": "Points gagnés pour avoir regardé : {points}, total : {balance}", 11 | "no_channel": "Aucune chaîne disponible à regarder. En attente d'une chaîne EN LIGNE...", 12 | "no_campaign": "Aucune campagne active pour laquelle miner des drops. En attente d'une campagne active..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Type de contenu inattendu renvoyé, généralement dû à une redirection. Avez-vous besoin de vous connecter pour accéder à Internet ?", 16 | "chrome": { 17 | "startup": "Ouverture de Chrome", 18 | "login_to_complete": "Terminez la procédure de connexion manuellement en appuyant à nouveau sur le bouton Connexion.", 19 | "no_token": "Aucun jeton d'autorisation (authorization token) n'a pu être trouvé.", 20 | "closed_window": "La fenêtre Chrome a été fermée avant la fin de la procédure de connexion." 21 | }, 22 | "error_code": "Code d'erreur de connexion: {error_code}", 23 | "incorrect_login_pass": "Le nom d'utilisateur ou le mot de passe saisi est incorrect.", 24 | "incorrect_email_code": "Le code de vérification de l'e-mail est incorrect.", 25 | "incorrect_twofa_code": "Le code 2FA est incorrect.", 26 | "email_code_required": "Code de vérification par e-mail requis. Merci de consulter vos emails.", 27 | "twofa_code_required": "Code 2FA requis." 28 | }, 29 | "error": { 30 | "captcha": "La connexion a été rejetée par CAPTCHA. Veuillez réessayer dans env. 12 heures.", 31 | "site_down": "Twitch n'est pas disponible. Veuillez réessayer dans {seconds} secondes...", 32 | "no_connection": "Impossible de se connecter à Twitch. Veuillez réessayer dans {seconds} secondes..." 33 | }, 34 | "gui": { 35 | "output": "Sortie", 36 | "status": { 37 | "name": "Etat", 38 | "idle": "Inactif", 39 | "exiting": "Sortie...", 40 | "terminated": "Terminé", 41 | "cleanup": "Nettoyage des canaux...", 42 | "gathering": "Recherche de chaînes en direct...", 43 | "switching": "Changement de chaîne...", 44 | "fetching_inventory": "Récupération de l'inventaire...", 45 | "fetching_campaigns": "Récupération des campagnes...", 46 | "adding_campaigns": "Ajout de la liste des campagnes à l'inventaire... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Général", 50 | "inventory": "Inventaire", 51 | "settings": "Paramètres", 52 | "help": "Aide" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop miné", 56 | "minimize": "Réduire dans la barre des tâches", 57 | "show": "Afficher", 58 | "quit": "Quitter" 59 | }, 60 | "login": { 61 | "name": "Formulaire de connexion", 62 | "labels": "Statut :\nIdentifiant utilisateur :", 63 | "logged_in": "Connecté", 64 | "logged_out": "Déconnecté", 65 | "logging_in": "Connexion en cours...", 66 | "required": "Connexion requise", 67 | "request": "Veuillez vous connecter pour continuer.", 68 | "username": "Nom d'utilisateur", 69 | "password": "Mot de passe", 70 | "twofa_code": "Code 2FA (facultatif)", 71 | "button": "Se connecter" 72 | }, 73 | "websocket": { 74 | "name": "État du Websocket", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Initialisation...", 77 | "connected": "Connecté", 78 | "disconnected": "Déconnecté", 79 | "connecting": "Connexion...", 80 | "disconnecting": "Déconnexion...", 81 | "reconnecting": "Reconnexion..." 82 | }, 83 | "progress": { 84 | "name": "Progression de la campagne", 85 | "drop": "Drop :", 86 | "game": "Jeu :", 87 | "campaign": "Campagne :", 88 | "remaining": "{time} restant", 89 | "drop_progress": "Avancement :", 90 | "campaign_progress": "Avancement :" 91 | }, 92 | "channels": { 93 | "name": "Chaînes", 94 | "switch": "Basculer", 95 | "load_points": "Charger les points", 96 | "online": "EN LIGNE ✔", 97 | "pending": "HORS LIGNE ⏳", 98 | "offline": "HORS LIGNE ❌", 99 | "headings": { 100 | "channel": "Chaîne", 101 | "status": "Statut", 102 | "game": "Jeu", 103 | "viewers": "Spectateurs", 104 | "points": "Points" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtre", 110 | "show": "Afficher :", 111 | "not_linked": "Non lié", 112 | "upcoming": "À venir", 113 | "expired": "Expiré", 114 | "excluded": "Exclu", 115 | "finished": "Terminé", 116 | "refresh": "Actualiser" 117 | }, 118 | "status": { 119 | "linked": "Lié ✔", 120 | "not_linked": "Non lié ❌", 121 | "active": "Actif ✔", 122 | "upcoming": "À venir ⏳", 123 | "expired": "Expiré ❌", 124 | "claimed": "Récupéré ✔", 125 | "ready_to_claim": "Prêt à récupérer ⏳" 126 | }, 127 | "starts": "Début : {time}", 128 | "ends": "Fin : {time}", 129 | "allowed_channels": "Chaînes autorisées :", 130 | "all_channels": "Toutes", 131 | "and_more": "et {amount} de plus...", 132 | "percent_progress": "{percent} de {minutes} minutes", 133 | "minutes_progress": "{minutes} minutes" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Général", 138 | "dark_theme": "Thème sombre :", 139 | "autostart": "Démarrage automatique :", 140 | "tray": "Démarrage automatique dans la barre des tâches :", 141 | "tray_notifications": "Notifications dans la barre des tâches :", 142 | "priority_only": "Priorité uniquement :", 143 | "prioritize_by_ending_soonest": "Prioriser les drops se terminant le plus tôt :", 144 | "proxy": "Proxy (nécessite un redémarrage) :" 145 | }, 146 | "game_name": "Nom du jeu", 147 | "priority": "Priorité", 148 | "exclude": "Exclure", 149 | "reload": "Recharger", 150 | "reload_text": "La plupart des modifications nécessitent un rechargement pour prendre effet immédiatement : " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Liens utiles", 155 | "inventory": "Afficher l'inventaire Twitch", 156 | "campaigns": "Afficher toutes les campagnes et gérer les comptes associés" 157 | }, 158 | "how_it_works": "Comment ça fonctionne", 159 | "how_it_works_text": "Toutes les ~20 secondes, l'application demande à Twitch une URL vers les données brutes du flux de la chaîne actuellement regardée. Elle récupère ensuite les métadonnées de ce flux de données - ce qui suffit pour faire avancer les drops. Notez que cela contourne complètement le besoin de télécharger la vidéo et le son du flux réel. Pour maintenir à jour le statut (EN LIGNE ou HORS LIGNE) des chaînes, une connexion websocket est établie pour recevoir des événements sur les flux qui montent ou descendent, ou des mises à jour concernant le nombre actuel de spectateurs.", 160 | "getting_started": "Premiers pas", 161 | "getting_started_text": "1. Connectez-vous à l'application.\n2. Assurez-vous que votre compte Twitch est lié à toutes les campagnes qui vous intéressent.\n3. Si vous voulez simplement miner tout, décochez \"Priorité uniquement\" et appuyez sur \"Recharger\".\n4. Si vous souhaitez miner d'abord certains jeux, utilisez la liste \"Priorité\" pour définir une liste ordonnée des jeux de votre choix. Les jeux en haut de la liste seront tentés d'être minés en premier, avant ceux plus bas dans la liste.\n5. Laissez l'option \"Priorité uniquement\" cochée pour éviter de miner des jeux qui ne sont pas dans la liste de priorité. Ou pas - c'est à vous de décider.\n6. Utilisez la liste \"Exclure\" pour indiquer à l'application les jeux qui ne doivent jamais être minés.\n7. Modifier le contenu de l'une des listes ou changer l'état de l'option \"Priorité uniquement\" nécessite d'appuyer sur \"Recharger\" pour que les changements prennent effet." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Indonesian.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Indonesian", 3 | "status": { 4 | "terminated": "\nAplikasi telah dihentikan. \nSilakan tutup aplikasi.", 5 | "watching": "Menonton : {channel}", 6 | "goes_online": "{channel} DARING, berganti...", 7 | "goes_offline": "{channel} menjadi LURING, berganti...", 8 | "claimed_drop": "Drop dipulihkan : {drop}", 9 | "claimed_points": "Poin bonus yang dipulihkan: {points}", 10 | "earned_points": "Poin yang diperoleh dari menonton : {points}, jumlah : {balance}", 11 | "no_channel": "Tidak ada saluran yang tersedia untuk ditonton. Menunggu kanal yang DARING...", 12 | "no_campaign": "Tidak ada kampanye aktif yang bisa ditambang. Menunggu kampanye aktif..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Jenis konten yang tidak diharapkan yang dikembalikan, biasanya karena pengalihan. Apakah Anda perlu login untuk mengakses Internet ?", 16 | "chrome": { 17 | "startup": "Membuka Chrome", 18 | "login_to_complete": "Selesaikan prosedur koneksi secara manual dengan menekan tombol Connection (Koneksi) sekali lagi..", 19 | "no_token": "Token otorisasi tidak dapat ditemukan.", 20 | "closed_window": "Jendela Chrome ditutup sebelum prosedur koneksi selesai." 21 | }, 22 | "error_code": "Kode kesalahan koneksi: {error_code}", 23 | "incorrect_login_pass": "Nama pengguna atau kata sandi yang dimasukkan salah.", 24 | "incorrect_email_code": "Kode verifikasi email salah.", 25 | "incorrect_twofa_code": "Kode verifikasi 2FA salah.", 26 | "email_code_required": "Diperlukan kode verifikasi email. Silakan periksa email Anda.", 27 | "twofa_code_required": "Diperlukan kode 2FA." 28 | }, 29 | "error": { 30 | "captcha": "Sambungan ditolak oleh CAPTCHA. Silakan coba lagi dalam waktu sekitar 12 jam.", 31 | "site_down": "Twitch tidak tersedia. Silakan coba lagi dalam {seconds} detik....", 32 | "no_connection": "Tidak dapat tersambung ke Twitch. Silakan coba lagi dalam {detik} detik...." 33 | }, 34 | "gui": { 35 | "output": "Keluaran", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Tidak aktif", 39 | "exiting": "Keluar...", 40 | "terminated": "Diakhiri", 41 | "cleanup": "Membersihkan saluran...", 42 | "gathering": "Pencarian saluran langsung...", 43 | "switching": "Koleksi saluran yang sedang berlangsung...", 44 | "fetching_inventory": "Mengambil inventaris...", 45 | "fetching_campaigns": "Mengambil kampanye...", 46 | "adding_campaigns": "Menambahkan daftar kampanye ke inventaris... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Umum", 50 | "inventory": "Inventaris", 51 | "settings": "Parameter", 52 | "help": "Bantuan" 53 | }, 54 | "tray": { 55 | "notification_title": "Tambang drop", 56 | "minimize": "Meminimalkan di bilah tugas", 57 | "show": "Lihat", 58 | "quit": "Keluar" 59 | }, 60 | "login": { 61 | "name": "Formulir masuk", 62 | "labels": "Status :\nID Pengguna :", 63 | "logged_in": "Terlogin", 64 | "logged_out": "Terlogout", 65 | "logging_in": "Dalam proses login...", 66 | "required": "Login diperlukan", 67 | "request": "Silakan login untuk melanjutkan.", 68 | "username": "Nama pengguna", 69 | "password": "Kata sandi", 70 | "twofa_code": "Kode 2FA (opsional)", 71 | "button": "Login" 72 | }, 73 | "websocket": { 74 | "name": "Status soket web", 75 | "websocket": "Soket web #{id}:", 76 | "initializing": "Menginisialisasi...", 77 | "connected": "Tersambung", 78 | "disconnected": "Terputus", 79 | "connecting": "Menyambungkan...", 80 | "disconnecting": "Memutusan sambungan...", 81 | "reconnecting": "Menyambungkan kembali..." 82 | }, 83 | "progress": { 84 | "name": "Progres kampanye", 85 | "drop": "Drop:", 86 | "game": "Game", 87 | "campaign": "Kampanye:", 88 | "remaining": "{time} tersisa", 89 | "drop_progress": "Progres:", 90 | "campaign_progress": "Progres:" 91 | }, 92 | "channels": { 93 | "name": "Saluran", 94 | "switch": "Beralih", 95 | "load_points": "Poin Pemuatan", 96 | "online": "DARING ✔", 97 | "pending": "DITUNDA ⏳", 98 | "offline": "LURING ❌", 99 | "headings": { 100 | "channel": "Kanal", 101 | "status": "Status", 102 | "game": "Game", 103 | "viewers": "Penonton", 104 | "points": "Poin" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Saring", 110 | "show": "Tampilkan :", 111 | "not_linked": "Tidak tertautkan", 112 | "upcoming": "Segera hadir", 113 | "expired": "Kedaluarsa", 114 | "excluded": "Dikecualikan", 115 | "finished": "Selesai", 116 | "refresh": "Muat ulang" 117 | }, 118 | "status": { 119 | "linked": "Ditautkan ✔", 120 | "not_linked": "Tidak tertautkan ❌", 121 | "active": "Aktif ✔", 122 | "upcoming": "Segera hadir ⏳", 123 | "expired": "Kadaluarsa ❌", 124 | "claimed": "Diklaim ✔", 125 | "ready_to_claim": "Siap untuk diklaim ⏳" 126 | }, 127 | "starts": "Mulai: {time}", 128 | "ends": "Akhir: {time}", 129 | "allowed_channels": "Kanal yang diperbolehkan:", 130 | "all_channels": "Semua", 131 | "and_more": "dan {amount} yang lainnya...", 132 | "percent_progress": "{percent} dari {minutes} menit", 133 | "minutes_progress": "{minutes} menit" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Umum", 138 | "dark_theme": "Tema gelap: ", 139 | "autostart": "Mulai secara otomatis:", 140 | "tray": "Mulai otomatis di tray:", 141 | "tray_notifications": "Notifikasi di tray:", 142 | "priority_only": "Prioritas Saja:", 143 | "prioritize_by_ending_soonest": "Memprioritaskan yang segera berakhir: ", 144 | "proxy": "Proxy (membutuhkan mulai ulang):" 145 | }, 146 | "game_name": "Nama game", 147 | "priority": "Prioritas", 148 | "exclude": "Kecualikan", 149 | "reload": "Muat Ulang", 150 | "reload_text": "Sebagian besar perubahan memerlukan pemuatan ulang untuk segera diterapkan : " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Tautan berguna", 155 | "inventory": "Lihat inventaris Twitch", 156 | "campaigns": "Lihat semua kampanye dan kelola akun terkait" 157 | }, 158 | "how_it_works": "Cara Kerja", 159 | "how_it_works_text": "Setiap ~20 detik, aplikasi ini mengirimkan permintaan URL pada Twitch untuk mendapatkan data tayangan mentah dari kanal yang sedang ditonton. Lalu aplikasi ini akan mengambil metadata dari tayangan tersebut - cukup untuk mempercepat proses drop. Perhatikan bahwa hal ini sama sekali tidak mengunduh gambar dan suara dari streaming yang sebenarnya. Untuk menjaga agar status (DARING atau LURING) saluran tetap terbarukan, koneksi socket web dibuat untuk menerima peristiwa tentang streaming yang naik atau turun, atau pembaruan tentang jumlah penonton saat ini", 160 | "getting_started": "Permulaan", 161 | "getting_started_text": "1. Masuk ke aplikasi.\n2. Pastikan akun Twitch Anda ditautkan ke semua kampanye yang Anda minati.\n3. Jika Anda hanya ingin menambang semuanya, hapus centang pada \"Hanya Prioritas\" dan tekan \"Muat Ulang\".\n4. Jika Anda ingin menambang game tertentu terlebih dahulu, gunakan daftar \"Prioritas\" untuk menentukan daftar game pilihan Anda. Game yang berada di bagian atas daftar akan tergoda untuk ditambang terlebih dahulu, sebelum game yang berada di bagian bawah daftar.\n5. Biarkan opsi \"Prioritas saja\" dicentang untuk menghindari menambang game yang tidak ada dalam daftar prioritas. Atau tidak - terserah Anda.\n6. Gunakan daftar \"Kecualikan\" untuk memberi tahu aplikasi game mana yang tidak boleh ditambang.\n7. Untuk memodifikasi konten dari salah satu daftar atau mengubah status opsi \"Hanya Prioritas\", Anda perlu menekan \"Muat Ulang\" agar perubahan dapat diterapkan." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Italiano.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Italian", 3 | "status": { 4 | "terminated": "\nApplicazione Terminata.\nChiudi la finestra per uscire dall'applicazione.", 5 | "watching": "Guardando: {channel}", 6 | "goes_online": "{channel} è ONLINE, cambiando...", 7 | "goes_offline": "{channel} è OFFLINE, cambiando...", 8 | "claimed_drop": "Contenuti riscattati: {drop}", 9 | "claimed_points": "Punti bonus riscattati: {points}", 10 | "earned_points": "Punti bonus guadagnati per aver guardato la stream: {points}, totale: {balance}", 11 | "no_channel": "Nessun canale disponibile da guardare. In attesa di un canale ONLINE...", 12 | "no_campaign": "Nessuna campagna attiva per ottenere i premi. In attesa di una campagna attiva..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Tipo di contenuto inaspettato restituito, di solito a causa di un reindirizzamento. Hai bisogno di fare il login per accedere a internet?", 16 | "chrome": { 17 | "startup": "Apertura di Chrome...", 18 | "login_to_complete": "Completa la procedura di login manualmente premendo nuovamente il pulsante Login.", 19 | "no_token": "Nessun token di autorizzazione trovato.", 20 | "closed_window": "La finestra di Chrome è stata chiusa prima che la procedura di login potesse completarsi." 21 | }, 22 | "error_code": "Codice di errore del login: {error_code}", 23 | "incorrect_login_pass": "Nome utente o password errati.", 24 | "incorrect_email_code": "Codice email errato.", 25 | "incorrect_twofa_code": "Codice 2FA errato.", 26 | "email_code_required": "Codice email richiesto. Controlla la tua email.", 27 | "twofa_code_required": "Token 2FA richiesto." 28 | }, 29 | "error": { 30 | "captcha": "Il tuo tentativo di login è stato negato da CAPTCHA.\nRiprova tra 12+ ore.", 31 | "site_down": "Twitch è irraggiungibile, riprovo tra {seconds} secondi...", 32 | "no_connection": "Impossibile connettersi a Twitch, riprovo tra {seconds} secondi..." 33 | }, 34 | "gui": { 35 | "output": "Output", 36 | "status": { 37 | "name": "Stato", 38 | "idle": "Inattivo", 39 | "exiting": "Uscendo...", 40 | "terminated": "Terminato", 41 | "cleanup": "Pulendo i canali...", 42 | "gathering": "Raccogliendo i canali...", 43 | "switching": "Cambiando canale...", 44 | "fetching_inventory": "Recupero dell'inventario...", 45 | "fetching_campaigns": "Recupero delle campagne...", 46 | "adding_campaigns": "Aggiunta delle campagne all'inventario... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Principale", 50 | "inventory": "Inventario", 51 | "settings": "Impostazioni", 52 | "help": "Aiuto" 53 | }, 54 | "tray": { 55 | "notification_title": "Premio Ottenuto", 56 | "minimize": "Minimizza nella barra delle applicazioni", 57 | "show": "Mostra", 58 | "quit": "Esci" 59 | }, 60 | "login": { 61 | "name": "Dettagli Login", 62 | "labels": "Stato:\nID Utente:", 63 | "logged_in": "Loggato", 64 | "logged_out": "Non loggato", 65 | "logging_in": "Loggando...", 66 | "required": "Login richiesto", 67 | "request": "Per favore, effettua il login per continuare.", 68 | "username": "Nome utente", 69 | "password": "Password", 70 | "twofa_code": "Codice 2FA (opzionale)", 71 | "button": "Login" 72 | }, 73 | "websocket": { 74 | "name": "Stato del Websocket", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Inizializzando...", 77 | "connected": "Connesso", 78 | "disconnected": "Disconnesso", 79 | "connecting": "Connettendo...", 80 | "disconnecting": "Disconnettendo...", 81 | "reconnecting": "Riconnettendo..." 82 | }, 83 | "progress": { 84 | "name": "Progresso della Campagna", 85 | "drop": "Contenuto:", 86 | "game": "Gioco:", 87 | "campaign": "Campagna:", 88 | "remaining": "{time} rimanenti", 89 | "drop_progress": "Progresso:", 90 | "campaign_progress": "Progresso:" 91 | }, 92 | "channels": { 93 | "name": "Canali", 94 | "switch": "Cambia", 95 | "load_points": "Carica Punti", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Canale", 101 | "status": "Stato", 102 | "game": "Gioco", 103 | "viewers": "Spettatori", 104 | "points": "Punti" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtro", 110 | "show": "Mostra:", 111 | "not_linked": "Non collegato", 112 | "upcoming": "In arrivo", 113 | "expired": "Scaduti", 114 | "excluded": "Esclusi", 115 | "finished": "Finiti", 116 | "refresh": "Aggiorna" 117 | }, 118 | "status": { 119 | "linked": "Collegato ✔", 120 | "not_linked": "Non collegato ❌", 121 | "active": "Attivo ✔", 122 | "upcoming": "In arrivo ⏳", 123 | "expired": "Scaduto ❌", 124 | "claimed": "Riscattato ✔", 125 | "ready_to_claim": "Pronto per essere riscattato ⏳" 126 | }, 127 | "starts": "Inizia: {time}", 128 | "ends": "Finisce: {time}", 129 | "allowed_channels": "Canali consentiti:", 130 | "all_channels": "Tutti", 131 | "and_more": "e altri {amount}...", 132 | "percent_progress": "{percent} di {minutes} minuti", 133 | "minutes_progress": "{minutes} minuti" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Generale", 138 | "dark_theme": "Tema scuro: ", 139 | "autostart": "Avvio automatico: ", 140 | "tray": "Avvio automatico nella barra delle applicazioni: ", 141 | "tray_notifications": "Notifiche: ", 142 | "priority_only": "Solo priorità: ", 143 | "prioritize_by_ending_soonest": "Dai la priorità in base all'ora di fine: ", 144 | "proxy": "Proxy (richiede il riavvio):" 145 | }, 146 | "game_name": "Nome del gioco", 147 | "priority": "Priorità", 148 | "exclude": "Escludi", 149 | "reload": "Ricarica", 150 | "reload_text": "La maggior parte delle modifiche richiede una ricarica con il botto nqui di fianco per avere un effetto immediato: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Link utili", 155 | "inventory": "Vedi l'inventario di Twitch", 156 | "campaigns": "Vedi tutte le campagne e gestisci i collegamenti dell'account" 157 | }, 158 | "how_it_works": "Come funziona", 159 | "how_it_works_text": "Circa ogni 20 secondi, l'applicazione chiede a Twitch un URL per il flusso di dati grezzi del canale attualmente guardato. Successivamente, recupera i metadati di questo flusso e ciò è sufficiente per far avanzare il progresso dei drop. Da notare che questo bypassa completamente la necessità di scaricare qualsiasi video o audio della diretta. Per mantenere aggiornato lo stato (ONLINE o OFFLINE) dei canali, viene stabilita una connessione websocket che riceve eventi sui canali che vanno online o offline, oppure aggiornamenti relativi al numero attuale di spettatori.", 160 | "getting_started": "Per iniziare", 161 | "getting_started_text": "1. Effettua il login nell'applicazione.\n2. Assicurati che il tuo account Twitch sia collegato a tutte le campagne per cui sei interessato a ottenere i drop.\n3. Se sei interessato a ottenere tutto, deseleziona \"Solo priorità\" e premi su \"Ricarica\".\n4. Se vuoi ottenere prima giochi specifici, usa la lista \"Priorità\" per impostare una lista ordinata di giochi a tua scelta. I giochi in cima alla lista verranno ottenuti prima.\n5. Mantieni l'opzione \"Solo priorità\" selezionata, per evitare di ottenere giochi che non sono nella lista delle priorità. Oppure non farlo - dipende da te.\n6. Usa la lista \"Escludi\" per dire all'applicazione quali giochi non devono mai essere ottenuti.\n7. Cambiare il contenuto di una delle liste, o cambiare lo stato dell'opzione \"Solo priorità\", richiede di premere su \"Ricarica\" perché le modifiche abbiano effetto." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Nederlandse.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Dutch", 3 | "status": { 4 | "terminated": "\nToepassing beëindigd.\nSluit het venster om de toepassing af te sluiten.", 5 | "watching": "{channel} wordt gekeken", 6 | "goes_online": "{channel} gaat ONLINE, overschakelen...", 7 | "goes_offline": "{channel} gaat OFFLINE, overschakelen...", 8 | "claimed_drop": "Drop opgeëist: {drop}", 9 | "claimed_points": "Bonuspunten opgeëist: {points}", 10 | "earned_points": "Punten verdiend voor het kijken: {points}, totaal: {balance}", 11 | "no_channel": "Geen beschikbare kanalen om naar te kijken. Wachten op een ONLINE-kanaal...", 12 | "no_campaign": "\"Geen actieve campagnes om drops te minen. Wachten op een actieve campagne..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Onverwacht inhoudstype geretourneerd, meestal vanwege een omleiding. Onverwacht inhoudstype geretourneerd, meestal vanwege een omleiding.", 16 | "chrome": { 17 | "startup": "Chrome starten...", 18 | "login_to_complete": "Druk nogmaals op Aanmelden om het aanmeldingsproces handmatig te voltooien.", 19 | "no_token": "Er kon geen autorisatietoken worden gevonden.", 20 | "closed_window": "Het Chrome-venster werd gesloten voordat de inlogprocedure kon worden voltooid" 21 | }, 22 | "error_code": "Inlogfoutcode: {error_code}", 23 | "incorrect_login_pass": "Onjuist gebruikersnaam of wachtwoord.", 24 | "incorrect_email_code": "Onjuiste e-mailcode.", 25 | "incorrect_twofa_code": "Onjuiste 2FA Code.", 26 | "email_code_required": "E-mailcode vereist. Controleer uw e-mail.", 27 | "twofa_code_required": "2FA-token vereist." 28 | }, 29 | "error": { 30 | "captcha": "Uw inlogpoging werd geweigerd door CAPTCHA.\nProbeer het over 12+ uur opnieuw.", 31 | "site_down": "Twitch is offline, opnieuw proberen over {seconds} seconden...", 32 | "no_connection": "Kan geen verbinding maken met Twitch, opnieuw proberen over {seconds} seconden..." 33 | }, 34 | "gui": { 35 | "output": "Uitvoer", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Inactief", 39 | "exiting": "Afsluiten...", 40 | "terminated": "Beëindigd", 41 | "cleanup": "Kanalen schoonmaken...", 42 | "gathering": "Kanalen verzamelen...", 43 | "switching": "Kanaal overschakelen...", 44 | "fetching_inventory": "Inventaris ophalen...", 45 | "fetching_campaigns": "Campagnes ophalen...", 46 | "adding_campaigns": "Campagnes toevoegen aan inventaris..." 47 | }, 48 | "tabs": { 49 | "main": "Algemeen", 50 | "inventory": "Inventaris", 51 | "settings": "Instellingen", 52 | "help": "Help" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop verkregen", 56 | "minimize": "Minimaliseren naar taakbalk", 57 | "show": "Tonen", 58 | "quit": "Verlaten" 59 | }, 60 | "login": { 61 | "name": "Inlogformulier", 62 | "labels": "Status:\nGebruiker ID:", 63 | "logged_in": "Ingelogd", 64 | "logged_out": "Uitgelogd", 65 | "logging_in": "Inloggen...", 66 | "required": "Inloggen vereist", 67 | "request": "Log in om door te gaan.", 68 | "username": "Gebruikersnaam", 69 | "password": "Wachtwoord", 70 | "twofa_code": "2FA-code (optioneel)", 71 | "button": "Inloggen" 72 | }, 73 | "websocket": { 74 | "name": "WebSocket Status", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "Initialiseren...", 77 | "connected": "Verbonden", 78 | "disconnected": "Verbroken", 79 | "connecting": "Verbinden...", 80 | "disconnecting": "Verbreken...", 81 | "reconnecting": "Opnieuw verbinden..." 82 | }, 83 | "progress": { 84 | "name": "Campagnevoortgang", 85 | "drop": "Drop:", 86 | "game": "Spel:", 87 | "campaign": "Campagne:", 88 | "remaining": "{time} resterend", 89 | "drop_progress": "Voortgang:", 90 | "campaign_progress": "Voortgang:" 91 | }, 92 | "channels": { 93 | "name": "Kanalen", 94 | "switch": "Wisselen", 95 | "load_points": "Punten laden", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Kanaal", 101 | "status": "Status", 102 | "game": "Spel", 103 | "viewers": "Kijkers", 104 | "points": "Punten" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filter", 110 | "show": "Weergeven:", 111 | "not_linked": "Niet gekoppeld", 112 | "upcoming": "Verwacht", 113 | "expired": "Verlopen", 114 | "excluded": "Uitgesloten", 115 | "finished": "Beëindigd", 116 | "refresh": "Vernieuwen" 117 | }, 118 | "status": { 119 | "linked": "Gekoppeld ✔", 120 | "not_linked": "Niet gekoppeld ❌", 121 | "active": "Actief ✔", 122 | "upcoming": "Verwacht ⏳", 123 | "expired": "Verlopen ❌", 124 | "claimed": "Verkregen ✔", 125 | "ready_to_claim": "Klaar om te verkrijgen ⏳" 126 | }, 127 | "starts": "Begint: {time}", 128 | "ends": "Eindigt: {time}", 129 | "allowed_channels": "Toegestane kanalen:", 130 | "all_channels": "Alle", 131 | "and_more": "en {amount} meer...", 132 | "percent_progress": "{percent} van {minutes} Minuten", 133 | "minutes_progress": "{minutes} Minuten" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Algemeen", 138 | "dark_theme": "Donker thema:", 139 | "autostart": "Autostart: ", 140 | "tray": "Automatisch starten in het taakbalk: ", 141 | "tray_notifications": "Taakbalkmeldingen:", 142 | "priority_only": "Alleen prioriteit: ", 143 | "prioritize_by_ending_soonest": "Prioriteren op basis van eerst eindigend:", 144 | "proxy": "Proxy (vereist herstart):" 145 | }, 146 | "game_name": "Spelnaam", 147 | "priority": "Prioriteit", 148 | "exclude": "Uitsluiten", 149 | "reload": "Herladen", 150 | "reload_text": "Voor de meeste wijzigingen is een herlaadbeurt vereist om onmiddellijk van kracht te worden: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Handige Links", 155 | "inventory": "Bekijk Twitch-inventaris", 156 | "campaigns": "Bekijk alle Twitch-campagnes" 157 | }, 158 | "how_it_works": "Hoe het werkt", 159 | "how_it_works_text": "Elke ~20 seconden vraagt de toepassing Twitch om een URL naar de rauwe streamdata van het kanaal dat momenteel wordt bekeken. Vervolgens haalt het de metadata van deze datastream op - dit is voldoende om de drops te laten vorderen. Merk op dat dit de noodzaak om echte streamvideo en geluid te downloaden volledig omzeilt en zo bandbreedte bespaart. Om de status (ONLINE of OFFLINE) van de kanalen up-to-date te houden, is er een websocket-verbinding opgezet die gebeurtenissen ontvangt over streams die omhoog of omlaag gaan, of updates met betrekking tot het huidige aantal kijkers.", 160 | "getting_started": "Eerste stappen", 161 | "getting_started_text": "1. Log in op de applicatie.\n2. Zorg ervoor dat uw Twitch-account is gekoppeld aan alle campagnes waarvoor u drops wilt minen.\n3. Als u alles wilt minen, schakel \"Prioriteit alleen\" uit en druk op \"Herladen\".\n4. Als u specifieke spellen eerst wilt minen, gebruik de \"Prioriteit\"-lijst om een geordende lijst van je gewenste games op te stellen. Spellen bovenaan de lijst zullen eerst geprobeerd worden te minen, voordat de lager geplaatste games aan de beurt komen.\n5. Houd de optie \"Alleen prioriteit\" aangevinkt om te voorkomen dat spellen die niet op de prioriteitenlijst staan gemined worden. Of niet - de keuze is aan u.\n6. Gebruik de \"Uitsluiten\"-lijst om de applicatie te vertellen welke spellen nooit gemined moeten worden.\n7. Het wijzigen van de inhoud van een van de lijsten of het wijzigen van de status van de optie \"Alleen prioriteit\" vereist dat je op \"Herladen\" drukt om de wijzigingen door te voeren." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Polski.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Polish", 3 | "status": { 4 | "terminated": "\nAplikacja została zatrzymana.\nZamknij okno, aby wyjść z aplikacji.", 5 | "watching": "Oglądam kanał: {channel}", 6 | "goes_online": "Nowy status kanału {channel}: ONLINE, zmieniam...", 7 | "goes_offline": "Nowy status kanału {channel}: OFFLINE, zmieniam...", 8 | "claimed_drop": "Odebrano drop: {drop}", 9 | "claimed_points": "Odebrano punkty: {points}", 10 | "earned_points": "Zdobyto punkty za oglądanie: {points} | Łącznie: {balance}", 11 | "no_channel": "Brak możliwych kanałów do oglądania. Oczekiwanie na nową kampanię...", 12 | "no_campaign": "Brak dostępnych aktywnych kampanii. Oczekiwanie na nową kampanię..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Nieoczekiwany błąd zawartości, zwykle z powodu przekierowania.\nUpewnij się że nie jest wymagane dodatkowe logowanie bądź potwierdzenie dostępu do internetu.", 16 | "chrome": { 17 | "startup": "Uruchamianie Chrome...", 18 | "login_to_complete": "Naciśnij ponownie zaloguj, aby zakończyć proces ręcznego logowania...", 19 | "no_token": "Nie znaleziono tokena autoryzacyjnego.", 20 | "closed_window": "Okno przeglądarki Chrome zostało zamknięte przed zakończeniem procesu logowania." 21 | }, 22 | "error_code": "Kod błędu logowania: {error_code}", 23 | "incorrect_login_pass": "Nieprawidłowa nazwa użytkownika lub hasło.", 24 | "incorrect_email_code": "Nieprawidłowy kod z e-maila.", 25 | "incorrect_twofa_code": "Nieprawidłowy kod 2FA.", 26 | "email_code_required": "Wymagany kod z e-maila.", 27 | "twofa_code_required": "Wymagany token 2FA." 28 | }, 29 | "error": { 30 | "captcha": "Próba logowania została odrzucona przez CAPTCHA.\nProszę spróbować ponownie za co najmniej 12 godzin.", 31 | "site_down": "Strona Twitcha nie jest dostępna. Spróbuj ponownie za {seconds} s....", 32 | "no_connection": "Nie można połączyć się z Twitchem. Spróbuj ponownie za {seconds} s...." 33 | }, 34 | "gui": { 35 | "output": "Dziennik zdarzeń", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Bezczynność", 39 | "exiting": "Zamykanie...", 40 | "terminated": "Zatrzymano", 41 | "cleanup": "Czyszczenie kanałów...", 42 | "gathering": "Szukanie kanałów...", 43 | "switching": "Zmiana kanałów...", 44 | "fetching_inventory": "Odświeżanie ekwipunku...", 45 | "fetching_campaigns": "Odświeżanie kampanii...", 46 | "adding_campaigns": "Dodawanie kampanii do ekwipunku... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Główna", 50 | "inventory": "Ekwipunek", 51 | "settings": "Ustawienia", 52 | "help": "Pomoc" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop odebrany", 56 | "minimize": "Zminimalizuj", 57 | "show": "Pokaż", 58 | "quit": "Wyjdź" 59 | }, 60 | "login": { 61 | "name": "Logowanie", 62 | "labels": "Status:\nIdentyfikator:", 63 | "logged_in": "Zalogowano", 64 | "logged_out": "Wylogowano", 65 | "logging_in": "Logowanie...", 66 | "required": "Wymagane zalogowanie", 67 | "request": "Zaloguj się, by kontynuować.", 68 | "username": "Nazwa użytkownika", 69 | "password": "Hasło", 70 | "twofa_code": "Kod 2FA (opcjonalnie)", 71 | "button": "Zaloguj" 72 | }, 73 | "websocket": { 74 | "name": "Status WebSocket", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "Inicjalizacja...", 77 | "connected": "Połączono", 78 | "disconnected": "Rozłączono", 79 | "connecting": "Łączenie...", 80 | "disconnecting": "Rozłączanie...", 81 | "reconnecting": "Ponowne łączenie..." 82 | }, 83 | "progress": { 84 | "name": "Postępy kampanii", 85 | "drop": "Drop:", 86 | "game": "Gra:", 87 | "campaign": "Kampania:", 88 | "remaining": "Pozostało: {time}", 89 | "drop_progress": "Postęp dropu:", 90 | "campaign_progress": "Postęp kampanii:" 91 | }, 92 | "channels": { 93 | "name": "Kanały", 94 | "switch": "Zmień", 95 | "load_points": "Załaduj punkty", 96 | "online": "ONLINE \u2714", 97 | "pending": "W TOKU \u23f3", 98 | "offline": "OFFLINE \u274c", 99 | "headings": { 100 | "channel": "Kanał", 101 | "status": "Status", 102 | "game": "Gra", 103 | "viewers": "Widzowie", 104 | "points": "Punkty" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtr", 110 | "show": "Pokaż:", 111 | "not_linked": "Niepołączone", 112 | "upcoming": "Nadchodzące", 113 | "expired": "Wygasłe", 114 | "excluded": "Wykluczone", 115 | "finished": "Ukończone", 116 | "refresh": "Odśwież" 117 | }, 118 | "status": { 119 | "linked": "Połączono \u2714", 120 | "not_linked": "Niepołączono \u274c", 121 | "active": "Aktywna \u2714", 122 | "upcoming": "Nadchodząca \u23f3", 123 | "expired": "Wygasła \u274c", 124 | "claimed": "Odebrano \u2714", 125 | "ready_to_claim": "Gotowe do odebrania \u23f3" 126 | }, 127 | "starts": "Rozpoczęcie: {time}", 128 | "ends": "Koniec: {time}", 129 | "allowed_channels": "Dozwolone kanały:", 130 | "all_channels": "Wszystkie kanały", 131 | "and_more": "i {amount} więcej...", 132 | "percent_progress": "{percent} z {minutes} min.", 133 | "minutes_progress": "{minutes} min." 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Ogólne", 138 | "dark_theme": "Ciemny motyw:", 139 | "autostart": "Autostart: ", 140 | "tray": "Autostart z zasobnika: ", 141 | "tray_notifications": "Powiadomienia z zasobnika: ", 142 | "priority_only": "Tylko priorytetowe: ", 143 | "prioritize_by_ending_soonest": "Priorytetyzuj według czasu zakończenia:", 144 | "proxy": "Proxy (wymaga restartu):" 145 | }, 146 | "game_name": "Nazwa gry", 147 | "priority": "Priorytety", 148 | "exclude": "Wykluczone", 149 | "reload": "Przeładuj", 150 | "reload_text": "Większość zmian wymaga przeładowania, które natychmiastowo je zastosuje: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Pomocne linki", 155 | "inventory": "Zobacz swój ekwipunek na Twitchu", 156 | "campaigns": "Zobacz wszystkie kampanie na Twitchu" 157 | }, 158 | "how_it_works": "Jak to działa?", 159 | "how_it_works_text": "Co około 20 sekund aplikacja wysyła zapytanie do Twitcha o adres URL do nieprzetworzonych danych transmisji aktualnie oglądanego kanału. Następnie pobiera metadane tej transmisji - to wystarczy, aby ukończyć dropienie. Zauważ, że w ten sposób możesz całkowicie pominąć konieczność pobierania rzeczywistego strumienia wideo i dźwięku. Do utrzymania statusu ONLINE lub OFFLINE kanałów używane jest połączenie WebSocket, które odbiera zdarzenia o zmianie statusu kanałów oraz aktualizuje aktualną ilość widzów.", 160 | "getting_started": "Pierwsze kroki", 161 | "getting_started_text": "1. Zaloguj się do aplikacji.\n2. Upewnij się, że twoje konto Twitch jest połączone ze wszystkimi kampaniami, z których chcesz dropić.\n3. Odznacz opcję „Tylko priorytetowe” i kliknij „Przeładuj”, jeśli chcesz rozpocząć dropienie ze wszystkich aktywnych kampanii.\n4. Użyj listy „Priorytety”, aby wybrać uporządkowaną listę gier, z których chcesz otrzymać dropy. Gry z górnej części listy będą miały większy priorytet dropienia niż te niżej na liście.\n5. Zaznacz opcję „Tylko priorytetowe”, aby wykluczyć z dropienia gry, które nie są na liście priorytetowej.\n6. Użyj listy „Wykluczone”, aby wskazać aplikacji gry, z których przedmioty nigdy nie powinny być dropione.\n7. Zmiana zawartości list lub opcji „Tylko priorytetowe” wymaga kliknięcia „Przeładuj”, aby aplikacja mogła zastosować wprowadzone zmiany." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Português.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Portuguese", 3 | "status": { 4 | "terminated": "\nAplicação Finalizada.\nFeche a janela para sair do programa.", 5 | "watching": "Assistindo o: {channel}", 6 | "goes_online": "{channel} está ONLINE, Mudando...", 7 | "goes_offline": "{channel} ficou OFFLINE, Mudando...", 8 | "claimed_drop": "Coletou o drop: {drop}", 9 | "claimed_points": "Coletou o bônus: {points}", 10 | "earned_points": "Ganhou este valor de pontos: {points}, total: {balance}", 11 | "no_channel": "Sem canal disponível para assistir. Esperando por um canal ONLINE...", 12 | "no_campaign": "Sem campanhas ativas no momento para minerar. Esperando por uma campanha ativa..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Ocorreu um erro inesperado, geralmente causado por redirecionamento de internet. Você precisa se autenticar para ter acesso à internet?", 16 | "chrome": { 17 | "startup": "Abrindo Google Chrome", 18 | "login_to_complete": "Por Favor complete o login pela página web manualmente pressionando o botão Login novamente.", 19 | "no_token": "Seu Token de autorização Twitch não foi encontrado", 20 | "closed_window": "O Google Chrome foi fechado antes da autenticação ser concluída." 21 | }, 22 | "error_code": "Código de erro do login: {error_code}", 23 | "incorrect_login_pass": "Usuário e/ou senha incorreto(s)", 24 | "incorrect_email_code": "Codigo de email incorreto", 25 | "incorrect_twofa_code": "Codigo F2A incorreto", 26 | "email_code_required": "Código no email necessário. Verifique sua caixa de entrada", 27 | "twofa_code_required": "Token 2FA necessário." 28 | }, 29 | "error": { 30 | "captcha": "Seu login foi negado pelo CAPTCHA.\nPor favor tente em aproximadamente 12+ horas.", 31 | "site_down": "Twitch está fora do ar, tentando novamente em {seconds} segundos...", 32 | "no_connection": "Sem conexão com a TWITCH, reconectando em {seconds} segundos..." 33 | }, 34 | "gui": { 35 | "output": "Saída", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Em espera", 39 | "exiting": "Saindo", 40 | "terminated": "Finalizado", 41 | "cleanup": "Limpando os Canais...", 42 | "gathering": "Coletando Canais...", 43 | "switching": "Mudando o canal...", 44 | "fetching_inventory": "Coletando inventário...", 45 | "fetching_campaigns": "Coletando campanhas...", 46 | "adding_campaigns": "Adicionando as campanhas para o sistema... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Tela Principal", 50 | "inventory": "Inventário", 51 | "settings": "Configurações", 52 | "help": "Ajuda" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop coletado", 56 | "minimize": "Minimizar para barra de tarefas", 57 | "show": "Mostrar", 58 | "quit": "Sair" 59 | }, 60 | "login": { 61 | "name": "Tela de login", 62 | "labels": "Status:\nUser ID:", 63 | "logged_in": "Entrou", 64 | "logged_out": "Saiu", 65 | "logging_in": "Entrando...", 66 | "required": "Login necessário", 67 | "request": "Por favor, entre para continuar.", 68 | "username": "Usuário", 69 | "password": "Senha", 70 | "twofa_code": "Código 2FA (opcional)", 71 | "button": "Entrar" 72 | }, 73 | "websocket": { 74 | "name": "Status Websocket", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Inicializando...", 77 | "connected": "Conectado", 78 | "disconnected": "Disconectado", 79 | "connecting": "Conectando...", 80 | "disconnecting": "Desconectando...", 81 | "reconnecting": "Reconectando..." 82 | }, 83 | "progress": { 84 | "name": "Progresso da campanha", 85 | "drop": "Drop:", 86 | "game": "Jogo:", 87 | "campaign": "Campanha:", 88 | "remaining": "{time} restante", 89 | "drop_progress": "Progresso:", 90 | "campaign_progress": "Progresso:" 91 | }, 92 | "channels": { 93 | "name": "Canais", 94 | "switch": "Trocar", 95 | "load_points": "Carregar pontos", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Canal", 101 | "status": "Status", 102 | "game": "Jogo", 103 | "viewers": "Espectadores", 104 | "points": "Pontos" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtro", 110 | "show": "Mostrar:", 111 | "not_linked": "Não ligado", 112 | "upcoming": "Se Aproximando", 113 | "expired": "Expirado", 114 | "excluded": "Excluído", 115 | "finished": "Finalizado", 116 | "refresh": "Atualizar" 117 | }, 118 | "status": { 119 | "linked": "Conectado ✔", 120 | "not_linked": "Desconectado ❌", 121 | "active": "Ativo ✔", 122 | "upcoming": "Se aproximando ⏳", 123 | "expired": "Expirado ❌", 124 | "claimed": "Coletado ✔", 125 | "ready_to_claim": "Pronto para coleta ⏳" 126 | }, 127 | "starts": "Começa em: {time}", 128 | "ends": "Termina em: {time}", 129 | "allowed_channels": "Canais Autorizados:", 130 | "all_channels": "Todos", 131 | "and_more": "e {amount} mais...", 132 | "percent_progress": "{percent} de {minutes} minutos", 133 | "minutes_progress": "{minutes} minutos" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Geral", 138 | "dark_theme": "Tema escuro: ", 139 | "autostart": "Inicialização Automática: ", 140 | "tray": "Auto-iniciar na área de notificação: ", 141 | "tray_notifications": "Notificações de bandeja: ", 142 | "priority_only": "Apenas com prioridade: ", 143 | "prioritize_by_ending_soonest": "Priorize o término mais rápido: ", 144 | "proxy": "Proxy (requer o reinício do app):" 145 | }, 146 | "game_name": "Nome do jogo", 147 | "priority": "Prioridade", 148 | "exclude": "Excluir", 149 | "reload": "Recarregar", 150 | "reload_text": "A maioria das mudanças requer o reinício do app para serem efetivadas: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Links Úteis", 155 | "inventory": "Ver inventário da Twitch", 156 | "campaigns": "Veja todas campanhas e gerencie conexões da sua conta" 157 | }, 158 | "how_it_works": "Como funciona", 159 | "how_it_works_text": "A cada 60 segundos, o app envia um event \"minute watched\" ao canal que está sendo assistido no momento - Isso é o suficiente para avançar o progresso dos drops. Note que esse modo evita a necessidade de baixar o áudio e vídeo da stream, como é feito tradicionalmente quando se utiliza um navegador web. Para manter atualizado o status do canal (ONLINE ou OFFLINE), é estabelecida uma conexão websocket, que recebe diversos eventos relacionados a stream atual, como: se ela está ligada ou desligada, ou número total de espectadores.", 160 | "getting_started": "Iniciando", 161 | "getting_started_text": "1. Autentique-se no app com a sua conta da Twitch.\n2. Certifique-se que todas campanhas estão conectadas com as contas dos respectivos aplicativos.\n3. Se quiser coletar tudo, desmarque \"Apenas com Prioridade\" e clique em \"Recarregar\".\n4. Se quiser coletar apenas de certos jogos, use a lista \"Prioridade\" para configurar a lista de jogos que você quer coletar. A lista segue a ordem que é apresentada, do primeiro ao último, sendo assim, a coleta continuará para os jogos seguintes assim que o jogo anterior esteja completo.\n5. Mantenha a opção \"Priority only\" ativa, para evitar que a coleta ocorra em jogos que não estão na lista de prioridades. Ou não - você quem manda.\n6. Use a lista \"Excluir\" para jogos que NUNCA devam ser coletados.\n7. Qualquer mudança feita nas listas requer que você clique no botão \"Recarregar\" para que as mudanças tenham efeito." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Türkçe.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Turkish", 3 | "status": { 4 | "terminated": "\nUygulama durduruldu.\nUygulamadan çıkmak için pencereyi kapatın", 5 | "watching": "{channel} izleniyor", 6 | "goes_online": "{channel} ÇEVRİM İÇİ oldu, geçiş yapılıyor...", 7 | "goes_offline": "{channel} ÇEVRİM DIŞI oldu, geçiş yapılıyor...", 8 | "claimed_drop": "Alınan ödül: {drop}", 9 | "claimed_points": "Kazanılan bonus kanal puanları: {points}", 10 | "earned_points": "İzleme karşılığında kazanılan kanal puanları: {points} | Toplam: {balance}", 11 | "no_channel": "İzlenebilecek kanal yok. ÇEVRİM İÇİ kanal bekleniyor...", 12 | "no_campaign": "Ödül madenciliği için aktif kampanya yok. Aktif bir kampanya bekleniyor..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Beklenmeyen içerik türü döndürüldü, genellikle yeniden yönlendirilme nedeniyle. İnternet erişimi için giriş yapmanız gerekiyor mu?", 16 | "chrome": { 17 | "startup": "Tarayıcı Açılıyor...", 18 | "login_to_complete": "Oturum aç düğmesine tekrar basarak oturum açma işlemini kendiniz tamamlayın.", 19 | "no_token": "Yetkilendirme anahtarı bulunamadı.", 20 | "closed_window": "Oturum açma işlemi tamamlanamadan tarayıcı penceresi kapatıldı." 21 | }, 22 | "error_code": "Oturum açma hatası kodu: {error_code}", 23 | "incorrect_login_pass": "Yanlış kullanıcı adı veya şifre.", 24 | "incorrect_email_code": "Yanlış e-posta kodu.", 25 | "incorrect_twofa_code": "Yanlış 2FA kodu.", 26 | "email_code_required": "E-posta kodu gerekli. Lütfen e-postanızı kontrol edin.", 27 | "twofa_code_required": "2FA kodu gerekli." 28 | }, 29 | "error": { 30 | "captcha": "Giriş denemeniz CAPTCHA tarafından reddedildi.\nLütfen en az 12 saat sonra tekrar deneyin.", 31 | "site_down": "Twitch kapalı, {seconds} saniye içinde tekrar denenecek...", 32 | "no_connection": "Twitch'e bağlanılamıyor. {seconds} saniye içinde tekrar deneniyor..." 33 | }, 34 | "gui": { 35 | "output": "Çıktı", 36 | "status": { 37 | "name": "Durum", 38 | "idle": "Boşta", 39 | "exiting": "Çıkılıyor...", 40 | "terminated": "Sonlandırıldı", 41 | "cleanup": " Kanallar temizleniyor...", 42 | "gathering": " Kanallar toplanıyor...", 43 | "switching": "Kanal değiştiriliyor...", 44 | "fetching_inventory": "Envanter getiriliyor...", 45 | "fetching_campaigns": "Kampanyalar getiriliyor...", 46 | "adding_campaigns": "Kampanyalar envantere ekleniyor... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Ana Sayfa", 50 | "inventory": "Envanter", 51 | "settings": "Ayarlar", 52 | "help": "Yardım" 53 | }, 54 | "tray": { 55 | "notification_title": "Ödül alındı", 56 | "minimize": "Sistem Tepsisine Küçült", 57 | "show": "Göster", 58 | "quit": "Çık" 59 | }, 60 | "login": { 61 | "name": "Giriş Kısmı", 62 | "labels": "Durum:\nKullanıcı ID:", 63 | "logged_in": "Giriş Yapıldı", 64 | "logged_out": "Çıkış Yapıldı", 65 | "logging_in": "Giriş yapılıyor...", 66 | "required": "Oturum açmak gerekli", 67 | "request": "Devam etmek için lütfen giriş yapınız.", 68 | "username": "Kullanıcı Adı", 69 | "password": "Parola", 70 | "twofa_code": "2FA kodu (isteğe bağlı)", 71 | "button": "Giriş yap" 72 | }, 73 | "websocket": { 74 | "name": "WebSocket Durumu", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "Başlatılıyor...", 77 | "connected": "Bağlandı", 78 | "disconnected": "Bağlantı koptu", 79 | "connecting": "Bağlanıyor...", 80 | "disconnecting": "Bağlantı kesiliyor...", 81 | "reconnecting": "Yeniden bağlanılıyor..." 82 | }, 83 | "progress": { 84 | "name": "Ödül İlerlemesi", 85 | "drop": "Ödül:", 86 | "game": "Oyun:", 87 | "campaign": "Kampanya:", 88 | "remaining": "{time} geriye kalan", 89 | "drop_progress": "İlerleme:", 90 | "campaign_progress": "İlerleme:" 91 | }, 92 | "channels": { 93 | "name": "Kanallar", 94 | "switch": "Değiştir", 95 | "load_points": "Puanları Yükle", 96 | "online": "ÇEVRİM İÇİ ✔", 97 | "pending": "ÇEVRİM DIŞI⏳", 98 | "offline": "ÇEVRİM DIŞI ❌", 99 | "headings": { 100 | "channel": "Kanal", 101 | "status": "Durum", 102 | "game": "Oyun", 103 | "viewers": "İzleyici", 104 | "points": "Puan" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtre", 110 | "show": "Göster:", 111 | "not_linked": "Bağlantılı değil", 112 | "upcoming": "Gelecekler", 113 | "expired": "Süresi dolanlar", 114 | "excluded": "Hariç tutulanlar", 115 | "finished": "Bitenler", 116 | "refresh": "Yenile" 117 | }, 118 | "status": { 119 | "linked": "Bağlantılı ✔", 120 | "not_linked": "Bağlantılı değil ❌", 121 | "active": "Aktif ✔", 122 | "upcoming": "Gelecek ⏳", 123 | "expired": "Süresi dolmuş ❌", 124 | "claimed": "Alındı ​​✔", 125 | "ready_to_claim": "Almaya hazır ⏳" 126 | }, 127 | "starts": "Başlangıç: {time}", 128 | "ends": "Bitiş: {time}", 129 | "allowed_channels": "Katılan Kanallar:", 130 | "all_channels": "Tüm Kanallar", 131 | "and_more": "ve {amount} diğer...", 132 | "percent_progress": "{percent} {minutes} dakika", 133 | "minutes_progress": "{minutes} dakika" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Genel", 138 | "dark_theme": "Karanlık tema:", 139 | "autostart": "Otomatik başlat: ", 140 | "tray": "Sistem tepsisine otomatik başlat: ", 141 | "tray_notifications": "Bildirim gönder: ", 142 | "priority_only": "Öncelik sıralamasına göre: ", 143 | "prioritize_by_ending_soonest": "En kısa sürede biteceklere öncelik ver:", 144 | "proxy": "Proxy (Yeniden başlatma gerektirir):" 145 | }, 146 | "game_name": "Oyun ismi", 147 | "priority": "Öncelik Sırası", 148 | "exclude": "Hariç Tutulacaklar", 149 | "reload": "Yeniden Yükle", 150 | "reload_text": "Değişikliklerin uygulanabilmesi için yeniden yükle tuşuna basılması lazım:" 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Faydalı Bağlantılar", 155 | "inventory": "Twitch Envanterini Görüntüle", 156 | "campaigns": "Tüm Twitch Kampanyalarını Görüntüle" 157 | }, 158 | "how_it_works": "Nasıl çalışır", 159 | "how_it_works_text": "Uygulama, her 20 saniyede bir Twitch'ten o anda izlenmekte olan kanalın ham akış verilerine ait bir URL ister. Daha sonra bu veri akışının meta verilerini alır bu ödülleri ilerletmek için yeterlidir. Bu herhangi bir video ve sesi indirme ihtiyacını tamamen atlar. Kanalların durumunu (Çevrim içi veya Çevrim dışı) güncel tutmak için, akışların yukarı veya aşağı gitmesiyle ilgili olayları veya mevcut izleyici miktarıyla ilgili güncellemeleri alan bir websocket bağlantısı kurulmuştur.", 160 | "getting_started": "Başlarken", 161 | "getting_started_text": "1. Uygulamaya giriş yapın.\n2. Twitch hesabınızın madencilikle ilgilendiğiniz tüm kampanyalara bağlı olduğundan emin olun.\n3. Her şeyi madencilikle ilgileniyorsanız, “Yalnızca öncelikli” seçeneğinin işaretini kaldırın ve “Yeniden Yükle” tuşuna basın.\n4. Önce belirli oyunların madenciliğini yapmak istiyorsanız, seçtiğiniz oyunların sıralı bir listesini oluşturmak için “Öncelik” listesini kullanın. Listenin en üstündeki oyunlar, listenin altındakilerden önce çıkarılmaya çalışılacaktır.\n5. Öncelik listesinde olmayan oyunların madenciliğini önlemek için “Yalnızca öncelik” seçeneğini işaretli tutun. Ya da yapmayın , bu size kalmış.\n6. Uygulamaya hangi oyunların asla madenciliğinin yapılmaması gerektiğini söylemek için “Hariç tut” listesini kullanın.\n7. Listelerden herhangi birinin içeriğini değiştirmek veya “Yalnızca öncelik” seçeneğinin durumunu değiştirmek, değişikliklerin etkili olması için “Yeniden Yükle” düğmesine basmanızı gerektirir." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Čeština.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Czech", 3 | "status": { 4 | "terminated": "\nAplikace byla ukončena.", 5 | "watching": "Sledování kanálu: {channel}", 6 | "goes_online": "Kanál {channel} je online, přepínám...", 7 | "goes_offline": "Kanál {channel} je offline, přepínám na další...", 8 | "claimed_drop": "Drop vyzvednut: {drop}", 9 | "claimed_points": "Vyzvednuto {points} bodů", 10 | "earned_points": "Získáno {points} bodů za sledování. Celkem: {balance}", 11 | "no_channel": "Žádný kanál není dostupný, čekání na další...", 12 | "no_campaign": "Žádné dropy k dispozici, čekání na další dostupné dropy..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Chyba při přesměrování. Nepoužíváte VPN?", 16 | "chrome": { 17 | "startup": "Otevírá se Chrome...", 18 | "login_to_complete": "Automaticky dokončete proces přihlášení opětovným kliknutím na tlačítko přihlásit.", 19 | "no_token": "Nebyl nalezen žádný autorizační token.", 20 | "closed_window": "Okno Chrome bylo zavřeno před dokončním procesu přihlášení." 21 | }, 22 | "error_code": "Chyba přihlášení: {error_code}", 23 | "incorrect_login_pass": "Nesprávné uživatelské jméno nebo heslo.", 24 | "incorrect_email_code": "Nesprávný E-Mail kód.", 25 | "incorrect_twofa_code": "Nesprávný dvoufaktorový token", 26 | "email_code_required": "K přihlášení je vyžadován kód který byl zaslán na váš E-Mail.", 27 | "twofa_code_required": "K přihlášení je vyžadován dvoufaktorový kód." 28 | }, 29 | "error": { 30 | "captcha": "Vaše připojení bylo zamítnuto systémem přihlásit.CAPTCHA, zkuste to znovu za 12 hodin.", 31 | "site_down": "Služba Twitch je nedostupná,zkuste to znovu za {seconds} sekund...", 32 | "no_connection": "Nelze se připojit k službe Twitch, zkuste to znovu za {seconds}..." 33 | }, 34 | "gui": { 35 | "output": "Výstup", 36 | "status": { 37 | "name": "Status", 38 | "idle": "Dokončeno načítání", 39 | "exiting": "Ukončování...", 40 | "terminated": "Aplikace ukončena", 41 | "cleanup": "Čištění...", 42 | "gathering": "Vyhledávání dostupného živého kanálu...", 43 | "switching": "Přepínám mezi kanály...", 44 | "fetching_inventory": "Načítání inventáře...", 45 | "fetching_campaigns": "Načítání dropů...", 46 | "adding_campaigns": "Přidávám dropy... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Hlavní Panel", 50 | "inventory": "Inventář", 51 | "settings": "Nastavení", 52 | "help": "Nápověda" 53 | }, 54 | "tray": { 55 | "notification_title": "Začít sbírat dropy", 56 | "minimize": "Minimalizovat", 57 | "show": "Zobrazit", 58 | "quit": "Ukončit" 59 | }, 60 | "login": { 61 | "name": "Přihlášení k službě Twitch", 62 | "labels": "Uživatelské ID:", 63 | "logged_in": "Přihlášeno", 64 | "logged_out": "Odhlášeno", 65 | "logging_in": "Přihlašování...", 66 | "required": "Potřebujete se nejdříve přihlásit", 67 | "request": "Pro přístup je potřeba přihlášení", 68 | "username": "Uživatelské Jméno", 69 | "password": "Heslo", 70 | "twofa_code": "2FA Kód", 71 | "button": "Přihlásit se" 72 | }, 73 | "websocket": { 74 | "name": "Status Připojení Síťového Protokolu", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "Načítání", 77 | "connected": "Připojeno", 78 | "disconnected": "Odpojeno", 79 | "connecting": "Připojování...", 80 | "disconnecting": "Odpojování...", 81 | "reconnecting": "Přepojování..." 82 | }, 83 | "progress": { 84 | "name": "Průběh Dropu", 85 | "drop": "Drop Odměny:", 86 | "game": "Hra:", 87 | "campaign": "Kampaň:", 88 | "remaining": "Zbývá {time}", 89 | "drop_progress": "Průběh dropu:", 90 | "campaign_progress": "Průběh kampaňe:" 91 | }, 92 | "channels": { 93 | "name": "Název", 94 | "switch": "Přepnout", 95 | "load_points": "Načíst body", 96 | "online": "ONLINE ✔", 97 | "pending": "PRŮBĚH ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "Kanál", 101 | "status": "Status", 102 | "game": "Hra", 103 | "viewers": "Diváci", 104 | "points": "Body" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Filtr", 110 | "show": "Drop:", 111 | "not_linked": "Nepropojeno", 112 | "upcoming": "Nadcházející", 113 | "expired": "Ukončeno", 114 | "excluded": "Vynecháno", 115 | "finished": "Dokončeno", 116 | "refresh": "Obnovit" 117 | }, 118 | "status": { 119 | "linked": "ŽIVĚ ✔", 120 | "not_linked": "Zatím nejsou dostupné žádné kanály ❌", 121 | "active": "ŽIVĚ ✔", 122 | "upcoming": "Nadcházející ⏳", 123 | "expired": "Ukončeno ❌", 124 | "claimed": "Vyzvednuto ✔", 125 | "ready_to_claim": "Připraveno k vyzvednutí ⏳" 126 | }, 127 | "starts": "Začíná: {time}", 128 | "ends": "Začíná: {time}", 129 | "allowed_channels": "Povolené kanály:", 130 | "all_channels": "Všechny kanály", 131 | "and_more": "Je tu {amount} ...", 132 | "percent_progress": "{minutes} minut {percent}", 133 | "minutes_progress": "{minutes} minut" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Nastavení", 138 | "dark_theme": "Tmavý vzhled: ", 139 | "autostart": "Automatické spuštění: ", 140 | "tray": "Automaticky spusti minimalizovaně: ", 141 | "tray_notifications": "Oznámení v systémové liště:", 142 | "priority_only": "Pouze prioritní: ", 143 | "prioritize_by_ending_soonest": "Upřednostnit kampaně podle data ukončení:", 144 | "proxy": "Proxy:" 145 | }, 146 | "game_name": "Název Hry", 147 | "priority": "Priorita", 148 | "exclude": "Vynechat", 149 | "reload": "Obnovit", 150 | "reload_text": "Většina změn vyžaduje restart aplikace nebo obnovení: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Nápověda", 155 | "inventory": "Zobrazit Twitch Inventář", 156 | "campaigns": "Zobrazit všechny kampaňe a spravovat propojené účty" 157 | }, 158 | "how_it_works": "Jak to funguje", 159 | "how_it_works_text": "Každých ~20 sekund aplikace požádá Twitch o adresu k čistým datám streamu od kanálu, který je v současnosti sledován. Poté stáhne dodatkové informace těchto dat streamu - to stačí k posunu umístění. Všimněte si, že tímto způsobem zcela odpadá nutnost stahovat skutečné streamované video a zvuk. Aby byl stav kanálu (online nebo offline) stále aktuální, je navázáno spojení přes websocket, které přijímá události o spuštění nebo vypnutí streamů nebo aktualizace o aktuálním počtu diváků.", 160 | "getting_started": "Jak začít", 161 | "getting_started_text": "1. Přihlaste se do aplikace. \n2. Ujistěte se, že je váš účet Twitch spojen se všemi reklamními sériemi, které chcete těžit. \n3. Pokud chcete klepnout pouze na veškerý obsah, zrušte zaškrtnutí políčka 'Pouze prioritní' a stiskněte tlačítko 'Obnovit'. \n4. Pokud se chcete nejprve věnovat konkrétním hrám, použijte seznam 'Prioritní' a nastavte si pořadí vybraných her. Nejprve se vyzkouší hry na začátku seznamu a poté hry na konci seznamu. \n5. Ponechte zaškrtnutou možnost 'Pouze prioritní', abyste se vyhnuli vyhledávání her, které nejsou na seznamu priorit. Nebo ne - záleží na vás. \n6. Pomocí seznamu 'vyloučit' můžete aplikaci sdělit, které hry by neměly být vytěženy. \n7. Změna obsahu obou seznamů nebo stavu možnosti Pouze prioritní vyžaduje stisknutí tlačítka Znovu načíst, aby se změna projevila." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Русский.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Russian", 3 | "status": { 4 | "terminated": "\nПриложение остановлено.\nЗакройте окно, чтобы выйти из приложения.", 5 | "watching": "Просмотр канала: {channel}", 6 | "goes_online": "Изменение: {channel} онлайн", 7 | "goes_offline": "Изменение: {channel} онлайн", 8 | "claimed_drop": "Drop получено: {drop}", 9 | "claimed_points": "Получены очки канала: {points}", 10 | "earned_points": "За просмотр начисляются очки канала: {points} | Сумма: {balance}", 11 | "no_channel": "Нет участвующих каналов онлайн. Ожидание каналов...", 12 | "no_campaign": "Нет участвующих кампаний онлайн. Ожидание кампаний..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Возвращен неожиданный тип содержимого, обычно из-за перенаправления. Требуется ли логин для доступа в Интернет?", 16 | "chrome": { 17 | "startup": "Запуск Браузера...", 18 | "login_to_complete": "Нажмите Log In еще раз, чтобы завершить процесс входа в систему вручную..", 19 | "no_token": "Не найден токен авторизации.", 20 | "closed_window": "Окно Браузера было закрыто до завершения процесса входа в систему." 21 | }, 22 | "error_code": "Код ошибки входа в систему: {error_code}", 23 | "incorrect_login_pass": "Неправильное имя пользователя или пароль.", 24 | "incorrect_email_code": "Неправильный код электронной почты.", 25 | "incorrect_twofa_code": "Неправильный код 2FA.", 26 | "email_code_required": "Требуется код электронной почты. Пожалуйста, проверьте электронную почту.", 27 | "twofa_code_required": "Требуется код 2FA." 28 | }, 29 | "error": { 30 | "captcha": "Попытка входа в систему была отклонена CAPTCHA.\nПожалуйста, повторите попытку не менее чем через 12 часов.", 31 | "site_down": "Twitch недоступен. Повторите попытку через {seconds} секунд...", 32 | "no_connection": "Невозможно подключиться к Twitch. Повторите попытку через {seconds} секунд..." 33 | }, 34 | "gui": { 35 | "output": "Протокол", 36 | "status": { 37 | "name": "Статус", 38 | "idle": "Вхолостую", 39 | "exiting": "Выход...", 40 | "terminated": "Прекращено", 41 | "cleanup": "Очистка каналов...", 42 | "gathering": "Поиск каналов...", 43 | "switching": "Переключение канала...", 44 | "fetching_inventory": "Получение инвентаря...", 45 | "fetching_campaigns": "Получение кампаний...", 46 | "adding_campaigns": "Добавление кампаний в инвентарь... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Главная", 50 | "inventory": "Инвентарь", 51 | "settings": "Настройки", 52 | "help": "Помощь" 53 | }, 54 | "tray": { 55 | "notification_title": "Drop получено", 56 | "minimize": "Свернуть в трей", 57 | "show": "Показать", 58 | "quit": "Закрыть" 59 | }, 60 | "login": { 61 | "name": "Авторизация", 62 | "labels": "Статус:\nID пользователя:", 63 | "logged_in": "Авторизован", 64 | "logged_out": "не авторизован", 65 | "logging_in": "Авторизация...", 66 | "required": "Требуется авторизация", 67 | "request": "Пожалуйста, авторизуйтесь, чтобы продолжить.", 68 | "username": "Имя пользователя", 69 | "password": "Пароль", 70 | "twofa_code": "2FA код (опционально)", 71 | "button": "Войти" 72 | }, 73 | "websocket": { 74 | "name": "WebSocket статус", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "Инициализация...", 77 | "connected": "Подключено", 78 | "disconnected": "Отключено", 79 | "connecting": "Подключение...", 80 | "disconnecting": "Отключение...", 81 | "reconnecting": "Переподключение..." 82 | }, 83 | "progress": { 84 | "name": "Ход кампании", 85 | "drop": "Drop:", 86 | "game": "Игра:", 87 | "campaign": "Кампания:", 88 | "remaining": "{time} осталось", 89 | "drop_progress": "Прогресс:", 90 | "campaign_progress": "Прогресс:" 91 | }, 92 | "channels": { 93 | "name": "Каналы", 94 | "switch": "Изменить", 95 | "load_points": "Загрузить баллы", 96 | "online": "ОНЛАЙН ✔", 97 | "pending": "ОЖИДАНИЕ ⏳", 98 | "offline": "ОФЛАЙН ❌", 99 | "headings": { 100 | "channel": "Канал", 101 | "status": "Статус", 102 | "game": "Игра", 103 | "viewers": "Зрители", 104 | "points": "Баллы" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Фильтр", 110 | "show": "Показать:", 111 | "not_linked": "Не связано", 112 | "upcoming": "Будущие", 113 | "expired": "Прошедшие", 114 | "excluded": "Исключенные", 115 | "finished": "Оконченные", 116 | "refresh": "Обновить" 117 | }, 118 | "status": { 119 | "linked": "Связанное ✔", 120 | "not_linked": "Не связанное ❌", 121 | "active": "Активное ✔", 122 | "upcoming": "Будущее ⏳", 123 | "expired": "Прошедшее ❌", 124 | "claimed": "Получено ✔", 125 | "ready_to_claim": "Готово к получению⏳" 126 | }, 127 | "starts": "Начало: {time}", 128 | "ends": "Окончание: {time}", 129 | "allowed_channels": "Участвующие каналы:", 130 | "all_channels": "Все", 131 | "and_more": "и еще {amount}...", 132 | "percent_progress": "{percent} от {minutes} минут", 133 | "minutes_progress": "{minutes} минут" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Общие", 138 | "dark_theme": "Тёмная тема: ", 139 | "autostart": "Автозапуск", 140 | "tray": "Автозапуск свёрнутым", 141 | "tray_notifications": "Всплывающие уведомления", 142 | "priority_only": "Только приоритет", 143 | "prioritize_by_ending_soonest": "Приоритизация кампаний по дате окончания:", 144 | "proxy": "Прокси (Требуется перезапуск):" 145 | }, 146 | "game_name": "Игра", 147 | "priority": "Приоритет", 148 | "exclude": "Исключения", 149 | "reload": "Перезагрузить", 150 | "reload_text": "Большинство изменений требуют перезагрузки, чтобы вступить в силу немедленно: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Полезные ссылки", 155 | "inventory": "Инвентарь Twitch", 156 | "campaigns": "Все кампании Twitch" 157 | }, 158 | "how_it_works": "Как это работает?", 159 | "how_it_works_text": "Каждые ~20 секунд приложение запрашивает у Twitch URL-адрес необработанных данных потока канала, который просматривается в данный момент. Затем он извлекает метаданные этого потока данных. \nЭтого достаточно, чтобы получить Drop. Таким образом, нет необходимости скачивать поток, что экономит трафик. \nДля поддержания актуального состояния каналов в режиме онлайн или офлайн устанавливается соединение websocket, которое проверяет состояние каналов.", 160 | "getting_started": "Первые шаги", 161 | "getting_started_text": "1. Войдите в приложение.\n2. Убедитесь, что аккаунт Twitch связан со всеми кампаниями, к которым есть интерес.\n3. Если вы хотите редактировать все Drops, снимите флажок \"Только приоритет\" и нажмите \"Перезагрузить\".\n4. Если необходимо смотреть только определенные игры, используйте список \"Приоритет\", чтобы сузить выбор игр.\n5. В списке приоритеты расставлены сверху вниз.\n6. Опция \"Только приоритет\" предотвращает просмотр игр, не входящих в список приоритетов.\n7. С помощью списка \"Исключения\" можно отфильтровать игры, которые не должны рассматриваться.\n8. Если списки или опции были изменены, нажмите \"Перезагрузить\", чтобы изменения были применены." 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/Українська.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Ukrainian", 3 | "status": { 4 | "terminated": "\nЗастосунок зупинено.\nЗакрийте вікно для виходу з програми.", 5 | "watching": "Переглядає: {channel}", 6 | "goes_online": "{channel} онлайн, зміна...", 7 | "goes_offline": "{channel} офлайн, зміна...", 8 | "claimed_drop": "Отримано дроп: {drop}", 9 | "claimed_points": "Отримано {points} бонусних балів", 10 | "earned_points": "Зароблено {points} балів за перегляд, усього: {balance}", 11 | "no_channel": "Немає активних каналів для перегляду. Очікування...", 12 | "no_campaign": "Немає активних каналів для видобутку дропів. Очікування..." 13 | }, 14 | "login": { 15 | "unexpected_content": "Повернуто неочікуваний тип вмісту, зазвичай через перенаправлення. Чи не потрібно вам увійти задля доступу в інтернет?", 16 | "chrome": { 17 | "startup": "Відкриття браузера...", 18 | "login_to_complete": "Завершіть процедуру входу власноруч, натиснувши кнопку \"Увійти ще раз\".", 19 | "no_token": "Жетон авторизації не знайдено.", 20 | "closed_window": "Вікно браузера було закрито до завершення процедури входу." 21 | }, 22 | "error_code": "Код помилки входу: {error_code}", 23 | "incorrect_login_pass": "Неправильне ім'я користувача або пароль.", 24 | "incorrect_email_code": "Неправильний код електронної пошти.", 25 | "incorrect_twofa_code": "Неправильний код двофакторної аутентифікації.", 26 | "email_code_required": "Потрібен код електронної пошти.", 27 | "twofa_code_required": "Потрібен жетон двофакторної аутентифікації." 28 | }, 29 | "error": { 30 | "captcha": "Ваша спроба входу була відхилена через капчу.\nБудь ласка, спробуйте ще раз через 12 або більше годин.", 31 | "site_down": "Twitch не працює, спроба через {seconds} секунд...", 32 | "no_connection": "Не вдається з'єднатися з Twitch, повторна спроба через {seconds} секунд..." 33 | }, 34 | "gui": { 35 | "output": "Вивід", 36 | "status": { 37 | "name": "Стан", 38 | "idle": "Бездіяльність", 39 | "exiting": "Вихід...", 40 | "terminated": "Зупинено", 41 | "cleanup": "Очищення каналів...", 42 | "gathering": "Збір каналів...", 43 | "switching": "Перемикання на канал...", 44 | "fetching_inventory": "Отримання інвентарю...", 45 | "fetching_campaigns": "Отримання кампаній...", 46 | "adding_campaigns": "Додавання кампаній до інвентарю... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "Основне", 50 | "inventory": "Інвентар", 51 | "settings": "Налаштування", 52 | "help": "Інформація" 53 | }, 54 | "tray": { 55 | "notification_title": "Дроп отримано", 56 | "minimize": "Згорнути в лоток", 57 | "show": "Показати", 58 | "quit": "Вийти" 59 | }, 60 | "login": { 61 | "name": "Форма для входу", 62 | "labels": "Стан:\nІдентифікатор користувача:", 63 | "logged_in": "Увійдено", 64 | "logged_out": "Вийдено", 65 | "logging_in": "Вхід...", 66 | "required": "Потрібен вхід", 67 | "request": "Будь ласка, увійдіть, щоб продовжити.", 68 | "username": "Ім'я користувача", 69 | "password": "Пароль", 70 | "twofa_code": "Код двофакторної автентифікації (необов'язково)", 71 | "button": "Вхід" 72 | }, 73 | "websocket": { 74 | "name": "Стан веб-сокета", 75 | "websocket": "Веб-сокет #{id}:", 76 | "initializing": "Ініціалізація...", 77 | "connected": "Підключено", 78 | "disconnected": "Відключено", 79 | "connecting": "З'єднання...", 80 | "disconnecting": "Від'єднання...", 81 | "reconnecting": "Перепідключення..." 82 | }, 83 | "progress": { 84 | "name": "Поступ кампанії", 85 | "drop": "Дроп:", 86 | "game": "Гра:", 87 | "campaign": "Кампанія:", 88 | "remaining": "{time} залишилося", 89 | "drop_progress": "Поступ:", 90 | "campaign_progress": "Поступ:" 91 | }, 92 | "channels": { 93 | "name": "Канали", 94 | "switch": "Перемкнути", 95 | "load_points": "Завантажити бали", 96 | "online": "ОНЛАЙН ✔", 97 | "pending": "ОФЛАЙН ⏳", 98 | "offline": "ОФЛАЙН ❌", 99 | "headings": { 100 | "channel": "Канал", 101 | "status": "Стан", 102 | "game": "Гра", 103 | "viewers": "Глядачі", 104 | "points": "Бали" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "Фільтри", 110 | "show": "Показати лише:", 111 | "not_linked": "Не пов'язано", 112 | "upcoming": "Наближаються", 113 | "expired": "Прострочені", 114 | "excluded": "Виключені", 115 | "finished": "Завершені", 116 | "refresh": "Оновити" 117 | }, 118 | "status": { 119 | "linked": "Пов'язано ✔", 120 | "not_linked": "Не пов'язано ❌", 121 | "active": "Діюча ✔", 122 | "upcoming": "Наближається ⏳", 123 | "expired": "Прострочено ❌", 124 | "claimed": "Отримано ✔", 125 | "ready_to_claim": "Готове до отримання ⏳" 126 | }, 127 | "starts": "Починається {time}", 128 | "ends": "Завершується {time}", 129 | "allowed_channels": "Дозволені канали:", 130 | "all_channels": "Усі", 131 | "and_more": "та ще {amount}...", 132 | "percent_progress": "{percent} від {minutes} хвилин", 133 | "minutes_progress": "{minutes} хвилин" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "Основні", 138 | "dark_theme": "Темна тема:", 139 | "autostart": "Автозапуск: ", 140 | "tray": "Автозапуск у лотку: ", 141 | "tray_notifications": "Сповіщення: ", 142 | "priority_only": "Лише пріоритетні: ", 143 | "prioritize_by_ending_soonest": "Пріоритизувати ті, що закінчуються раніше:", 144 | "proxy": "Проксі (потребує перезапуску):" 145 | }, 146 | "game_name": "Назва гри", 147 | "priority": "Пріоритет", 148 | "exclude": "Виключити", 149 | "reload": "Перезавантажити", 150 | "reload_text": "Більшість змін потребують перезавантаження, щоб набути негайної дії: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "Корисні посилання", 155 | "inventory": "Переглянути інвентар Twitch", 156 | "campaigns": "Переглянути усі кампанії та керувати пов'язаними обліковими записами" 157 | }, 158 | "how_it_works": "Як це працює?", 159 | "how_it_works_text": "Приблизно кожні 20 секунд програма запитує у Twitch URL-адресу необробленого потоку даних каналу, який ви зараз переглядаєте. Потім він отримує метадані цього потоку даних - цього достатньо, щоб здобувати дропи. Зауважте, що це повністю обходить необхідність завантажувати будь-які потокові відео та звук. Щоб підтримувати актуальний стан каналів (ОНЛАЙН або ОФЛАЙН), встановлюється з'єднання з веб-сокетом, який отримує події про збільшення або зменшення кількості трансляцій, або оновлення поточної кількості глядачів.", 160 | "getting_started": "Інструкція", 161 | "getting_started_text": "1. Увійдіть у застосунок.\n2. Переконайтеся, що ваш обліковий запис Twitch пов'язаний з усіма обліковими записами ігор, з яких ви хочете здобувати дропи.\n3. Якщо ви зацікавлені у здобуванні всього, зніміть прапорець \"Тільки пріоритет\" і натисніть \"Перезавантажити\".\n4. Якщо ви хочете здобувати дропи з певних ігор, скористайтеся списком \"Пріоритет\", щоб створити впорядкований список ігор за вашим вибором. Ігри будуть здобуватися в порядку розташування в списку.\n5. Не знімайте прапорець \"Тільки пріоритет\", щоб уникнути здобування з ігор, які не входять до пріоритетного списку. Або ні - вирішувати вам.\n6. Використовуйте список \"Виключено\", щоб вказати застосунку, з яких ігор ніколи не слід здобувати.\n7. Зміна вмісту будь-якого зі списків або зміна стану опції \"Тільки пріоритет\" вимагає натискання кнопки \"Перезавантажити\" для набуття змінами чинності.\n\nПереклад виконав @Nollasko" 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/العربية.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Arabic", 3 | "status": { 4 | "terminated": "تم إنهاء التطبيق. \n أغلق النافذة للخروج من التطبيق.", 5 | "watching": "{channel} :يتم حالياَ مشاهدة", 6 | "goes_online": "...اصبح نشط ، تبديل {channel}", 7 | "goes_offline": "...اصبح غير نشط ، تبديل {channel}", 8 | "claimed_drop": "{drop} :تم الحصول على", 9 | "claimed_points": "تم الحصول على: {points} من النقاط الإضافية", 10 | "earned_points": "تم الحصول على:{points} من نقاط المشاهدة ، الإجمالي: {balance}", 11 | "no_channel": "...لا توجد قنوات نشطة متاحة للمشاهدة. في انتظار قناة نشطة", 12 | "no_campaign": "...لا توجد حملات نشطة من اجل تنقيب الإسقاطات. في انتظار حملة نشطة" 13 | }, 14 | "login": { 15 | "unexpected_content": "تم إرجاع نوع محتوى غير متوقع ، يحدث عادة بسبب إعادة التوجيه. هل تحتاج إلى تسجيل الدخول للوصول إلى الإنترنت؟", 16 | "chrome": { 17 | "startup": "...فتح متصفح كروم", 18 | "login_to_complete": "أكمل إجراء تسجيل الدخول يدويًا عن طريق الضغط على زر تسجيل الدخول مرة أخرى.", 19 | "no_token": ".لا يمكن العثور على رمز التفويض", 20 | "closed_window": ".تم إغلاق نافذة متصفح كروم قبل أن يكتمل إجراء تسجيل الدخول" 21 | }, 22 | "error_code": "{error_code} :رمز خطأ تسجيل الدخول", 23 | "incorrect_login_pass": ".اسم المستخدم أو كلمة المرور غير صحيحة", 24 | "incorrect_email_code": ".كود البريد الإلكتروني غير صحيح", 25 | "incorrect_twofa_code": ".كود المصادقة الثنائية غير صحيح", 26 | "email_code_required": ".كود البريد الإلكتروني مطلوب. تحقق من بريدك الالكتروني", 27 | "twofa_code_required": ".رمز المصادقة الثنائية مطلوب" 28 | }, 29 | "error": { 30 | "captcha": ".يرجى المحاولة مجدداَ بعد مرور 12 ساعة \n .CAPTCHA تم رفض محاولة تسجيل الدخول الخاصة بك من قبل", 31 | "site_down": "...ثانية {seconds} معطل ، إعادة المحاولة خلال Twitch", 32 | "no_connection": "...ثانية {seconds} إعادة المحاولة خلال ، Twitch لا يمكن الإتصال بـ" 33 | }, 34 | "gui": { 35 | "output": "الإخراج", 36 | "status": { 37 | "name": "الحالة", 38 | "idle": "خامل", 39 | "exiting": "...جاري الخروج", 40 | "terminated": "تم الانهاء", 41 | "cleanup": "...مسح القنوات", 42 | "gathering": "...جمع القنوات", 43 | "switching": "...تبديل القناة", 44 | "fetching_inventory": "...جلب محتويات الحقيبة", 45 | "fetching_campaigns": "...جلب الحملات", 46 | "adding_campaigns": "{counter} ...إضافة الحملات إلى الحقيبة" 47 | }, 48 | "tabs": { 49 | "main": "الرئيسية", 50 | "inventory": "الحقيبة", 51 | "settings": "الإعدادات", 52 | "help": "مساعدة" 53 | }, 54 | "tray": { 55 | "notification_title": "تم الحصول على هذا الإسقاط", 56 | "minimize": "تصغير الى الدرج", 57 | "show": "عرض", 58 | "quit": "خروج" 59 | }, 60 | "login": { 61 | "name": "تسجيل الدخول و معلومات عن الحساب", 62 | "labels": "الحالة ➜\nالمستخدم ID ➜", 63 | "logged_in": "تم تسجيل الدخول", 64 | "logged_out": "تم تسجيل الخروج", 65 | "logging_in": "...تسجيل الدخول", 66 | "required": "تسجيل الدخول مطلوب", 67 | "request": ".الرجاء تسجيل الدخول للمتابعة", 68 | "username": "اسم المستخدم", 69 | "password": "كلمة المرور", 70 | "twofa_code": "المصادقة الثنائية (اختياري)", 71 | "button": "تسجيل الدخول" 72 | }, 73 | "websocket": { 74 | "name": "WebSocket حالة الـ", 75 | "websocket": "WebSocket #{id}:", 76 | "initializing": "...جاري التهيئة", 77 | "connected": "متصل", 78 | "disconnected": "غير متصل", 79 | "connecting": "...جاري الاتصال", 80 | "disconnecting": "...جاري قطع الاتصال", 81 | "reconnecting": "...إعادة الاتصال" 82 | }, 83 | "progress": { 84 | "name": "تقدم الحملة", 85 | "drop": ":الإسقاط", 86 | "game": ":اللعبة", 87 | "campaign": ":الحملة", 88 | "remaining": "{time} متبقي", 89 | "drop_progress": "التقدم ➜", 90 | "campaign_progress": "التقدم ➜" 91 | }, 92 | "channels": { 93 | "name": "القنوات", 94 | "switch": "تبديل", 95 | "load_points": "تحميل النقاط", 96 | "online": "✔ نشط", 97 | "pending": "⏳ غير نشط", 98 | "offline": "❌ غير نشط", 99 | "headings": { 100 | "channel": "القناة", 101 | "status": "الحالة", 102 | "game": "اللعبة", 103 | "viewers": "المشاهدين", 104 | "points": "النقاط" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "تصفية", 110 | "show": "عرض ➜", 111 | "not_linked": "غير مرتبط", 112 | "upcoming": "القادمة", 113 | "expired": "المنتهية", 114 | "excluded": "المستبعدة", 115 | "finished": "المكتملة", 116 | "refresh": "تحديث" 117 | }, 118 | "status": { 119 | "linked": "✔ مرتبط", 120 | "not_linked": "❌ غير مرتبط", 121 | "active": "✔ نشط", 122 | "upcoming": "⏳ قادم", 123 | "expired": "❌ منتهي", 124 | "claimed": "✔ تم الحصول عليه", 125 | "ready_to_claim": "⏳ جاهز للحصول عليه" 126 | }, 127 | "starts": "{time} :يبدأ", 128 | "ends": "{time} :ينتهي", 129 | "allowed_channels": ":القنوات المسموح بها", 130 | "all_channels": "الكل", 131 | "and_more": "...و {amount} قناة اخرى", 132 | "percent_progress": "تم التقدم {percent} من {minutes} دقيقة", 133 | "minutes_progress": "متبقي {minutes} دقيقة" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "عام", 138 | "dark_theme": "المظهر الداكن: ", 139 | "autostart": "تشغيل تلقائي ", 140 | "tray": "تشغيل تلقائي في الدرج ", 141 | "tray_notifications": "إشعارات الدرج ", 142 | "priority_only": "الأولوية فقط ", 143 | "prioritize_by_ending_soonest": "تحديد الأولويات من خلال الانتهاء في أقرب وقت: ", 144 | "proxy": "بروكسي (يتطلب إعادة التشغيل) " 145 | }, 146 | "game_name": "اسم اللعبة", 147 | "priority": "أولوية", 148 | "exclude": "إستبعاد", 149 | "reload": "إعادة تحميل", 150 | "reload_text": ".ملاحظة: تتطلب معظم التغييرات إعادة التحميل حتى تصبح سارية المفعول " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "روابط مفيدة", 155 | "inventory": "Twitch رؤية الحقيبة على", 156 | "campaigns": "الإطلاع على جميع الحملات وإدارة روابط الحساب" 157 | }, 158 | "how_it_works": "!كيف يعمل التطبيق", 159 | "how_it_works_text": " ، كل 60 ثانية تقريباَ يرسل التطبيق حدثاَ \"دقيقة تمت مشاهدتها\" إلى القناة التي تتم مشاهدتها حالياَ\n.وهذا يكفي لتعزيز تقدم الإسقاطات\n.لاحظ أن هذا يتخطى تمامًا الحاجة إلى تنزيل أي فيديو أو صوت فعلي\nو للمحافظة على تحديث حالة القنوات هناك اتصال ويب سوكت والذي يتلقى احداثاَ\nحول البثوت المباشرة التي تصبح نشطة او غير نشطة ، او عن العدد الحالي للمشاهدين", 160 | "getting_started": "البدء", 161 | "getting_started_text": "1. قم بتسجيل الدخول للتطبيق\n2. مرتبط بجميع الحملات المهتم بتنقيبها Twitch تأكد من ان حسابك على\n3. إذا كنت مهتم بتنقيب كل شي فقم بإلغاء تحديد خيار \"الأولوية فقط\" واضغط على إعادة التحميل\n4. اذا كنت تريد تنقيب لعبة معينة اولا ، فقم بإستخدام قائمة \"أولوية\" لإعداد قائمة مرتبة من الألعاب الي تختارها\n وسيتم محاولة تعدين الألعاب من اعلى القائمة اولاَ ، قبل الألعاب الموجودة في الاسفل\n5. \"قم بتحديد خيار \"الأولوية فقط\" ، لتجنب تنقيب العاب غير مدرجة في قائمة \"أولوية\n6. استخدم قائمة \"إستبعاد\" لإخبار التطبيق بالألعاب التي يجب ألا يتم تنقيبها ابداَ\n7. \"لإحداث تغيير في احد القوائم او تحديد او الغاء تحديد خيار \"الأولويةفقط\nيتطلب الضغط على خيار \"إعادة التحميل\" لتصبح التغييرات سارية المفعول" 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/简体中文.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Simplified Chinese", 3 | "status": { 4 | "terminated": "\n应用程序已终止,请关闭窗口以退出应用程序。", 5 | "watching": "正在观看: {channel}", 6 | "goes_online": "{channel} 该频道已上线, 正在切换...", 7 | "goes_offline": "{channel} 该频道已离线, 正在切换...", 8 | "claimed_drop": "领取的掉宝: {drop}", 9 | "claimed_points": "领取的积分奖励: {points}", 10 | "earned_points": "观看直播获得积分: {points}, 总积分: {balance}", 11 | "no_channel": "没有可观看的频道,等待一个在线直播频道...", 12 | "no_campaign": "没有可用于掉宝的活动,等待一个有效的掉宝活动..." 13 | }, 14 | "login": { 15 | "unexpected_content": "返回的内容类型有误通常是由于被重定向.您需要设置VPN或者确认登录页面才能联网吗?", 16 | "chrome": { 17 | "startup": "正在打开Chrome浏览器...", 18 | "login_to_complete": "请再次点击登录按钮来完成登陆.", 19 | "no_token": "无法获取授权令牌.", 20 | "closed_window": "在登录完成之前关闭了Chrome浏览器." 21 | }, 22 | "error_code": "登录出错: {error_code}", 23 | "incorrect_login_pass": "用户名或密码错误.", 24 | "incorrect_email_code": "电子邮箱验证码错误.", 25 | "incorrect_twofa_code": "双重验证2FA令牌错误.", 26 | "email_code_required": "需要电子邮件验证码 查看你的邮件.", 27 | "twofa_code_required": "需要双重验证2FA令牌." 28 | }, 29 | "error": { 30 | "captcha": "您的登录存在异常,Twitch官方已进行限制,请在12小时后重试.", 31 | "site_down": "Twitch无法连接,{seconds} 秒后重试...", 32 | "no_connection": "无法连接到Twitch,请确保已经开启VPN并添加了代理,{seconds} 秒后重试..." 33 | }, 34 | "gui": { 35 | "output": "输出日志", 36 | "status": { 37 | "name": "状态", 38 | "idle": "已完成获取", 39 | "exiting": "正在退出...", 40 | "terminated": "应用已终止运行", 41 | "cleanup": "清除频道中...", 42 | "gathering": "搜索直播频道...", 43 | "switching": "切换直播频道中...", 44 | "fetching_inventory": "获取库存中...", 45 | "fetching_campaigns": "获取掉宝活动...", 46 | "adding_campaigns": "将掉宝活动列表添加至库存... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "主面板", 50 | "inventory": "掉宝库存", 51 | "settings": "设置", 52 | "help": "帮助" 53 | }, 54 | "tray": { 55 | "notification_title": "开始掉宝活动", 56 | "minimize": "窗口最小化", 57 | "show": "掉宝活动", 58 | "quit": "退出" 59 | }, 60 | "login": { 61 | "name": "登录Twitch账号", 62 | "labels": "状态:\n用户ID:", 63 | "logged_in": "已登录", 64 | "logged_out": "未登录", 65 | "logging_in": "正在登录中...", 66 | "required": "请先登录", 67 | "request": "请先登录以访问更多内容.", 68 | "username": "用户名", 69 | "password": "密码", 70 | "twofa_code": "邮箱2FA令牌", 71 | "button": "登录" 72 | }, 73 | "websocket": { 74 | "name": "网络协议连接状态", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "初始化...", 77 | "connected": "已连接", 78 | "disconnected": "断开连接", 79 | "connecting": "连接中...", 80 | "disconnecting": "断开连接中...", 81 | "reconnecting": "重新连接中..." 82 | }, 83 | "progress": { 84 | "name": "掉宝活动进度", 85 | "drop": "掉宝奖励:", 86 | "game": "游戏:", 87 | "campaign": "掉宝活动名称:", 88 | "remaining": "剩余 {time}", 89 | "drop_progress": "掉宝进度:", 90 | "campaign_progress": "活动进度:" 91 | }, 92 | "channels": { 93 | "name": "活动频道", 94 | "switch": "切换", 95 | "load_points": "加载积分", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "直播频道", 101 | "status": "在线状态", 102 | "game": "游戏", 103 | "viewers": "观众", 104 | "points": "积分" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "筛选", 110 | "show": "掉宝活动:", 111 | "not_linked": "未关联账户的游戏掉宝", 112 | "upcoming": "即将上线", 113 | "expired": "已结束", 114 | "excluded": "未添加的掉宝活动", 115 | "finished": "已完成", 116 | "refresh": "刷新" 117 | }, 118 | "status": { 119 | "linked": "已关联 ✔", 120 | "not_linked": "未关联账户 ❌", 121 | "active": "已上线 ✔", 122 | "upcoming": "即将上线 ⏳", 123 | "expired": "已结束 ❌", 124 | "claimed": "已领取 ✔", 125 | "ready_to_claim": "可以领取 ⏳" 126 | }, 127 | "starts": "开始时间: {time}", 128 | "ends": "结束时间: {time}", 129 | "allowed_channels": "允许的频道:", 130 | "all_channels": "全部", 131 | "and_more": "还有 {amount} 个...", 132 | "percent_progress": "{minutes} 分钟的 {percent}", 133 | "minutes_progress": "{minutes} 分钟" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "功能设置", 138 | "dark_theme": "黑夜模式: ", 139 | "autostart": "开机自启动: ", 140 | "tray": "开机自启动并最小化: ", 141 | "tray_notifications": "启用托盘通知: ", 142 | "priority_only": "仅参与优先掉宝游戏: ", 143 | "prioritize_by_ending_soonest": "按掉宝游戏的结束日期决定优先度: ", 144 | "proxy": "代理:" 145 | }, 146 | "game_name": "游戏名称", 147 | "priority": "优先掉宝游戏", 148 | "exclude": "不参与掉宝游戏", 149 | "reload": "刷新", 150 | "reload_text": "大多数设置更改后需要重新启动软件或刷新才能生效: " 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "帮助", 155 | "inventory": "查看Twitch库存", 156 | "campaigns": "查看所有掉宝活动并管理连接账号" 157 | }, 158 | "how_it_works": "工作原理", 159 | "how_it_works_text": "每隔约 20 秒,程序就会向 Twitch 询问当前正在观看的频道的原始数据流 URL。然后,它会获取这个数据流的元数据。 - 这足以推进掉宝进度。这完全不需要下载任何实际的视频流和音频流。为了使频道的状态(在线或离线)保持最新,程序会建立 Websocket 连接,用于接收有关频道开播和下播的事件,并更新当前的观众数量。", 160 | "getting_started": "入门设置", 161 | "getting_started_text": "1. 登录应用程序。\n2.确保所有感兴趣挖掘的游戏的账号已连接到您的Twitch账号。\n3.如果您想挖掘所有掉宝,请不要勾选“仅参与优先掉宝游戏”,然后按“刷新”。\n4.如果您想优先挖掘特定游戏,请使用“优先掉宝游戏”列表设置您想挖掘的游戏的优先顺序。程序将尝试先挖掘列表顶部的游戏,然后再挖掘后边的游戏。\n5.勾选“仅参与优先掉宝游戏”时,不会挖掘不在优先列表中的游戏。勾不勾选取决于你。\n6。使用“不参与掉宝游戏”列表设置永不挖掘的游戏。\n7.更改任一列表的内容,或更改“仅参与优先掉宝游戏”勾选状态,需要手动按“刷新”才能使更改生效。" 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lang/繁體中文.json: -------------------------------------------------------------------------------- 1 | { 2 | "english_name": "Traditional Chinese", 3 | "status": { 4 | "terminated": "\n應用程序已終止,請關閉窗口以退出應用程序。", 5 | "watching": "正在觀看: {channel}", 6 | "goes_online": "{channel} 該頻道已上線, 正在切換...", 7 | "goes_offline": "{channel} 該頻道已離線, 正在切換...", 8 | "claimed_drop": "領取的掉寶: {drop}", 9 | "claimed_points": "領取的積分獎勵: {points}", 10 | "earned_points": "觀看直播獲得積分: {points}, 總積分: {balance}", 11 | "no_channel": "沒有可觀看的頻道,等待一個在線直播頻道...", 12 | "no_campaign": "沒有可用於掉寶的活動,等待一個有效的掉寶活動..." 13 | }, 14 | "login": { 15 | "unexpected_content": "返回的內容類型有誤通常是由於被重定向.您需要設置VPN或者確認登錄頁面才能聯網嗎?", 16 | "chrome": { 17 | "startup": "正在打開Chrome瀏覽器...", 18 | "login_to_complete": "請再次點擊登錄按鈕來完成登陸.", 19 | "no_token": "無法獲取授權令牌.", 20 | "closed_window": "在登錄完成之前關閉了Chrome瀏覽器." 21 | }, 22 | "error_code": "登錄出錯: {error_code}", 23 | "incorrect_login_pass": "用戶名或密碼錯誤.", 24 | "incorrect_email_code": "電子郵箱驗證碼錯誤.", 25 | "incorrect_twofa_code": "雙重驗證2FA令牌錯誤.", 26 | "email_code_required": "需要電子郵件驗證碼 查看你的郵件.", 27 | "twofa_code_required": "需要雙重驗證2FA令牌." 28 | }, 29 | "error": { 30 | "captcha": "您的登錄存在異常,Twitch官方已進行限制,請在12小時後重試.", 31 | "site_down": "Twitch無法連接,{seconds} 秒後重試...", 32 | "no_connection": "無法連接到Twitch,請確保已經開啟VPN並添加了代理,{seconds} 秒後重試..." 33 | }, 34 | "gui": { 35 | "output": "輸出日志", 36 | "status": { 37 | "name": "狀態", 38 | "idle": "已完成獲取", 39 | "exiting": "正在退出...", 40 | "terminated": "應用已終止運行", 41 | "cleanup": "清除頻道中...", 42 | "gathering": "搜索直播頻道...", 43 | "switching": "切換直播頻道中...", 44 | "fetching_inventory": "獲取庫存中...", 45 | "fetching_campaigns": "獲取掉寶活動...", 46 | "adding_campaigns": "將掉寶活動列表添加至庫存... {counter}" 47 | }, 48 | "tabs": { 49 | "main": "主面板", 50 | "inventory": "掉寶庫存", 51 | "settings": "設置", 52 | "help": "幫助" 53 | }, 54 | "tray": { 55 | "notification_title": "開始掉寶活動", 56 | "minimize": "窗口最小化", 57 | "show": "掉寶活動", 58 | "quit": "退出" 59 | }, 60 | "login": { 61 | "name": "登錄Twitch賬號", 62 | "labels": "狀態:\n用戶ID:", 63 | "logged_in": "已登錄", 64 | "logged_out": "未登錄", 65 | "logging_in": "正在登錄中...", 66 | "required": "請先登錄", 67 | "request": "請先登錄以訪問更多內容.", 68 | "username": "用戶名", 69 | "password": "密碼", 70 | "twofa_code": "郵箱2FA令牌", 71 | "button": "登錄" 72 | }, 73 | "websocket": { 74 | "name": "網絡協議連接狀態", 75 | "websocket": "Websocket #{id}:", 76 | "initializing": "初始化...", 77 | "connected": "已連接", 78 | "disconnected": "斷開連接", 79 | "connecting": "連接中...", 80 | "disconnecting": "斷開連接中...", 81 | "reconnecting": "重新連接中..." 82 | }, 83 | "progress": { 84 | "name": "掉寶活動進度", 85 | "drop": "掉寶獎勵:", 86 | "game": "遊戲:", 87 | "campaign": "掉寶活動名稱:", 88 | "remaining": "剩余 {time}", 89 | "drop_progress": "掉寶進度:", 90 | "campaign_progress": "活動進度:" 91 | }, 92 | "channels": { 93 | "name": "活動頻道", 94 | "switch": "切換", 95 | "load_points": "加載積分", 96 | "online": "ONLINE ✔", 97 | "pending": "OFFLINE ⏳", 98 | "offline": "OFFLINE ❌", 99 | "headings": { 100 | "channel": "直播頻道", 101 | "status": "在線狀態", 102 | "game": "遊戲", 103 | "viewers": "觀眾", 104 | "points": "積分" 105 | } 106 | }, 107 | "inventory": { 108 | "filter": { 109 | "name": "篩選", 110 | "show": "掉寶活動:", 111 | "not_linked": "未關聯賬戶的遊戲掉寶", 112 | "upcoming": "即將上線", 113 | "expired": "已結束", 114 | "excluded": "未添加的掉寶活動", 115 | "finished": "已完成", 116 | "refresh": "刷新" 117 | }, 118 | "status": { 119 | "linked": "已關聯 ✔", 120 | "not_linked": "未關聯賬戶 ❌", 121 | "active": "已上線 ✔", 122 | "upcoming": "即將上線 ⏳", 123 | "expired": "已結束 ❌", 124 | "claimed": "已領取 ✔", 125 | "ready_to_claim": "可以領取 ⏳" 126 | }, 127 | "starts": "開始時間: {time}", 128 | "ends": "結束時間: {time}", 129 | "allowed_channels": "允許的頻道:", 130 | "all_channels": "全部", 131 | "and_more": "還有 {amount} 個...", 132 | "percent_progress": "{minutes} 分鐘的 {percent}", 133 | "minutes_progress": "{minutes} 分鐘" 134 | }, 135 | "settings": { 136 | "general": { 137 | "name": "功能設置", 138 | "dark_theme": "暗色主題: ", 139 | "autostart": "開機自啟動: ", 140 | "tray": "開機自啟動並最小化: ", 141 | "tray_notifications": "啟用托盤通知: ", 142 | "priority_only": "僅參與優先掉寶遊戲: ", 143 | "prioritize_by_ending_soonest": "按掉寶遊戲的結束日期決定優先度: ", 144 | "proxy": "代理:" 145 | }, 146 | "game_name": "遊戲名稱", 147 | "priority": "優先掉寶遊戲", 148 | "exclude": "不參與掉寶遊戲", 149 | "reload": "刷新", 150 | "reload_text": "大多數設置更改後需要重新啟動軟件或刷新才能生效:" 151 | }, 152 | "help": { 153 | "links": { 154 | "name": "幫助", 155 | "inventory": "查看Twitch庫存", 156 | "campaigns": "查看所有掉寶活動並管理連接賬號" 157 | }, 158 | "how_it_works": "工作原理", 159 | "how_it_works_text": "每隔約 20 秒,程序就會向 Twitch 詢問當前正在觀看的頻道的原始數據流 URL。然後,它會獲取這個數據流的元數據。 - 這足以推進掉寶進度。這完全不需要下載任何實際的視頻流和音頻流。為了使頻道的狀態(在線或離線)保持最新,程序會建立 Websocket 連接,用於接收有關頻道開播和下播的事件,並更新當前的觀眾數量。", 160 | "getting_started": "入門設置", 161 | "getting_started_text": "1. 登錄應用程序。\n2.確保所有感興趣挖掘的遊戲的賬號已連接到您的Twitch賬號。\n3.如果您想挖掘所有掉寶,請不要勾選“僅參與優先掉寶遊戲”,然後按“刷新”。\n4.如果您想優先挖掘特定遊戲,請使用“優先掉寶遊戲”列表設置您想挖掘的遊戲的優先順序。程序將嘗試先挖掘列表頂部的遊戲,然後再挖掘後邊的遊戲。\n5.勾選“僅參與優先掉寶遊戲”時,不會挖掘不在優先列表中的遊戲。勾不勾選取決於你。\n6。使用“不參與掉寶遊戲”列表設置永不挖掘的遊戲。\n7.更改任一列表的內容,或更改“僅參與優先掉寶遊戲”勾選狀態,需要手動按“刷新”才能使更改生效。" 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # import an additional thing for proper PyInstaller freeze support 4 | from multiprocessing import freeze_support 5 | from datetime import datetime 6 | 7 | 8 | if __name__ == "__main__": 9 | print(f"{datetime.now().strftime('%Y-%m-%d %X')}: Starting: Twitch Drops Miner") 10 | freeze_support() 11 | import io 12 | import sys 13 | import signal 14 | import asyncio 15 | import logging 16 | import argparse 17 | import warnings 18 | import traceback 19 | import tkinter as tk 20 | from tkinter import messagebox 21 | from typing import IO, NoReturn 22 | 23 | if sys.version_info >= (3, 10): 24 | import truststore 25 | truststore.inject_into_ssl() 26 | 27 | from translate import _ 28 | from twitch import Twitch 29 | from settings import Settings 30 | from version import __version__ 31 | from exceptions import CaptchaRequired 32 | from utils import lock_file, resource_path, set_root_icon 33 | from constants import CALL, SELF_PATH, FILE_FORMATTER, LOG_PATH, LOCK_PATH 34 | 35 | warnings.simplefilter("default", ResourceWarning) 36 | 37 | class Parser(argparse.ArgumentParser): 38 | def __init__(self, *args, **kwargs) -> None: 39 | super().__init__(*args, **kwargs) 40 | self._message: io.StringIO = io.StringIO() 41 | 42 | def _print_message(self, message: str, file: IO[str] | None = None) -> None: 43 | self._message.write(message) 44 | # print(message, file=self._message) 45 | 46 | def exit(self, status: int = 0, message: str | None = None) -> NoReturn: 47 | try: 48 | super().exit(status, message) # sys.exit(2) 49 | finally: 50 | messagebox.showerror("Argument Parser Error", self._message.getvalue()) 51 | 52 | class ParsedArgs(argparse.Namespace): 53 | _verbose: int 54 | _debug_ws: bool 55 | _debug_gql: bool 56 | log: bool 57 | tray: bool 58 | no_run_check: bool 59 | 60 | # TODO: replace int with union of literal values once typeshed updates 61 | @property 62 | def logging_level(self) -> int: 63 | return { 64 | 0: logging.ERROR, 65 | 1: logging.WARNING, 66 | 2: logging.INFO, 67 | 3: CALL, 68 | 4: logging.DEBUG, 69 | }[min(self._verbose, 4)] 70 | 71 | @property 72 | def debug_ws(self) -> int: 73 | """ 74 | If the debug flag is True, return DEBUG. 75 | If the main logging level is DEBUG, return INFO to avoid seeing raw messages. 76 | Otherwise, return NOTSET to inherit the global logging level. 77 | """ 78 | if self._debug_ws: 79 | return logging.DEBUG 80 | elif self._verbose >= 4: 81 | return logging.INFO 82 | return logging.NOTSET 83 | 84 | @property 85 | def debug_gql(self) -> int: 86 | if self._debug_gql: 87 | return logging.DEBUG 88 | elif self._verbose >= 4: 89 | return logging.INFO 90 | return logging.NOTSET 91 | 92 | # handle input parameters 93 | # NOTE: parser output is shown via message box 94 | # we also need a dummy invisible window for the parser 95 | root = tk.Tk() 96 | root.overrideredirect(True) 97 | root.withdraw() 98 | set_root_icon(root, resource_path("pickaxe.ico")) 99 | root.update() 100 | parser = Parser( 101 | SELF_PATH.name, 102 | description="A program that allows you to mine timed drops on Twitch.", 103 | ) 104 | parser.add_argument("--version", action="version", version=f"v{__version__}") 105 | parser.add_argument("-v", dest="_verbose", action="count", default=0) 106 | parser.add_argument("--tray", action="store_true") 107 | parser.add_argument("--log", action="store_true") 108 | # debug options 109 | parser.add_argument( 110 | "--debug-ws", dest="_debug_ws", action="store_true" 111 | ) 112 | parser.add_argument( 113 | "--debug-gql", dest="_debug_gql", action="store_true" 114 | ) 115 | args = parser.parse_args(namespace=ParsedArgs()) 116 | # load settings 117 | try: 118 | settings = Settings(args) 119 | except Exception: 120 | messagebox.showerror( 121 | "Settings error", 122 | f"There was an error while loading the settings file:\n\n{traceback.format_exc()}" 123 | ) 124 | sys.exit(4) 125 | # dummy window isn't needed anymore 126 | root.destroy() 127 | # get rid of unneeded objects 128 | del root, parser 129 | 130 | # client run 131 | async def main(): 132 | # set language 133 | try: 134 | _.set_language(settings.language) 135 | except ValueError: 136 | # this language doesn't exist - stick to English 137 | pass 138 | 139 | # handle logging stuff 140 | if settings.logging_level > logging.DEBUG: 141 | # redirect the root logger into a NullHandler, effectively ignoring all logging calls 142 | # that aren't ours. This always runs, unless the main logging level is DEBUG or lower. 143 | logging.getLogger().addHandler(logging.NullHandler()) 144 | logger = logging.getLogger("TwitchDrops") 145 | logger.setLevel(settings.logging_level) 146 | if settings.log: 147 | handler = logging.FileHandler(LOG_PATH) 148 | handler.setFormatter(FILE_FORMATTER) 149 | logger.addHandler(handler) 150 | logging.getLogger("TwitchDrops.gql").setLevel(settings.debug_gql) 151 | logging.getLogger("TwitchDrops.websocket").setLevel(settings.debug_ws) 152 | 153 | exit_status = 0 154 | client = Twitch(settings) 155 | loop = asyncio.get_running_loop() 156 | if sys.platform == "linux": 157 | loop.add_signal_handler(signal.SIGINT, lambda *_: client.gui.close()) 158 | loop.add_signal_handler(signal.SIGTERM, lambda *_: client.gui.close()) 159 | try: 160 | await client.run() 161 | except CaptchaRequired: 162 | exit_status = 1 163 | client.prevent_close() 164 | client.print(_("error", "captcha")) 165 | except Exception: 166 | exit_status = 1 167 | client.prevent_close() 168 | client.print("Fatal error encountered:\n") 169 | client.print(traceback.format_exc()) 170 | finally: 171 | if sys.platform == "linux": 172 | loop.remove_signal_handler(signal.SIGINT) 173 | loop.remove_signal_handler(signal.SIGTERM) 174 | client.print(_("gui", "status", "exiting")) 175 | await client.shutdown() 176 | if not client.gui.close_requested: 177 | # user didn't request the closure 178 | client.print(_("status", "terminated")) 179 | client.gui.status.update(_("gui", "status", "terminated")) 180 | # notify the user about the closure 181 | client.gui.grab_attention(sound=True) 182 | await client.gui.wait_until_closed() 183 | # save the application state 184 | # NOTE: we have to do it after wait_until_closed, 185 | # because the user can alter some settings between app termination and closing the window 186 | client.save(force=True) 187 | client.gui.stop() 188 | client.gui.close_window() 189 | sys.exit(exit_status) 190 | 191 | try: 192 | # use lock_file to check if we're not already running 193 | success, file = lock_file(LOCK_PATH) 194 | if not success: 195 | # already running - exit 196 | sys.exit(3) 197 | 198 | asyncio.run(main()) 199 | finally: 200 | file.close() 201 | -------------------------------------------------------------------------------- /manual.txt: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Twitch Drops Miner (by DevilXD) # 3 | ################################### 4 | 5 | Available command line arguments: 6 | 7 | • --tray 8 | Start application as minimised into tray. 9 | • -v 10 | Increase verbosity level. Can be stacked up several times (-vv, -vvv, etc.) to show 11 | increasingly more information during application runtime. 12 | • --log 13 | Enables logging of runtime information into a 'log.txt' file. Verbosity level of this logging 14 | matches the level set by `-v`. 15 | 16 | • --version 17 | Show application version information and exit 18 | • --debug-gql 19 | Show GQL debug info 20 | • --debug-ws 21 | Show WS debug info 22 | • --help, -h 23 | Show help prompt and exit 24 | 25 | Note: Additional settings are available within the application GUI. 26 | 27 | Exit codes: 28 | 29 | • 0: Application exited successfully 30 | • 1: Exit caused by the CAPTCHA or a Fatal Exception 31 | • 2: Incorrect command line arguments 32 | • 3: Application already running 33 | • 4: Loading of the settings file failed 34 | -------------------------------------------------------------------------------- /pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | IF NOT EXIST 7z.exe GOTO NO7Z 3 | IF NOT EXIST "Twitch Drops Miner" mkdir "Twitch Drops Miner" 4 | rem Prepare files 5 | copy /y /v dist\*.exe "Twitch Drops Miner" 6 | copy /y /v manual.txt "Twitch Drops Miner" 7 | IF EXIST "Twitch Drops Miner.zip" ( 8 | rem Add action 9 | set action=a 10 | ) ELSE ( 11 | rem Update action 12 | set action=u 13 | ) 14 | rem Pack and test 15 | 7z %action% "Twitch Drops Miner.zip" "Twitch Drops Miner/" -r 16 | 7z t "Twitch Drops Miner.zip" * -r 17 | rem Cleanup 18 | IF EXIST "Twitch Drops Miner" rmdir /s /q "Twitch Drops Miner" 19 | GOTO EXIT 20 | :NO7Z 21 | echo No 7z.exe detected, skipping packaging! 22 | GOTO EXIT 23 | :EXIT 24 | exit %errorlevel% 25 | -------------------------------------------------------------------------------- /patch_notes.txt: -------------------------------------------------------------------------------- 1 | ## v15.10.0 2 | 3 | 13.12.2024 4 | - Updated persisted queries 5 | - (Fix to appimage builder by [guihkx](https://github.com/guihkx)) 6 | 7 | ## v15.9.1 8 | 9 | 31.8.2024 10 | - Finished Indonesian translation and added corresponding credits 11 | 12 | 13 | 14 | ## v15.9.0 15 | 16 | 18.8.2024 17 | - Added game name to `0 required minutes` error message 18 | 19 | 27.8.2024 20 | - Clarified that 0 required minutes error is not critical 21 | - Fixed "Twitch is down, retrying" for my case. It seems like some might still experience the issue. Tracked in #172 22 | 23 | 24 | 25 | ## v15.8.2 26 | 27 | 13.8.2024 28 | - update hash and add new variable includeIsDJ to fix `PersistedQueryNotFound` as reported in #159 29 | - Thanks to @Nazar1ky for the fix 30 | 31 | 32 | 33 | ## v15.8.1 34 | 35 | 5.8.2024 36 | - Changed ClientType to android app to fix `KeyError: 'data'` as reported in #161 37 | - Thanks to @Nazar1ky for the fix 38 | 39 | 23.7.2024 40 | - Updated `ViewerDropsDashboard` request to mitigate crashes if the old one becomes obsolete. 41 | 42 | 43 | 44 | ## v15.8.0 45 | 46 | 22.7.2024 47 | - Changed GQL persistant query `DropCampaignDetails` hash to match a change on Twitch's side and prevent `PersistedQueryNotFound` crash on startup. 48 | 49 | 16.7.2024 50 | - Updated **French** and **Indonesian** translation 51 | 52 | 53 | 54 | ## v15.7.1 55 | 56 | 1.7.2024 57 | - Patched bug due to Twitch falsely reporting time claimed 58 | - Campaigns, that are both repeating **AND** are falsely reported, could still cause issues. Tracked in #139. 59 | 60 | 61 | 62 | ## v15.7.0 63 | 64 | 13.6.2024 65 | - The miner saves and restores the window position 66 | - You can reset the position with a new tray icon option: `Show (Refresh)` 67 | - Tray icon is now always visible 68 | - Updated **French** and **Dutch** translation as well as corresponding credits 69 | 70 | 10.6.2024 71 | - Fixed crash upon Twitch returning `"broadcaster": null` 72 | 73 | 10.6.2024 74 | - Updated **Danish**, **Indonesian** and **Portuguese** translation as well as corresponding credits 75 | 76 | 77 | 78 | ## v15.6.1 79 | 80 | 7.6.2024 81 | - Hotfix for campaigns with subscription requirement having `requiredMinutesWatched` set to `0` causing `division by 0` crash, tracked in #101 82 | 83 | 5.6.2024 84 | - Fixed progress reporting not being translated to other languages 85 | 86 | 87 | 88 | ## v15.6.0 89 | 90 | 1.6.2024 91 | - Fixed bug where long campaign names caused a crash when trying to update tray description 92 | - Fixed `UnboundLocalError` crash due to wrong indentation 93 | 94 | 30.5.2024 95 | - Updated **Arabic**, **Turkish**, **Simplified Chinese** and **English** translation as well as corresponding credits 96 | 97 | 28.5.2024 98 | - Updated **Italian**, **Polish**, **Turkish** and **Ukrainian** translation as well as corresponding credits 99 | 100 | 101 | 102 | ## v15.5.0 103 | 104 | 25.5.2024 105 | - Added ability to prioritize by Campaign end date (made by @jaredkotoff) 106 | - Updated **Simplified and Traditional Chinese**, **Turkish** and **Ukrainian** translation as well as corresponding credits 107 | 108 | 109 | 110 | ## v15.4.0 111 | 112 | 23.5.2024 113 | - Fixed crash on Linux caused by trying to apply a Windows-exclusive Tkinter theme 114 | - Updated **English**, **German**, **Czech**, **Spanish** and **Russian** translation as well as corresponding credits for dark themes and potential future Campaign prioritization by end date 115 | 116 | 117 | 118 | ## v15.3.0 119 | 120 | 22.5.2024 121 | - Completed dark mode 🎉 122 | 123 | 20.5.2024 124 | - Added incomplete dark mode 125 | 126 | 127 | 128 | ## v15.2.0 129 | 130 | 19.5.2024 131 | - Updated **French** translation as well as corresponding credits 132 | 133 | 18.5.2024 134 | - Updated **Russian**, **Ukrainian** and **Traditional Chinese** translation as well as corresponding credits 135 | - Various changes to github workflows 136 | 137 | 138 | 139 | ## v15.1.0 140 | 141 | 17.5.2024 142 | - Updated **Italian**, **Simplified Chinese** and **Spanish** translation as well as corresponding credits 143 | - Various changes to github workflows 144 | -------------------------------------------------------------------------------- /pickaxe.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valentin-Metz/TwitchDropsMiner/4dbd5391f1e4be481709d0f0acda40ab023948e4/pickaxe.ico -------------------------------------------------------------------------------- /registry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import winreg as reg 4 | from typing import Any 5 | from enum import Enum, Flag 6 | from collections.abc import Generator 7 | 8 | 9 | class RegistryError(Exception): 10 | pass 11 | 12 | 13 | class ValueNotExists(RegistryError): 14 | pass 15 | 16 | 17 | class Access(Flag): 18 | KEY_READ = reg.KEY_READ 19 | KEY_WRITE = reg.KEY_WRITE 20 | KEY_NOTIFY = reg.KEY_NOTIFY 21 | KEY_EXECUTE = reg.KEY_EXECUTE 22 | KEY_SET_VALUE = reg.KEY_SET_VALUE 23 | KEY_ALL_ACCESS = reg.KEY_ALL_ACCESS 24 | KEY_CREATE_LINK = reg.KEY_CREATE_LINK 25 | KEY_QUERY_VALUE = reg.KEY_QUERY_VALUE 26 | KEY_CREATE_SUB_KEY = reg.KEY_CREATE_SUB_KEY 27 | KEY_ENUMERATE_SUB_KEYS = reg.KEY_ENUMERATE_SUB_KEYS 28 | 29 | 30 | class MainKey(Enum): 31 | HKU = reg.HKEY_USERS 32 | HKCR = reg.HKEY_CLASSES_ROOT 33 | HKCU = reg.HKEY_CURRENT_USER 34 | HKLM = reg.HKEY_LOCAL_MACHINE 35 | HKEY_USERS = reg.HKEY_USERS 36 | HKEY_CLASSES_ROOT = reg.HKEY_CLASSES_ROOT 37 | HKEY_CURRENT_USER = reg.HKEY_CURRENT_USER 38 | HKEY_LOCAL_MACHINE = reg.HKEY_LOCAL_MACHINE 39 | HKEY_CURRENT_CONFIG = reg.HKEY_CURRENT_CONFIG 40 | HKEY_PERFORMANCE_DATA = reg.HKEY_PERFORMANCE_DATA 41 | 42 | 43 | class ValueType(Enum): 44 | REG_SZ = reg.REG_SZ 45 | REG_NONE = reg.REG_NONE 46 | REG_LINK = reg.REG_LINK 47 | REG_DWORD = reg.REG_DWORD 48 | REG_QWORD = reg.REG_QWORD 49 | REG_BINARY = reg.REG_BINARY 50 | REG_MULTI_SZ = reg.REG_MULTI_SZ 51 | REG_EXPAND_SZ = reg.REG_EXPAND_SZ 52 | REG_RESOURCE_LIST = reg.REG_RESOURCE_LIST 53 | REG_DWORD_BIG_ENDIAN = reg.REG_DWORD_BIG_ENDIAN 54 | REG_DWORD_LITTLE_ENDIAN = reg.REG_DWORD_LITTLE_ENDIAN 55 | REG_QWORD_LITTLE_ENDIAN = reg.REG_QWORD_LITTLE_ENDIAN 56 | REG_FULL_RESOURCE_DESCRIPTOR = reg.REG_FULL_RESOURCE_DESCRIPTOR 57 | REG_RESOURCE_REQUIREMENTS_LIST = reg.REG_RESOURCE_REQUIREMENTS_LIST 58 | 59 | 60 | class RegistryKey: 61 | def __init__(self, path: str): 62 | main_key, _, path = path.replace('/', '\\').partition('\\') 63 | self.main_key = MainKey[main_key] 64 | self.path = path 65 | self._handle = reg.OpenKey( 66 | self.main_key.value, path, access=(Access.KEY_QUERY_VALUE | Access.KEY_SET_VALUE).value 67 | ) 68 | 69 | def __enter__(self) -> RegistryKey: 70 | return self 71 | 72 | def __exit__(self, exc_type, exc, tb): 73 | self._handle.Close() 74 | 75 | def get(self, name: str) -> tuple[ValueType, Any]: 76 | try: 77 | value, value_type = reg.QueryValueEx(self._handle, name) 78 | except FileNotFoundError: 79 | # TODO: consider returning None for missing values 80 | raise ValueNotExists(name) 81 | return (ValueType(value_type), value) 82 | 83 | def set(self, name: str, value_type: ValueType, value: Any) -> bool: 84 | reg.SetValueEx(self._handle, name, 0, value_type.value, value) 85 | return True # TODO: return False if the set operation fails 86 | 87 | def delete(self, name: str, *, silent: bool = False) -> bool: 88 | try: 89 | reg.DeleteValue(self._handle, name) 90 | except FileNotFoundError: 91 | if not silent: 92 | raise ValueNotExists(name) 93 | return False 94 | return True 95 | 96 | def values(self) -> Generator[tuple[str, ValueType, Any], None, None]: 97 | len_keys, len_values, last_modified = reg.QueryInfoKey(self._handle) 98 | for i in range(len_values): 99 | try: 100 | name, value, value_type = reg.EnumValue(self._handle, i) 101 | yield name, ValueType(value_type), value 102 | except OSError: 103 | return 104 | 105 | 106 | if __name__ == "__main__": 107 | with RegistryKey("HKCU/Software/Microsoft/Windows/CurrentVersion/Run") as key: 108 | # key.get("test") 109 | # key.set("test", ValueType.REG_SZ, "test\\path") 110 | for name, value_type, value in key.values(): 111 | print(name, value_type, value) 112 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.9,<4.0 2 | Pillow 3 | pystray 4 | PyGObject; sys_platform == "linux" # required for better system tray support on Linux 5 | validators 6 | 7 | # environment-dependent dependencies 8 | pywin32; sys_platform == "win32" 9 | truststore; python_version >= "3.10" 10 | 11 | # unused dependencies 12 | # selenium-wire 13 | # undetected-chromedriver 14 | # this is installed only on windows 15 | pywin32; sys_platform == "win32" 16 | truststore; sys_platform == "linux" and python_version >= "3.10" 17 | yarl~=1.9.2 18 | -------------------------------------------------------------------------------- /run_dev.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | set dirpath=%~dp0 4 | if "%dirpath:~-1%" == "\" set dirpath=%dirpath:~0,-1% 5 | set /p "choice=Start with a console? (y/n) " 6 | if "%choice%"=="y" ( 7 | set "exepath=%dirpath%\env\scripts\python" 8 | ) else ( 9 | if "%choice%"=="" ( 10 | set "exepath=%dirpath%\env\scripts\python" 11 | ) else ( 12 | set "exepath=%dirpath%\env\scripts\pythonw" 13 | ) 14 | ) 15 | start "TwitchDropsMiner" "%exepath%" "%dirpath%\main.py" 16 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import Any, TypedDict, TYPE_CHECKING 5 | 6 | from yarl import URL 7 | 8 | from utils import json_load, json_save 9 | from constants import SETTINGS_PATH, DEFAULT_LANG 10 | 11 | if TYPE_CHECKING: 12 | from main import ParsedArgs 13 | 14 | 15 | class SettingsFile(TypedDict): 16 | proxy: URL 17 | language: str 18 | dark_theme: bool 19 | autostart: bool 20 | exclude: set[str] 21 | priority: list[str] 22 | priority_only: bool 23 | prioritize_by_ending_soonest: bool 24 | unlinked_campaigns: bool 25 | autostart_tray: bool 26 | connection_quality: int 27 | tray_notifications: bool 28 | window_position: str 29 | 30 | 31 | default_settings: SettingsFile = { 32 | "proxy": URL(), 33 | "priority": [], 34 | "exclude": set(), 35 | "dark_theme": False, 36 | "autostart": False, 37 | "priority_only": True, 38 | "prioritize_by_ending_soonest": False, 39 | "unlinked_campaigns": False, 40 | "autostart_tray": False, 41 | "connection_quality": 1, 42 | "language": DEFAULT_LANG, 43 | "tray_notifications": True, 44 | "window_position": "", 45 | } 46 | 47 | 48 | class Settings: 49 | # from args 50 | log: bool 51 | tray: bool 52 | no_run_check: bool 53 | # args properties 54 | debug_ws: int 55 | debug_gql: int 56 | logging_level: int 57 | # from settings file 58 | proxy: URL 59 | language: str 60 | dark_theme: bool 61 | autostart: bool 62 | exclude: set[str] 63 | priority: list[str] 64 | priority_only: bool 65 | prioritize_by_ending_soonest: bool 66 | unlinked_campaigns: bool 67 | autostart_tray: bool 68 | connection_quality: int 69 | tray_notifications: bool 70 | window_position: str 71 | 72 | PASSTHROUGH = ("_settings", "_args", "_altered") 73 | 74 | def __init__(self, args: ParsedArgs): 75 | self._settings: SettingsFile = json_load(SETTINGS_PATH, default_settings) 76 | self.__get_settings_from_env__() 77 | self._args: ParsedArgs = args 78 | self._altered: bool = False 79 | 80 | def __get_settings_from_env__(self): 81 | if(os.environ.get('prioritize_by_ending_soonest') == '1'): 82 | self._settings["prioritize_by_ending_soonest"] = True 83 | if(os.environ.get('UNLINKED_CAMPAIGNS') == '1'): 84 | self._settings["unlinked_campaigns"] = True 85 | 86 | # default logic of reading settings is to check args first, then the settings file 87 | def __getattr__(self, name: str, /) -> Any: 88 | if name in self.PASSTHROUGH: 89 | # passthrough 90 | return getattr(super(), name) 91 | elif hasattr(self._args, name): 92 | return getattr(self._args, name) 93 | elif name in self._settings: 94 | return self._settings[name] # type: ignore[literal-required] 95 | return getattr(super(), name) 96 | 97 | def __setattr__(self, name: str, value: Any, /) -> None: 98 | if name in self.PASSTHROUGH: 99 | # passthrough 100 | return super().__setattr__(name, value) 101 | elif name in self._settings: 102 | self._settings[name] = value # type: ignore[literal-required] 103 | self._altered = True 104 | return 105 | raise TypeError(f"{name} is missing a custom setter") 106 | 107 | def __delattr__(self, name: str, /) -> None: 108 | raise RuntimeError("settings can't be deleted") 109 | 110 | def alter(self) -> None: 111 | self._altered = True 112 | 113 | def save(self, *, force: bool = False) -> None: 114 | if self._altered or force: 115 | json_save(SETTINGS_PATH, self._settings, sort=True) 116 | -------------------------------------------------------------------------------- /setup_env.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | set dirpath=%~dp0 4 | if "%dirpath:~-1%" == "\" set dirpath=%dirpath:~0,-1% 5 | 6 | git --version > nul 7 | if %errorlevel% NEQ 0 ( 8 | echo No git executable found in PATH! 9 | echo: 10 | pause 11 | exit 12 | ) 13 | 14 | if not exist "%dirpath%\env" ( 15 | echo: 16 | echo Creating the env folder... 17 | python -m venv "%dirpath%\env" 18 | if %errorlevel% NEQ 0 ( 19 | echo: 20 | echo No python executable found in PATH! 21 | echo: 22 | pause 23 | ) 24 | ) 25 | 26 | echo: 27 | echo Installing requirements.txt... 28 | "%dirpath%\env\scripts\python" -m pip install -U pip 29 | "%dirpath%\env\scripts\pip" install wheel 30 | "%dirpath%\env\scripts\pip" install -r "%dirpath%\requirements.txt" 31 | 32 | echo: 33 | echo All done! 34 | echo: 35 | pause 36 | -------------------------------------------------------------------------------- /translate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import abc 4 | from typing import Any, TypedDict, TYPE_CHECKING 5 | 6 | from exceptions import MinerException 7 | from utils import json_load 8 | from constants import LANG_PATH, DEFAULT_LANG 9 | import json 10 | 11 | if TYPE_CHECKING: 12 | from typing_extensions import NotRequired 13 | 14 | 15 | class StatusMessages(TypedDict): 16 | terminated: str 17 | watching: str 18 | goes_online: str 19 | goes_offline: str 20 | claimed_drop: str 21 | claimed_points: str 22 | earned_points: str 23 | no_channel: str 24 | no_campaign: str 25 | 26 | 27 | class ChromeMessages(TypedDict): 28 | startup: str 29 | login_to_complete: str 30 | no_token: str 31 | closed_window: str 32 | 33 | 34 | class LoginMessages(TypedDict): 35 | chrome: ChromeMessages 36 | error_code: str 37 | unexpected_content: str 38 | email_code_required: str 39 | twofa_code_required: str 40 | incorrect_login_pass: str 41 | incorrect_email_code: str 42 | incorrect_twofa_code: str 43 | 44 | 45 | class ErrorMessages(TypedDict): 46 | captcha: str 47 | no_connection: str 48 | site_down: str 49 | 50 | 51 | class GUIStatus(TypedDict): 52 | name: str 53 | idle: str 54 | exiting: str 55 | terminated: str 56 | cleanup: str 57 | gathering: str 58 | switching: str 59 | fetching_inventory: str 60 | fetching_campaigns: str 61 | adding_campaigns: str 62 | 63 | 64 | class GUITabs(TypedDict): 65 | main: str 66 | inventory: str 67 | settings: str 68 | help: str 69 | 70 | 71 | class GUITray(TypedDict): 72 | notification_title: str 73 | minimize: str 74 | show: str 75 | quit: str 76 | 77 | 78 | class GUILoginForm(TypedDict): 79 | name: str 80 | labels: str 81 | logging_in: str 82 | logged_in: str 83 | logged_out: str 84 | request: str 85 | required: str 86 | username: str 87 | password: str 88 | twofa_code: str 89 | button: str 90 | 91 | 92 | class GUIWebsocket(TypedDict): 93 | name: str 94 | websocket: str 95 | initializing: str 96 | connected: str 97 | disconnected: str 98 | connecting: str 99 | disconnecting: str 100 | reconnecting: str 101 | 102 | 103 | class GUIProgress(TypedDict): 104 | name: str 105 | drop: str 106 | game: str 107 | campaign: str 108 | remaining: str 109 | drop_progress: str 110 | campaign_progress: str 111 | 112 | 113 | class GUIChannelHeadings(TypedDict): 114 | channel: str 115 | status: str 116 | game: str 117 | points: str 118 | viewers: str 119 | 120 | 121 | class GUIChannels(TypedDict): 122 | name: str 123 | switch: str 124 | load_points: str 125 | online: str 126 | pending: str 127 | offline: str 128 | headings: GUIChannelHeadings 129 | 130 | 131 | class GUIInvFilter(TypedDict): 132 | name: str 133 | show: str 134 | not_linked: str 135 | upcoming: str 136 | expired: str 137 | excluded: str 138 | finished: str 139 | refresh: str 140 | 141 | 142 | class GUIInvStatus(TypedDict): 143 | linked: str 144 | not_linked: str 145 | active: str 146 | expired: str 147 | upcoming: str 148 | claimed: str 149 | ready_to_claim: str 150 | 151 | 152 | class GUIInventory(TypedDict): 153 | filter: GUIInvFilter 154 | status: GUIInvStatus 155 | starts: str 156 | ends: str 157 | allowed_channels: str 158 | all_channels: str 159 | and_more: str 160 | percent_progress: str 161 | minutes_progress: str 162 | 163 | 164 | class GUISettingsGeneral(TypedDict): 165 | name: str 166 | autostart: str 167 | tray: str 168 | tray_notifications: str 169 | priority_only: str 170 | prioritize_by_ending_soonest: str 171 | unlinked_campaigns: str 172 | proxy: str 173 | dark_theme: str 174 | 175 | 176 | class GUISettings(TypedDict): 177 | general: GUISettingsGeneral 178 | game_name: str 179 | priority: str 180 | exclude: str 181 | reload: str 182 | reload_text: str 183 | 184 | 185 | class GUIHelpLinks(TypedDict): 186 | name: str 187 | inventory: str 188 | campaigns: str 189 | 190 | 191 | class GUIHelp(TypedDict): 192 | links: GUIHelpLinks 193 | how_it_works: str 194 | how_it_works_text: str 195 | getting_started: str 196 | getting_started_text: str 197 | 198 | 199 | class GUIMessages(TypedDict): 200 | output: str 201 | status: GUIStatus 202 | tabs: GUITabs 203 | tray: GUITray 204 | login: GUILoginForm 205 | websocket: GUIWebsocket 206 | progress: GUIProgress 207 | channels: GUIChannels 208 | inventory: GUIInventory 209 | settings: GUISettings 210 | help: GUIHelp 211 | 212 | 213 | class Translation(TypedDict): 214 | language_name: NotRequired[str] 215 | english_name: str 216 | status: StatusMessages 217 | login: LoginMessages 218 | error: ErrorMessages 219 | gui: GUIMessages 220 | 221 | with open(LANG_PATH.joinpath(f"{DEFAULT_LANG}.json"), 'r', encoding='utf-8') as file: 222 | default_translation: Translation = json.load(file) 223 | 224 | class Translator: 225 | def __init__(self) -> None: 226 | self._langs: list[str] = [] 227 | # start with (and always copy) the default translation 228 | self._translation: Translation = default_translation.copy() 229 | self._translation["language_name"] = DEFAULT_LANG 230 | # load available translation names 231 | for filepath in LANG_PATH.glob("*.json"): 232 | self._langs.append(filepath.stem) 233 | self._langs.sort() 234 | if DEFAULT_LANG in self._langs: 235 | self._langs.remove(DEFAULT_LANG) 236 | self._langs.insert(0, DEFAULT_LANG) 237 | 238 | @property 239 | def languages(self) -> abc.Iterable[str]: 240 | return iter(self._langs) 241 | 242 | @property 243 | def current(self) -> str: 244 | return self._translation["language_name"] 245 | 246 | def set_language(self, language: str): 247 | if language not in self._langs: 248 | raise ValueError("Unrecognized language") 249 | elif self._translation["language_name"] == language: 250 | # same language as loaded selected 251 | return 252 | elif language == DEFAULT_LANG: 253 | # default language selected - use the memory value 254 | self._translation = default_translation.copy() 255 | else: 256 | self._translation = json_load( 257 | LANG_PATH.joinpath(f"{language}.json"), default_translation 258 | ) 259 | if "language_name" in self._translation: 260 | raise ValueError("Translations cannot define 'language_name'") 261 | self._translation["language_name"] = language 262 | 263 | def __call__(self, *path: str) -> str: 264 | if not path: 265 | raise ValueError("Language path expected") 266 | v: Any = self._translation 267 | try: 268 | for key in path: 269 | v = v[key] 270 | except KeyError: 271 | # this can only really happen for the default translation 272 | raise MinerException( 273 | f"{self.current} translation is missing the '{' -> '.join(path)}' translation key" 274 | ) 275 | return v 276 | 277 | 278 | _ = Translator() 279 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = "16.dev" 2 | --------------------------------------------------------------------------------