Compare commits

..
13 Commits
Author SHA1 Message Date
root 90d2173a83 fix volumes folder missing 2026-08-30 22:15:38 +02:00
EaZyCode 48e5ac5e5c Update README.md 2026-08-30 22:03:27 +02:00
root 67ecf3b8d7 Docker setup 2026-08-30 21:56:28 +02:00
Kajetan AbtandClaude Sonnet 5 65d1092898 Fix Linux AppIndicator library path detection on non-multiarch distros (#1131)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 22:51:57 +02:00
DevilXD a3fda85acc GQL hashes update 2026-07-30 23:47:33 +02:00
EvilGrinUK 3aec843bbb Fix UI freeze during maintenance reloads on Wayland/Linux (#1114) 2026-07-21 08:14:46 +02:00
Patriot99 866663c0b1 Update Polish translation (#1109) 2026-07-15 23:25:37 +02:00
旋风SELF c233d5a9c2 Update Chinese(Simplified) and Chinese(Traditional) translation
- `简体中文.json` and `繁體中文.json`
2026-07-15 08:07:32 +02:00
DevilXD 7f09d3a147 DirectoryPage_Game hash update 2026-07-14 22:20:34 +02:00
DevilXD 4148c71986 Change the watching method back to using Spade 2026-07-11 14:04:02 +02:00
Prof. Zsolt Zoltán Fehér Dr. f225638daa Add Hungarian translation (#1103) 2026-07-11 14:01:26 +02:00
Forero-0 abadd5356b Update Español.json 2026-07-05 11:12:49 +02:00
DevilXD 8ecedcb5df Fix CI 2026-07-04 23:59:28 +02:00
18 changed files with 326 additions and 50 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
.env*
.git*
+1
View File
@@ -0,0 +1 @@
VNC_PORT=5900
+4 -1
View File
@@ -275,8 +275,11 @@ jobs:
- name: Install appimage-builder
run: |
python3 -m pip install --upgrade pip
python3 -m pip install wheel
python3 -m pip install setuptools setuptools_scm
# TODO: Switch to a stable release if/when a version newer than 1.1.0 is released.
python3 -m pip install git+https://github.com/AppImageCrafters/appimage-builder.git@e995e8edcc227d14524cf39f9824c238f9435a22
python3 -m pip install git+https://github.com/AppImageCrafters/appimage-builder.git@1a8794d3c89f288fbd82b9accd7c0b74611147d5
- name: Create AppImage
env:
+4
View File
@@ -26,3 +26,7 @@ settings.json
/*.AppImage.zsync
.DS_Store
*.app
# Docker
/volumes/*
!volumes/.gitkeep
/.env
+45
View File
@@ -0,0 +1,45 @@
# Python 3.14 base & pip installation
FROM python:3.14 AS base
WORKDIR /app
RUN apt-get update && apt-get install -y \
libgirepository1.0-dev \
libcairo2-dev \
pkg-config \
python3-dev \
gtk+3.0 \
dbus-x11 \
x11vnc \
xvfb \
fluxbox \
net-tools \
libappindicator3-dev \
libayatana-appindicator3-dev \
x11-utils
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Create the final image
FROM base
WORKDIR /app
# Copy the rest of the project files
COPY . .
# Start Xvfb, Fluxbox, and x11vnc, then run the application
CMD rm -f /tmp/.X1-lock && \
Xvfb :1 -screen 0 1280x720x16 & \
fluxbox & \
# Wait for the X server to be up
while ! xdpyinfo -display :1 > /dev/null 2>&1; do \
echo "Waiting for X server to be available..."; \
sleep 1; \
done && \
echo "X server is running" || (echo "X server failed to start, exiting..." && exit 1); \
# Start x11vnc with additional logging
x11vnc -display :1 -nopw -listen 0.0.0.0 -forever -xkb -no6 -noxdamage -o /var/log/x11vnc.log & \
# Explicitly set the DISPLAY variable for the Python application
export DISPLAY=:1 && python main.py 2>&1 | tee /var/log/main.log
+8 -1
View File
@@ -1,4 +1,10 @@
# Twitch Drops Miner
# Twitch Drops Miner - Docker version
This is a fork of [DevilXD/TwitchDropsMiner](https://github.com/DevilXD/TwitchDropsMiner), adding Docker support. \
Copy the `.env.example` to `.env` and adjust the port as needed. \
Then run `docker compose up -d --build` and connect via VNC to your port.
## Twitch Drops Miner
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 you save on bandwidth and hassle.
@@ -164,6 +170,7 @@ placed past the period character at the end.
@roobini-gamer - For the entirety of the French (Français) translation.
@Calvineries - For the French (Français) translation revisions.
@ThisIsCyreX - For the entirety of the German (Deutsch) translation.
@Nagyhoho1234 - For the entirety of the Hungarian (Magyar) translation.
@Eriza-Z - For the entirety of the Indonesian translation.
@casungo - For the entirety of the Italian (Italiano) translation.
@ShimadaNanaki - For the entirety of the Japanese (日本語) translation.
+8 -3
View File
@@ -71,9 +71,14 @@ hiddenimports: list[str] = [
if sys.platform == "linux":
# Needed files for better system tray support on Linux via pystray (AppIndicator backend).
arch: str = platform.machine()
libraries_path: Path = Path(f"/usr/lib/{arch}-linux-gnu")
if not libraries_path.exists():
libraries_path = Path("/usr/lib64")
candidate_library_paths: list[Path] = [
Path(f"/usr/lib/{arch}-linux-gnu"), # Debian/Ubuntu multiarch
Path("/usr/lib64"), # Fedora/RHEL
Path("/usr/lib"), # Arch and other single-dir distros
]
for libraries_path in candidate_library_paths:
if (libraries_path / "libayatana-appindicator3.so.1").exists():
break
datas.append(
(libraries_path / "girepository-1.0/AyatanaAppIndicator3-0.1.typelib", "gi_typelibs")
)
+19 -29
View File
@@ -44,29 +44,8 @@ class Stream:
self._stream_url: URLType | None = None
@cached_property
def _spade_payload(self) -> JsonType:
payload = [
{
"event": "minute-watched",
"properties": {
"broadcast_id": str(self.broadcast_id),
"channel_id": str(self.channel.id),
"channel": self.channel._login,
"hidden": False,
"live": True,
"location": "channel",
"logged_in": True,
"muted": False,
"player": "site",
"user_id": self.channel._twitch._auth_state.user_id,
}
}
]
return {"data": (b64encode(json_minify(payload).encode("utf8"))).decode("utf8")}
@cached_property
def _gql_payload(self) -> GQLQuery:
payload = [
def _watch_payload(self) -> list[JsonType]:
return [
{
"event": "minute-watched",
"properties": {
@@ -86,12 +65,23 @@ class Stream:
}
}
]
@cached_property
def spade_payload(self) -> JsonType:
return {
"data": (b64encode(json_minify(self._watch_payload).encode("utf8"))).decode("utf8")
}
@cached_property
def gql_payload(self) -> GQLQuery:
return GQLQuery(
(
"\n mutation SendEvents($input: SendSpadeEventsInput!) "
"{\n sendSpadeEvents(input: $input) {\n statusCode\n}\n}\n"
),
b64encode(gzip.compress(json_minify(payload).encode("utf8"))).decode("utf8")
b64encode(
gzip.compress(json_minify(self._watch_payload).encode("utf8"))
).decode("utf8")
)
@classmethod
@@ -491,25 +481,25 @@ class Channel:
async with self._twitch.request("HEAD", stream_chunk_url) as head_response:
return head_response.status == 200
# NOTE: This is currently unused.
async def _send_watch_spade(self) -> bool:
async def send_watch(self) -> bool:
if self._stream is None:
return False
if self._spade_url is None:
self._spade_url = await self.get_spade_url()
try:
async with self._twitch.request(
"POST", self._spade_url, data=self._stream._spade_payload
"POST", self._spade_url, data=self._stream.spade_payload
) as response:
return response.status == 204
except RequestException:
return False
async def send_watch(self) -> bool: # send_watch_gql
# NOTE: This is currently unused.
async def _send_watch_gql(self) -> bool:
if self._stream is None:
return False
try:
watch_response = await self._twitch.gql_request(self._stream._gql_payload)
watch_response = await self._twitch.gql_request(self._stream.gql_payload)
return watch_response["data"]["sendSpadeEvents"]["statusCode"] == 204
except RequestException:
return False
+14
View File
@@ -0,0 +1,14 @@
services:
app:
build:
context: ./
dockerfile: ./Dockerfile
environment:
- TZ=Europe/Berlin
env_file:
- .env
restart: unless-stopped
ports:
- "127.0.0.1:${VNC_PORT:-5900}:5900"
volumes:
- ./volumes/data:/app/data
+10 -9
View File
@@ -92,6 +92,7 @@ else:
if SELF_PATH.stem == "pyinstaller" or SELF_PATH.name == "gui.py":
SELF_PATH = Path(__file__).with_name("main.py").resolve()
WORKING_DIR = SELF_PATH.parent
DATA_DIR = Path(WORKING_DIR, "data")
# Development paths
VENV_PATH = Path(WORKING_DIR, "env")
SITE_PACKAGES_PATH = Path(VENV_PATH, SYS_SITE_PACKAGES)
@@ -100,13 +101,13 @@ SCRIPTS_PATH = Path(VENV_PATH, SYS_SCRIPTS)
# NOTE: These don't have to be available to the end-user, so the path points to the internal dir
LANG_PATH = _resource_path("lang")
# Other Paths
LOG_PATH = Path(WORKING_DIR, "log.txt")
DUMP_PATH = Path(WORKING_DIR, "dump.dat")
LOCK_PATH = Path(WORKING_DIR, "lock.file")
CACHE_PATH = Path(WORKING_DIR, "cache")
LOG_PATH = Path(DATA_DIR, "log.txt")
DUMP_PATH = Path(DATA_DIR, "dump.dat")
LOCK_PATH = Path(DATA_DIR, "lock.file")
CACHE_PATH = Path(DATA_DIR, "cache")
CACHE_DB = Path(CACHE_PATH, "mapping.json")
COOKIES_PATH = Path(WORKING_DIR, "cookies.jar")
SETTINGS_PATH = Path(WORKING_DIR, "settings.json")
COOKIES_PATH = Path(DATA_DIR, "cookies.jar")
SETTINGS_PATH = Path(DATA_DIR, "settings.json")
# Typing
JsonType = Dict[str, Any]
URLType = NewType("URLType", str)
@@ -345,7 +346,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns all in-progress campaigns
"Inventory": GQLPersistedQuery(
"Inventory",
"d86775d0ef16a63a33ad52e80eaff963b2d5b72fada7c991504a57496e1d8e4b",
"8337eb8541b314040b0edde0c09c5c7a2783ba1960aa9edfbf3bac16d0fec404",
variables={
"fetchRewardCampaigns": False,
}
@@ -362,7 +363,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns all available campaigns
"Campaigns": GQLPersistedQuery(
"ViewerDropsDashboard",
"5a4da2ab3d5b47c9f9ce864e727b2cb346af1e3ea8b897fe8f704a97ff017619",
"d9cae7761dafab85908c85e6683cb4201b449e66ac3bb5e894f15ff12aeafaa7",
variables={
"fetchRewardCampaigns": False,
}
@@ -400,7 +401,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns live channels for a particular game
"GameDirectory": GQLPersistedQuery(
"DirectoryPage_Game",
"cb5dc816e139dcb8a118f14b4b677d59abc224a4b016c4bc2bb00a47fe0ddec4",
"86bcceb4e8b1a51256ff8eed8bd8aae4acacf80d737efe904f84f3aeadf8cafd",
variables={
"limit": 30, # limit of channels returned
"slug": ..., # game slug
+13 -3
View File
@@ -2407,15 +2407,25 @@ class GUIManager:
0.05s gives similar performance and CPU usage.
Not ideal, but the simplest way to avoid threads, thread safety,
loop.call_soon_threadsafe, futures and all of that.
Uses TKINTER_DONT_WAIT to prevent Tcl/Tk from hanging inside native
system calls (e.g. X11/Wayland input contexts) during heavy UI redraws.
"""
update = self._root.update
do_one_event = self._root.dooneevent
# TKINTER_DONT_WAIT (1 << 1) tells Tcl to return immediately
# if no events are ready in the queue.
DONT_WAIT = 1 << 1
while True:
try:
update()
# Drain pending Tk events non-blockingly
while do_one_event(DONT_WAIT):
pass
except tk.TclError:
# root has been destroyed
# Root window was destroyed
break
await asyncio.sleep(0.05)
self._poll_task = None
def close(self, *args) -> int:
+5 -1
View File
@@ -165,7 +165,11 @@
"how_it_works": "Cómo funciona",
"how_it_works_text": "Cada varios segundos, la aplicación pretende ver una transmisión en particular, \ny obtiene los metadatos de la misma para progresar con los drops. \nDe esta forma, no es necesario descargar el video o sonido de la transmisión. Ahorrando ancho de banda al usuario. \nCon el fin de mantener actualizados los estados (ONLINE u OFFLINE) de los canales, se establece una conexión websocket, \nque recibe información de los streams para poder actualizar el estado, o cantidad de espectadores actuales.",
"getting_started": "Primeros pasos",
"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 posibles, cambie el \"Modo de prioridad\" a cualquier opción que no sea \"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. Seleccione la opción \"Solo juegos preferidos\" en el \"Modo de prioridad\" si desea evitar minar juegos que no estén en la lista de juegos preferidos. \n6. Utilice la \"Lista de juegos excluidos\" para indicarle a la aplicación cuáles juegos no deben minarse nunca. \n7. Al cambiar el contenido de las listas, o de las opciones del \"Modo de prioridad\", \nserá necesario presionar el botón \"Recargar\" para que los cambios sean aplicados."
"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 posibles, cambie el \"Modo de prioridad\" a cualquier opción que no sea \"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. Seleccione la opción \"Solo juegos preferidos\" en el \"Modo de prioridad\" si desea evitar minar juegos que no estén en la lista de juegos preferidos. \n6. Utilice la \"Lista de juegos excluidos\" para indicarle a la aplicación cuáles juegos no deben minarse nunca. \n7. Al cambiar el contenido de las listas, o de las opciones del \"Modo de prioridad\", \nserá necesario presionar el botón \"Recargar\" para que los cambios sean aplicados.",
"invalidate": {
"button": "Invalidar",
"text": "Invalidar el token de autenticación (cerrar sesión):"
}
}
}
}
+171
View File
@@ -0,0 +1,171 @@
{
"english_name": "Hungarian",
"status": {
"terminated": "\nAz alkalmazás leállt.\nA kilépéshez zárd be az ablakot.",
"watching": "Most nézve: {channel}",
"goes_online": "{channel} ONLINE lett, átváltás...",
"goes_offline": "{channel} OFFLINE lett, átváltás...",
"claimed_drop": "Átvett drop: {drop}",
"no_channel": "Nincs nézhető csatorna. Várakozás egy ONLINE csatornára...",
"no_campaign": "Nincs aktív kampány, amelyből dropokat lehetne bányászni. Várakozás aktív kampányra..."
},
"login": {
"unexpected_content": "Váratlan tartalomtípus érkezett, ez általában átirányítás miatt történik. Nem kell esetleg bejelentkezned az internet eléréséhez?",
"chrome": {
"startup": "A Chrome indítása...",
"login_to_complete": "Fejezd be a bejelentkezést kézzel, majd nyomd meg újra a Bejelentkezés gombot.",
"no_token": "Nem található hitelesítési token.",
"closed_window": "A Chrome ablaka bezárult, mielőtt a bejelentkezés befejeződhetett volna."
},
"error_code": "Bejelentkezési hibakód: {error_code}",
"incorrect_login_pass": "Hibás felhasználónév vagy jelszó.",
"incorrect_email_code": "Hibás az e-mailben kapott kód.",
"incorrect_twofa_code": "Hibás 2FA kód.",
"email_code_required": "E-mailben kapott kód szükséges. Nézd meg a postaládádat.",
"twofa_code_required": "2FA kód szükséges."
},
"error": {
"captcha": "A bejelentkezési kísérletet a CAPTCHA elutasította.\nPróbáld újra legalább 12 óra múlva.",
"site_down": "A Twitch nem elérhető, újrapróbálkozás {seconds} másodperc múlva...",
"no_connection": "Nem sikerült csatlakozni a Twitchhez, újrapróbálkozás {seconds} másodperc múlva... ({url})"
},
"gui": {
"output": "Kimenet",
"status": {
"name": "Állapot",
"idle": "Tétlen",
"exiting": "Kilépés...",
"terminated": "Leállítva",
"cleanup": "Csatornalista tisztítása...",
"gathering": "Csatornák gyűjtése...",
"switching": "Csatornaváltás...",
"fetching_inventory": "Készlet lekérése...",
"fetching_campaigns": "Kampányok lekérése...",
"adding_campaigns": "Kampányok hozzáadása a készlethez... {counter}"
},
"tabs": {
"main": "Főoldal",
"inventory": "Készlet",
"settings": "Beállítások",
"help": "Súgó"
},
"tray": {
"notification_title": "Kibányászott drop",
"minimize": "Kicsinyítés a tálcára",
"show": "Megjelenítés",
"quit": "Kilépés"
},
"login": {
"name": "Bejelentkezés",
"labels": "Állapot:\nFelhasználó-azonosító:",
"logged_in": "Bejelentkezve",
"logged_out": "Kijelentkezve",
"logging_in": "Bejelentkezés...",
"required": "Bejelentkezés szükséges",
"request": "A folytatáshoz jelentkezz be.",
"username": "Felhasználónév",
"password": "Jelszó",
"twofa_code": "2FA kód (nem kötelező)",
"button": "Bejelentkezés"
},
"websocket": {
"name": "Websocket állapota",
"websocket": "Websocket #{id}:",
"initializing": "Előkészítés...",
"connected": "Csatlakozva",
"disconnected": "Nincs kapcsolat",
"connecting": "Csatlakozás...",
"disconnecting": "Kapcsolat bontása...",
"reconnecting": "Újracsatlakozás..."
},
"progress": {
"name": "Kampány előrehaladása",
"drop": "Drop:",
"game": "Játék:",
"campaign": "Kampány:",
"remaining": "{time} van hátra",
"drop_progress": "Előrehaladás:",
"campaign_progress": "Előrehaladás:"
},
"channels": {
"name": "Csatornák",
"switch": "Váltás",
"online": "ONLINE ✔",
"pending": "OFFLINE ⏳",
"offline": "OFFLINE ❌",
"headings": {
"channel": "Csatorna",
"status": "Állapot",
"game": "Játék",
"viewers": "Nézők"
}
},
"inventory": {
"filter": {
"name": "Szűrő",
"show": "Megjelenítés:",
"not_linked": "Nem összekapcsolt",
"upcoming": "Közelgő",
"expired": "Lejárt",
"excluded": "Kizárt",
"finished": "Befejezett",
"refresh": "Frissítés"
},
"status": {
"linked": "Összekapcsolva ✔",
"not_linked": "Nincs összekapcsolva ❌",
"active": "Aktív ✔",
"upcoming": "Közelgő ⏳",
"expired": "Lejárt ❌",
"claimed": "Átvéve ✔",
"ready_to_claim": "Átvehető ⏳"
},
"starts": "Kezdete: {time}",
"ends": "Vége: {time}",
"allowed_channels": "Engedélyezett csatornák:",
"all_channels": "Mind",
"and_more": "és még {amount}...",
"percent_progress": "{minutes} percből {percent}",
"minutes_progress": "{minutes} perc"
},
"settings": {
"general": {
"name": "Általános",
"autostart": "Automatikus indítás: ",
"tray": "Indítás a tálcára kicsinyítve: ",
"tray_notifications": "Tálcaértesítések: ",
"dark_mode": "Sötét mód: ",
"priority_mode": "Prioritási mód: ",
"proxy": "Proxy (újraindítást igényel):"
},
"advanced": {
"name": "Haladó",
"warning": "Figyelem!",
"warning_text": "Ezek a beállítások a bányászó hibás működését okozhatják.\nHa bármilyen problémát tapasztalsz, győződj meg róla, hogy mindegyik ki van kapcsolva.",
"enable_badges_emotes": "Jelvények és emote-ok részleges támogatása: ",
"available_drops_check": "Extra ellenőrzés az elérhető dropokhoz: "
},
"priority_modes": {
"priority_only": "Csak a prioritási lista",
"ending_soonest": "A leghamarabb véget érő először",
"low_availability": "A legkevésbé elérhető először"
},
"game_name": "Játék neve",
"priority": "Prioritás",
"exclude": "Kizárás",
"reload": "Újratöltés",
"reload_text": "A legtöbb módosítás életbe lépéséhez újratöltés szükséges: "
},
"help": {
"links": {
"name": "Hasznos hivatkozások",
"inventory": "Twitch-készlet megtekintése",
"campaigns": "Összes kampány megtekintése és a fiók-összekapcsolások kezelése"
},
"how_it_works": "Hogyan működik",
"how_it_works_text": "Az alkalmazás néhány másodpercenként úgy tesz, mintha nézne egy adott streamet: lekéri a stream metaadatait – ennyi elég a dropok gyűjtéséhez. Így egyáltalán nem kell letölteni a tényleges videó- és hangfolyamot. A csatornák állapotának (ONLINE vagy OFFLINE) naprakészen tartásához websocket-kapcsolat épül ki, amelyen keresztül az alkalmazás értesül a streamek indulásáról és leállásáról, valamint a nézőszám változásairól.",
"getting_started": "Első lépések",
"getting_started_text": "1. Jelentkezz be az alkalmazásba.\n2. Győződj meg róla, hogy a Twitch-fiókod össze van kapcsolva minden olyan kampánnyal, amelyet bányászni szeretnél.\n3. Ha mindent bányásznál, ami csak lehetséges, állítsd a Prioritási módot bármire a „Csak a prioritási lista” kivételével, majd nyomd meg az „Újratöltés” gombot.\n4. Ha bizonyos játékokat előbb szeretnél bányászni, a „Prioritás” listában állíts össze egy sorrendet a kívánt játékokból. A lista tetején lévő játékokat az alkalmazás előbb próbálja bányászni, mint a lejjebb lévőket.\n5. Hagyd a „Prioritási módot” a „Csak a prioritási lista” beállításon, ha nem szeretnél a listán nem szereplő játékokat bányászni. Vagy mégis – ahogy szeretnéd.\n6. A „Kizárás” listával adhatod meg, mely játékokat ne bányássza soha az alkalmazás.\n7. A listák tartalmának vagy a „Prioritási mód” módosítása után nyomd meg az „Újratöltés” gombot, hogy a változtatások életbe lépjenek."
}
}
}
+4
View File
@@ -166,6 +166,10 @@
"how_it_works_text": "Co kilkanaście sekund aplikacja pobiera metadane strumienia, udając oglądanie streamu - to wystarczy, aby uzyskać postęp dropu. 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 na bieżąco aktualizuje liczbę widzów.",
"getting_started": "Pierwsze kroki",
"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 otrzymywać dropy.\n3. Zmień tryb priorytetu na coś innego niż „Tylko lista priorytetów” i kliknij „Przeładuj”, jeśli chcesz otrzymywać dropy ze wszystkich aktywnych kampanii.\n4. Użyj listy „Priorytety”, aby wybrać uporządkowaną listę gier, z których chcesz otrzymać dropy. Gry znajdujące się wyżej na liście będą miały wyższy priorytet przy zdobywaniu dropów.\n5. Wybierz opcję „Tylko lista priorytetów”, aby wykluczyć gry, które nie znajdują się na liście priorytetowej.\n6. Użyj listy „Wykluczone”, aby wskazać gry, z których przedmioty nigdy nie powinny być zdobywane.\n7. Zmiana zawartości list „Priorytety” i „Wykluczone” oraz opcji „Tryb priorytetu” wymaga kliknięcia „Przeładuj”, aby aplikacja mogła zastosować wprowadzone zmiany."
},
"invalidate": {
"button": "Unieważnij",
"text": "Unieważnij token uwierzytelniający (wyloguj się):"
}
}
}
+5 -1
View File
@@ -165,7 +165,11 @@
"how_it_works": "工作原理",
"how_it_works_text": "该应用程序每隔几秒钟就会抓取流媒体元数据,以假装在观看特定的直播频道——这足以推进掉宝进度,且完全不需要下载任何实际视频或声音流。为了使频道的状态(在线或离线)保持最新,建立了Websocket连接,以接收有关直播的开播下播事件,并更新当前观众数量。",
"getting_started": "入门设置",
"getting_started_text": "1. 在应用程序中登录您的Twitch账户。\n2. 确保您想领取掉宝的所有游戏的账户都已关联到您的Twitch账户。\n3. 若您想挖掘所有掉宝,请在“掉宝挖掘模式”中选择“仅观看优先列表中的游戏”以外的模式,然后点击“刷新”。\n4. 若您想优先挖掘特定游戏的掉宝,请在“优先列表”中按照挖掘优先级顺序排列游戏。程序将尝试先挖掘列表顶部的游戏,然后再挖掘后面的游戏。\n5. 勾选“仅观看优先列表中的游戏”时,不会挖掘不在“优先列表”中的游戏。\n6. 在“排除列表”中添加永不挖掘的游戏。\n7. 更改任一列表的内容,或更改“仅观看优先列表中的游戏”勾选状态后,需要手动点击“刷新”才能使改动生效。"
"getting_started_text": "1. 在应用程序中登录您的Twitch账户。\n2. 确保您想领取掉宝的所有游戏的账户都已关联到您的Twitch账户。\n3. 若您想挖掘所有掉宝,请在“掉宝挖掘模式”中选择“仅观看优先列表中的游戏”以外的模式,然后点击“刷新”。\n4. 若您想优先挖掘特定游戏的掉宝,请在“优先列表”中按照挖掘优先级顺序排列游戏。程序将尝试先挖掘列表顶部的游戏,然后再挖掘后面的游戏。\n5. 勾选“仅观看优先列表中的游戏”时,不会挖掘不在“优先列表”中的游戏。\n6. 在“排除列表”中添加永不挖掘的游戏。\n7. 更改任一列表的内容,或更改“仅观看优先列表中的游戏”勾选状态后,需要手动点击“刷新”才能使改动生效。",
"invalidate": {
"button": "退出登录",
"text": "使身份验证令牌失效(退出Twitch账号登录):"
}
}
}
}
+5 -1
View File
@@ -165,7 +165,11 @@
"how_it_works": "工作原理",
"how_it_works_text": "每隔一段時間,應用程式就會向 Twitch 請求當前觀看頻道的原始串流數據的 URL。\n然後提取此串流的元數據——這足以推進掉寶進度。請注意,這完全繞過了下載實際的串流視頻和音頻的需求。\n為了使頻道的狀態(上線或離線)的即時更新,應用程式建立了一個 WebSocket 連接,\n該連接會接收有關串流上下線的事件或當前觀眾數量的更新。",
"getting_started": "事前準備",
"getting_started_text": "1. 透過應用程式登入。\n2. 確保您的 Twitch 帳號已經和您所有感興趣的遊戲帳號已經連接。\n3. 如果您想要獲取所有感興趣的掉寶,請不要勾選「僅參與優先掉寶遊戲」,然後按「重新載入」。\n4. 如果您想優先獲取特定遊戲,請使用「優先掉寶遊戲」列表設置您想獲取的遊戲的優先順序。\n 應用程式將嘗試先獲取列表頂端的遊戲,然後依序獲取之後的遊戲。\n5. 勾選「僅參與優先掉寶遊戲」以避免獲取不在優先列表中的遊戲。當然,你也可以選擇不勾選——這取決於您。\n6. 使用「排除掉寶遊戲」列表設置永不獲取的遊戲。\n7. 更改任一列表的內容,或更改「僅參與優先掉寶遊戲」勾選狀態,需要手動按「重新載入」才能使更改生效。"
"getting_started_text": "1. 透過應用程式登入。\n2. 確保您的 Twitch 帳號已經和您所有感興趣的遊戲帳號已經連接。\n3. 如果您想要獲取所有感興趣的掉寶,請不要勾選「僅參與優先掉寶遊戲」,然後按「重新載入」。\n4. 如果您想優先獲取特定遊戲,請使用「優先掉寶遊戲」列表設置您想獲取的遊戲的優先順序。\n 應用程式將嘗試先獲取列表頂端的遊戲,然後依序獲取之後的遊戲。\n5. 勾選「僅參與優先掉寶遊戲」以避免獲取不在優先列表中的遊戲。當然,你也可以選擇不勾選——這取決於您。\n6. 使用「排除掉寶遊戲」列表設置永不獲取的遊戲。\n7. 更改任一列表的內容,或更改「僅參與優先掉寶遊戲」勾選狀態,需要手動按「重新載入」才能使更改生效。",
"invalidate": {
"button": "登出",
"text": "使身份驗證權杖失效(登出Twitch):"
}
}
}
}
+8 -1
View File
@@ -7,6 +7,7 @@ from multiprocessing import freeze_support
if __name__ == "__main__":
freeze_support()
import io
import os
import sys
import signal
import asyncio
@@ -40,6 +41,11 @@ if __name__ == "__main__":
if sys.version_info < (3, 10):
raise RuntimeError("Python 3.10 or higher is required")
# Suppress X11 Input Method registration on Linux to prevent
# XWayland/Mutter lockups during heavy Tkinter layout updates.
if sys.platform.startswith("linux") and "XMODIFIERS" not in os.environ:
os.environ["XMODIFIERS"] = "@im=none"
class Parser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@@ -181,7 +187,8 @@ if __name__ == "__main__":
client.gui.status.update(_("gui", "status", "terminated"))
# notify the user about the closure
client.gui.grab_attention(sound=True)
await client.gui.wait_until_closed()
# Docker: do not wait until close so docker can recognize the exit and restart
# await client.gui.wait_until_closed()
# save the application state
# NOTE: we have to do it after wait_until_closed,
# because the user can alter some settings between app termination and closing the window
View File
Whitespace-only changes.