├── .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 | 
33 | 
34 | 
35 | 
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 | [](
64 | https://www.buymeacoffee.com/DevilXD
65 | )
66 | [](
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 |
--------------------------------------------------------------------------------