Compare commits

..
10 Commits
12 changed files with 244 additions and 42 deletions

No files matched your search

+4 -1
View File
@@ -275,8 +275,11 @@ jobs:
- name: Install appimage-builder - name: Install appimage-builder
run: | 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. # 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 - name: Create AppImage
env: env:
+1
View File
@@ -164,6 +164,7 @@ placed past the period character at the end.
@roobini-gamer - For the entirety of the French (Français) translation. @roobini-gamer - For the entirety of the French (Français) translation.
@Calvineries - For the French (Français) translation revisions. @Calvineries - For the French (Français) translation revisions.
@ThisIsCyreX - For the entirety of the German (Deutsch) translation. @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. @Eriza-Z - For the entirety of the Indonesian translation.
@casungo - For the entirety of the Italian (Italiano) translation. @casungo - For the entirety of the Italian (Italiano) translation.
@ShimadaNanaki - For the entirety of the Japanese (日本語) translation. @ShimadaNanaki - For the entirety of the Japanese (日本語) translation.
+8 -3
View File
@@ -71,9 +71,14 @@ hiddenimports: list[str] = [
if sys.platform == "linux": if sys.platform == "linux":
# Needed files for better system tray support on Linux via pystray (AppIndicator backend). # Needed files for better system tray support on Linux via pystray (AppIndicator backend).
arch: str = platform.machine() arch: str = platform.machine()
libraries_path: Path = Path(f"/usr/lib/{arch}-linux-gnu") candidate_library_paths: list[Path] = [
if not libraries_path.exists(): Path(f"/usr/lib/{arch}-linux-gnu"), # Debian/Ubuntu multiarch
libraries_path = Path("/usr/lib64") 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( datas.append(
(libraries_path / "girepository-1.0/AyatanaAppIndicator3-0.1.typelib", "gi_typelibs") (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 self._stream_url: URLType | None = None
@cached_property @cached_property
def _spade_payload(self) -> JsonType: def _watch_payload(self) -> list[JsonType]:
payload = [ return [
{
"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 = [
{ {
"event": "minute-watched", "event": "minute-watched",
"properties": { "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( return GQLQuery(
( (
"\n mutation SendEvents($input: SendSpadeEventsInput!) " "\n mutation SendEvents($input: SendSpadeEventsInput!) "
"{\n sendSpadeEvents(input: $input) {\n statusCode\n}\n}\n" "{\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 @classmethod
@@ -491,25 +481,25 @@ class Channel:
async with self._twitch.request("HEAD", stream_chunk_url) as head_response: async with self._twitch.request("HEAD", stream_chunk_url) as head_response:
return head_response.status == 200 return head_response.status == 200
# NOTE: This is currently unused. async def send_watch(self) -> bool:
async def _send_watch_spade(self) -> bool:
if self._stream is None: if self._stream is None:
return False return False
if self._spade_url is None: if self._spade_url is None:
self._spade_url = await self.get_spade_url() self._spade_url = await self.get_spade_url()
try: try:
async with self._twitch.request( 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: ) as response:
return response.status == 204 return response.status == 204
except RequestException: except RequestException:
return False 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: if self._stream is None:
return False return False
try: 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 return watch_response["data"]["sendSpadeEvents"]["statusCode"] == 204
except RequestException: except RequestException:
return False return False
+3 -3
View File
@@ -345,7 +345,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns all in-progress campaigns # returns all in-progress campaigns
"Inventory": GQLPersistedQuery( "Inventory": GQLPersistedQuery(
"Inventory", "Inventory",
"d86775d0ef16a63a33ad52e80eaff963b2d5b72fada7c991504a57496e1d8e4b", "8337eb8541b314040b0edde0c09c5c7a2783ba1960aa9edfbf3bac16d0fec404",
variables={ variables={
"fetchRewardCampaigns": False, "fetchRewardCampaigns": False,
} }
@@ -362,7 +362,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns all available campaigns # returns all available campaigns
"Campaigns": GQLPersistedQuery( "Campaigns": GQLPersistedQuery(
"ViewerDropsDashboard", "ViewerDropsDashboard",
"5a4da2ab3d5b47c9f9ce864e727b2cb346af1e3ea8b897fe8f704a97ff017619", "d9cae7761dafab85908c85e6683cb4201b449e66ac3bb5e894f15ff12aeafaa7",
variables={ variables={
"fetchRewardCampaigns": False, "fetchRewardCampaigns": False,
} }
@@ -400,7 +400,7 @@ GQL_QUERIES: dict[str, GQLPersistedQuery] = {
# returns live channels for a particular game # returns live channels for a particular game
"GameDirectory": GQLPersistedQuery( "GameDirectory": GQLPersistedQuery(
"DirectoryPage_Game", "DirectoryPage_Game",
"cb5dc816e139dcb8a118f14b4b677d59abc224a4b016c4bc2bb00a47fe0ddec4", "86bcceb4e8b1a51256ff8eed8bd8aae4acacf80d737efe904f84f3aeadf8cafd",
variables={ variables={
"limit": 30, # limit of channels returned "limit": 30, # limit of channels returned
"slug": ..., # game slug "slug": ..., # game slug
+13 -3
View File
@@ -2407,15 +2407,25 @@ class GUIManager:
0.05s gives similar performance and CPU usage. 0.05s gives similar performance and CPU usage.
Not ideal, but the simplest way to avoid threads, thread safety, Not ideal, but the simplest way to avoid threads, thread safety,
loop.call_soon_threadsafe, futures and all of that. 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: while True:
try: try:
update() # Drain pending Tk events non-blockingly
while do_one_event(DONT_WAIT):
pass
except tk.TclError: except tk.TclError:
# root has been destroyed # Root window was destroyed
break break
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
self._poll_task = None self._poll_task = None
def close(self, *args) -> int: def close(self, *args) -> int:
+5 -1
View File
@@ -165,7 +165,11 @@
"how_it_works": "Cómo funciona", "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.", "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": "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.", "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": "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." "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": "工作原理",
"how_it_works_text": "该应用程序每隔几秒钟就会抓取流媒体元数据,以假装在观看特定的直播频道——这足以推进掉宝进度,且完全不需要下载任何实际视频或声音流。为了使频道的状态(在线或离线)保持最新,建立了Websocket连接,以接收有关直播的开播下播事件,并更新当前观众数量。", "how_it_works_text": "该应用程序每隔几秒钟就会抓取流媒体元数据,以假装在观看特定的直播频道——这足以推进掉宝进度,且完全不需要下载任何实际视频或声音流。为了使频道的状态(在线或离线)保持最新,建立了Websocket连接,以接收有关直播的开播下播事件,并更新当前观众数量。",
"getting_started": "入门设置", "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": "工作原理",
"how_it_works_text": "每隔一段時間,應用程式就會向 Twitch 請求當前觀看頻道的原始串流數據的 URL。\n然後提取此串流的元數據——這足以推進掉寶進度。請注意,這完全繞過了下載實際的串流視頻和音頻的需求。\n為了使頻道的狀態(上線或離線)的即時更新,應用程式建立了一個 WebSocket 連接,\n該連接會接收有關串流上下線的事件或當前觀眾數量的更新。", "how_it_works_text": "每隔一段時間,應用程式就會向 Twitch 請求當前觀看頻道的原始串流數據的 URL。\n然後提取此串流的元數據——這足以推進掉寶進度。請注意,這完全繞過了下載實際的串流視頻和音頻的需求。\n為了使頻道的狀態(上線或離線)的即時更新,應用程式建立了一個 WebSocket 連接,\n該連接會接收有關串流上下線的事件或當前觀眾數量的更新。",
"getting_started": "事前準備", "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):"
}
} }
} }
} }
+6
View File
@@ -7,6 +7,7 @@ from multiprocessing import freeze_support
if __name__ == "__main__": if __name__ == "__main__":
freeze_support() freeze_support()
import io import io
import os
import sys import sys
import signal import signal
import asyncio import asyncio
@@ -40,6 +41,11 @@ if __name__ == "__main__":
if sys.version_info < (3, 10): if sys.version_info < (3, 10):
raise RuntimeError("Python 3.10 or higher is required") 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): class Parser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)