From 3a961eaa0e5bc75b4e8ecfb7cefa8a5a58f8c58d Mon Sep 17 00:00:00 2001 From: "V.O.I.D" Date: Wed, 15 Apr 2026 17:19:15 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B0=D1=80=D1=81=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parser.py | 982 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 982 insertions(+) create mode 100644 parser.py diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..3c7d437 --- /dev/null +++ b/parser.py @@ -0,0 +1,982 @@ +import asyncio +import aiohttp +import aiofiles +import re +import os +import time +import json +import subprocess +import tempfile +import requests +import threading +import hashlib +import socket +import random +import urllib.parse +import ssl +import sys +from datetime import datetime, timedelta +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor, as_completed + +# ========= ДИАГНОСТИКА ========= +print(f"🚀 Запуск парсера...") +print(f"📂 Текущая директория: {os.getcwd()}") +print(f"🐍 Python версия: {sys.version}") + +# ========= ФАЙЛЫ ========= +SOURCES_FILE = "sources.txt" +OUTPUT_FILE = "url.txt" +CLEAN_FILE = "url_clean.txt" +FILTERED_FILE = "url_filtered.txt" +NAMED_FILE = "url_named.txt" +ENCODED_FILE = "url_encoded.txt" +WORK_FILE = "url_work.txt" +LOG_FILE = "log.txt" +PROCESSED_FILE = "processed.json" +CACHE_FILE = "cache_results.json" +DEBUG_FILE = "debug_failed.txt" +XRAY_LOG_FILE = "xray_errors.log" + +# ========= НАСТРОЙКИ ========= +THREADS_DOWNLOAD = 50 +CYCLE_DELAY = 3600 +LOG_CLEAN_INTERVAL = 86400 +CYCLES_BEFORE_DEBUG_CLEAN = 5 + +XRAY_MAX_WORKERS = 30 +XRAY_TEST_URL = "https://www.gstatic.com/generate_204" +XRAY_TIMEOUT = 3 +MAX_RETRIES = 2 +RETRY_DELAY = 1 + +print(f"⚡ Настройки: XRAY_MAX_WORKERS={XRAY_MAX_WORKERS}, TIMEOUT={XRAY_TIMEOUT}") + +cycle_counter = 0 + +# ========= РЕГУЛЯРКИ ========= +VLESS_REGEX = re.compile(r"vless://[0-9a-fA-F\-]{36}@[^\s\"'<]+", re.IGNORECASE) +UUID_REGEX = re.compile( + r"[0-9a-fA-F]{8}-" + r"[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{12}" +) + +# ========= СЛОВАРЬ ДОМЕНОВ ========= +DOMAIN_NAMES = { + 'x5.ru': 'Пятёрочка', '5ka.ru': 'Пятёрочка', '5ka-cdn.ru': 'Пятёрочка', + '5ka.static.ru': 'Пятёрочка', 'ads.x5.ru': 'Пятёрочка', 'perekrestok.ru': 'Перекрёсток', + 'vprok.ru': 'Перекрёсток', 'dixy.ru': 'Дикси', 'fasssst.ru': 'Fasssst', + 'rontgen.fasssst.ru': 'Fasssst', 'res.fasssst.ru': 'Fasssst', 'yt.fasssst.ru': 'Fasssst', + 'fast.strelkavpn.ru': 'StrelkaVPN', 'strelkavpn.ru': 'StrelkaVPN', 'maviks.ru': 'Maviks', + 'ru.maviks.ru': 'Maviks', 'a.ru.maviks.ru': 'Maviks', 'tree-top.cc': 'TreeTop', + 'a.ru.tree-top.cc': 'TreeTop', 'connect-iskra.ru': 'Iskra', '212-wl.connect-iskra.ru': 'Iskra', + 'vk.com': 'VK', 'vk.ru': 'VK', 'vkontakte.ru': 'VK', 'userapi.com': 'VK', + 'cdn.vk.com': 'VK', 'cdn.vk.ru': 'VK', 'id.vk.com': 'VK', 'id.vk.ru': 'VK', + 'login.vk.com': 'VK', 'login.vk.ru': 'VK', 'api.vk.com': 'VK', 'api.vk.ru': 'VK', + 'im.vk.com': 'VK', 'm.vk.com': 'VK', 'm.vk.ru': 'VK', 'sun6-22.userapi.com': 'VK', + 'sun6-21.userapi.com': 'VK', 'sun6-20.userapi.com': 'VK', 'sun9-38.userapi.com': 'VK', + 'sun9-101.userapi.com': 'VK', 'pptest.userapi.com': 'VK', 'vk-portal.net': 'VK', + 'stats.vk-portal.net': 'VK', 'akashi.vk-portal.net': 'VK', 'vkvideo.ru': 'VK Видео', + 'm.vkvideo.ru': 'VK Видео', 'queuev4.vk.com': 'VK', 'eh.vk.com': 'VK', 'cloud.vk.com': 'VK', + 'cloud.vk.ru': 'VK', 'admin.cs7777.vk.ru': 'VK', 'admin.tau.vk.ru': 'VK', + 'analytics.vk.ru': 'VK', 'api.cs7777.vk.ru': 'VK', 'api.tau.vk.ru': 'VK', + 'away.cs7777.vk.ru': 'VK', 'away.tau.vk.ru': 'VK', 'business.vk.ru': 'VK', + 'connect.cs7777.vk.ru': 'VK', 'cs7777.vk.ru': 'VK', 'dev.cs7777.vk.ru': 'VK', + 'dev.tau.vk.ru': 'VK', 'expert.vk.ru': 'VK', 'id.cs7777.vk.ru': 'VK', 'id.tau.vk.ru': 'VK', + 'login.cs7777.vk.ru': 'VK', 'login.tau.vk.ru': 'VK', 'm.cs7777.vk.ru': 'VK', + 'm.tau.vk.ru': 'VK', 'm.vkvideo.cs7777.vk.ru': 'VK Видео', 'me.cs7777.vk.ru': 'VK', + 'ms.cs7777.vk.ru': 'VK', 'music.vk.ru': 'VK Музыка', 'oauth.cs7777.vk.ru': 'VK', + 'oauth.tau.vk.ru': 'VK', 'oauth2.cs7777.vk.ru': 'VK', 'ord.vk.ru': 'VK', 'push.vk.ru': 'VK', + 'r.vk.ru': 'VK', 'target.vk.ru': 'VK', 'tech.vk.ru': 'VK', 'ui.cs7777.vk.ru': 'VK', + 'ui.tau.vk.ru': 'VK', 'vkvideo.cs7777.vk.ru': 'VK Видео', 'speedload.ru': 'Speedload', + 'api.speedload.ru': 'Speedload', 'chat.speedload.ru': 'Speedload', 'serverstats.ru': 'ServerStats', + 'cdnfive.serverstats.ru': 'ServerStats', 'cdncloudtwo.serverstats.ru': 'ServerStats', + 'furypay.ru': 'FuryPay', 'api.furypay.ru': 'FuryPay', 'jojack.ru': 'JoJack', + 'spb.jojack.ru': 'JoJack', 'at.jojack.ru': 'JoJack', 'tcp-reset-club.net': 'TCP Reset', + 'est01-ss01.tcp-reset-club.net': 'TCP Reset', 'nl01-ss01.tcp-reset-club.net': 'TCP Reset', + 'ru01-blh01.tcp-reset-club.net': 'TCP Reset', 'gov.ru': 'Госуслуги', 'kremlin.ru': 'Кремль', + 'government.ru': 'Правительство', 'duma.gov.ru': 'Госдума', 'genproc.gov.ru': 'Генпрокуратура', + 'epp.genproc.gov.ru': 'Генпрокуратура', 'cikrf.ru': 'ЦИК', 'izbirkom.ru': 'Избирком', + 'gosuslugi.ru': 'Госуслуги', 'sfd.gosuslugi.ru': 'Госуслуги', 'esia.gosuslugi.ru': 'Госуслуги', + 'bot.gosuslugi.ru': 'Госуслуги', 'contract.gosuslugi.ru': 'Госуслуги', + 'novorossiya.gosuslugi.ru': 'Госуслуги', 'pos.gosuslugi.ru': 'Госуслуги', + 'lk.gosuslugi.ru': 'Госуслуги', 'map.gosuslugi.ru': 'Госуслуги', + 'partners.gosuslugi.ru': 'Госуслуги', 'gosweb.gosuslugi.ru': 'Госуслуги', + 'voter.gosuslugi.ru': 'Госуслуги', 'gu-st.ru': 'Госуслуги', 'nalog.ru': 'ФНС', + 'pfr.gov.ru': 'ПФР', 'digital.gov.ru': 'Минцифры', 'adm.digital.gov.ru': 'Минцифры', + 'xn--80ajghhoc2aj1c8b.xn--p1ai': 'Минцифры', 'a.res-nsdi.ru': 'NSDI', + 'b.res-nsdi.ru': 'NSDI', 'a.auth-nsdi.ru': 'NSDI', 'b.auth-nsdi.ru': 'NSDI', + 'ok.ru': 'Одноклассники', 'odnoklassniki.ru': 'Одноклассники', 'cdn.ok.ru': 'Одноклассники', + 'st.okcdn.ru': 'Одноклассники', 'st.ok.ru': 'Одноклассники', 'apiok.ru': 'Одноклассники', + 'jira.apiok.ru': 'Одноклассники', 'api.ok.ru': 'Одноклассники', 'm.ok.ru': 'Одноклассники', + 'live.ok.ru': 'Одноклассники', 'multitest.ok.ru': 'Одноклассники', 'dating.ok.ru': 'Одноклассники', + 'tamtam.ok.ru': 'Одноклассники', '742231.ms.ok.ru': 'Одноклассники', 'ozon.ru': 'Ozon', + 'www.ozon.ru': 'Ozon', 'seller.ozon.ru': 'Ozon', 'bank.ozon.ru': 'Ozon', 'pay.ozon.ru': 'Ozon', + 'securepay.ozon.ru': 'Ozon', 'adv.ozon.ru': 'Ozon', 'invest.ozon.ru': 'Ozon', + 'ord.ozon.ru': 'Ozon', 'autodiscover.ord.ozon.ru': 'Ozon', 'st.ozone.ru': 'Ozon', + 'ir.ozone.ru': 'Ozon', 'vt-1.ozone.ru': 'Ozon', 'ir-2.ozone.ru': 'Ozon', 'xapi.ozon.ru': 'Ozon', + 'owa.ozon.ru': 'Ozon', 'learning.ozon.ru': 'Ozon', 'mapi.learning.ozon.ru': 'Ozon', + 'ws.seller.ozon.ru': 'Ozon', 'wildberries.ru': 'Wildberries', 'wb.ru': 'Wildberries', + 'static.wb.ru': 'Wildberries', 'seller.wildberries.ru': 'Wildberries', + 'banners.wildberries.ru': 'Wildberries', 'fw.wb.ru': 'Wildberries', 'finance.wb.ru': 'Wildberries', + 'jitsi.wb.ru': 'Wildberries', 'dnd.wb.ru': 'Wildberries', 'user-geo-data.wildberries.ru': 'Wildberries', + 'banners-website.wildberries.ru': 'Wildberries', 'chat-prod.wildberries.ru': 'Wildberries', + 'a.wb.ru': 'Wildberries', 'avito.ru': 'Avito', 'm.avito.ru': 'Avito', 'api.avito.ru': 'Avito', + 'avito.st': 'Avito', 'img.avito.st': 'Avito', 'sntr.avito.ru': 'Avito', 'stats.avito.ru': 'Avito', + 'cs.avito.ru': 'Avito', 'www.avito.st': 'Avito', 'st.avito.ru': 'Avito', 'www.avito.ru': 'Avito', + **{f'{i:02d}.img.avito.st': 'Avito' for i in range(100)}, + 'sberbank.ru': 'Сбербанк', 'online.sberbank.ru': 'Сбербанк', 'sber.ru': 'Сбербанк', + 'id.sber.ru': 'Сбербанк', 'bfds.sberbank.ru': 'Сбербанк', 'cms-res-web.online.sberbank.ru': 'Сбербанк', + 'esa-res.online.sberbank.ru': 'Сбербанк', 'pl-res.online.sberbank.ru': 'Сбербанк', + 'www.sberbank.ru': 'Сбербанк', 'vtb.ru': 'ВТБ', 'www.vtb.ru': 'ВТБ', 'online.vtb.ru': 'ВТБ', + 'chat3.vtb.ru': 'ВТБ', 's.vtb.ru': 'ВТБ', 'sso-app4.vtb.ru': 'ВТБ', 'sso-app5.vtb.ru': 'ВТБ', + 'gazprombank.ru': 'Газпромбанк', 'alfabank.ru': 'Альфа-Банк', 'metrics.alfabank.ru': 'Альфа-Банк', + 'tinkoff.ru': 'Тинькофф', 'tbank.ru': 'Тинькофф', 'cdn.tbank.ru': 'Тинькофф', 'hrc.tbank.ru': 'Тинькофф', + 'cobrowsing.tbank.ru': 'Тинькофф', 'le.tbank.ru': 'Тинькофф', 'id.tbank.ru': 'Тинькофф', + 'imgproxy.cdn-tinkoff.ru': 'Тинькофф', 'banki.ru': 'Банки.ру', 'yandex.ru': 'Яндекс', + 'ya.ru': 'Яндекс', 'dzen.ru': 'Дзен', 'kinopoisk.ru': 'Кинопоиск', 'yastatic.net': 'Яндекс', + 'yandex.net': 'Яндекс', 'mail.yandex.ru': 'Яндекс Почта', 'disk.yandex.ru': 'Яндекс Диск', + 'maps.yandex.ru': 'Яндекс Карты', 'api-maps.yandex.ru': 'Яндекс Карты', + 'enterprise.api-maps.yandex.ru': 'Яндекс Карты', 'music.yandex.ru': 'Яндекс Музыка', + 'yandex.by': 'Яндекс', 'yandex.com': 'Яндекс', 'travel.yandex.ru': 'Яндекс Путешествия', + 'informer.yandex.ru': 'Яндекс', 'mediafeeds.yandex.ru': 'Яндекс', 'mediafeeds.yandex.com': 'Яндекс', + 'uslugi.yandex.ru': 'Яндекс Услуги', 'kiks.yandex.ru': 'Яндекс', 'kiks.yandex.com': 'Яндекс', + 'frontend.vh.yandex.ru': 'Яндекс', 'favicon.yandex.ru': 'Яндекс', 'favicon.yandex.com': 'Яндекс', + 'favicon.yandex.net': 'Яндекс', 'browser.yandex.ru': 'Яндекс Браузер', + 'browser.yandex.com': 'Яндекс Браузер', 'api.browser.yandex.ru': 'Яндекс Браузер', + 'api.browser.yandex.com': 'Яндекс Браузер', 'wap.yandex.ru': 'Яндекс', 'wap.yandex.com': 'Яндекс', + '300.ya.ru': 'Яндекс', 'brontp-pre.yandex.ru': 'Яндекс', 'suggest.dzen.ru': 'Дзен', + 'suggest.sso.dzen.ru': 'Дзен', 'sso.dzen.ru': 'Дзен', 'mail.yandex.com': 'Яндекс Почта', + 'yabs.yandex.ru': 'Яндекс', 'neuro.translate.yandex.ru': 'Яндекс Перевод', 'cdn.yandex.ru': 'Яндекс', + 'zen.yandex.ru': 'Дзен', 'zen.yandex.com': 'Дзен', 'zen.yandex.net': 'Дзен', + 'collections.yandex.ru': 'Яндекс Коллекции', 'collections.yandex.com': 'Яндекс Коллекции', + 'an.yandex.ru': 'Яндекс', 'sba.yandex.ru': 'Яндекс', 'sba.yandex.com': 'Яндекс', + 'sba.yandex.net': 'Яндекс', 'surveys.yandex.ru': 'Яндекс Опросы', + 'yabro-wbplugin.edadeal.yandex.ru': 'Яндекс', 'api.events.plus.yandex.net': 'Яндекс Плюс', + 'speller.yandex.net': 'Яндекс Спеллер', 'avatars.mds.yandex.net': 'Яндекс', + 'avatars.mds.yandex.com': 'Яндекс', 'mc.yandex.ru': 'Яндекс', 'mc.yandex.com': 'Яндекс', + '3475482542.mc.yandex.ru': 'Яндекс', 'zen-yabro-morda.mediascope.mc.yandex.ru': 'Яндекс', + 'travel.yastatic.net': 'Яндекс', 'api.uxfeedback.yandex.net': 'Яндекс', + 'api.s3.yandex.net': 'Яндекс', 'cdn.s3.yandex.net': 'Яндекс', + 'uxfeedback-cdn.s3.yandex.net': 'Яндекс', 'uxfeedback.yandex.ru': 'Яндекс', + 'cloudcdn-m9-15.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-14.cdn.yandex.net': 'Яндекс', + 'cloudcdn-m9-13.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-12.cdn.yandex.net': 'Яндекс', + 'cloudcdn-m9-10.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-9.cdn.yandex.net': 'Яндекс', + 'cloudcdn-m9-7.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-6.cdn.yandex.net': 'Яндекс', + 'cloudcdn-m9-5.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-4.cdn.yandex.net': 'Яндекс', + 'cloudcdn-m9-3.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-2.cdn.yandex.net': 'Яндекс', + 'cloudcdn-ams19.cdn.yandex.net': 'Яндекс', 'http-check-headers.yandex.ru': 'Яндекс', + 'cloud.cdn.yandex.net': 'Яндекс', 'cloud.cdn.yandex.com': 'Яндекс', + 'cloud.cdn.yandex.ru': 'Яндекс', 'dr2.yandex.net': 'Яндекс', 'dr.yandex.net': 'Яндекс', + 's3.yandex.net': 'Яндекс', 'static-mon.yandex.net': 'Яндекс', 'sync.browser.yandex.net': 'Яндекс', + 'storage.ape.yandex.net': 'Яндекс', 'strm-rad-23.strm.yandex.net': 'Яндекс', + 'strm.yandex.net': 'Яндекс', 'strm.yandex.ru': 'Яндекс', 'log.strm.yandex.ru': 'Яндекс', + 'egress.yandex.net': 'Яндекс', 'cdnrhkgfkkpupuotntfj.svc.cdn.yandex.net': 'Яндекс', + 'csp.yandex.net': 'Яндекс', 'mail.ru': 'Mail.ru', 'e.mail.ru': 'Mail.ru', 'my.mail.ru': 'Mail.ru', + 'cloud.mail.ru': 'Mail.ru', 'inbox.ru': 'Mail.ru', 'list.ru': 'Mail.ru', 'bk.ru': 'Mail.ru', + 'myteam.mail.ru': 'Mail.ru', 'trk.mail.ru': 'Mail.ru', '1l-api.mail.ru': 'Mail.ru', + 'rutube.ru': 'Rutube', 'static.rutube.ru': 'Rutube', 'rutubelist.ru': 'Rutube', + 'pic.rutubelist.ru': 'Rutube', 'ssp.rutube.ru': 'Rutube', 'preview.rutube.ru': 'Rutube', + 'goya.rutube.ru': 'Rutube', 'smotrim.ru': 'Смотрим', 'ivi.ru': 'Ivi', 'cdn.ivi.ru': 'Ivi', + 'okko.tv': 'Okko', 'start.ru': 'Start', 'wink.ru': 'Wink', 'kion.ru': 'Kion', + 'premier.one': 'Premier', 'more.tv': 'More.tv', 'm.47news.ru': '47 News', + 'lenta.ru': 'Лента.ру', 'gazeta.ru': 'Газета.ру', 'kp.ru': 'Комсомольская Правда', + 'rambler.ru': 'Рамблер', 'ria.ru': 'РИА Новости', 'tass.ru': 'ТАСС', + 'interfax.ru': 'Интерфакс', 'kommersant.ru': 'Коммерсант', 'vedomosti.ru': 'Ведомости', + 'rbc.ru': 'РБК', 'russian.rt.com': 'RT', 'iz.ru': 'Известия', 'mk.ru': 'Московский Комсомолец', + 'rg.ru': 'Российская Газета', 'www.kinopoisk.ru': 'Кинопоиск', 'widgets.kinopoisk.ru': 'Кинопоиск', + 'payment-widget.plus.kinopoisk.ru': 'Кинопоиск', 'external-api.mediabilling.kinopoisk.ru': 'Кинопоиск', + 'external-api.plus.kinopoisk.ru': 'Кинопоиск', 'graphql-web.kinopoisk.ru': 'Кинопоиск', + 'graphql.kinopoisk.ru': 'Кинопоиск', 'tickets.widget.kinopoisk.ru': 'Кинопоиск', + 'st.kinopoisk.ru': 'Кинопоиск', 'quiz.kinopoisk.ru': 'Кинопоиск', + 'payment-widget.kinopoisk.ru': 'Кинопоиск', 'payment-widget-smarttv.plus.kinopoisk.ru': 'Кинопоиск', + 'oneclick-payment.kinopoisk.ru': 'Кинопоиск', 'microapps.kinopoisk.ru': 'Кинопоиск', + 'ma.kinopoisk.ru': 'Кинопоиск', 'hd.kinopoisk.ru': 'Кинопоиск', + 'crowdtest.payment-widget-smarttv.plus.tst.kinopoisk.ru': 'Кинопоиск', + 'crowdtest.payment-widget.plus.tst.kinopoisk.ru': 'Кинопоиск', 'api.plus.kinopoisk.ru': 'Кинопоиск', + 'st-im.kinopoisk.ru': 'Кинопоиск', 'sso.kinopoisk.ru': 'Кинопоиск', 'touch.kinopoisk.ru': 'Кинопоиск', + '2gis.ru': '2ГИС', '2gis.com': '2ГИС', 'api.2gis.ru': '2ГИС', 'keys.api.2gis.com': '2ГИС', + 'tutu.ru': 'Туту.ру', 'img.tutu.ru': 'Туту.ру', 'rzd.ru': 'РЖД', 'ticket.rzd.ru': 'РЖД', + 'cdek.ru': 'СДЭК', 'cdek.market': 'СДЭК', 'calc.cdek.ru': 'СДЭК', 'pochta.ru': 'Почта России', + 'rostelecom.ru': 'Ростелеком', 'rt.ru': 'Ростелеком', 'mts.ru': 'МТС', 'megafon.ru': 'Мегафон', + 'beeline.ru': 'Билайн', 'tele2.ru': 'Tele2', 't2.ru': 'Tele2', 'www.t2.ru': 'Tele2', + 'msk.t2.ru': 'Tele2', 's3.t2.ru': 'Tele2', 'yota.ru': 'Yota', 'domru.ru': 'Дом.ру', + 'ertelecom.ru': 'ЭР-Телеком', 'selectel.ru': 'Selectel', 'timeweb.ru': 'Timeweb', + 'gismeteo.ru': 'Гисметео', 'meteoinfo.ru': 'Метео', 'rp5.ru': 'RП5', 'hh.ru': 'HeadHunter', + 'superjob.ru': 'SuperJob', 'rabota.ru': 'Работа.ру', 'auto.ru': 'Auto.ru', + 'sso.auto.ru': 'Auto.ru', 'drom.ru': 'Drom', 'avto.ru': 'Avto.ru', 'eda.ru': 'Eda.ru', + 'food.ru': 'Food.ru', 'edadeal.ru': 'Edadeal', 'delivery-club.ru': 'Delivery Club', + 'leroymerlin.ru': 'Леруа Мерлен', 'lemanapro.ru': 'Лемана Про', 'cdn.lemanapro.ru': 'Лемана Про', + 'static.lemanapro.ru': 'Лемана Про', 'dmp.dmpkit.lemanapro.ru': 'Лемана Про', + 'receive-sentry.lmru.tech': 'Лемана Про', 'partners.lemanapro.ru': 'Лемана Про', + 'petrovich.ru': 'Петрович', 'maxidom.ru': 'Максидом', 'vseinstrumenti.ru': 'ВсеИнструменты', + '220-volt.ru': '220 Вольт', 'max.ru': 'Max', 'dev.max.ru': 'Max', 'web.max.ru': 'Max', + 'api.max.ru': 'Max', 'legal.max.ru': 'Max', 'st.max.ru': 'Max', 'botapi.max.ru': 'Max', + 'link.max.ru': 'Max', 'download.max.ru': 'Max', 'i.max.ru': 'Max', 'help.max.ru': 'Max', + 'mos.ru': 'Мос.ру', 'taximaxim.ru': 'Такси Максим', 'moskva.taximaxim.ru': 'Такси Максим' +} + +print(f"📋 Загружено {len(DOMAIN_NAMES)} доменов в словарь") + +# ========= ЛОГ ========= +async def log(message: str): + try: + now = datetime.now() + if os.path.exists(LOG_FILE): + mtime = datetime.fromtimestamp(os.path.getmtime(LOG_FILE)) + if now - mtime > timedelta(seconds=LOG_CLEAN_INTERVAL): + open(LOG_FILE, "w").close() + + async with aiofiles.open(LOG_FILE, "a", encoding="utf-8") as f: + await f.write(f"[{now}] {message}\n") + except: + pass + +def log_xray_error(message: str): + try: + with open(XRAY_LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {message}\n") + except: + pass + +# ========= ВАЛИДАЦИЯ ========= +def validate_vless(url: str) -> bool: + if not url.startswith("vless://"): return False + if not UUID_REGEX.search(url): return False + if "@" not in url or ":" not in url: return False + return True + +# ========= WHITELIST ========= +def load_whitelist_domains(): + domains = set() + suffixes = [] + if os.path.exists("whitelist.txt"): + try: + with open("whitelist.txt", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + d = line.strip().lower() + if d: + domains.add(d) + suffixes.append("." + d) + print(f"📋 Загружено {len(domains)} доменов из whitelist.txt") + except: + print("⚠️ Ошибка загрузки whitelist.txt") + return domains, suffixes + +# ========= ПРОТОКОЛ / SNI / НАЗВАНИЕ ========= +def detect_protocol(vless_url: str) -> str: + try: + no_scheme = vless_url[len("vless://"):] + after_at = no_scheme.split("@", 1)[1] + query = after_at.split("?", 1)[1] if "?" in after_at else "" + params = dict(urllib.parse.parse_qsl(query)) + + transport = params.get("type", "").lower() + security = params.get("security", "").lower() + + if transport in ("ws", "websocket"): return "WS" + if transport in ("grpc", "gun"): return "gRPC" + if transport in ("xhttp", "httpupgrade"): return "XHTTP" + if transport in ("h2", "http2"): return "H2" + if transport == "tcp": return "TCP" + if security == "reality": return "Reality" + if security in ("tls", "xtls"): return "TLS" + return "TCP" + except: + return "Неизвестно" + +def extract_all_possible_domains(vless_url: str) -> list: + domains = set() + try: + if not vless_url.startswith("vless://"): return [] + content = vless_url[8:] + at_pos = content.find('@') + if at_pos == -1: return [] + + after_at = content[at_pos+1:] + q_pos = after_at.find('?') + + if q_pos != -1: + host_part = after_at[:q_pos] + query_part = after_at[q_pos+1:] + else: + host_part = after_at + query_part = "" + + host = host_part.split(':', 1)[0] + if host and '.' in host: + domains.add(host.lower()) + + if query_part: + if '#' in query_part: query_part = query_part.split('#', 1)[0] + params = dict(urllib.parse.parse_qsl(query_part)) + + for k in ['sni', 'host']: + if k in params and '.' in params[k]: + domains.add(params[k].lower()) + + if 'path' in params: + path_parts = re.findall(r'[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]\.[a-zA-Z]{2,}', params['path']) + domains.update(d.lower() for d in path_parts) + + return list(domains) + except: + return [] + +def get_human_name(domain: str) -> str: + if not domain: return "Неизвестно" + d = domain.lower() + + if d in DOMAIN_NAMES: return DOMAIN_NAMES[d] + + parts = d.split('.') + for i in range(len(parts) - 1): + if ".".join(parts[i:]) in DOMAIN_NAMES: + return DOMAIN_NAMES[".".join(parts[i:])] + + if len(parts) >= 2 and ".".join(parts[-2:]) in DOMAIN_NAMES: + return DOMAIN_NAMES[".".join(parts[-2:])] + + return "Неизвестно" + +def filter_by_sni(vless_url: str, whitelist_domains: set, whitelist_suffixes: list) -> bool: + domains = extract_all_possible_domains(vless_url) + + if whitelist_domains: + for domain in domains: + if domain in whitelist_domains or any(domain.endswith(s) for s in whitelist_suffixes): return True + if len(domain.split('.')) >= 2 and '.'.join(domain.split('.')[-2:]) in whitelist_domains: return True + return False + + for domain in domains: + if domain in DOMAIN_NAMES: return True + parts = domain.split('.') + for i in range(len(parts) - 1): + if ".".join(parts[i:]) in DOMAIN_NAMES: return True + if len(parts) >= 2 and ".".join(parts[-2:]) in DOMAIN_NAMES: return True + + return False + +# ========= СКАЧИВАНИЕ ========= +async def fetch(session, url, sem): + async with sem: + try: + print(f"Скачиваю: {url}") + async with session.get(url, timeout=15) as resp: + if resp.status == 200: + return await resp.text() + except Exception as e: + await log(f"Ошибка при скачивании {url}: {e}") + return None + +async def process_url(session, url, sem, output_queue, stats_lock, stats): + content = await fetch(session, url, sem) + async with stats_lock: + stats["processed"] += 1 + + if content: + matches = VLESS_REGEX.findall(content) + if matches: + async with stats_lock: + stats["found"] += len(matches) + for match in matches: + await output_queue.put(match) + + async with stats_lock: + print(f"Обработано: {stats['processed']} | Найдено VLESS: {stats['found']}", end="\r") + +async def writer_task(output_queue, output_file): + """Асинхронно пишет строки из очереди в файл.""" + async with aiofiles.open(output_file, "w", encoding="utf-8") as f: + while True: + line = await output_queue.get() + if line is None: # сигнал завершения + break + await f.write(line + "\n") + output_queue.task_done() + +# ========= ОЧИСТКА ========= +async def clean_vless(): + print("\nОчищаю дубликаты и проверяю валидность...") + if not os.path.exists(OUTPUT_FILE): return + + try: + async with aiofiles.open(OUTPUT_FILE, "r", encoding="utf-8") as f: + lines = await f.readlines() + except: + return + + unique_valid = {url.strip() for url in lines if url.strip() and validate_vless(url.strip())} + + async with aiofiles.open(CLEAN_FILE, "w", encoding="utf-8") as f: + await f.write("\n".join(unique_valid) + "\n") + + print(f"Очистка завершена. Итоговых конфигов: {len(unique_valid)}") + +# ========= ФИЛЬТРАЦИЯ ПО WHITELIST ========= +async def filter_vless(): + print("\n=== Фильтрация по whitelist ===") + if not os.path.exists(CLEAN_FILE): return + + domains, suffixes = load_whitelist_domains() + try: + with open(CLEAN_FILE, "r", encoding="utf-8", errors="ignore") as f: + total = sum(1 for _ in f) + except: + total = 0 + + passed, processed = 0, 0 + + async with aiofiles.open(CLEAN_FILE, "r", encoding="utf-8") as f_in, \ + aiofiles.open(FILTERED_FILE, "w", encoding="utf-8") as f_out: + async for line in f_in: + processed += 1 + url = line.strip() + if not url: continue + + if filter_by_sni(url, domains, suffixes): + await f_out.write(url + "\n") + passed += 1 + + if processed % 100 == 0 and total > 0: + print(f"Фильтрация: {processed}/{total} | Подошло: {passed}", end="\r") + + print(f"\nФильтрация завершена. Итог: {passed} конфигов.") + +# ========= ПЕРЕИМЕНОВАНИЕ ========= +async def rename_configs(): + print("\n=== Переименование конфигов по протоколу и SNI ===") + if not os.path.exists(FILTERED_FILE): return + + try: + with open(FILTERED_FILE, "r", encoding="utf-8", errors="ignore") as f: + total = sum(1 for _ in f) + except: + total = 0 + + processed = 0 + + async with aiofiles.open(FILTERED_FILE, "r", encoding="utf-8") as f_in, \ + aiofiles.open(NAMED_FILE, "w", encoding="utf-8") as f_out: + async for line in f_in: + processed += 1 + url = line.strip() + if not url: continue + + protocol = detect_protocol(url) + domains = extract_all_possible_domains(url) + human_name = "Неизвестно" + + if domains: + for domain in domains: + name = get_human_name(domain) + if name != "Неизвестно": + human_name = name + break + if human_name == "Неизвестно" and domains: + human_name = domains[0].split('.')[-2].capitalize() if len(domains[0].split('.')) >= 2 else domains[0] + + title = f"{protocol}, {human_name} [V.O.I.D]" + base = url.split("#", 1)[0] + new_url = f"{base}#{title}" + + await f_out.write(new_url + "\n") + + if processed % 500 == 0 and total > 0: + print(f"Переименовано: {processed}/{total}", end="\r") + + print(f"\nПереименование завершено. Итог: {processed} конфигов.") + +# ========= НОРМАЛИЗАЦИЯ И КОДИРОВАНИЕ URL ========= +def encode_vless_url(url: str) -> str: + """Корректное кодирование VLESS URL, сохраняя уже закодированные символы.""" + try: + if not url.startswith("vless://"): return url + + content = url[8:] + at_pos = content.find('@') + if at_pos == -1: return url + + uuid = content[:at_pos] + after_at = content[at_pos+1:] + + q_pos = after_at.find('?') + if q_pos != -1: + host_part = after_at[:q_pos] + params_part = after_at[q_pos+1:] + else: + host_part = after_at + params_part = "" + + hash_pos = host_part.find('#') + if hash_pos != -1: + host_only = host_part[:hash_pos] + fragment = host_part[hash_pos+1:] + else: + host_only = host_part + fragment = "" + + if not fragment and params_part: + hash_pos = params_part.find('#') + if hash_pos != -1: + params_only = params_part[:hash_pos] + fragment = params_part[hash_pos+1:] + params_part = params_only + + # Парсим параметры с сохранением оригинального кодирования + params = dict(urllib.parse.parse_qsl(params_part, keep_blank_values=True)) + encoded_params = [] + for k, v in params.items(): + if k in ['security', 'type', 'fp', 'pbk', 'sid', 'flow']: + encoded_params.append(f"{k}={v}") + else: + # Кодируем значение, но оставляем уже закодированные %XX + # Для простоты используем quote с safe='%' + encoded_v = urllib.parse.quote(v, safe='%') + encoded_params.append(f"{k}={encoded_v}") + + new_params = "&".join(encoded_params) + + # Фрагмент кодируем только если есть не-ASCII символы + try: + fragment.encode('ascii') + encoded_fragment = fragment + except UnicodeEncodeError: + encoded_fragment = urllib.parse.quote(fragment, safe='') + + base = f"vless://{uuid}@{host_only}" + if new_params: base += f"?{new_params}" + if encoded_fragment: base += f"#{encoded_fragment}" + + return base + + except Exception: + return url + +async def encode_all_configs(): + print("\n=== Кодирование конфигов для Xray ===") + if not os.path.exists(NAMED_FILE): return + + try: + with open(NAMED_FILE, 'r', encoding='utf-8') as f: + configs = [line.strip() for line in f if line.strip()] + except: + return + + total = len(configs) + changed = 0 + + async with aiofiles.open(ENCODED_FILE, "w", encoding="utf-8") as f_out: + for i, url in enumerate(configs, 1): + encoded_url = encode_vless_url(url) + await f_out.write(encoded_url + "\n") + if encoded_url != url: changed += 1 + if i % 500 == 0: + print(f"Закодировано: {i}/{total} | Изменено: {changed}", end="\r") + + print(f"\nКодирование завершено. Всего: {total}, изменено: {changed}") + +# ========= АЛЬТЕРНАТИВНЫЕ МЕТОДЫ ПРОВЕРКИ ========= +def check_tcp_connection(host: str, port: int, timeout: int = 2) -> bool: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except: + return False + +def check_tls_handshake(host: str, port: int = 443, sni: str = None, timeout: int = 2) -> tuple: + try: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + + ssl_sock = context.wrap_socket(sock, server_hostname=sni or host) + ssl_sock.do_handshake() + version = ssl_sock.version() + ssl_sock.close() + sock.close() + return (True, version, None) + except Exception as e: + return (False, None, str(e)) + +# ========= XRAY-ТЕСТЕР ========= +class SimpleProgress: + def __init__(self, total): + self.total = total + self.current = 0 + self.start_time = time.time() + self.lock = threading.Lock() + self.working_count = 0 + self.retry_count = 0 + + def update(self, status='', working=False, retry=False): + with self.lock: + self.current += 1 + if working: self.working_count += 1 + if retry: self.retry_count += 1 + if self.current % 10 == 0 or self.current == self.total: + elapsed = time.time() - self.start_time + speed = self.current / elapsed if elapsed > 0 else 0 + print(f"\r📊 [{self.current}/{self.total}] ✅:{self.working_count} 🔄:{self.retry_count} {speed:.1f} к/с {status}", end='', flush=True) + + def finish(self): + elapsed = time.time() - self.start_time + print(f"\r✅ Готово! {self.current} конфигов за {elapsed:.1f}с ({self.current/elapsed:.1f} к/с), рабочих: {self.working_count}, повторов: {self.retry_count}") + +class PortManager: + def __init__(self, start=20000, end=25000): + self.ports = list(range(start, end + 1)) + self.used = set() + self.lock = threading.Lock() + + def get_port(self): + with self.lock: + available = [p for p in self.ports if p not in self.used] + if not available: return None + port = random.choice(available) + self.used.add(port) + return port + + def release_port(self, port): + with self.lock: + self.used.discard(port) + +class XrayTester: + def __init__(self, input_file='url_encoded.txt', output_file='url_work.txt', max_workers=30): + self.input_file = input_file + self.output_file = output_file + self.max_workers = max_workers + self.test_url = XRAY_TEST_URL + self.timeout = XRAY_TIMEOUT + self.max_retries = MAX_RETRIES + self.retry_delay = RETRY_DELAY + self.xray_dir = Path('./xray_bin') + self.xray_path = self.xray_dir / ('xray.exe' if os.name == 'nt' else 'xray') + self.port_manager = PortManager() + self.debug_file = DEBUG_FILE + self.xray_log_file = XRAY_LOG_FILE + + print(f"🔍 XrayTester инициализирован") + # Синхронная проверка наличия Xray при инициализации (вызывается из executor) + self._ensure_xray() + + def _ensure_xray(self): + """Проверяет наличие Xray и скачивает при необходимости.""" + if not self.xray_path.exists(): + print("⬇️ Скачиваю Xray...") + self._download_xray() + else: + try: + result = subprocess.run([str(self.xray_path), '-version'], capture_output=True, text=True, timeout=5) + version = result.stdout.split('\n')[0] if result.stdout else 'Unknown' + print(f"✅ Xray готов: {version}") + except Exception as e: + print(f"⚠️ Ошибка версии Xray: {e}, пробую переустановить") + self._download_xray() + + def _download_xray(self): + """Загружает Xray с официального репозитория.""" + import urllib.request, zipfile, platform + self.xray_dir.mkdir(exist_ok=True) + system = platform.system().lower() + if system == 'windows': + url = "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-windows-64.zip" + elif system == 'linux': + url = "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip" + else: + raise RuntimeError(f"Unsupported OS: {system}") + + zip_path = self.xray_dir / "xray.zip" + try: + urllib.request.urlretrieve(url, zip_path) + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(self.xray_dir) + zip_path.unlink() + # Устанавливаем права на исполнение для Linux/Mac + if os.name != 'nt': + os.chmod(self.xray_path, 0o755) + print("✅ Xray загружен") + except Exception as e: + print(f"❌ Ошибка загрузки Xray: {e}") + + def parse_vless_url(self, url): + try: + if not url.startswith('vless://'): return None + content = url[8:] + at_pos = content.find('@') + if at_pos == -1: return None + + uuid = content[:at_pos] + after_at = content[at_pos+1:] + + q_pos = after_at.find('?') + if q_pos != -1: + host_part = after_at[:q_pos] + query = after_at[q_pos+1:] + else: + host_part = after_at + query = "" + + host_part = host_part.split('#')[0] + host, port = (host_part.split(':', 1) + [443])[:2] + + params = dict(urllib.parse.parse_qsl(query)) + return {'uuid': uuid, 'host': host, 'port': int(port), 'params': params, 'url': url} + except Exception: + return None + + def create_xray_config(self, parsed, port): + try: + params = parsed['params'] + flow = params.get('flow', '') + security = params.get('security', '') + + config = { + "log": {"loglevel": "error"}, + "inbounds": [{"port": port, "protocol": "socks", "settings": {"auth": "noauth", "udp": False}, "tag": "socks-in"}], + "outbounds": [{ + "protocol": "vless", + "settings": {"vnext": [{"address": parsed['host'], "port": parsed['port'], "users": [{"id": parsed['uuid'], "encryption": "none", "flow": flow}]}]}, + "streamSettings": {"network": params.get('type', 'tcp'), "security": security}, + "tag": "proxy" + }] + } + + if security == 'reality': + config["outbounds"][0]["streamSettings"]["realitySettings"] = { + "serverName": params.get('sni', parsed['host']), "fingerprint": params.get('fp', 'chrome'), + "publicKey": params.get('pbk', ''), "shortId": params.get('sid', ''), "spiderX": params.get('spx', '/') + } + elif security == 'tls': + config["outbounds"][0]["streamSettings"]["tlsSettings"] = {"serverName": params.get('sni', parsed['host']), "allowInsecure": True} + + if params.get('type') in ('ws', 'websocket'): + config["outbounds"][0]["streamSettings"]["wsSettings"] = {"path": params.get('path', '/'), "headers": {"Host": params.get('host', params.get('sni', parsed['host']))}} + + if params.get('type') in ('grpc', 'gun'): + config["outbounds"][0]["streamSettings"]["grpcSettings"] = {"serviceName": params.get('servicename', params.get('service', '')), "multiMode": True} + + return config + except Exception: + return None + + def _read_stderr(self, process): + """Читает stderr процесса, чтобы избежать блокировки.""" + try: + for line in process.stderr: + pass # игнорируем + except: + pass + + def test_with_xray(self, parsed, port, attempt=1): + config_file, process = None, None + try: + config = self.create_xray_config(parsed, port) + if not config: return None + + fd, config_file = tempfile.mkstemp(suffix='.json') + os.close(fd) + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config, f) + + creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 + process = subprocess.Popen( + [str(self.xray_path), '-c', config_file], + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + creationflags=creationflags + ) + # Запускаем поток для чтения stderr + stderr_thread = threading.Thread(target=self._read_stderr, args=(process,), daemon=True) + stderr_thread.start() + + time.sleep(1.0) + if process.poll() is not None: + return "CRASH" + + start = time.time() + session = requests.Session() + session.proxies = {'https': f'socks5://127.0.0.1:{port}'} + + try: + r = session.get(self.test_url, timeout=self.timeout) + if r.status_code in [200, 204]: + return {'working': True, 'ping': (time.time() - start) * 1000, 'method': 'xray'} + return "FAIL" + except requests.exceptions.Timeout: return "TIMEOUT" + except requests.exceptions.ConnectionError: return "CONN_ERROR" + except Exception: return "ERROR" + + except Exception: + return "EXCEPTION" + finally: + if process: + try: + process.terminate() + process.wait(timeout=2.0) + except: + process.kill() + if config_file and os.path.exists(config_file): + try: os.remove(config_file) + except: pass + + def check_alternative_methods(self, parsed, url): + host, port = parsed['host'], parsed['port'] + params = parsed['params'] + if not check_tcp_connection(host, port, timeout=2): return None + + security = params.get('security', '') + if security in ['reality', 'tls']: + tls_ok, _, _ = check_tls_handshake(host, port, params.get('sni', host), timeout=2) + if tls_ok: + return {'url': url, 'ping': 100, 'method': 'tls_check', 'security': security} + return None + + def test_one(self, url): + parsed = self.parse_vless_url(url) + if not parsed: return None + + port = self.port_manager.get_port() + if port: + try: + for attempt in range(1, self.max_retries + 1): + result = self.test_with_xray(parsed, port, attempt) + if isinstance(result, dict) and result.get('working'): + self.port_manager.release_port(port) + return {'url': url, 'ping': result['ping'], 'method': 'xray'} + if result in ["TIMEOUT", "FAIL", "CONN_ERROR", "CRASH"] and attempt < self.max_retries: + time.sleep(self.retry_delay) + continue + break + finally: + self.port_manager.release_port(port) + + return self.check_alternative_methods(parsed, url) + + def test_all(self): + if not os.path.exists(self.input_file): return + + try: + with open(self.input_file, 'r', encoding='utf-8') as f: + all_urls = [line.strip() for line in f if line.strip()] + except: + return + + if not all_urls: return + + print(f"\n🔍 Тестирование {len(all_urls)} конфигов") + if os.path.exists(self.debug_file): os.remove(self.debug_file) + + working = [] + progress = SimpleProgress(len(all_urls)) + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + futures = {executor.submit(self.test_one, url): url for url in all_urls} + for future in as_completed(futures): + try: + result = future.result(timeout=self.timeout + 5) + if result: + working.append(result) + progress.update('✅', working=True) + else: + progress.update('❌', working=False) + except Exception: + progress.update('⚠️', working=False) + + progress.finish() + working.sort(key=lambda x: x['ping']) + + with open(self.output_file, 'w', encoding='utf-8') as f: + for w in working: + f.write(w['url'] + '\n') + + print(f"\n📊 Результаты: ✅ Рабочих: {len(working)} | ❌ Не рабочих: {len(all_urls)-len(working)}\n") + return working + + def run(self): + self.test_all() + +# ========= ОСНОВНОЙ ЦИКЛ ========= +async def main_cycle(): + global cycle_counter + cycle_counter += 1 + print(f"\n=== Новый цикл #{cycle_counter} ===") + + if cycle_counter % CYCLES_BEFORE_DEBUG_CLEAN == 0 and os.path.exists(DEBUG_FILE): + os.remove(DEBUG_FILE) + + if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE) + if not os.path.exists(SOURCES_FILE): return + + try: + with open(SOURCES_FILE, "r", encoding="utf-8", errors="ignore") as f: + urls = [line.strip() for line in f if line.strip()] + except: + return + + if not urls: return + print(f"📥 Загружаю {len(urls)} источников...") + + sem = asyncio.Semaphore(THREADS_DOWNLOAD) + output_queue = asyncio.Queue() + stats = {"processed": 0, "found": 0} + stats_lock = asyncio.Lock() + + # Запускаем писатель в фоне + writer = asyncio.create_task(writer_task(output_queue, OUTPUT_FILE)) + + async with aiohttp.ClientSession() as session: + tasks = [process_url(session, url, sem, output_queue, stats_lock, stats) for url in urls] + await asyncio.gather(*tasks) + + # Сигнализируем писателю о завершении + await output_queue.put(None) + await writer + + print(f"\n✅ Скачивание завершено. Найдено VLESS: {stats['found']}") + await log(f"Скачивание завершено. Найдено VLESS: {stats['found']}") + + if stats['found'] > 0: + await clean_vless() + await filter_vless() + await rename_configs() + await encode_all_configs() + + print("\n=== Запуск Xray-проверки ===") + tester = XrayTester(input_file=ENCODED_FILE, output_file=WORK_FILE, max_workers=XRAY_MAX_WORKERS) + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, tester.run) + else: + print("⏭️ Нет новых конфигов для обработки") + +async def run_forever(): + print("\n🔄 Запуск бесконечного цикла...") + while True: + try: + cycle_start = time.time() + await main_cycle() + print(f"✅ Цикл завершен за {time.time() - cycle_start:.1f}с") + await asyncio.sleep(CYCLE_DELAY) + except KeyboardInterrupt: + break + except Exception as e: + print(f"⚠️ Критическая ошибка в цикле: {e}") + await log(f"Критическая ошибка: {e}") + await asyncio.sleep(60) + +if __name__ == "__main__": + try: + asyncio.run(main_cycle()) + except KeyboardInterrupt: + print("\n👋 Программа остановлена")