import json, time, re, urllib.parse, urllib.request, urllib.error, html as html_lib, threading, uuid from concurrent.futures import ThreadPoolExecutor, wait, as_completed, TimeoutError as FuturesTimeoutError from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler import hashlib import uuid PORT = 7860 OPENER = urllib.request.build_opener() FAST_WORKERS = 3 FULL_WORKERS = 48 FETCH_TIMEOUT = 1.6 FAST_SEARCH_TIMEOUT = 5.0 FULL_SEARCH_TIMEOUT = 8.0 CACHE_TTL = 21600 VALIDATE_TIMEOUT = 1.6 VALIDATE_RETRY_TIMEOUT = 4.0 UNKNOWN_CACHE_TTL = 60 UC_VALID_CACHE_TTL = 300 UC_SEARCH_CACHE_TTL = 300 BAIDU_VALID_CACHE_TTL = 300 BAIDU_SEARCH_CACHE_TTL = 300 MASTER_SYNC_VALIDATE_BUDGET = 2.5 BUDGET_UNKNOWN_TYPES = {'115', '189', '139', 'other'} VALIDATE_WORKERS = 96 FAST_MAX_CHANNELS = 9 FULL_MAX_CHANNELS = 150 SEARCH_CACHE = {} VALIDATE_CACHE = {} VALIDATION_TASKS = {} TASK_LOCK = threading.Lock() RECENT_REQUESTS = [] REQUEST_LOG_LOCK = threading.Lock() REQUEST_LOG_LIMIT = 200 TASK_TTL = 3600 EXCLUDED_CHANNELS = {'yoyokuakeduanju'} PRIORITY_CHANNELS = [ 'Quark_Movies', 'leoziyuan', 'yunpanxunlei', 'NewQuark', 'shareAliyun', 'alyp_TV', 'alyp_4K_Movies', 'ucquark', 'kuakeyun', 'tianyifc', 'yunpan189', 'yunpanuc' ] CLOUD_DOMAINS = ( 'pan.quark.cn', 'drive.uc.cn', 'pan.baidu.com', 'alipan.com', 'aliyundrive.com', '115.com', '115cdn.com', 'anxia.com', '123pan.com', '123pan.cn', '123684.com', '123865.com', '123912.com', '123592.com', 'cloud.189.cn', 'caiyun.139.com', 'pan.xunlei.com', ) URL_RE = re.compile(r'https?://[^\s<>"\']+', re.I) NON_VIDEO_TITLE_RE = re.compile(r'(?:有声剧|有声书|听书|广播剧|多人有声|主播[::]|音频|评书|小说朗读|朗读版)') def is_cloud_url(url): try: host = urllib.parse.urlparse(url).netloc.lower() except Exception: return False return any(d in host for d in CLOUD_DOMAINS) def is_excluded_cloud_url(url): try: host = urllib.parse.urlparse(url).netloc.lower() except Exception: return False return 'alipan.com' in host or 'aliyundrive.com' in host def clean_tianyi_url(url): raw = (url or '').strip().replace('&', '&') decoded = urllib.parse.unquote(raw) m = re.search(r'(?:[?&]code=|/web/share\?code=)([A-Za-z0-9]+)', decoded) if not m: return raw code = m.group(1) access = '' q = urllib.parse.parse_qs(urllib.parse.urlparse(raw).query) for key in ('accessCode', 'access_code', 'pwd', 'password'): if q.get(key): access = (q.get(key) or [''])[0] break if not access: am = re.search(r'(?:访问码|提取码|密码)\s*[::]\s*([A-Za-z0-9]{3,12})', decoded) if am: access = am.group(1) clean = 'https://cloud.189.cn/web/share?code=' + urllib.parse.quote(code) if access: clean += '&accessCode=' + urllib.parse.quote(access) return clean def clean_url(url): url = (url or '').strip().replace('&', '&').lstrip('#') if 'cloud.189.cn' in url: url = clean_tianyi_url(url) # Cloud share URLs do not need URL fragments. Keeping fragments can break # the legacy result format because channel items are separated by "##". if '#' in url: url = url.split('#', 1)[0] return url.rstrip('。,,;;、))]】>&?#') def is_non_video_title(title): return bool(NON_VIDEO_TITLE_RE.search(title or '')) def title_from_text(text, keyword): lines = [x.strip() for x in text.splitlines() if x.strip()] for i, line in enumerate(lines): m = re.match(r'^(?:名称|片名|标题)\s*[::]\s*(.+)$', line) if m and m.group(1).strip(): return m.group(1).strip()[:120] if re.match(r'^(?:名称|片名|标题)\s*[::]?$', line) and i + 1 < len(lines): return lines[i + 1].strip()[:120] for line in lines: if keyword and keyword in line: return line[:120] return (lines[0][:120] if lines else keyword or '未知') TITLE_LABEL_RE = re.compile(r'^(?:名称|片名|标题|剧名|资源名称)\s*[::]\s*(.+)$') TITLE_LABEL_ONLY_RE = re.compile(r'^(?:名称|片名|标题|剧名|资源名称)\s*[::]?$') DESC_LINE_RE = re.compile(r'^(?:简介|描述|介绍|剧情|资源简介|主演|导演|类型|地区|语言|年份|集数|更新|标签)\s*[::]?', re.I) PLATFORM_LABEL_RE = re.compile(r'^(?:UC|UC网盘|夸克|夸克网盘|百度|百度网盘|迅雷|迅雷云盘|P?123|123网盘|115|115网盘|天翼|天翼云|移动|移动云|和彩云|阿里|阿里云盘|网盘|云盘|链接|下载|备用|在线)\s*[:::-]?.*$', re.I) def is_probable_title_line(line): line = (line or '').strip() if not line or URL_RE.search(line) or line.startswith(('http://', 'https://')): return False if DESC_LINE_RE.match(line) or PLATFORM_LABEL_RE.match(line): return False # Long prose/synopsis is not a resource title. Exact mode should not keep a # link just because its description says e.g. "遮天三部曲...完美世界". if len(line) > 80: return False return True def title_from_context(text, keyword): lines = [x.strip() for x in (text or '').splitlines() if x.strip()] if not lines: return '' for i, line in enumerate(lines): m = TITLE_LABEL_RE.match(line) if m and m.group(1).strip(): return m.group(1).strip()[:120] if TITLE_LABEL_ONLY_RE.match(line) and i + 1 < len(lines): return lines[i + 1].strip()[:120] # Pick the nearest probable title line before the link, not synopsis lines. for line in reversed(lines): if is_probable_title_line(line): return line[:120] return '' def norm_text_for_match(text): text = html_lib.unescape(text or '').lower() # Remove spaces/punctuation/decorative separators so variants like # "大丨主宰", "大|主宰", "灵魂摆渡·十年" still match the plain keyword. text = re.sub(r'[丨||·・•\-—–_\s]+', '', text) return re.sub(r'[\W_]+', '', text, flags=re.U) def title_candidates_from_text(text, keyword): lines = [x.strip() for x in (text or '').splitlines() if x.strip()] candidates = [] for i, line in enumerate(lines): m = re.match(r'^(?:名称|片名|标题|剧名|资源名称)\s*[::]\s*(.+)$', line) if m and m.group(1).strip(): candidates.append(m.group(1).strip()) if re.match(r'^(?:名称|片名|标题|剧名|资源名称)\s*[::]?$', line) and i + 1 < len(lines): candidates.append(lines[i + 1].strip()) # Most Telegram resource posts put the real title in the first few text # lines. Keep these as candidates, but do not scan arbitrary long body text # in exact mode, otherwise unrelated posts mentioning the keyword are kept. for line in lines[:6]: if not URL_RE.search(line) and not line.startswith(('http://', 'https://')): candidates.append(line) title = title_from_text(text, keyword) if title: candidates.append(title) return list(dict.fromkeys(candidates)) def message_matches_keyword(text, keyword): if not keyword: return True return norm_text_for_match(keyword) in norm_text_for_match(text) def context_has_probable_title(text): lines = [x.strip() for x in (text or '').splitlines() if x.strip()] for i, line in enumerate(lines): if TITLE_LABEL_RE.match(line) or TITLE_LABEL_ONLY_RE.match(line): return True return any(is_probable_title_line(line) for line in lines) def keyword_title_line_from_context(text, keyword): kw = norm_text_for_match(keyword) if not kw: return '' lines = [x.strip() for x in (text or '').splitlines() if x.strip()] # Use only short title-like lines near the link. This is intentionally not # a full-message fuzzy match, so synopsis lines such as "遮天三部曲...完美世界" # will not pull unrelated resources in. for line in reversed(lines): if is_probable_title_line(line) and kw in norm_text_for_match(line): return line[:120] return '' def context_matches_exact_keyword(text, keyword): if not keyword: return True kw = norm_text_for_match(keyword) if not kw: return True title = title_from_context(text, keyword) return kw in norm_text_for_match(title) def exact_title_matches_keyword(text, keyword): if not keyword: return True kw = norm_text_for_match(keyword) if not kw: return True for cand in title_candidates_from_text(text, keyword): if kw in norm_text_for_match(cand): return True return False def plain_text_from_html(block): def repl_anchor(m): href = html_lib.unescape(m.group(1) or '') inner = m.group(2) or '' inner = re.sub(r'', '\n', inner, flags=re.I) inner = re.sub(r'<[^>]+>', '', inner) inner = html_lib.unescape(inner).strip() # Telegram often stores cloud URLs in href attributes while the visible # text is just "查看链接"/domain. Inject the href into plain text so the # per-link context filter can evaluate this link instead of falling back # to the whole message. if href: return (inner + '\n' if inner else '') + href return inner block = re.sub(r']+href=["\'](https?://[^"\']+)["\'][^>]*>(.*?)', repl_anchor, block, flags=re.I | re.S) plain = re.sub(r'', '\n', block, flags=re.I) plain = re.sub(r'<[^>]+>', '', plain) return html_lib.unescape(plain) def split_plain_by_links(plain): matches = list(URL_RE.finditer(plain or '')) if not matches: return [] out = [] prev_end = 0 prev_context_start = 0 for idx, m in enumerate(matches): # Context for a link starts after the previous link. This ties each link # to its closest local title/description instead of the whole message. raw_link = m.group(0) context = plain[prev_context_start:m.start()] # If there are many lines since the previous link, keep only the nearby # block to avoid an earlier matched title leaking into this link. lines = [x for x in context.splitlines()] if len(lines) > 8: context = '\n'.join(lines[-8:]) out.append((raw_link, context)) prev_context_start = m.end() prev_end = m.end() return out def fetch_tg(channel, keyword): q = urllib.parse.quote(keyword) url = f'https://telegram.me/s/{channel}?q={q}' req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', }) with OPENER.open(req, timeout=FETCH_TIMEOUT) as resp: return resp.read().decode('utf-8', 'ignore') def parse_channel(channel, keyword, exact=True): try: text = fetch_tg(channel, keyword) except Exception: return '' items, seen = [], set() blocks = re.findall(r'